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": ".*",
63 "headers": {
64 "Accept": "text/xml"
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 Process.__init__(self)
245 self.port = port
246 self.host = "0.0.0.0"
247 self.localhost = self.host_ip()
248 self.base_url = "http://{host}:{port}".format(host=self.localhost, port=self.port)
249 self.responses_url = self.base_url + "/mock/responses"
250 self.requests_url = self.base_url + "/mock/requests"
251 self.shutdown_url = self.base_url + "/mock/shutdown"
252
254 """
255 start the mock server
256 """
257 self.server.run(host=self.host, port=self.port)
258
260 """
261 Shutdown the server and terminate process
262 :return:
263 """
264 self.wait_for_server_to_start()
265 requests.get(self.shutdown_url)
266 self.terminate()
267
268 - def add_response(self, url_filter=None, status_code=200, headers=None, body="", encoding="text", data=None):
269 """
270 Add the response to the mock server
271
272 :param url_filter: Regular expression to match the url path
273 :type url_filter: str
274 :param status_code: Expected status code
275 :type status_code: int
276 :param headers: Headers to return in the response
277 :type headers: dict
278 :param body: response body as plain text
279 :type body: str
280 :param encoding: If body is binary, this should be set to string "base64".
281 Otherwise, leave empty or set to "text"
282 :type encoding: str
283 :param payload: a python dictionary
284 :type payload: dict
285 :param data: If provided, it will be the rule to add to the mock.
286 :type data: str
287 :return: Mock server response
288 """
289 self.wait_for_server_to_start()
290
291 if not data:
292 data = { "url_filter": url_filter,
293 "status_code": status_code,
294 "headers": headers if headers else {},
295 "body": body,
296 "encoding": encoding}
297
298 return requests.post(self.responses_url, data=json.dumps(data))
299
301 """
302 Adds a binary response to the mock server
303
304 :param url_filter: Regular expression to match the url path
305 :type url_filter: str
306 :param status_code: Expected status code
307 :type status_code: int
308 :param headers: Headers to return in the response
309 :type headers: dict
310 :param body: response body as plain text
311 :type body: str
312 :param encoding: If body is binary, this should be set to string "base64".
313 Otherwise, leave empty or set to "text"
314 :type encoding: str
315 :param payload: a python dictionary
316 :type payload: dict
317 :return: Mock server response
318 """
319 return self.add_response(url_filter, status_code=status_code, headers=headers, encoding="base64",
320 body=base64.b64encode(body))
321
322 - def add_text_response(self, url_filter=None, status_code=200, headers=None, body=""):
323 """
324 Adds a binary response to the mock server
325
326 :param url_filter: Regular expression to match the url path
327 :type url_filter: str
328 :param status_code: Expected status code
329 :type status_code: int
330 :param headers: Headers to return in the response
331 :type headers: dict
332 :param body: response body as plain text
333 :type body: str
334 :param encoding: If body is binary, this should be set to string "base64".
335 Otherwise, leave empty or set to "text"
336 :type encoding: str
337 :param payload: a python dictionary
338 :type payload: dict
339 :return: Mock server response
340 """
341 return self.add_response(url_filter, status_code=status_code, headers=headers, body=body)
342
344 """
345 Returns all the responses stored on the MockServer
346 :return:
347 """
348 self.wait_for_server_to_start()
349 response = requests.get(self.responses_url)
350 return response if response else None
351
353 """
354 Delete all the responses stored on the MockServer
355 :return:
356 """
357 self.wait_for_server_to_start()
358 return requests.delete(self.responses_url)
359
361 """
362 Returns all the requests stored on the MockServer
363 :return:
364 """
365 self.wait_for_server_to_start()
366 response = requests.get(self.requests_url)
367 if response:
368 return response
369 return None
370
372 """
373 Delete all the requests stored on the MockServer
374 :return:
375 """
376 self.wait_for_server_to_start()
377 return requests.delete(self.requests_url)
378
380 """
381 Dirty hack to return the localhost IP,
382 works only when you have an internet connection
383 :return:
384 """
385 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]
386
388 """
389 check if the server is running and port is open
390 """
391 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
392 result = sock.connect_ex((self.host_ip(),self.port))
393 return result == 0
394
396 """
397 Latency to avoid the access when the server is not started yet
398 """
399 while not self._is_server_running():
400 time.sleep(0.1)
401
404
405 if __name__ == '__main__':
406
407
408 server = ApiMockServer(port=10000)
409 server.start()
410