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 try:
58 payload = request.get_json(force=True)
59 except Exception as e:
60 return generate_error_response("Payload is not a valid JSON string", 400)
61
62 if not isinstance(payload, dict):
63 return generate_error_response("Payload is not a valid JSON string", 400)
64
65
66 if "url_filter" in payload.keys():
67 key = payload["url_filter"]
68
69 if key == "":
70 return generate_error_response("url_filter can't be an empty string", 400)
71
72 try:
73 url_filter = re.compile(payload["url_filter"])
74 except Exception as e:
75 return generate_error_response("url_filter is not a valid regular expression:\\n%s" % e.message, 400)
76 else:
77 return generate_error_response("url_filter is a required parameter", 400)
78
79 if "headers" in payload.keys():
80 if type(payload["headers"]) is dict:
81 headers = payload["headers"]
82 else:
83 return generate_error_response("headers is not a dictionary:\\n%s" % payload["headers"], 400)
84 else:
85 headers = {}
86
87 if "body" in payload.keys():
88 if "encoding" in payload.keys():
89
90 if payload["encoding"] not in ("base64", "text"):
91 return generate_error_response("Encoding '%s' is not valid. Supported encodings are text and base64" %
92 payload["encoding"], 400)
93
94 if payload["encoding"] == "base64":
95 body = base64.b64decode(payload["body"])
96 else:
97 body = payload["body"]
98 else:
99
100 body = payload["body"]
101 else:
102 body = ""
103
104 if "status_code" in payload.keys():
105 if not isinstance(payload["status_code"], int):
106 return generate_error_response("Status code is not an integer: %s" % payload["status_code"], 400)
107
108 status_code = payload["status_code"]
109 else:
110 status_code = 200
111
112
113 new_rule = {"url_filter": url_filter, "headers": headers, "body": body, "status_code": status_code}
114 _rules[key] = new_rule
115
116 return jsonify(status="OK")
117
118
119 @mock_server.route("/mock/responses", methods=['DELETE'])
120 -def clear_responses():
121 """
122 Delete existing responses
123 """
124 _rules.clear()
125 return jsonify(status="All rules were deleted")
126
127
128 @mock_server.route("/mock/responses", methods=['GET'])
129 -def get_responses():
130 """
131 Get all responses
132 """
133 rules_as_text = []
134 for rule in _rules.values():
135
136 rule = rule.copy()
137
138
139 rule["url_filter"] = rule["url_filter"].pattern
140
141
142 rules_as_text.append(rule)
143
144 return jsonify(rules=rules_as_text)
145
146
147 @mock_server.route("/mock/requests", methods=['DELETE'])
148 -def clear_requests():
149 """
150 Delete existing requests
151 """
152 while _req_urls:
153 _req_urls.pop()
154
155 while _requests:
156 _requests.pop()
157
158 return jsonify(status="All requests were deleted")
159
160
161 @mock_server.route("/mock/requests", methods=['GET'])
162 -def get_requests():
163 """
164 Get all requests
165 """
166 if request.args.get('version') == '2':
167 return jsonify(requests=_requests)
168 else:
169 return jsonify(requests=_req_urls)
170
171
172 @mock_server.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE'])
173 @mock_server.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
174 -def catch_all(path):
175 """
176 This method will catch all requests for which there are no explicit routes.
177 Here is where we build responses based on the rules that have been configured
178 It will go though the list of rules and apply one by one until a match is found.
179 If there is no match, it will return a 500 response
180 """
181 _req_urls.append(request.url)
182
183 headers = {}
184 for h in str(request.headers).split("\r\n"):
185 if h:
186 parts = h.split(": ")
187 h_name = parts[0]
188 h_value = ":".join(parts[1:])
189 headers[h_name] = h_value
190
191
192 req = {
193 'Request': {
194 "Scheme": request.scheme,
195 "Host": NetworkHelper.get_hostname_from_url(request.url),
196 "Port": NetworkHelper.get_port_from_url(request.url),
197 "Path": request.path,
198 "Args": dict(request.args),
199 "Method": request.method,
200 "Headers": headers,
201 "Body": request.data
202 }
203 }
204
205 _requests.append(req)
206
207 for rule in _rules.values():
208 regex = rule["url_filter"]
209 if regex.search("/" + path):
210 r = make_response()
211 r .headers = rule["headers"]
212 r .data = rule["body"]
213 r .status_code = rule["status_code"]
214 return r
215
216
217 return generate_error_response("Mock has not been configured", 500)
218
221 """
222 Helper Class to interact with the ApiMockServer
223 Provides methods to add, remove mock objects as well as incoming requests
224 """
225
226 server = mock_server
227
229 Process.__init__(self)
230 self.port = port
231 self.host = "0.0.0.0"
232 self.localhost = self.host_ip()
233 self.base_url = "http://{host}:{port}".format(host=self.localhost, port=self.port)
234 self.responses_url = self.base_url + "/mock/responses"
235 self.requests_url = self.base_url + "/mock/requests"
236 self.shutdown_url = self.base_url + "/mock/shutdown"
237
239 """
240 start the mock server
241 """
242 self.server.run(host=self.host, port=self.port)
243
245 """
246 Shutdown the server and terminate process
247 :return:
248 """
249 self.wait_for_server_to_start()
250 requests.get(self.shutdown_url)
251 self.terminate()
252
253 - def add_response(self, url_filter=None, status_code=200, headers=None, body="", encoding="text", data=None):
254 """
255 Add the response to the mock server
256
257 :param url_filter: Regular expression to match the url path
258 :param status_code: Expected status code
259 :param headers: Headers as a dictionary
260 :param body: response body as plain text
261 :param payload: a python dictionary
262 :return: HTTP status code
263 """
264 self.wait_for_server_to_start()
265
266 if not data:
267 data = { "url_filter": url_filter,
268 "status_code": status_code,
269 "headers": headers if headers else {},
270 "body": body,
271 "encoding": encoding}
272
273 return requests.post(self.responses_url, data=json.dumps(data))
274
276 """
277 Adds a binary response to the mock server
278
279 :param url_filter: Regular expression to match the url path
280 :param status_code: Expected status code
281 :param headers: Headers as a dictionary
282 :param body: response body as plain text
283 :param payload: a python dictionary
284 :return: HTTP status code
285 """
286 return self.add_response(url_filter, status_code=status_code, headers=headers, encoding="base64",
287 body=base64.b64encode(body))
288
289 - def add_text_response(self, url_filter=None, status_code=200, headers=None, body=""):
290 """
291 Adds a binary response to the mock server
292
293 :param url_filter: Regular expression to match the url path
294 :param status_code: Expected status code
295 :param headers: Headers as a dictionary
296 :param body: response body as plain text
297 :param payload: a python dictionary
298 :return: HTTP status code
299 """
300 return self.add_response(url_filter, status_code=status_code, headers=headers, body=body)
301
303 """
304 Returns all the responses stored on the MockServer
305 :return:
306 """
307 self.wait_for_server_to_start()
308 response = requests.get(self.responses_url)
309 return response if response else None
310
312 """
313 Delete all the responses stored on the MockServer
314 :return:
315 """
316 self.wait_for_server_to_start()
317 return requests.delete(self.responses_url)
318
320 """
321 Returns all the requests stored on the MockServer
322 :return:
323 """
324 self.wait_for_server_to_start()
325 response = requests.get(self.requests_url)
326 if response:
327 return response
328 return None
329
331 """
332 Delete all the requests stored on the MockServer
333 :return:
334 """
335 self.wait_for_server_to_start()
336 return requests.delete(self.requests_url)
337
339 """
340 Dirty hack to return the localhost IP,
341 works only when you have an internet connection
342 :return:
343 """
344 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]
345
347 """
348 check if the server is running and port is open
349 """
350 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
351 result = sock.connect_ex((self.host_ip(),self.port))
352 return result == 0
353
355 """
356 Latency to avoid the access when the server is not started yet
357 """
358 while not self._is_server_running():
359 time.sleep(0.1)
360
363
364 if __name__ == '__main__':
365
366
367 server = ApiMockServer(port=10000)
368 server.start()
369