1 from flask import jsonify, request, Flask, make_response, g, current_app, session
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 import base64
13
14 mock_server = Flask(__name__)
15 _rules = OrderedDict()
16 _req_urls = []
17 _requests = []
20 """
21 Method to build an error response and log it
22 """
23 logging.error(message)
24 logging.error(request.data)
25
26 r = jsonify(status=message)
27 r.status_code = status_code
28 return r
29
30 @mock_server.route("/mock/shutdown", methods=['GET'])
31 -def shutdown():
32 func = request.environ.get('werkzeug.server.shutdown')
33 if func is None:
34 raise RuntimeError('Not running with the Werkzeug Server')
35 func()
36 return jsonify(status='Server is shutting down')
37
38
39 @mock_server.route("/mock/responses", methods=['POST'])
40 -def add_response():
41 """
42 This method adds new responses to the mock.
43 To add a response send a POST request with a payload like this:
44
45 {
46 "url_filter": ".*",
47 "headers": {
48 "Accept": "text/xml"
49 },
50 "body": "Sample body",
51 "status_code": 200
52 }
53
54 Server will validate each matching rule and apply the first match
55 If there is no match, it will return a 500 response
56
57 To add binary responses, send data encoded in base64 and add this value to the request
58 "encoding": "base64".
59 For example:
60
61 {
62 "url_filter": "/file.zip",
63 "headers": {
64 "Accept": "application/zip"
65 },
66 "body": "ADRFDRRFJFJJSMMSK",
67 "encoding": "base64",
68 "status_code": 200
69 }
70
71 """
72 try:
73 payload = request.get_json(force=True)
74 except Exception as e:
75 return generate_error_response("Payload is not a valid JSON string", 400)
76
77 if not isinstance(payload, dict):
78 return generate_error_response("Payload is not a valid JSON string", 400)
79
80
81 if "url_filter" in payload.keys():
82 key = payload["url_filter"]
83
84 if key == "":
85 return generate_error_response("url_filter can't be an empty string", 400)
86
87 try:
88 url_filter = re.compile(payload["url_filter"])
89 except Exception as e:
90 return generate_error_response("url_filter is not a valid regular expression:\\n%s" % e.message, 400)
91 else:
92 return generate_error_response("url_filter is a required parameter", 400)
93
94 if "headers" in payload.keys():
95 if type(payload["headers"]) is dict:
96 headers = payload["headers"]
97 else:
98 return generate_error_response("headers is not a dictionary:\\n%s" % payload["headers"], 400)
99 else:
100 headers = {}
101
102 if "body" in payload.keys():
103 if "encoding" in payload.keys():
104
105 if payload["encoding"] not in ("base64", "text"):
106 return generate_error_response("Encoding '%s' is not valid. Supported encodings are text and base64" %
107 payload["encoding"], 400)
108
109 if payload["encoding"] == "base64":
110 body = base64.b64decode(payload["body"])
111 else:
112 body = payload["body"]
113 else:
114
115 body = payload["body"]
116 else:
117 body = ""
118
119 if "status_code" in payload.keys():
120 if not isinstance(payload["status_code"], int):
121 return generate_error_response("Status code is not an integer: %s" % payload["status_code"], 400)
122
123 status_code = payload["status_code"]
124 else:
125 status_code = 200
126
127
128 new_rule = {"url_filter": url_filter, "headers": headers, "body": body, "status_code": status_code}
129 _rules[key] = new_rule
130
131 return jsonify(status="OK")
132
133
134 @mock_server.route("/mock/responses", methods=['DELETE'])
135 -def clear_responses():
136 """
137 Delete existing responses
138 """
139 _rules.clear()
140 return jsonify(status="All rules were deleted")
141
142
143 @mock_server.route("/mock/responses", methods=['GET'])
144 -def get_responses():
145 """
146 Get all responses
147 """
148 rules_as_text = []
149 for rule in _rules.values():
150
151 rule = rule.copy()
152
153
154 rule["url_filter"] = rule["url_filter"].pattern
155
156
157 rules_as_text.append(rule)
158
159 return jsonify(rules=rules_as_text)
160
161
162 @mock_server.route("/mock/requests", methods=['DELETE'])
163 -def clear_requests():
164 """
165 Delete existing requests
166 """
167 while _req_urls:
168 _req_urls.pop()
169
170 while _requests:
171 _requests.pop()
172
173 return jsonify(status="All requests were deleted")
174
175
176 @mock_server.route("/mock/requests", methods=['GET'])
177 -def get_requests():
178 """
179 Get all requests
180 """
181 if request.args.get('version') == '2':
182 return jsonify(requests=_requests)
183 else:
184 return jsonify(requests=_req_urls)
185
186
187 @mock_server.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE'])
188 @mock_server.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
189 -def catch_all(path):
190 """
191 This method will catch all requests for which there are no explicit routes.
192 Here is where we build responses based on the rules that have been configured
193 It will go though the list of rules and apply one by one until a match is found.
194 If there is no match, it will return a 500 response
195 """
196 _req_urls.append(request.url)
197
198 headers = {}
199 for h in str(request.headers).split("\r\n"):
200 if h:
201 parts = h.split(": ")
202 h_name = parts[0]
203 h_value = ":".join(parts[1:])
204 headers[h_name] = h_value
205
206
207 req = {
208 'Request': {
209 "Scheme": request.scheme,
210 "Host": NetworkHelper.get_hostname_from_url(request.url),
211 "Port": NetworkHelper.get_port_from_url(request.url),
212 "Path": request.path,
213 "Args": dict(request.args),
214 "Method": request.method,
215 "Headers": headers,
216 "Body": request.data
217 }
218 }
219
220 _requests.append(req)
221
222 for rule in _rules.values():
223 regex = rule["url_filter"]
224 if regex.search("/" + path):
225 r = make_response()
226 r .headers = rule["headers"]
227 r .data = rule["body"]
228 r .status_code = rule["status_code"]
229 return r
230
231
232 return generate_error_response("Mock has not been configured", 500)
233
236 """
237 Helper Class to interact with the ApiMockServer
238 Provides methods to add, remove mock objects as well as incoming requests
239 """
240
241 server = mock_server
242
244 super(ApiMockServer, self).__init__()
245
246 self.port = port
247 self.host = "0.0.0.0"
248 self.localhost = self.host_ip()
249 self.base_url = "http://{host}:{port}".format(host=self.localhost, port=self.port)
250 self.responses_url = self.base_url + "/mock/responses"
251 self.requests_url = self.base_url + "/mock/requests"
252 self.shutdown_url = self.base_url + "/mock/shutdown"
253
255 """
256 start the mock server
257 """
258 super(ApiMockServer, self).run()
259 self.server.run(host=self.host, port=self.port)
260
264
266 """
267 Shutdown the server and terminate process
268 :return:
269 """
270 self._wait_for_server_to_start()
271 requests.get(self.shutdown_url)
272 self.terminate()
273
274 - def add_response(self, url_filter=None, status_code=200, headers=None, body="", encoding="text", data=None):
275 """
276 Add the response to the mock server
277
278 :param url_filter: Regular expression to match the url path
279 :type url_filter: str
280 :param status_code: Expected status code
281 :type status_code: int
282 :param headers: Headers to return in the response
283 :type headers: dict
284 :param body: response body as plain text
285 :type body: str
286 :param encoding: If body is binary, this should be set to string "base64".
287 Otherwise, leave empty or set to "text"
288 :type encoding: str
289 :param payload: a python dictionary
290 :type payload: dict
291 :param data: If provided, it will be the rule to add to the mock.
292 :type data: str
293 :return: Mock server response
294 """
295 self._wait_for_server_to_start()
296
297 if not data:
298 data = { "url_filter": url_filter,
299 "status_code": status_code,
300 "headers": headers if headers else {},
301 "body": body,
302 "encoding": encoding}
303
304 return requests.post(self.responses_url, data=json.dumps(data))
305
307 """
308 Adds a binary response to the mock server
309
310 :param url_filter: Regular expression to match the url path
311 :type url_filter: str
312 :param status_code: Expected status code
313 :type status_code: int
314 :param headers: Headers to return in the response
315 :type headers: dict
316 :param body: response body as plain text
317 :type body: str
318 :param encoding: If body is binary, this should be set to string "base64".
319 Otherwise, leave empty or set to "text"
320 :type encoding: str
321 :param payload: a python dictionary
322 :type payload: dict
323 :return: Mock server response
324 """
325 return self.add_response(url_filter, status_code=status_code, headers=headers, encoding="base64",
326 body=base64.b64encode(body))
327
328 - def add_text_response(self, url_filter=None, status_code=200, headers=None, body=""):
329 """
330 Adds a binary response to the mock server
331
332 :param url_filter: Regular expression to match the url path
333 :type url_filter: str
334 :param status_code: Expected status code
335 :type status_code: int
336 :param headers: Headers to return in the response
337 :type headers: dict
338 :param body: response body as plain text
339 :type body: str
340 :param encoding: If body is binary, this should be set to string "base64".
341 Otherwise, leave empty or set to "text"
342 :type encoding: str
343 :param payload: a python dictionary
344 :type payload: dict
345 :return: Mock server response
346 """
347 return self.add_response(url_filter, status_code=status_code, headers=headers, body=body)
348
350 """
351 Returns all the responses stored on the MockServer
352 :return:
353 """
354 self._wait_for_server_to_start()
355 response = requests.get(self.responses_url)
356 return response if response else None
357
359 """
360 Delete all the responses stored on the MockServer
361 :return:
362 """
363 self._wait_for_server_to_start()
364 return requests.delete(self.responses_url)
365
367 """
368 Returns all the requests stored on the MockServer
369 :return:
370 """
371 self._wait_for_server_to_start()
372 response = requests.get(self.requests_url)
373 if response:
374 return response
375 return None
376
378 """
379 Delete all the requests stored on the MockServer
380 :return:
381 """
382 self._wait_for_server_to_start()
383 return requests.delete(self.requests_url)
384
386 """
387 Dirty hack to return the localhost IP,
388 works only when you have an internet connection
389 :return:
390 """
391 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]
392
394 """
395 check if the server is running and port is open
396 """
397 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
398 result = sock.connect_ex((self.host_ip(),self.port))
399 return result == 0
400
402 """
403 Latency to avoid the access when the server is not started yet
404 """
405 while not self._is_server_running():
406 time.sleep(0.1)
407
410
411 if __name__ == '__main__':
412
413
414 server = ApiMockServer(port=10000)
415 server.start()
416