Skip to content

Commit 0d4c37c

Browse files
aws-okta-flask-migration (#482)
Summary: - Migrated `aws` mocks from `mockserver` to `flask`. - Migrated `okta` mocks from `mockserver` to `flask`.
1 parent 86ff3e9 commit 0d4c37c

File tree

91 files changed

+4113
-3333
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+4113
-3333
lines changed

test/assets/expected/aws/cloud_control/select-list-operations-desc.txt

Lines changed: 236 additions & 227 deletions
Large diffs are not rendered by default.

test/mockserver/expectations/static-aws-expectations.json

Lines changed: 0 additions & 2549 deletions
This file was deleted.

test/mockserver/expectations/static-okta-expectations.json

Lines changed: 0 additions & 441 deletions
This file was deleted.

test/python/flask/aws/app.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
from flask import Flask, request, Request, render_template, make_response, jsonify
2+
import os
3+
import logging
4+
import re
5+
import json
6+
import base64
7+
8+
app = Flask(__name__)
9+
app.template_folder = os.path.join(os.path.dirname(__file__), "templates")
10+
11+
# Configure logging
12+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
13+
logger = logging.getLogger(__name__)
14+
15+
@app.before_request
16+
def log_request_info():
17+
logger.info(f"Request: {request.method} {request.path}\n - Query: {request.args}\n - Headers: {request.headers}\n - Body: {request.get_data()}")
18+
19+
class GetMatcherConfig:
20+
21+
_ROOT_PATH_CFG: dict = {}
22+
23+
@staticmethod
24+
def load_config_from_file(file_path):
25+
try:
26+
with open(file_path, 'r') as f:
27+
GetMatcherConfig._ROOT_PATH_CFG = json.load(f)
28+
29+
# Decode base64 responses in templates
30+
for route_name, cfg in GetMatcherConfig._ROOT_PATH_CFG.items():
31+
if "base64_template" in cfg:
32+
try:
33+
decoded_content = base64.b64decode(cfg["base64_template"]).decode("utf-8")
34+
template_path = os.path.join(app.template_folder, cfg["template"])
35+
with open(template_path, "w") as tpl_file:
36+
tpl_file.write(decoded_content)
37+
logger.info(f"Decoded base64 template for route: {route_name}")
38+
except Exception as e:
39+
logger.error(f"Failed to decode base64 template for route: {route_name}: {e}")
40+
41+
logger.info("Configuration loaded and templates processed successfully.")
42+
except Exception as e:
43+
logger.error(f"Failed to load configuration: {e}")
44+
45+
def __init__(self):
46+
config_path = os.path.join(os.path.dirname(__file__), "root_path_cfg.json")
47+
self.load_config_from_file(config_path)
48+
49+
@staticmethod
50+
def get_config(path_name):
51+
return GetMatcherConfig._ROOT_PATH_CFG.get(path_name, None)
52+
53+
def _match_json_strict(self, lhs: dict, rhs: dict) -> bool:
54+
matches = json.dumps(
55+
lhs, sort_keys=True) == json.dumps(
56+
rhs, sort_keys=True)
57+
return matches
58+
59+
def _match_json_by_key(self, lhs: dict, rhs: dict) -> bool:
60+
for key, value in rhs.items():
61+
if key not in lhs:
62+
return False
63+
if isinstance(value, dict):
64+
if not self._match_json_by_key(lhs[key], value):
65+
return False
66+
elif isinstance(value, list):
67+
for item in value:
68+
if not self._match_string(lhs[key], item):
69+
return False
70+
elif isinstance(value, str):
71+
if not self._match_string(lhs[key], value):
72+
return False
73+
else:
74+
if lhs[key] != value:
75+
return False
76+
return True
77+
78+
def _match_json_request_body(self, lhs: dict, rhs: dict, match_type: str) -> bool:
79+
if match_type.lower() == 'strict':
80+
return self._match_json_strict(lhs, rhs)
81+
elif match_type.lower() == 'only_matching_fields':
82+
return self._match_json_by_key(lhs, rhs)
83+
return False
84+
85+
def _match_request_body(self, req: Request, entry: dict) -> bool:
86+
body_conditions = entry.get('body_conditions', {})
87+
88+
if not body_conditions:
89+
return True
90+
91+
logger.warning('evaluating body conditions')
92+
json_body = body_conditions.get('json', {})
93+
if json_body:
94+
request_body = request.get_json(silent=True, force=True)
95+
logger.debug(f'comparing expected body = {json_body}, with request body = {request_body}')
96+
if json_body:
97+
return self._match_json_request_body(request_body, json_body, body_conditions.get('matchType', 'strict'))
98+
form_body = body_conditions.get('parameters', {})
99+
if form_body:
100+
request_body = request.form
101+
logger.debug(f'comparing expected body = {form_body}, with request body = {request_body}')
102+
return self._match_json_by_key(request_body, form_body)
103+
string_body = body_conditions.get('type', '').lower() == 'string'
104+
if string_body:
105+
request_body = request.get_data(as_text=True)
106+
logger.warning(f'comparing expected body = {body_conditions.get("value")}, with request body = {request_body}')
107+
return self._match_string(request_body, body_conditions.get('value'))
108+
return False
109+
110+
def _match_string(self, lhs: str, rhs: str) -> bool:
111+
if lhs == rhs:
112+
return True
113+
if re.match(rhs, lhs):
114+
return True
115+
return False
116+
117+
def _match_request_headers(self, req: Request, entry: dict) -> bool:
118+
for k, v in entry.get('headers', {}).items():
119+
if type(v) == str:
120+
if not self._match_string(req.headers.get(k), v):
121+
return False
122+
elif type(v) == list:
123+
## Could make thi smore complex if needed
124+
match_found = False
125+
for item in v:
126+
if self._match_string(req.headers.get(k), item):
127+
match_found = True
128+
break
129+
if not match_found:
130+
return False
131+
return True
132+
133+
def _is_method_match(self, req: Request, cfg: dict) -> bool:
134+
method = cfg.get("method", '')
135+
if not method:
136+
return True
137+
return req.method.lower() == method.lower()
138+
139+
def _is_path_match(self, req: Request, cfg: dict) -> bool:
140+
path = cfg.get("path", '')
141+
if not path:
142+
return True
143+
return req.path == path
144+
145+
146+
def match_route(self, req: Request) -> dict:
147+
matching_routes = []
148+
149+
for route_name, cfg in self._ROOT_PATH_CFG.items():
150+
logger.debug(f"Evaluating route: {route_name}")
151+
152+
is_method_match: bool = self._is_method_match(req, cfg)
153+
if not is_method_match:
154+
logger.debug(f"Method mismatch for route {route_name}")
155+
continue
156+
157+
is_query_match: bool = self._match_json_by_key(req.args, cfg.get("queryStringParameters", {}))
158+
if not is_query_match:
159+
logger.debug(f"Query mismatch for route {route_name}")
160+
continue
161+
162+
is_path_match: bool = self._is_path_match(req, cfg)
163+
if not is_path_match:
164+
logger.debug(f"Path mismatch for route {route_name}")
165+
continue
166+
167+
is_header_match: bool = self._match_request_headers(req, cfg)
168+
if not is_header_match:
169+
logger.debug(f"Header mismatch for route {route_name}")
170+
continue
171+
172+
is_body_match: bool = self._match_request_body(req, cfg)
173+
if not is_body_match:
174+
logger.warning(f"Body mismatch for route {route_name}")
175+
continue
176+
177+
matching_routes.append((route_name, cfg))
178+
179+
# Prioritize routes with body conditions
180+
matching_routes.sort(key=lambda x: bool(x[1].get("body_conditions")), reverse=True)
181+
182+
if not matching_routes:
183+
data = req.get_data()
184+
logger.warning(f"No matching route found for request: {req} with {data}")
185+
if data == b'{"DesiredState":"{\\"BucketName\\":\\"my-bucket\\",\\"ObjectLockEnabled\\":true,\\"Tags\\":[{\\"Key\\":\\"somekey\\",\\"Value\\":\\"v4\\"}]}","TypeName":"AWS::S3::Bucket"}':
186+
return {
187+
"template": "template_71.json",
188+
"status": 200,
189+
"response_headers": {
190+
"Content-Type": ["application/json"]
191+
}
192+
}
193+
else:
194+
return {
195+
"template": "empty-response.json",
196+
"status": 404
197+
}
198+
199+
if matching_routes:
200+
selected_route, cfg = matching_routes[0]
201+
return cfg
202+
203+
204+
205+
# Load the configuration at startup
206+
config_path = os.path.join(os.path.dirname(__file__), "root_path_cfg.json")
207+
cfg_obj: GetMatcherConfig = GetMatcherConfig()
208+
209+
# Routes generated from mockserver configuration
210+
@app.route('/', methods=['POST', "GET"])
211+
def handle_root_requests():
212+
return generic_handler(request)
213+
214+
@app.route('/2013-04-01/hostedzone/<rrset_id>/rrset/', methods=['POST', 'GET'])
215+
def handle_rrset_requests(rrset_id: str):
216+
return generic_handler(request)
217+
218+
@app.route('/2013-04-01/hostedzone/<rrset_id>/rrset', methods=['GET'])
219+
def handle_rrset_requests_unterminated(rrset_id: str):
220+
return generic_handler(request)
221+
222+
def generic_handler(request: Request):
223+
"""Route POST requests to the correct template based on mockserver rules."""
224+
route_cfg: dict = cfg_obj.match_route(request)
225+
if "template" not in route_cfg:
226+
logger.error(f"Missing template for route: {request}")
227+
return jsonify({'error': f'Missing template for route: {request}'}), 500
228+
logger.info(f"routing to template: {route_cfg['template']}")
229+
response = make_response(render_template(route_cfg["template"], request=request))
230+
response.headers.update(route_cfg.get("response_headers", {}))
231+
response.status_code = route_cfg.get("status", 200)
232+
return response
233+
234+
if __name__ == "__main__":
235+
app.run(debug=True, host="0.0.0.0", port=5000)

0 commit comments

Comments
 (0)