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