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  import base64 
 13   
 14  mock_server = Flask(__name__) 
 15  _rules = OrderedDict() 
 16  _req_urls = [] 
 17  _requests = [] 
18 19 -def generate_error_response(message, status_code):
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 #Parse data from request 81 if "url_filter" in payload.keys(): 82 key = payload["url_filter"] 83 #Check filter is not empty 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 #Payload can be plain text or encoded in base64 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 #If encoding is not specified, assume it's plain text 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 #Save parsed data 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 #make a copy so we don't modify original 151 rule = rule.copy() 152 153 #Convert regex to str 154 rule["url_filter"] = rule["url_filter"].pattern 155 156 #Add rule to list 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 #Store full request 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 # Default values returned when there wasn't a match in the rules 232 return generate_error_response("Mock has not been configured", 500)
233
234 235 -class ApiMockServer(Process):
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
243 - def __init__(self, port):
244 Process.__init__(self) 245 self.port = port 246 self.host = "0.0.0.0" # host IP to be server by the Flask server 247 self.localhost = self.host_ip() # IP to communicate with server 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
253 - def run(self):
254 """ 255 start the mock server 256 """ 257 self.server.run(host=self.host, port=self.port)
258
259 - def stop(self):
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
300 - def add_binary_response(self, url_filter=None, status_code=200, headers=None, body=""):
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
343 - def get_responses(self):
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
352 - def clear_responses(self):
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
360 - def get_requests(self):
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
371 - def clear_requests(self):
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
379 - def host_ip(self):
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
387 - def _is_server_running(self):
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
395 - def wait_for_server_to_start(self):
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
402 - def get_base_url(self):
403 return self.base_url
404 405 if __name__ == '__main__': 406 #to us as standalone application 407 #run 'python ApiMockServer.py' 408 server = ApiMockServer(port=10000) 409 server.start() 410