apitestgen-cli 0.5.35__cp315-cp315-win_amd64.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.
Files changed (59) hide show
  1. apitestgen/__init__.py +0 -0
  2. apitestgen/_version.cp315-win_amd64.pyd +0 -0
  3. apitestgen/analyzer/__init__.py +0 -0
  4. apitestgen/analyzer/archive.cp315-win_amd64.pyd +0 -0
  5. apitestgen/analyzer/backend_enrich/__init__.py +13 -0
  6. apitestgen/analyzer/backend_enrich/class_index.cp315-win_amd64.pyd +0 -0
  7. apitestgen/analyzer/backend_enrich/cli.cp315-win_amd64.pyd +0 -0
  8. apitestgen/analyzer/backend_enrich/controller_index.cp315-win_amd64.pyd +0 -0
  9. apitestgen/analyzer/backend_enrich/dto_resolver.cp315-win_amd64.pyd +0 -0
  10. apitestgen/analyzer/backend_enrich/error_codes.cp315-win_amd64.pyd +0 -0
  11. apitestgen/analyzer/backend_enrich/java_lex.cp315-win_amd64.pyd +0 -0
  12. apitestgen/analyzer/backend_enrich/merge.cp315-win_amd64.pyd +0 -0
  13. apitestgen/analyzer/backend_enrich/meta_extractor.cp315-win_amd64.pyd +0 -0
  14. apitestgen/analyzer/backend_enrich/module_inference.cp315-win_amd64.pyd +0 -0
  15. apitestgen/analyzer/backend_enrich/response_resolver.cp315-win_amd64.pyd +0 -0
  16. apitestgen/analyzer/curl_auth.cp315-win_amd64.pyd +0 -0
  17. apitestgen/analyzer/dependency_detector.cp315-win_amd64.pyd +0 -0
  18. apitestgen/analyzer/generated_project.cp315-win_amd64.pyd +0 -0
  19. apitestgen/analyzer/json_converter.cp315-win_amd64.pyd +0 -0
  20. apitestgen/analyzer/links_viewer.cp315-win_amd64.pyd +0 -0
  21. apitestgen/analyzer/list_sessions.cp315-win_amd64.pyd +0 -0
  22. apitestgen/analyzer/login_detector.cp315-win_amd64.pyd +0 -0
  23. apitestgen/analyzer/output_formatter.cp315-win_amd64.pyd +0 -0
  24. apitestgen/analyzer/planning.cp315-win_amd64.pyd +0 -0
  25. apitestgen/analyzer/report_html.cp315-win_amd64.pyd +0 -0
  26. apitestgen/analyzer/response_contract_inferrer.cp315-win_amd64.pyd +0 -0
  27. apitestgen/analyzer/run_diff.cp315-win_amd64.pyd +0 -0
  28. apitestgen/analyzer/schema_inferrer.cp315-win_amd64.pyd +0 -0
  29. apitestgen/analyzer/validate_generated_project.cp315-win_amd64.pyd +0 -0
  30. apitestgen/analyzer/validate_recording_baseline.cp315-win_amd64.pyd +0 -0
  31. apitestgen/cli.cp315-win_amd64.pyd +0 -0
  32. apitestgen/commands/__init__.py +0 -0
  33. apitestgen/commands/init.cp315-win_amd64.pyd +0 -0
  34. apitestgen/commands/upgrade.cp315-win_amd64.pyd +0 -0
  35. apitestgen/common/__init__.py +3 -0
  36. apitestgen/common/host_classifier.cp315-win_amd64.pyd +0 -0
  37. apitestgen/common/path_utils.cp315-win_amd64.pyd +0 -0
  38. apitestgen/common/stdio.cp315-win_amd64.pyd +0 -0
  39. apitestgen/common/value_flattener.cp315-win_amd64.pyd +0 -0
  40. apitestgen/integrations/__init__.py +1 -0
  41. apitestgen/integrations/cloudtest_pusher.cp315-win_amd64.pyd +0 -0
  42. apitestgen/mcp_server.cp315-win_amd64.pyd +0 -0
  43. apitestgen/recorder/__init__.py +0 -0
  44. apitestgen/recorder/_normalize.cp315-win_amd64.pyd +0 -0
  45. apitestgen/recorder/addon.py +244 -0
  46. apitestgen/recorder/cli.cp315-win_amd64.pyd +0 -0
  47. apitestgen/recorder/db.cp315-win_amd64.pyd +0 -0
  48. apitestgen/recorder/inproc.cp315-win_amd64.pyd +0 -0
  49. apitestgen/recorder/runner.cp315-win_amd64.pyd +0 -0
  50. apitestgen/templates/apitestgen.yaml +101 -0
  51. apitestgen/templates/conftest.py +1108 -0
  52. apitestgen/templates/env.yaml +107 -0
  53. apitestgen/templates/fake_strategies.py +184 -0
  54. apitestgen/templates/pytest.ini +7 -0
  55. apitestgen_cli-0.5.35.dist-info/METADATA +443 -0
  56. apitestgen_cli-0.5.35.dist-info/RECORD +59 -0
  57. apitestgen_cli-0.5.35.dist-info/WHEEL +5 -0
  58. apitestgen_cli-0.5.35.dist-info/entry_points.txt +2 -0
  59. apitestgen_cli-0.5.35.dist-info/top_level.txt +1 -0
