Package tlib :: Package base :: Module ApiMockServer
[hide private]
[frames] | no frames]

Source Code for Module tlib.base.ApiMockServer

  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 = [] 
17 18 -def generate_error_response(message, status_code):
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 #Parse data from request 65 if "url_filter" in payload.keys(): 66 key = payload["url_filter"] 67 #Check filter is not empty 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 #Save parsed data 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 #make a copy so we don't modify original 123 rule = rule.copy() 124 125 #Convert regex to str 126 rule["url_filter"] = rule["url_filter"].pattern 127 128 #Add rule to list 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 #Store full request 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 # Default values returned when there wasn't a match in the rules 204 return generate_error_response("Mock has not been configured", 500)
205
206 207 -class ApiMockServer(Process):
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
215 - def __init__(self, port):
216 Process.__init__(self) 217 self.port = port 218 self.host = "0.0.0.0" # host IP to be server by the Flask server 219 self.localhost = self.host_ip() # IP to communicate with server 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
225 - def run(self):
226 """ 227 start the mock server 228 """ 229 self.server.run(host=self.host, port=self.port)
230
231 - def stop(self):
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 # if payload is provided, all other other parameters will be ignored 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
263 - def get_responses(self):
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
272 - def clear_responses(self):
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
280 - def get_requests(self):
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
291 - def clear_requests(self):
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
299 - def host_ip(self):
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
307 - def _is_server_running(self):
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
315 - def wait_for_server_to_start(self):
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
322 - def get_base_url(self):
323 return self.base_url
324 325 if __name__ == '__main__': 326 #to us as standalone application 327 #run 'python ApiMockServer.py' 328 server = ApiMockServer(port=10000) 329 server.start() 330