sefrone-api-e2e 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
import requests
|
|
3
|
+
import re
|
|
4
|
+
import os
|
|
5
|
+
import datetime
|
|
6
|
+
|
|
7
|
+
class ApiE2ETestsManager:
|
|
8
|
+
@staticmethod
|
|
9
|
+
def run_all_tests(folder_path, is_verbose=False):
|
|
10
|
+
print("\nRunning API E2E tests...")
|
|
11
|
+
for filename in os.listdir(folder_path):
|
|
12
|
+
if filename.endswith(".yaml") or filename.endswith(".yml"):
|
|
13
|
+
file_path = os.path.join(folder_path, filename)
|
|
14
|
+
print(f"\n--- Running scenario: {filename} ---")
|
|
15
|
+
ApiE2ETestsManager.run_yaml_test(file_path, is_verbose)
|
|
16
|
+
print("\nAPI E2E tests completed.")
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def load_yaml(path):
|
|
20
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
21
|
+
return yaml.safe_load(f)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def check_type(value, expected_type):
|
|
25
|
+
"""Check if value matches expected type keyword."""
|
|
26
|
+
type_map = {
|
|
27
|
+
"string": str,
|
|
28
|
+
"number": (int, float),
|
|
29
|
+
"boolean": bool,
|
|
30
|
+
"datetime": str # basic check; could extend with parsing
|
|
31
|
+
}
|
|
32
|
+
if expected_type not in type_map:
|
|
33
|
+
raise ValueError(f"Unknown type in YAML: {expected_type}")
|
|
34
|
+
if expected_type == "datetime":
|
|
35
|
+
try:
|
|
36
|
+
datetime.datetime.fromisoformat(value)
|
|
37
|
+
return True
|
|
38
|
+
except Exception:
|
|
39
|
+
try:
|
|
40
|
+
if re.match(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,6}$", value):
|
|
41
|
+
datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f")
|
|
42
|
+
return True
|
|
43
|
+
except Exception:
|
|
44
|
+
return False
|
|
45
|
+
return isinstance(value, type_map[expected_type])
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def validate_structure(data, expected):
|
|
49
|
+
"""Recursively validate structure & type expectations."""
|
|
50
|
+
if isinstance(expected, dict):
|
|
51
|
+
if not isinstance(data, dict):
|
|
52
|
+
raise AssertionError(f"Expected dict but got {type(data)}")
|
|
53
|
+
for k, v in expected.items():
|
|
54
|
+
if k not in data:
|
|
55
|
+
raise AssertionError(f"Missing key: {k} (Data keys: {list(data.keys())})")
|
|
56
|
+
ApiE2ETestsManager.validate_structure(data[k], v)
|
|
57
|
+
elif isinstance(expected, list):
|
|
58
|
+
if not isinstance(data, list):
|
|
59
|
+
raise AssertionError(f"Expected list but got {type(data)}")
|
|
60
|
+
if len(expected) > 0:
|
|
61
|
+
for item in data:
|
|
62
|
+
ApiE2ETestsManager.validate_structure(item, expected[0])
|
|
63
|
+
elif isinstance(expected, str): # type keyword
|
|
64
|
+
if not ApiE2ETestsManager.check_type(data, expected):
|
|
65
|
+
raise AssertionError(f"Expected {expected}, got {data} ({type(data)})")
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError(f"Invalid expected type: {expected}")
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def render_template(template_str, context, add_quotes=False):
|
|
71
|
+
def resolve_path(expr, ctx):
|
|
72
|
+
parts = expr.split(".")
|
|
73
|
+
val = ctx
|
|
74
|
+
for p in parts:
|
|
75
|
+
if isinstance(val, dict) and p in val:
|
|
76
|
+
val = val[p]
|
|
77
|
+
else:
|
|
78
|
+
raise KeyError(f"Cannot resolve '{expr}' in context: {p} not found")
|
|
79
|
+
return val
|
|
80
|
+
|
|
81
|
+
def replacer(match):
|
|
82
|
+
expr = match.group(1).strip()
|
|
83
|
+
for root_key in context:
|
|
84
|
+
if expr.startswith(root_key + "."):
|
|
85
|
+
val = resolve_path(expr, context)
|
|
86
|
+
if isinstance(val, str):
|
|
87
|
+
return repr(val) if add_quotes else str(val) # adds quotes safely
|
|
88
|
+
elif val is True:
|
|
89
|
+
return "True"
|
|
90
|
+
elif val is False:
|
|
91
|
+
return "False"
|
|
92
|
+
elif val is None:
|
|
93
|
+
return "None"
|
|
94
|
+
else:
|
|
95
|
+
return str(val)
|
|
96
|
+
raise KeyError(f"Unknown variable: {expr}")
|
|
97
|
+
|
|
98
|
+
return re.sub(r"\{\{\s*([^}]+?)\s*\}\}", replacer, template_str)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def substitute_stored(endpoint, stored):
|
|
102
|
+
"""Substitute {$stored.key} placeholders."""
|
|
103
|
+
def repl(match):
|
|
104
|
+
key = match.group(1)
|
|
105
|
+
return str(stored.get(key, f"<MISSING:{key}>"))
|
|
106
|
+
return re.sub(r"\{\$stored\.([a-zA-Z0-9_]+)\}", repl, endpoint)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def run_yaml_test(path, is_verbose=False):
|
|
110
|
+
spec = ApiE2ETestsManager.load_yaml(path)
|
|
111
|
+
base_url = spec.get("base_url", "")
|
|
112
|
+
steps = spec.get("steps", [])
|
|
113
|
+
stored = {}
|
|
114
|
+
|
|
115
|
+
print(f"Running test: {spec['name']}")
|
|
116
|
+
print("=" * 60)
|
|
117
|
+
|
|
118
|
+
for step in steps:
|
|
119
|
+
print(f"Step: {step['name']}")
|
|
120
|
+
url = base_url + ApiE2ETestsManager.substitute_stored(step['endpoint'], stored)
|
|
121
|
+
method = step['method'].upper()
|
|
122
|
+
body = step.get("body")
|
|
123
|
+
expect = step.get("expect", {})
|
|
124
|
+
|
|
125
|
+
# Make HTTP request
|
|
126
|
+
resp = requests.request(method, url, json=body)
|
|
127
|
+
print(f" → {method} {url} -> {resp.status_code}")
|
|
128
|
+
|
|
129
|
+
# Validate status code
|
|
130
|
+
expected_status = expect.get("status")
|
|
131
|
+
if expected_status and resp.status_code != expected_status:
|
|
132
|
+
if is_verbose:
|
|
133
|
+
print(f"Response body: {resp.text}")
|
|
134
|
+
raise AssertionError(f"Expected {expected_status}, got {resp.status_code}")
|
|
135
|
+
|
|
136
|
+
# Parse response JSON
|
|
137
|
+
try:
|
|
138
|
+
resp_json = resp.json()
|
|
139
|
+
except Exception:
|
|
140
|
+
raise AssertionError("Response is not valid JSON")
|
|
141
|
+
|
|
142
|
+
# Validate body structure
|
|
143
|
+
if "body" in expect:
|
|
144
|
+
if is_verbose:
|
|
145
|
+
print(f"Response body: {resp_json}")
|
|
146
|
+
ApiE2ETestsManager.validate_structure(resp_json, expect["body"])
|
|
147
|
+
|
|
148
|
+
# Save variables
|
|
149
|
+
if "save" in step:
|
|
150
|
+
for key, path_expr in step["save"].items():
|
|
151
|
+
rendered = ApiE2ETestsManager.render_template(path_expr, {"body": resp_json, "stored": stored})
|
|
152
|
+
stored[key] = rendered
|
|
153
|
+
print(f" Saved: {key} = {rendered}")
|
|
154
|
+
|
|
155
|
+
# Assertions
|
|
156
|
+
if "assertions" in step:
|
|
157
|
+
for expr in step["assertions"]:
|
|
158
|
+
rendered = ApiE2ETestsManager.render_template(expr, {"body": resp_json, "stored": stored}, add_quotes=True)
|
|
159
|
+
try:
|
|
160
|
+
if not eval(rendered):
|
|
161
|
+
raise AssertionError(f"Assertion failed: {expr}, rendered as '{rendered}'")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise AssertionError(f"Assertion error in '{expr}', rendered as '{rendered}': {e}")
|
|
164
|
+
|
|
165
|
+
print(" ✅ Step passed.\n")
|
|
166
|
+
|
|
167
|
+
print("=" * 60)
|
|
168
|
+
print("🎉 All steps passed successfully!")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sefrone-api-e2e
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python package to provide e2e testing helpers for sefrone API projects
|
|
5
|
+
Home-page: https://bitbucket.org/sefrone/sefrone_pypi
|
|
6
|
+
Author: Sefrone
|
|
7
|
+
Author-email: contact@sefrone.com
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: Other/Proprietary License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
16
|
+
|
|
17
|
+
This is not a usable Python package, but the name is reserved by SARL Sefrone.
|
|
18
|
+
|
|
19
|
+
You can find other packages published by Sefrone at pypi.org/user/gnasreddine
|
|
20
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
sefrone_api_e2e/__init__.py,sha256=NNUHigTCBkjdwCfQXVEA7urxF1-A8HF1_IuWJ8LuO-k,91
|
|
2
|
+
sefrone_api_e2e/api_e2e_manager.py,sha256=3LDA-kIZLyhVTvxZFE4huJdVTKQfsZmIdiTpeGwoVPs,6984
|
|
3
|
+
sefrone_api_e2e-1.0.0.dist-info/METADATA,sha256=k4CtrauVJStV0vCz3tb5hkNM552nz5KyMro1wG7AK4U,696
|
|
4
|
+
sefrone_api_e2e-1.0.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
5
|
+
sefrone_api_e2e-1.0.0.dist-info/top_level.txt,sha256=19WO3CsUWUiGtZBotT587N-tkxxjctKOPDEHWpHpS8M,16
|
|
6
|
+
sefrone_api_e2e-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sefrone_api_e2e
|