apitestgen/__init__.py ADDED
File without changes
Binary file
File without changes
@@ -0,0 +1,13 @@
1
+ """Enrich `.apitestgen/*.json` with information mined from a Java Spring backend repo.
2
+
3
+ The package is a self-contained `analyze enrich` subcommand. Submodules:
4
+
5
+ - ``cli`` — argparse entry point mounted via ``apitestgen analyze enrich``
6
+ - ``java_lex`` — shared regex / Java string-literal helpers
7
+ - ``controller_index`` — A1-A4: scan controllers, build (method, path) → (file, java_method, ...)
8
+ - ``dto_resolver`` — B5-B9: locate request DTOs, extract JSR-380 annotations + enums
9
+ - ``response_resolver`` — C10: identify wrapper class (R<T> / Result<T> / ...) + inner T
10
+ - ``error_codes`` — C11-C12: error-code constant dictionary + service-layer throw inversion
11
+ - ``meta_extractor`` — E13-E14: @Tag module label + BaseEntity audit fields
12
+ - ``merge`` — fold resolver outputs into ``<api>.enriched.json``
13
+ """
Binary file
File without changes
@@ -0,0 +1,3 @@
1
+ """Shared helpers for recorder and analyzer modules."""
2
+
3
+ from .stdio import configure_utf8_stdio
@@ -0,0 +1 @@
1
+ """Outbound integrations with external test-management platforms."""
File without changes
@@ -0,0 +1,244 @@
1
+ """mitmproxy addon that records API/browser traffic into SQLite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ from urllib.parse import urlparse
8
+
9
+ from mitmproxy import ctx, http
10
+ from mitmproxy.addonmanager import Loader
11
+
12
+ from apitestgen.common.host_classifier import infer_primary_base_url
13
+ from apitestgen.recorder._normalize import (
14
+ TEST_ID_HEADER,
15
+ extract_response_headers,
16
+ filter_request_headers,
17
+ flatten_query_params,
18
+ host_with_port,
19
+ normalize_allowed_hosts,
20
+ parse_request_body,
21
+ parse_response_body,
22
+ should_record,
23
+ )
24
+ from apitestgen.recorder.db import create_session, init_db, insert_api_call, update_session_base_url
25
+
26
+ logger = logging.getLogger("apitestgen.recorder")
27
+
28
+
29
+ class RecorderAddon:
30
+ """Record filtered browser/API flows into SQLite."""
31
+
32
+ def __init__(self):
33
+ self.db_path: str = ".apitestgen/recordings.db"
34
+ self.session_name: str = "default"
35
+ self.filter_prefixes: list[str] = []
36
+ self.allowed_hosts: list[str] = []
37
+ self.role: str = ""
38
+ self.include_browser_noise: bool = False
39
+ self.conn = None
40
+ self.session_id: int | None = None
41
+ self.seq_counter: int = 0
42
+ self._flow_start_times: dict[str, float] = {}
43
+ self._recorded_calls: list[dict] = []
44
+ self._seen_host_keys: set[tuple[str, str]] = set()
45
+ self._current_base_url: str = ""
46
+
47
+ def load(self, loader: Loader):
48
+ loader.add_option(
49
+ "apitestgen_db",
50
+ str,
51
+ ".apitestgen/recordings.db",
52
+ "SQLite database path",
53
+ )
54
+ loader.add_option(
55
+ "apitestgen_session",
56
+ str,
57
+ "default",
58
+ "Recording session name",
59
+ )
60
+ loader.add_option(
61
+ "apitestgen_filter",
62
+ str,
63
+ "",
64
+ "Comma-separated URL path prefixes to record",
65
+ )
66
+ loader.add_option(
67
+ "apitestgen_domains",
68
+ str,
69
+ "",
70
+ "Comma-separated exact hostnames to record",
71
+ )
72
+ loader.add_option(
73
+ "apitestgen_role",
74
+ str,
75
+ "",
76
+ "Role label for this session — every flow gets tagged with it. For multi-role flows, run one mitmdump per role on different ports.",
77
+ )
78
+ loader.add_option(
79
+ "apitestgen_include_browser_noise",
80
+ bool,
81
+ False,
82
+ "Record browser-internal chatter (Safe Browsing, OCSP, DoH, telemetry, etc.). Off by default.",
83
+ )
84
+
85
+ def configure(self, updated):
86
+ self.db_path = ctx.options.apitestgen_db
87
+ self.session_name = ctx.options.apitestgen_session
88
+ self.filter_prefixes = [
89
+ prefix.strip() for prefix in ctx.options.apitestgen_filter.split(",") if prefix.strip()
90
+ ]
91
+ self.allowed_hosts = normalize_allowed_hosts(ctx.options.apitestgen_domains)
92
+ self.role = (ctx.options.apitestgen_role or "").strip()
93
+ self.include_browser_noise = bool(ctx.options.apitestgen_include_browser_noise)
94
+
95
+ if self.conn is None:
96
+ self.conn = init_db(self.db_path)
97
+ self.session_id = create_session(
98
+ self.conn, self.session_name, role=self.role or None
99
+ )
100
+ logger.info(
101
+ "Recording session #%d (%s) -> %s (role=%s)",
102
+ self.session_id,
103
+ self.session_name,
104
+ self.db_path,
105
+ self.role or "-",
106
+ )
107
+
108
+ def request(self, flow: http.HTTPFlow):
109
+ self._flow_start_times[flow.id] = time.time()
110
+
111
+ def response(self, flow: http.HTTPFlow):
112
+ if not self._should_record(flow):
113
+ self._flow_start_times.pop(flow.id, None)
114
+ return
115
+
116
+ start_time = self._flow_start_times.pop(flow.id, None)
117
+ duration_ms = int((time.time() - start_time) * 1000) if start_time else 0
118
+
119
+ self.seq_counter += 1
120
+ req = flow.request
121
+ resp = flow.response
122
+ if resp is None:
123
+ return
124
+
125
+ query_params = flatten_query_params(urlparse(req.url).query)
126
+
127
+ test_id: str | None = None
128
+ raw_request_headers: dict[str, str] = {}
129
+ for key, value in req.headers.items():
130
+ if key.lower() == TEST_ID_HEADER.lower():
131
+ if not test_id:
132
+ test_id = value
133
+ continue
134
+ raw_request_headers[key] = value
135
+ request_headers = filter_request_headers(raw_request_headers)
136
+ # mitmproxy can multi-value headers; expand via get_all so the shared
137
+ # extract_response_headers sees every occurrence.
138
+ response_header_pairs = [
139
+ (name, value)
140
+ for name in resp.headers.keys()
141
+ for value in resp.headers.get_all(name)
142
+ ]
143
+ response_headers = extract_response_headers(response_header_pairs)
144
+
145
+ request_content_type = req.headers.get("content-type", "")
146
+ request_body, request_body_text = parse_request_body(
147
+ self._safe_get_text(req),
148
+ request_content_type,
149
+ )
150
+ response_body, response_body_text = parse_response_body(
151
+ self._safe_get_text(resp),
152
+ resp.headers.get("content-type", ""),
153
+ )
154
+
155
+ scheme = req.scheme
156
+ host_str = host_with_port(req.host, req.port)
157
+
158
+ actor = self.role or None
159
+
160
+ call_snapshot = {
161
+ "seq": self.seq_counter,
162
+ "scheme": scheme,
163
+ "host": host_str,
164
+ "path": req.path.split("?")[0],
165
+ "method": req.method,
166
+ "status_code": resp.status_code,
167
+ "content_type": resp.headers.get("content-type", ""),
168
+ "request_content_type": request_content_type,
169
+ "request_headers": request_headers,
170
+ "actor": actor,
171
+ }
172
+
173
+ try:
174
+ call_id = insert_api_call(
175
+ self.conn,
176
+ self.session_id,
177
+ self.seq_counter,
178
+ method=req.method,
179
+ scheme=scheme,
180
+ host=host_str,
181
+ path=req.path.split("?")[0],
182
+ query_params=query_params if query_params else None,
183
+ request_content_type=request_content_type,
184
+ request_headers=request_headers if request_headers else None,
185
+ request_body=request_body,
186
+ request_body_text=request_body_text,
187
+ status_code=resp.status_code,
188
+ response_body=response_body,
189
+ response_body_text=response_body_text,
190
+ response_headers=response_headers if response_headers else None,
191
+ content_type=resp.headers.get("content-type", ""),
192
+ duration_ms=duration_ms,
193
+ test_id=test_id,
194
+ actor=actor,
195
+ )
196
+
197
+ self._recorded_calls.append(call_snapshot)
198
+ host_key = (scheme, host_str)
199
+ if host_key not in self._seen_host_keys:
200
+ self._seen_host_keys.add(host_key)
201
+ inferred_base_url = infer_primary_base_url(self._recorded_calls)
202
+ if inferred_base_url and inferred_base_url != self._current_base_url:
203
+ self._current_base_url = inferred_base_url
204
+ update_session_base_url(self.conn, self.session_id, inferred_base_url)
205
+ logger.info("Updated inferred base_url: %s", inferred_base_url)
206
+
207
+ logger.info(
208
+ "[#%d] %s %s -> %d (%dms)%s",
209
+ call_id,
210
+ req.method,
211
+ req.path.split("?")[0],
212
+ resp.status_code,
213
+ duration_ms,
214
+ f" actor={actor}" if actor else "",
215
+ )
216
+ except Exception:
217
+ logger.exception("Failed to record API call")
218
+
219
+ def _should_record(self, flow: http.HTTPFlow) -> bool:
220
+ if flow.response is None:
221
+ return False
222
+ path = flow.request.path.split("?")[0]
223
+ return should_record(
224
+ flow.request.host,
225
+ path,
226
+ self.filter_prefixes,
227
+ self.include_browser_noise,
228
+ self.allowed_hosts,
229
+ )
230
+
231
+ @staticmethod
232
+ def _safe_get_text(message) -> str | None:
233
+ try:
234
+ return message.get_text(strict=False)
235
+ except TypeError:
236
+ try:
237
+ return message.get_text()
238
+ except Exception:
239
+ return None
240
+ except Exception:
241
+ return None
242
+
243
+
244
+ addons = [RecorderAddon()]
@@ -0,0 +1,101 @@
1
+ # ApiTestGen v2 配置
2
+ # 后端 Java Web 仓库本地绝对路径
3
+ # - 仅 /apitestgen-backend-enrich(及底层 `apitestgen analyze enrich`)需要它
4
+ # - 录制 → 分析 → 生成 主流程不依赖此字段;留默认占位也能跑
5
+ # - 配置后会:扫 controller 注解填 controller_file/permission/path_params,
6
+ # 读 DTO 类拿精确的 @NotNull/@Size/@Pattern,识别 R<T>/CommonResult<T> 包装类,
7
+ # 倒排 service 层 throw 链路给 negative case 提供具体错误码
8
+ backend:
9
+ repo_path: /absolute/path/to/your/spring-backend
10
+ # 可选:限定扫描子目录,加速 Grep。逗号分隔多个根。
11
+ module_roots:
12
+ - src/main/java
13
+
14
+ # 测试输出目录(相对当前项目)
15
+ output_dir: api_tests
16
+
17
+ # 测试策略:控制每个接口生成的用例数量
18
+ # 同时也会影响 `.apitestgen/plans/test_strategy.json` 中的默认测试深度描述
19
+ # - minimal: 1 正向 + 1 必填缺失 + 1 安全(无 token),共 3 条;仅用于冒烟
20
+ # - balanced: 每字段最多 1 条负向 + 1 边界,去重(默认,适合日常回归)
21
+ # - full: 所有组合,包括全部边界/枚举负向用例
22
+ test_strategy: balanced
23
+
24
+ # 说明:
25
+ # - `record-analyze` / `json_converter` 会自动生成 `.apitestgen/*.json`
26
+ # - 同时自动刷新 `.apitestgen/plans/requirement_matrix.json`
27
+ # `.apitestgen/plans/test_strategy.json`
28
+ # `.apitestgen/plans/quality_review.json`
29
+ # - 这些 sidecar 不参与运行时请求发送,但会被后续人工审查和校验脚本使用
30
+
31
+ # 代理录制配置(可选,使用 /apitestgen-record 时生效)
32
+ proxy:
33
+ port: 8888
34
+ filter_paths:
35
+ - "/admin-api/"
36
+ # 排除的路径模式
37
+ # exclude_patterns:
38
+ # - "/ws/"
39
+
40
+ # 依赖检测算法配置(P2-2)
41
+ dependency_detector:
42
+ # 高频值过滤阈值:值出现在超过此比例的响应中则视为噪音(0~1)
43
+ high_freq_threshold: 0.5
44
+ # 最低置信度阈值:低于此值的关联不写入数据库
45
+ min_confidence: 0.5
46
+ # 时间窗(秒):依赖必须在目标请求之前 N 秒内出现;0 = 不限制
47
+ time_window_seconds: 0
48
+
49
+ # 测试用例文案语言:zh-CN(默认)或 en
50
+ # 影响 `_generate_analysis_test_cases` 的 testpoint / expectation / precondition 字面值
51
+ locale: zh-CN
52
+
53
+ # CloudTest 平台对接配置(供 tools/analyzer/output_formatter.py --format cloudtest
54
+ # 及 tools/integrations/cloudtest_pusher.py 读取)
55
+ # cloudtest:
56
+ # # 创建用例接口的 featureUri 默认值;后续也可按项目自定义成真实特性目录 URI
57
+ # default_feature_uri: REPLACE_WITH_FEATURE_URI
58
+ # # 可选:CloudTest/SVN 中脚本路径的仓库前缀;不配时使用 api_tests/... 相对路径
59
+ # # svn_script_path_prefix: /svn/your-project
60
+ # # CSV 列顺序(可选,默认按下方 headers 字面量输出)
61
+ # # columns:
62
+ # # - expectOutput
63
+ # # - featureUri
64
+ # # - interfaceName
65
+ # # - name
66
+ # # - number
67
+ # # - preparation
68
+ # # - svnScriptPath
69
+ # # - testStep
70
+ #
71
+ # # REST 推送契约(dry-run 时可留空;--apply 前必须填完)
72
+ # api:
73
+ # base_url: https://cloudtest.example.com
74
+ # # 认证:CloudTest 走 Cookie + csrftoken(Django Session 风格)
75
+ # # 浏览器登录 CloudTest 后从 DevTools 复制;rotating,过期后需手动刷新。
76
+ # headers:
77
+ # Cookie: "sessionid=...; csrftoken=..."
78
+ # csrftoken: REPLACE_WITH_CSRF_TOKEN_VALUE
79
+ # X-Project-Id: your-project-id # 如平台需要再加
80
+ # external_id_column: number # CSV 列名,默认就是 number
81
+ # search_query_field: number # 查询时用的 query 参数名
82
+ # case_search_path: /api/testcases # GET 此地址 + ?search_query_field=xxx
83
+ # case_create_path: /api/testcases
84
+ # case_create_method: POST
85
+ # case_update_path: /api/testcases/{id}
86
+ # case_update_method: PUT
87
+ # # 默认 CSV 列已对齐创建用例接口参数;只有自定义 columns 时才需要 field_map
88
+ # # field_map 的 value 语义:
89
+ # # - 若是 CSV 列名 → 取该行该列的值
90
+ # # - 若不是任何列名 → 作为字面值原样写进 body(保留 YAML 类型:int / bool / null …)
91
+ # # 用后者来传 CSV 里没有的固定参数,例如 stage / type 之类的枚举值。
92
+ # field_map:
93
+ # expectOutput: expectOutput
94
+ # featureUri: featureUri
95
+ # interfaceName: interfaceName
96
+ # name: name
97
+ # number: number
98
+ # preparation: preparation
99
+ # svnScriptPath: svnScriptPath
100
+ # testStep: testStep
101
+ # stage: 2 # 字面值示例:body 里发 "stage": 2(数字,不是 "2")