synapse-hub 2.0.1 → 2.1.1
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.
- package/.agents/rules/lessons-learned.md +1 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +8 -5
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +13 -0
- package/README.md +4 -4
- package/package.json +6 -2
- package/skills_manifest.json +260 -260
- package/tools/TOOLS_RUNBOOK.md +54 -3
- package/tools/api_fuzzer.py +468 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/bump_version.py +211 -0
- package/tools/cache_resolver.py +128 -0
- package/tools/cli.py +41 -1
- package/tools/export_postman_collection.py +606 -0
- package/tools/flaky_detector.py +180 -14
- package/tools/generate_jmx_plan.py +486 -0
- package/tools/mcp_excel_server.ps1 +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/micro_mutator.py +56 -0
- package/tools/network_mock_shield.py +141 -0
- package/tools/visual_som_helper.py +121 -9
- package/tools/repo_map.json +0 -12
- package/tools/repo_map.txt +0 -7
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
tools/export_postman_collection.py — Enterprise API to Postman Collection v2.1.0 Exporter
|
|
5
|
+
-----------------------------------------------------------------------------------------
|
|
6
|
+
Converts OpenAPI / Swagger specifications, JSON endpoint schemas, or HTTP Archives (HAR)
|
|
7
|
+
into a production-ready Postman Collection v2.1.0 (with Pre-request Scripts, Tests Scripts,
|
|
8
|
+
Response Time Assertions, and Auto-Extracted Environment Variables).
|
|
9
|
+
|
|
10
|
+
Features:
|
|
11
|
+
1. Ingests OpenAPI 3.0 / Swagger 2.0 JSON, HAR files, or built-in sample schemas.
|
|
12
|
+
2. Generates organized folders by Tags / Resources with parameterized routes.
|
|
13
|
+
3. Pre-request Scripts: Dynamic request timestamps, UUIDs, and automated auth headers.
|
|
14
|
+
4. Test Scripts (pm.test):
|
|
15
|
+
- Expected HTTP Status assertions.
|
|
16
|
+
- Response time SLA latency assertions (< 1000ms).
|
|
17
|
+
- Response Content-Type JSON header validation.
|
|
18
|
+
- Autonomous Token & ID Extractor: Auto-saves `token`, `access_token`, or `id` into
|
|
19
|
+
`pm.environment` / `pm.collectionVariables` for chaining sequential API calls.
|
|
20
|
+
5. Companion Environment Generator: Outputs `postman_environment.json` with base URLs.
|
|
21
|
+
6. 100% UTF-8 No BOM, portable, Newman CLI and CI/CD compatible.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
python tools/export_postman_collection.py --spec swagger.json -o collection.json
|
|
25
|
+
python tools/export_postman_collection.py --har traffic.har -o collection.json
|
|
26
|
+
python tools/export_postman_collection.py --sample -o sample_collection.json --env-output sample_env.json
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
import json
|
|
32
|
+
import uuid
|
|
33
|
+
import argparse
|
|
34
|
+
from typing import Dict, Any, List, Optional
|
|
35
|
+
from urllib.parse import urlparse, parse_qs
|
|
36
|
+
|
|
37
|
+
# Enforce UTF-8 stdout encoding for Windows/macOS/Linux console compatibility
|
|
38
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
39
|
+
try:
|
|
40
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
41
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_uuid() -> str:
|
|
47
|
+
return str(uuid.uuid4())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_pre_request_script() -> List[str]:
|
|
51
|
+
"""Generates standard Postman Pre-request script lines."""
|
|
52
|
+
return [
|
|
53
|
+
"// Auto-generated Pre-request Script by Agent-QA Hub",
|
|
54
|
+
"// 1. Dynamic Timestamp & Request ID generation",
|
|
55
|
+
"pm.variables.set('req_timestamp', new Date().toISOString());",
|
|
56
|
+
"pm.variables.set('req_nonce', Math.random().toString(36).substring(2, 12));",
|
|
57
|
+
"",
|
|
58
|
+
"// 2. Automated Bearer Token Injection if present in environment",
|
|
59
|
+
"var token = pm.environment.get('bearerToken') || pm.collectionVariables.get('bearerToken');",
|
|
60
|
+
"if (token && !pm.request.headers.has('Authorization')) {",
|
|
61
|
+
" pm.request.headers.add({ key: 'Authorization', value: 'Bearer ' + token });",
|
|
62
|
+
"}"
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_test_script(expected_status: int = 200, max_time_ms: int = 1000) -> List[str]:
|
|
67
|
+
"""Generates standard Postman Test script lines."""
|
|
68
|
+
return [
|
|
69
|
+
"// Auto-generated Test Assertions by Agent-QA Hub",
|
|
70
|
+
f"// 1. Assert Status Code is {expected_status}",
|
|
71
|
+
f"pm.test('Status code is {expected_status}', function () {{",
|
|
72
|
+
f" pm.expect(pm.response.code).to.be.oneOf([{expected_status}, 200, 201]);",
|
|
73
|
+
"});",
|
|
74
|
+
"",
|
|
75
|
+
f"// 2. Assert SLA Latency (< {max_time_ms}ms)",
|
|
76
|
+
f"pm.test('Response time is below {max_time_ms}ms SLA', function () {{",
|
|
77
|
+
f" pm.expect(pm.response.responseTime).to.be.below({max_time_ms});",
|
|
78
|
+
"});",
|
|
79
|
+
"",
|
|
80
|
+
"// 3. Assert Content-Type JSON Header",
|
|
81
|
+
"pm.test('Response header Content-Type is JSON', function () {",
|
|
82
|
+
" if (pm.response.code !== 204) {",
|
|
83
|
+
" pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');",
|
|
84
|
+
" }",
|
|
85
|
+
"});",
|
|
86
|
+
"",
|
|
87
|
+
"// 4. Autonomous Token / Entity ID Extraction & Session Chaining",
|
|
88
|
+
"if (pm.response.code >= 200 && pm.response.code < 300) {",
|
|
89
|
+
" try {",
|
|
90
|
+
" var jsonBody = pm.response.json();",
|
|
91
|
+
" // Auto-extract JWT / OAuth tokens",
|
|
92
|
+
" var authToken = jsonBody.token || jsonBody.access_token || (jsonBody.data && (jsonBody.data.token || jsonBody.data.access_token));",
|
|
93
|
+
" if (authToken) {",
|
|
94
|
+
" pm.environment.set('bearerToken', authToken);",
|
|
95
|
+
" pm.collectionVariables.set('bearerToken', authToken);",
|
|
96
|
+
" console.log('[Agent-QA] Captured dynamic bearerToken: ' + authToken.substring(0, 15) + '...');",
|
|
97
|
+
" }",
|
|
98
|
+
" // Auto-extract newly created ID",
|
|
99
|
+
" var entityId = jsonBody.id || jsonBody.code || (jsonBody.data && (jsonBody.data.id || jsonBody.data.code));",
|
|
100
|
+
" if (entityId) {",
|
|
101
|
+
" pm.environment.set('lastCreatedId', String(entityId));",
|
|
102
|
+
" }",
|
|
103
|
+
" } catch (e) {",
|
|
104
|
+
" // Response is non-JSON or empty body",
|
|
105
|
+
" }",
|
|
106
|
+
"}"
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def mock_schema_payload(schema: Dict[str, Any], depth: int = 0) -> Any:
|
|
111
|
+
"""Recursively generates realistic mock JSON payload from OpenAPI schema."""
|
|
112
|
+
if depth > 3 or not schema:
|
|
113
|
+
return {}
|
|
114
|
+
|
|
115
|
+
stype = schema.get("type", "object")
|
|
116
|
+
|
|
117
|
+
if stype == "string":
|
|
118
|
+
fmt = schema.get("format", "")
|
|
119
|
+
if fmt == "email":
|
|
120
|
+
return "{{$randomEmail}}"
|
|
121
|
+
elif fmt == "uuid":
|
|
122
|
+
return "{{$guid}}"
|
|
123
|
+
elif fmt in ("date-time", "date"):
|
|
124
|
+
return "{{$isoTimestamp}}"
|
|
125
|
+
return schema.get("example") or f"AUTO_{{{{$randomAlphaNumeric}}}}"
|
|
126
|
+
|
|
127
|
+
elif stype in ("integer", "number"):
|
|
128
|
+
return schema.get("example") or schema.get("minimum", 100)
|
|
129
|
+
|
|
130
|
+
elif stype == "boolean":
|
|
131
|
+
return schema.get("example", True)
|
|
132
|
+
|
|
133
|
+
elif stype == "array":
|
|
134
|
+
items_schema = schema.get("items", {})
|
|
135
|
+
return [mock_schema_payload(items_schema, depth + 1)]
|
|
136
|
+
|
|
137
|
+
elif stype == "object":
|
|
138
|
+
props = schema.get("properties", {})
|
|
139
|
+
obj = {}
|
|
140
|
+
for p_name, p_schema in props.items():
|
|
141
|
+
obj[p_name] = mock_schema_payload(p_schema, depth + 1)
|
|
142
|
+
return obj
|
|
143
|
+
|
|
144
|
+
return {}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[str] = None) -> Dict[str, Any]:
|
|
148
|
+
"""Parses OpenAPI 2.0 / 3.0 dictionary into a Postman Collection v2.1.0 structure."""
|
|
149
|
+
info_dict = spec_data.get("info", {})
|
|
150
|
+
col_name = info_dict.get("title", "Enterprise API Collection")
|
|
151
|
+
col_desc = info_dict.get("description", "Auto-generated Postman Collection from OpenAPI / Swagger")
|
|
152
|
+
|
|
153
|
+
# Determine Base URL
|
|
154
|
+
base_url = "{{baseUrl}}"
|
|
155
|
+
if base_url_override:
|
|
156
|
+
base_url_default = base_url_override
|
|
157
|
+
elif "servers" in spec_data and spec_data["servers"]:
|
|
158
|
+
base_url_default = spec_data["servers"][0].get("url", "https://api.staging.enterprise.local")
|
|
159
|
+
elif "host" in spec_data:
|
|
160
|
+
schemes = spec_data.get("schemes", ["https"])
|
|
161
|
+
base_url_default = f"{schemes[0]}://{spec_data['host']}{spec_data.get('basePath', '')}"
|
|
162
|
+
else:
|
|
163
|
+
base_url_default = "https://api.staging.enterprise.local"
|
|
164
|
+
|
|
165
|
+
# Postman root structure
|
|
166
|
+
collection = {
|
|
167
|
+
"info": {
|
|
168
|
+
"_postman_id": generate_uuid(),
|
|
169
|
+
"name": col_name,
|
|
170
|
+
"description": col_desc,
|
|
171
|
+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
172
|
+
},
|
|
173
|
+
"variable": [
|
|
174
|
+
{
|
|
175
|
+
"key": "baseUrl",
|
|
176
|
+
"value": base_url_default,
|
|
177
|
+
"type": "string"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"key": "bearerToken",
|
|
181
|
+
"value": "",
|
|
182
|
+
"type": "string"
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
"item": []
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
folders: Dict[str, List[Dict[str, Any]]] = {}
|
|
189
|
+
paths = spec_data.get("paths", {})
|
|
190
|
+
|
|
191
|
+
for path, path_item in paths.items():
|
|
192
|
+
for method in ("get", "post", "put", "patch", "delete", "options", "head"):
|
|
193
|
+
if method not in path_item:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
op = path_item[method]
|
|
197
|
+
summary = op.get("summary") or f"{method.upper()} {path}"
|
|
198
|
+
tags = op.get("tags") or ["Default"]
|
|
199
|
+
tag_name = tags[0]
|
|
200
|
+
|
|
201
|
+
# Request Headers
|
|
202
|
+
headers = [
|
|
203
|
+
{"key": "Content-Type", "value": "application/json", "type": "text"},
|
|
204
|
+
{"key": "Accept", "value": "application/json", "type": "text"}
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
# Query Parameters
|
|
208
|
+
query_params = []
|
|
209
|
+
for param in op.get("parameters", []):
|
|
210
|
+
p_in = param.get("in")
|
|
211
|
+
if p_in == "query":
|
|
212
|
+
query_params.append({
|
|
213
|
+
"key": param.get("name"),
|
|
214
|
+
"value": str(param.get("example") or param.get("default", "")),
|
|
215
|
+
"description": param.get("description", "")
|
|
216
|
+
})
|
|
217
|
+
elif p_in == "header":
|
|
218
|
+
headers.append({
|
|
219
|
+
"key": param.get("name"),
|
|
220
|
+
"value": str(param.get("example", "")),
|
|
221
|
+
"type": "text",
|
|
222
|
+
"description": param.get("description", "")
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
# Request Body
|
|
226
|
+
body_dict = None
|
|
227
|
+
if method in ("post", "put", "patch"):
|
|
228
|
+
# OpenAPI 3.0 requestBody
|
|
229
|
+
req_body = op.get("requestBody", {})
|
|
230
|
+
content = req_body.get("content", {})
|
|
231
|
+
json_content = content.get("application/json", {})
|
|
232
|
+
schema = json_content.get("schema")
|
|
233
|
+
|
|
234
|
+
# Swagger 2.0 body parameter fallback
|
|
235
|
+
if not schema:
|
|
236
|
+
for p in op.get("parameters", []):
|
|
237
|
+
if p.get("in") == "body":
|
|
238
|
+
schema = p.get("schema")
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
if schema:
|
|
242
|
+
payload = mock_schema_payload(schema)
|
|
243
|
+
body_dict = {
|
|
244
|
+
"mode": "raw",
|
|
245
|
+
"raw": json.dumps(payload, indent=2, ensure_ascii=False),
|
|
246
|
+
"options": {"raw": {"language": "json"}}
|
|
247
|
+
}
|
|
248
|
+
else:
|
|
249
|
+
body_dict = {
|
|
250
|
+
"mode": "raw",
|
|
251
|
+
"raw": "{}",
|
|
252
|
+
"options": {"raw": {"language": "json"}}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# URL decomposition
|
|
256
|
+
path_segments = [seg for seg in path.strip("/").split("/") if seg]
|
|
257
|
+
url_obj = {
|
|
258
|
+
"raw": f"{{{{baseUrl}}}}{path}",
|
|
259
|
+
"host": ["{{baseUrl}}"],
|
|
260
|
+
"path": path_segments
|
|
261
|
+
}
|
|
262
|
+
if query_params:
|
|
263
|
+
url_obj["query"] = query_params
|
|
264
|
+
|
|
265
|
+
# Status code for assertion
|
|
266
|
+
responses = op.get("responses", {})
|
|
267
|
+
expected_status = 200
|
|
268
|
+
for code_str in responses.keys():
|
|
269
|
+
if code_str.isdigit():
|
|
270
|
+
expected_status = int(code_str)
|
|
271
|
+
break
|
|
272
|
+
|
|
273
|
+
postman_request = {
|
|
274
|
+
"name": summary,
|
|
275
|
+
"event": [
|
|
276
|
+
{
|
|
277
|
+
"listen": "prerequest",
|
|
278
|
+
"script": {
|
|
279
|
+
"type": "text/javascript",
|
|
280
|
+
"exec": build_pre_request_script()
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"listen": "test",
|
|
285
|
+
"script": {
|
|
286
|
+
"type": "text/javascript",
|
|
287
|
+
"exec": build_test_script(expected_status=expected_status)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
"request": {
|
|
292
|
+
"method": method.upper(),
|
|
293
|
+
"header": headers,
|
|
294
|
+
"url": url_obj,
|
|
295
|
+
"description": op.get("description", "")
|
|
296
|
+
},
|
|
297
|
+
"response": []
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if body_dict:
|
|
301
|
+
postman_request["request"]["body"] = body_dict
|
|
302
|
+
|
|
303
|
+
if tag_name not in folders:
|
|
304
|
+
folders[tag_name] = []
|
|
305
|
+
folders[tag_name].append(postman_request)
|
|
306
|
+
|
|
307
|
+
# Attach folders into collection
|
|
308
|
+
for folder_name, items in folders.items():
|
|
309
|
+
collection["item"].append({
|
|
310
|
+
"name": folder_name,
|
|
311
|
+
"item": items
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
return collection
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def parse_har_file(har_data: Dict[str, Any], base_url_override: Optional[str] = None) -> Dict[str, Any]:
|
|
318
|
+
"""Parses HTTP Archive (.har) into a Postman Collection v2.1.0."""
|
|
319
|
+
entries = har_data.get("log", {}).get("entries", [])
|
|
320
|
+
collection = {
|
|
321
|
+
"info": {
|
|
322
|
+
"_postman_id": generate_uuid(),
|
|
323
|
+
"name": "HAR Session API Replay",
|
|
324
|
+
"description": "Replay collection generated from production HAR network telemetry",
|
|
325
|
+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
326
|
+
},
|
|
327
|
+
"variable": [
|
|
328
|
+
{"key": "baseUrl", "value": base_url_override or "https://api.staging.enterprise.local", "type": "string"},
|
|
329
|
+
{"key": "bearerToken", "value": "", "type": "string"}
|
|
330
|
+
],
|
|
331
|
+
"item": []
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
requests_by_host: Dict[str, List[Dict[str, Any]]] = {}
|
|
335
|
+
|
|
336
|
+
for entry in entries:
|
|
337
|
+
req = entry.get("request", {})
|
|
338
|
+
resp = entry.get("response", {})
|
|
339
|
+
url_str = req.get("url", "")
|
|
340
|
+
method = req.get("method", "GET").upper()
|
|
341
|
+
|
|
342
|
+
parsed_url = urlparse(url_str)
|
|
343
|
+
# Filter static files
|
|
344
|
+
if any(parsed_url.path.lower().endswith(ext) for ext in (".png", ".jpg", ".css", ".js", ".woff2", ".ico")):
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
host = parsed_url.netloc or "DefaultHost"
|
|
348
|
+
path = parsed_url.path
|
|
349
|
+
query = parse_qs(parsed_url.query)
|
|
350
|
+
|
|
351
|
+
query_list = [{"key": k, "value": v[0] if v else ""} for k, v in query.items()]
|
|
352
|
+
|
|
353
|
+
headers = []
|
|
354
|
+
for h in req.get("headers", []):
|
|
355
|
+
name = h.get("name", "")
|
|
356
|
+
if name.lower() not in ("cookie", "host", "content-length"):
|
|
357
|
+
headers.append({"key": name, "value": h.get("value", ""), "type": "text"})
|
|
358
|
+
|
|
359
|
+
body_dict = None
|
|
360
|
+
post_data = req.get("postData")
|
|
361
|
+
if post_data and post_data.get("text"):
|
|
362
|
+
body_dict = {
|
|
363
|
+
"mode": "raw",
|
|
364
|
+
"raw": post_data.get("text"),
|
|
365
|
+
"options": {"raw": {"language": "json"}}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
status_code = resp.get("status", 200)
|
|
369
|
+
|
|
370
|
+
postman_item = {
|
|
371
|
+
"name": f"{method} {path}",
|
|
372
|
+
"event": [
|
|
373
|
+
{
|
|
374
|
+
"listen": "prerequest",
|
|
375
|
+
"script": {"type": "text/javascript", "exec": build_pre_request_script()}
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"listen": "test",
|
|
379
|
+
"script": {"type": "text/javascript", "exec": build_test_script(expected_status=status_code)}
|
|
380
|
+
}
|
|
381
|
+
],
|
|
382
|
+
"request": {
|
|
383
|
+
"method": method,
|
|
384
|
+
"header": headers,
|
|
385
|
+
"url": {
|
|
386
|
+
"raw": url_str,
|
|
387
|
+
"host": [parsed_url.scheme + "://" + parsed_url.netloc],
|
|
388
|
+
"path": [p for p in path.strip("/").split("/") if p],
|
|
389
|
+
"query": query_list
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
"response": []
|
|
393
|
+
}
|
|
394
|
+
if body_dict:
|
|
395
|
+
postman_item["request"]["body"] = body_dict
|
|
396
|
+
|
|
397
|
+
if host not in requests_by_host:
|
|
398
|
+
requests_by_host[host] = []
|
|
399
|
+
requests_by_host[host].append(postman_item)
|
|
400
|
+
|
|
401
|
+
for host_name, items in requests_by_host.items():
|
|
402
|
+
collection["item"].append({"name": host_name, "item": items})
|
|
403
|
+
|
|
404
|
+
return collection
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def generate_postman_environment(name: str = "Enterprise-Staging-Env", base_url: str = "https://api.staging.enterprise.local") -> Dict[str, Any]:
|
|
408
|
+
"""Generates companion Postman environment JSON for Newman/CLI integration."""
|
|
409
|
+
return {
|
|
410
|
+
"id": generate_uuid(),
|
|
411
|
+
"name": name,
|
|
412
|
+
"values": [
|
|
413
|
+
{"key": "baseUrl", "value": base_url, "type": "default", "enabled": True},
|
|
414
|
+
{"key": "bearerToken", "value": "", "type": "secret", "enabled": True},
|
|
415
|
+
{"key": "lastCreatedId", "value": "", "type": "default", "enabled": True}
|
|
416
|
+
],
|
|
417
|
+
"_postman_variable_scope": "environment"
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def get_sample_spec() -> Dict[str, Any]:
|
|
422
|
+
"""Provides standard built-in OpenAPI 3.0 spec for testing & demonstration."""
|
|
423
|
+
return {
|
|
424
|
+
"openapi": "3.0.0",
|
|
425
|
+
"info": {
|
|
426
|
+
"title": "Enterprise E-Commerce & Auth Service",
|
|
427
|
+
"version": "1.0.0",
|
|
428
|
+
"description": "Sample API spec for Postman & JMeter automated generation"
|
|
429
|
+
},
|
|
430
|
+
"servers": [{"url": "https://api.staging.enterprise.local"}],
|
|
431
|
+
"paths": {
|
|
432
|
+
"/api/v1/auth/login": {
|
|
433
|
+
"post": {
|
|
434
|
+
"tags": ["Authentication"],
|
|
435
|
+
"summary": "Authenticate user and issue JWT token",
|
|
436
|
+
"requestBody": {
|
|
437
|
+
"content": {
|
|
438
|
+
"application/json": {
|
|
439
|
+
"schema": {
|
|
440
|
+
"type": "object",
|
|
441
|
+
"required": ["username", "password"],
|
|
442
|
+
"properties": {
|
|
443
|
+
"username": {"type": "string", "example": "admin_qa"},
|
|
444
|
+
"password": {"type": "string", "example": "SecretPass#2026"}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
"responses": {
|
|
451
|
+
"200": {
|
|
452
|
+
"description": "JWT Token generated successfully",
|
|
453
|
+
"content": {
|
|
454
|
+
"application/json": {
|
|
455
|
+
"schema": {
|
|
456
|
+
"type": "object",
|
|
457
|
+
"properties": {
|
|
458
|
+
"token": {"type": "string"},
|
|
459
|
+
"expires_in": {"type": "integer"}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
"/api/v1/categories": {
|
|
469
|
+
"get": {
|
|
470
|
+
"tags": ["Category Management"],
|
|
471
|
+
"summary": "List categories with pagination",
|
|
472
|
+
"parameters": [
|
|
473
|
+
{"name": "page", "in": "query", "example": 1},
|
|
474
|
+
{"name": "limit", "in": "query", "example": 20}
|
|
475
|
+
],
|
|
476
|
+
"responses": {"200": {"description": "List of categories"}}
|
|
477
|
+
},
|
|
478
|
+
"post": {
|
|
479
|
+
"tags": ["Category Management"],
|
|
480
|
+
"summary": "Create a new category",
|
|
481
|
+
"requestBody": {
|
|
482
|
+
"content": {
|
|
483
|
+
"application/json": {
|
|
484
|
+
"schema": {
|
|
485
|
+
"type": "object",
|
|
486
|
+
"required": ["code", "name"],
|
|
487
|
+
"properties": {
|
|
488
|
+
"code": {"type": "string", "minLength": 3, "maxLength": 20},
|
|
489
|
+
"name": {"type": "string", "minLength": 1, "maxLength": 100},
|
|
490
|
+
"priority": {"type": "integer", "minimum": 1, "maximum": 100}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
"responses": {"201": {"description": "Category created successfully"}}
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
"/api/v1/categories/{id}": {
|
|
500
|
+
"delete": {
|
|
501
|
+
"tags": ["Category Management"],
|
|
502
|
+
"summary": "Delete category by ID (Idempotent cleanup)",
|
|
503
|
+
"parameters": [
|
|
504
|
+
{"name": "id", "in": "path", "required": True, "example": "{{lastCreatedId}}"}
|
|
505
|
+
],
|
|
506
|
+
"responses": {"204": {"description": "Deleted"}}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def main():
|
|
514
|
+
parser = argparse.ArgumentParser(
|
|
515
|
+
description="Export OpenAPI or HAR to Postman Collection v2.1.0 with auto-asserts & token chaining."
|
|
516
|
+
)
|
|
517
|
+
parser.add_argument("--spec", "-s", help="Path to OpenAPI / Swagger JSON specification file.")
|
|
518
|
+
parser.add_argument("--har", help="Path to HTTP Archive (.har) recording file.")
|
|
519
|
+
parser.add_argument("--base-url", "-b", help="Override base URL for {{baseUrl}} parameter.")
|
|
520
|
+
parser.add_argument("--output", "-o", help="Target output Postman Collection file path (.json).")
|
|
521
|
+
parser.add_argument("--env-output", help="Optional companion Postman Environment output file (.json).")
|
|
522
|
+
parser.add_argument("--sample", action="store_true", help="Generate sample collection using built-in enterprise schema.")
|
|
523
|
+
parser.add_argument("--portman", action="store_true", help="Delegate conversion to @apideck/portman via npx (contract-driven assertions).")
|
|
524
|
+
parser.add_argument("--newman", action="store_true", help="Automatically trigger headless Newman execution on the generated collection.")
|
|
525
|
+
|
|
526
|
+
args = parser.parse_args()
|
|
527
|
+
|
|
528
|
+
# If --portman is requested, run via npx @apideck/portman
|
|
529
|
+
if args.portman:
|
|
530
|
+
import subprocess
|
|
531
|
+
import tempfile
|
|
532
|
+
import shutil
|
|
533
|
+
|
|
534
|
+
target_output = args.output or "postman_collection.json"
|
|
535
|
+
spec_path = args.spec
|
|
536
|
+
|
|
537
|
+
# Handle sample spec if no explicit spec file provided
|
|
538
|
+
temp_spec_file = None
|
|
539
|
+
if not spec_path or args.sample:
|
|
540
|
+
fd, temp_spec_file = tempfile.mkstemp(suffix=".json", prefix="sample_spec_")
|
|
541
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
542
|
+
json.dump(get_sample_spec(), f)
|
|
543
|
+
spec_path = temp_spec_file
|
|
544
|
+
|
|
545
|
+
portman_cmd = ["npx", "--yes", "@apideck/portman", "-l", spec_path, "-o", target_output]
|
|
546
|
+
if args.base_url:
|
|
547
|
+
portman_cmd.extend(["-b", args.base_url])
|
|
548
|
+
if args.newman:
|
|
549
|
+
portman_cmd.extend(["-n"])
|
|
550
|
+
|
|
551
|
+
print(f"⚡ [PORTMAN BRIDGE] Executing: {' '.join(portman_cmd)}")
|
|
552
|
+
try:
|
|
553
|
+
res = subprocess.run(portman_cmd, check=True)
|
|
554
|
+
print(f"✅ [PORTMAN SUCCESS] Collection generated via @apideck/portman: {target_output}")
|
|
555
|
+
if temp_spec_file and os.path.exists(temp_spec_file):
|
|
556
|
+
os.remove(temp_spec_file)
|
|
557
|
+
return
|
|
558
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
559
|
+
print(f"⚠️ [PORTMAN FALLBACK] Could not run npx portman ({e}). Falling back to internal Pure-Python engine.")
|
|
560
|
+
if temp_spec_file and os.path.exists(temp_spec_file):
|
|
561
|
+
os.remove(temp_spec_file)
|
|
562
|
+
|
|
563
|
+
if args.sample or (not args.spec and not args.har):
|
|
564
|
+
spec_data = get_sample_spec()
|
|
565
|
+
collection = parse_openapi_spec(spec_data, base_url_override=args.base_url)
|
|
566
|
+
elif args.har:
|
|
567
|
+
if not os.path.exists(args.har):
|
|
568
|
+
print(f"❌ Error: HAR file not found: {args.har}", file=sys.stderr)
|
|
569
|
+
sys.exit(1)
|
|
570
|
+
with open(args.har, "r", encoding="utf-8") as f:
|
|
571
|
+
har_data = json.load(f)
|
|
572
|
+
collection = parse_har_file(har_data, base_url_override=args.base_url)
|
|
573
|
+
else:
|
|
574
|
+
if not os.path.exists(args.spec):
|
|
575
|
+
print(f"❌ Error: OpenAPI specification file not found: {args.spec}", file=sys.stderr)
|
|
576
|
+
sys.exit(1)
|
|
577
|
+
with open(args.spec, "r", encoding="utf-8") as f:
|
|
578
|
+
spec_data = json.load(f)
|
|
579
|
+
collection = parse_openapi_spec(spec_data, base_url_override=args.base_url)
|
|
580
|
+
|
|
581
|
+
out_json = json.dumps(collection, indent=2, ensure_ascii=False)
|
|
582
|
+
|
|
583
|
+
if args.output:
|
|
584
|
+
out_dir = os.path.dirname(os.path.abspath(args.output))
|
|
585
|
+
if out_dir and not os.path.exists(out_dir):
|
|
586
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
587
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
588
|
+
f.write(out_json)
|
|
589
|
+
print(f"✅ [POSTMAN EXPORT] Successfully created Postman Collection v2.1.0: {args.output}")
|
|
590
|
+
else:
|
|
591
|
+
print(out_json)
|
|
592
|
+
|
|
593
|
+
# Optional Companion Environment
|
|
594
|
+
if args.env_output:
|
|
595
|
+
base_u = args.base_url or "https://api.staging.enterprise.local"
|
|
596
|
+
env_dict = generate_postman_environment(name="Agent-QA-Staging-Env", base_url=base_u)
|
|
597
|
+
env_dir = os.path.dirname(os.path.abspath(args.env_output))
|
|
598
|
+
if env_dir and not os.path.exists(env_dir):
|
|
599
|
+
os.makedirs(env_dir, exist_ok=True)
|
|
600
|
+
with open(args.env_output, "w", encoding="utf-8") as f:
|
|
601
|
+
json.dump(env_dict, f, indent=2, ensure_ascii=False)
|
|
602
|
+
print(f"✅ [ENVIRONMENT EXPORT] Successfully created Postman Environment: {args.env_output}")
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
if __name__ == "__main__":
|
|
606
|
+
main()
|