1 from flask import jsonify, request, Flask, make_response
2 import logging
3 import re
4 from multiprocessing import Process
5 import requests
6 from requests.exceptions import RequestException
7 import json
8 import socket
9 import time
10 from collections import OrderedDict
11 from tlib.base.NetworkHelper import NetworkHelper
12
13 mock_server = Flask(__name__)
14 _rules = OrderedDict()
15 _req_urls = []
16 _requests = []
19 """
20 Method to build an error response and log it
21 """
22 logging.error(message)
23 logging.error(request.data)
24
25 r = jsonify(status=message)
26 r.status_code = status_code
27 return r
28
29 @mock_server.route("/mock/shutdown", methods=['GET'])
30 -def shutdown():
31 func = request.environ.get('werkzeug.server.shutdown')
32 if func is None:
33 raise RuntimeError('Not running with the Werkzeug Server')
34 func()
35 return jsonify(status='Server is shutting down')
36
37
38 @mock_server.route("/mock/responses", methods=['POST'])
39 -def add_response():
40 """
41 This method adds new responses to the mock.
42 To add a response send a POST request with a payload like this:
43
44 {
45 "url_filter": ".*",
46 "headers": {
47 "Accept": "text/xml"
48 },
49 "body": "Sample body",
50 "status_code": 200
51 }
52
53 Server will validate each matching rule and apply the first match
54 If there is no match, it will return a 500 response
55 """
56 try:
57 payload = request.get_json(force=True)
58 except Exception as e:
59 return generate_error_response("Payload is not a valid JSON string", 400)
60
61 if not isinstance(payload, dict):
62 return generate_error_response("Payload is not a valid JSON string", 400)
63
64
65 if "url_filter" in payload.keys():
66 key = payload["url_filter"]
67
68 if key == "":
69 return generate_error_response("url_filter can't be an empty string", 400)
70
71 try:
72 url_filter = re.compile(payload["url_filter"])
73 except Exception as e:
74 return generate_error_response("url_filter is not a valid regular expression:\\n%s" % e.message, 400)
75 else:
76 return generate_error_response("url_filter is a required parameter", 400)
77
78 if "headers" in payload.keys():
79 if type(payload["headers"]) is dict:
80 headers = payload["headers"]
81 else:
82 return generate_error_response("headers is not a dictionary:\\n%s" % payload["headers"], 400)
83 else:
84 headers = {}
85
86 if "body" in payload.keys():
87 body = payload["body"]
88 else:
89 body = ""
90
91 if "status_code" in payload.keys():
92 if not isinstance(payload["status_code"], int):
93 return generate_error_response("Status code is not an integer: %s" % payload["status_code"], 400)
94
95 status_code = payload["status_code"]
96 else:
97 status_code = 200
98
99
100 new_rule = {"url_filter": url_filter, "headers": headers, "body": body, "status_code": status_code}
101 _rules[key] = new_rule
102
103 return jsonify(status="OK")
104
105
106 @mock_server.route("/mock/responses", methods=['DELETE'])
107 -def clear_responses():
108 """
109 Delete existing responses
110 """
111 _rules.clear()
112 return jsonify(status="All rules were deleted")
113
114
115 @mock_server.route("/mock/responses", methods=['GET'])
116 -def get_responses():
117 """
118 Get all responses
119 """
120 rules_as_text = []
121 for rule in _rules.values():
122
123 rule = rule.copy()
124
125
126 rule["url_filter"] = rule["url_filter"].pattern
127
128
129 rules_as_text.append(rule)
130
131 return jsonify(rules=rules_as_text)
132
133
134 @mock_server.route("/mock/requests", methods=['DELETE'])
135 -def clear_requests():
136 """
137 Delete existing requests
138 """
139 _req_urls = []
140 _requests = []
141
142 return jsonify(status="All requests were deleted")
143
144
145 @mock_server.route("/mock/requests", methods=['GET'])
146 -def get_requests():
147 """
148 Get all requests
149 """
150 if request.args.get('version') == '2':
151 return jsonify(requests=_requests)
152 else:
153 return jsonify(requests=_req_urls)
154
155
156 @mock_server.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE'])
157 @mock_server.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
158 -def catch_all(path):
159 """
160 This method will catch all requests for which there are no explicit routes.
161 Here is where we build responses based on the rules that have been configured
162 It will go though the list of rules and apply one by one until a match is found.
163 If there is no match, it will return a 500 response
164 """
165 _req_urls.append(request.url)
166
167 headers = {}
168 for h in str(request.headers).split("\r\n"):
169 if h:
170 parts = h.split(": ")
171 h_name = parts[0]
172 h_value = ":".join(parts[1:])
173 headers[h_name] = h_value
174
175
176 req = {
177 'Request': {
178 "Scheme": request.scheme,
179 "Host": NetworkHelper.get_hostname_from_url(request.url),
180 "Port": NetworkHelper.get_port_from_url(request.url),
181 "Path": request.path,
182 "Args": dict(request.args),
183 "Method": request.method,
184 "Headers": headers,
185 "Body": request.data
186 }
187 }
188
189 _requests.append(req)
190
191 for rule in _rules.values():
192 regex = rule["url_filter"]
193 if regex.search("/" + path):
194 r = make_response()
195 r .headers = rule["headers"]
196 r .data = rule["body"]
197 r .status_code = rule["status_code"]
198 return r
199
200
201 return generate_error_response("Mock has not been configured", 500)
202
205 """
206 Helper Class to interact with the ApiMockServer
207 Provides methods to add, remove mock objects as well as incoming requests
208 """
209
210 server = mock_server
211
213 Process.__init__(self)
214 self.port = port
215 self.host = "0.0.0.0"
216 self.localhost = self.host_ip()
217 self.base_url = "http://{host}:{port}".format(host=self.localhost, port=self.port)
218 self.responses_url = self.base_url + "/mock/responses"
219 self.requests_url = self.base_url + "/mock/requests"
220 self.shutdown_url = self.base_url + "/mock/shutdown"
221
223 """
224 start the mock server
225 """
226 self.server.run(host=self.host, port=self.port)
227
229 """
230 Shutdown the server and terminate process
231 :return:
232 """
233 self.wait_for_server_to_start()
234 requests.get(self.shutdown_url)
235 self.terminate()
236
237 - def add_response(self, url_filter=None, status_code=200, headers=None, body="", data=None):
238 """
239 Add the response to the mock server
240 if the payload is provided, other parameters are ignored
241
242 :param url_filter: Regular expression to match the url path
243 :param status_code: Expected status code
244 :param headers: Headers as a dictionary
245 :param body: response body as plain text
246 :param payload: a python dictionary
247 :return: HTTP status code
248 """
249
250 self.wait_for_server_to_start()
251 if data is None:
252 if not headers:
253 headers = {}
254 data = {"url_filter": url_filter,
255 "status_code": status_code,
256 "headers": headers,
257 "body": body}
258 return requests.post(self.responses_url, data=json.dumps(data))
259
261 """
262 Returns all the responses stored on the MockServer
263 :return:
264 """
265 self.wait_for_server_to_start()
266 response = requests.get(self.responses_url)
267 return response if response else None
268
270 """
271 Delete all the responses stored on the MockServer
272 :return:
273 """
274 self.wait_for_server_to_start()
275 return requests.delete(self.responses_url)
276
278 """
279 Returns all the requests stored on the MockServer
280 :return:
281 """
282 self.wait_for_server_to_start()
283 response = requests.get(self.requests_url)
284 if response:
285 return response
286 return None
287
289 """
290 Delete all the requests stored on the MockServer
291 :return:
292 """
293 self.wait_for_server_to_start()
294 return requests.delete(self.requests_url)
295
297 """
298 Dirty hack to return the localhost IP,
299 works only when you have an internet connection
300 :return:
301 """
302 return [(s.connect(('8.8.8.8', 80)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]
303
305 """
306 check if the server is running and port is open
307 """
308 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
309 result = sock.connect_ex((self.host_ip(),self.port))
310 return result == 0
311
313 """
314 Latency to avoid the access when the server is not started yet
315 """
316 while not self._is_server_running():
317 time.sleep(0.1)
318
321
322 if __name__ == '__main__':
323
324
325 api_server = ApiMockServer(port=10000)
326 api_server.start()
327