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
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 while _req_urls:
140 _req_urls.pop()
141
142 while _requests:
143 _requests.pop()
144
145 return jsonify(status="All requests were deleted")
146
147
148 @mock_server.route("/mock/requests", methods=['GET'])
149 -def get_requests():
150 """
151 Get all requests
152 """
153 if request.args.get('version') == '2':
154 return jsonify(requests=_requests)
155 else:
156 return jsonify(requests=_req_urls)
157
158
159 @mock_server.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE'])
160 @mock_server.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
161 -def catch_all(path):
162 """
163 This method will catch all requests for which there are no explicit routes.
164 Here is where we build responses based on the rules that have been configured
165 It will go though the list of rules and apply one by one until a match is found.
166 If there is no match, it will return a 500 response
167 """
168 _req_urls.append(request.url)
169
170 headers = {}
171 for h in str(request.headers).split("\r\n"):
172 if h:
173 parts = h.split(": ")
174 h_name = parts[0]
175 h_value = ":".join(parts[1:])
176 headers[h_name] = h_value
177
178
179 req = {
180 'Request': {
181 "Scheme": request.scheme,
182 "Host": NetworkHelper.get_hostname_from_url(request.url),
183 "Port": NetworkHelper.get_port_from_url(request.url),
184 "Path": request.path,
185 "Args": dict(request.args),
186 "Method": request.method,
187 "Headers": headers,
188 "Body": request.data
189 }
190 }
191
192 _requests.append(req)
193
194 for rule in _rules.values():
195 regex = rule["url_filter"]
196 if regex.search("/" + path):
197 r = make_response()
198 r .headers = rule["headers"]
199 r .data = rule["body"]
200 r .status_code = rule["status_code"]
201 return r
202
203
204 return generate_error_response("Mock has not been configured", 500)
205
208 """
209 Helper Class to interact with the ApiMockServer
210 Provides methods to add, remove mock objects as well as incoming requests
211 """
212
213 server = mock_server
214
216 Process.__init__(self)
217 self.port = port
218 self.host = "0.0.0.0"
219 self.localhost = self.host_ip()
220 self.base_url = "http://{host}:{port}".format(host=self.localhost, port=self.port)
221 self.responses_url = self.base_url + "/mock/responses"
222 self.requests_url = self.base_url + "/mock/requests"
223 self.shutdown_url = self.base_url + "/mock/shutdown"
224
226 """
227 start the mock server
228 """
229 self.server.run(host=self.host, port=self.port)
230
232 """
233 Shutdown the server and terminate process
234 :return:
235 """
236 self.wait_for_server_to_start()
237 requests.get(self.shutdown_url)
238 self.terminate()
239
240 - def add_response(self, url_filter=None, status_code=200, headers=None, body="", data=None):
241 """
242 Add the response to the mock server
243 if the payload is provided, other parameters are ignored
244
245 :param url_filter: Regular expression to match the url path
246 :param status_code: Expected status code
247 :param headers: Headers as a dictionary
248 :param body: response body as plain text
249 :param payload: a python dictionary
250 :return: HTTP status code
251 """
252
253 self.wait_for_server_to_start()
254 if data is None:
255 if not headers:
256 headers = {}
257 data = {"url_filter": url_filter,
258 "status_code": status_code,
259 "headers": headers,
260 "body": body}
261 return requests.post(self.responses_url, data=json.dumps(data))
262
264 """
265 Returns all the responses stored on the MockServer
266 :return:
267 """
268 self.wait_for_server_to_start()
269 response = requests.get(self.responses_url)
270 return response if response else None
271
273 """
274 Delete all the responses stored on the MockServer
275 :return:
276 """
277 self.wait_for_server_to_start()
278 return requests.delete(self.responses_url)
279
281 """
282 Returns all the requests stored on the MockServer
283 :return:
284 """
285 self.wait_for_server_to_start()
286 response = requests.get(self.requests_url)
287 if response:
288 return response
289 return None
290
292 """
293 Delete all the requests stored on the MockServer
294 :return:
295 """
296 self.wait_for_server_to_start()
297 return requests.delete(self.requests_url)
298
300 """
301 Dirty hack to return the localhost IP,
302 works only when you have an internet connection
303 :return:
304 """
305 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]
306
308 """
309 check if the server is running and port is open
310 """
311 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
312 result = sock.connect_ex((self.host_ip(),self.port))
313 return result == 0
314
316 """
317 Latency to avoid the access when the server is not started yet
318 """
319 while not self._is_server_running():
320 time.sleep(0.1)
321
324
325 if __name__ == '__main__':
326
327
328 server = ApiMockServer(port=10000)
329 server.start()
330