fapitest 0.2.2__tar.gz

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 (93) hide show
  1. fapitest-0.2.2/PKG-INFO +196 -0
  2. fapitest-0.2.2/README.md +173 -0
  3. fapitest-0.2.2/core/__init__.py +1 -0
  4. fapitest-0.2.2/core/__main__.py +99 -0
  5. fapitest-0.2.2/core/assert_engine.py +34 -0
  6. fapitest-0.2.2/core/bundled/__init__.py +1 -0
  7. fapitest-0.2.2/core/bundled/suites/demo-ui/cases/flows/post_then_get.json +23 -0
  8. fapitest-0.2.2/core/bundled/suites/demo-ui/cases/get-basic.json +6 -0
  9. fapitest-0.2.2/core/bundled/suites/demo-ui/cases/get-delay.json +6 -0
  10. fapitest-0.2.2/core/bundled/suites/demo-ui/cases/get-query.json +7 -0
  11. fapitest-0.2.2/core/bundled/suites/demo-ui/cases/post-form.json +8 -0
  12. fapitest-0.2.2/core/bundled/suites/demo-ui/cases/post-json.json +7 -0
  13. fapitest-0.2.2/core/bundled/suites/demo-ui/forms/post_form.json +6 -0
  14. fapitest-0.2.2/core/bundled/suites/demo-ui/groups/flows/group.json +9 -0
  15. fapitest-0.2.2/core/bundled/suites/demo-ui/groups/readonly/group.json +11 -0
  16. fapitest-0.2.2/core/bundled/suites/demo-ui/groups/write/group.json +10 -0
  17. fapitest-0.2.2/core/bundled/suites/demo-ui/payloads/get_query.json +4 -0
  18. fapitest-0.2.2/core/bundled/suites/demo-ui/payloads/post_json.json +4 -0
  19. fapitest-0.2.2/core/bundled/suites/demo-ui/payloads/post_seed.json +3 -0
  20. fapitest-0.2.2/core/bundled/suites/demo-ui/suite.json +48 -0
  21. fapitest-0.2.2/core/bundled/suites/xsiam-credential/cases/agent-list.json +7 -0
  22. fapitest-0.2.2/core/bundled/suites/xsiam-credential/cases/associate-by-mcp-name.json +7 -0
  23. fapitest-0.2.2/core/bundled/suites/xsiam-credential/cases/associate.json +7 -0
  24. fapitest-0.2.2/core/bundled/suites/xsiam-credential/cases/detail.json +7 -0
  25. fapitest-0.2.2/core/bundled/suites/xsiam-credential/cases/disassociate.json +7 -0
  26. fapitest-0.2.2/core/bundled/suites/xsiam-credential/cases/flows/create_then_delete.json +20 -0
  27. fapitest-0.2.2/core/bundled/suites/xsiam-credential/cases/page.json +7 -0
  28. fapitest-0.2.2/core/bundled/suites/xsiam-credential/cases/restore.json +7 -0
  29. fapitest-0.2.2/core/bundled/suites/xsiam-credential/groups/readonly/group.json +11 -0
  30. fapitest-0.2.2/core/bundled/suites/xsiam-credential/groups/write/group.json +12 -0
  31. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/agent_list.json +3 -0
  32. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/associate.json +5 -0
  33. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/associate_by_mcp_name.json +4 -0
  34. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/create.json +12 -0
  35. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/delete.json +3 -0
  36. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/detail.json +3 -0
  37. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/disassociate.json +4 -0
  38. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/page.json +5 -0
  39. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/restore.json +4 -0
  40. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/token_bind.json +6 -0
  41. fapitest-0.2.2/core/bundled/suites/xsiam-credential/payloads/update.json +13 -0
  42. fapitest-0.2.2/core/bundled/suites/xsiam-credential/suite.json +30 -0
  43. fapitest-0.2.2/core/context.py +67 -0
  44. fapitest-0.2.2/core/errors.py +6 -0
  45. fapitest-0.2.2/core/extract.py +15 -0
  46. fapitest-0.2.2/core/hooks.py +70 -0
  47. fapitest-0.2.2/core/http.py +120 -0
  48. fapitest-0.2.2/core/init_cmd.py +25 -0
  49. fapitest-0.2.2/core/loader.py +330 -0
  50. fapitest-0.2.2/core/menu.py +131 -0
  51. fapitest-0.2.2/core/paths.py +84 -0
  52. fapitest-0.2.2/core/plugins/__init__.py +3 -0
  53. fapitest-0.2.2/core/plugins/auth_hengnao.py +28 -0
  54. fapitest-0.2.2/core/plugins/auth_none.py +10 -0
  55. fapitest-0.2.2/core/plugins/base.py +14 -0
  56. fapitest-0.2.2/core/plugins/registry.py +83 -0
  57. fapitest-0.2.2/core/report.py +111 -0
  58. fapitest-0.2.2/core/report_html.py +78 -0
  59. fapitest-0.2.2/core/runner.py +337 -0
  60. fapitest-0.2.2/core/setuptools_build.py +19 -0
  61. fapitest-0.2.2/core/template.py +34 -0
  62. fapitest-0.2.2/core/templates/report.html +112 -0
  63. fapitest-0.2.2/core/tui/__init__.py +3 -0
  64. fapitest-0.2.2/core/tui/app.py +690 -0
  65. fapitest-0.2.2/fapitest.egg-info/PKG-INFO +196 -0
  66. fapitest-0.2.2/fapitest.egg-info/SOURCES.txt +91 -0
  67. fapitest-0.2.2/fapitest.egg-info/dependency_links.txt +1 -0
  68. fapitest-0.2.2/fapitest.egg-info/entry_points.txt +3 -0
  69. fapitest-0.2.2/fapitest.egg-info/requires.txt +3 -0
  70. fapitest-0.2.2/fapitest.egg-info/top_level.txt +1 -0
  71. fapitest-0.2.2/pyproject.toml +52 -0
  72. fapitest-0.2.2/setup.cfg +4 -0
  73. fapitest-0.2.2/tests/test_apitest_import.py +4 -0
  74. fapitest-0.2.2/tests/test_assert_engine.py +34 -0
  75. fapitest-0.2.2/tests/test_auth_hengnao.py +41 -0
  76. fapitest-0.2.2/tests/test_cli_run.py +46 -0
  77. fapitest-0.2.2/tests/test_cli_smoke.py +16 -0
  78. fapitest-0.2.2/tests/test_client_request.py +13 -0
  79. fapitest-0.2.2/tests/test_context.py +51 -0
  80. fapitest-0.2.2/tests/test_demo_ui_suite.py +28 -0
  81. fapitest-0.2.2/tests/test_extract.py +17 -0
  82. fapitest-0.2.2/tests/test_http.py +37 -0
  83. fapitest-0.2.2/tests/test_init_cmd.py +40 -0
  84. fapitest-0.2.2/tests/test_loader.py +127 -0
  85. fapitest-0.2.2/tests/test_paths.py +56 -0
  86. fapitest-0.2.2/tests/test_plugin_discovery.py +68 -0
  87. fapitest-0.2.2/tests/test_report.py +102 -0
  88. fapitest-0.2.2/tests/test_report_html.py +99 -0
  89. fapitest-0.2.2/tests/test_runner.py +31 -0
  90. fapitest-0.2.2/tests/test_template.py +16 -0
  91. fapitest-0.2.2/tests/test_tui_smoke.py +238 -0
  92. fapitest-0.2.2/tests/test_xsiam_suite_layout.py +25 -0
  93. fapitest-0.2.2/tests/test_xsiam_suite_loader.py +27 -0
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: fapitest
3
+ Version: 0.2.2
4
+ Summary: JSON-driven OpenAPI integration test CLI (TUI + suites)
5
+ Author: DBApp Security
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://github.com/dbappsecurity/apitest-standalone
8
+ Project-URL: Documentation, https://github.com/dbappsecurity/apitest-standalone/blob/main/docs/2026-05-29-apitest-framework-usage.md
9
+ Project-URL: Issues, https://github.com/dbappsecurity/apitest-standalone/issues
10
+ Keywords: fapitest,apitest,openapi,api-testing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: requests>=2.32.0
21
+ Requires-Dist: python-dotenv>=1.0.1
22
+ Requires-Dist: textual>=0.80.0
23
+
24
+ # XSIAM Credential / apitest
25
+
26
+ ## 环境准备
27
+
28
+ ### 方式 A:从 PyPI / 内网 PyPI 运行(推荐分发)
29
+
30
+ PyPI **包名与命令**均为 **`fapitest`**(`uvx fapitest` 即可;公网 [apitest](https://pypi.org/project/apitest/) 为其他项目,勿混用)。
31
+
32
+ 需已安装 [uv](https://docs.astral.sh/uv/):
33
+
34
+ ```powershell
35
+ uvx fapitest --user-data init
36
+ uvx fapitest --user-data
37
+ uvx fapitest --user-data run --suite demo-ui --group readonly --env httpbin
38
+
39
+ # 内网 PyPI(示例)
40
+ # uvx --index-url https://pypi.your-company.com/simple/ fapitest --user-data init
41
+ ```
42
+
43
+ 持久安装:
44
+
45
+ ```powershell
46
+ uv tool install fapitest
47
+ fapitest --user-data init
48
+ fapitest --user-data
49
+ ```
50
+
51
+ 发布流程见 [`docs/publishing-pypi.md`](./docs/publishing-pypi.md)。
52
+
53
+ ### 方式 A′:`uvx --from` 本地或 Git(未上 PyPI 时)
54
+
55
+ ```powershell
56
+ uvx --from "C:/Programs/dbapp-workspace/apitest-standalone" apitest --user-data init
57
+ uvx --from "C:/Programs/dbapp-workspace/apitest-standalone" apitest --user-data
58
+ ```
59
+
60
+ 说明:
61
+
62
+ - `uvx` 会临时安装到 uv 缓存,不污染当前目录;
63
+ - 包内已内置 suite 模板;`init --user-data` 写入 `~/.apitest/`;
64
+ - 已 `init` 且存在 `~/.apitest/suites` 时,可省略每次的 `--user-data`;
65
+ - 在**已 clone 的仓库根目录**内开发时,默认使用仓库下的 `suites/`。
66
+
67
+ ### 方式 B:仓库内开发(仅 uv)
68
+
69
+ ```powershell
70
+ uv venv --python 3.12
71
+ .\.venv\Scripts\Activate.ps1
72
+ uv pip install -e .
73
+ uv pip install "pytest>=8.0.0" "responses>=0.25.0"
74
+ ```
75
+
76
+ `.env` 示例(勿提交仓库):
77
+
78
+ ```env
79
+ HENGNAO_APPKEY=your_key
80
+ HENGNAO_APPSECRET=your_secret
81
+ ```
82
+
83
+ ## apitest(推荐)
84
+
85
+ JSON 配置驱动的 OpenAPI 联调框架。日常在仓库内用 `uv run fapitest`;任意目录用 `uvx fapitest`(见上文方式 A)。
86
+
87
+ - **使用文档**:[`docs/2026-05-29-apitest-framework-usage.md`](./docs/2026-05-29-apitest-framework-usage.md)
88
+ - **设计文档**:[`docs/superpowers/specs/2026-05-29-apitest-framework-design.md`](./docs/superpowers/specs/2026-05-29-apitest-framework-design.md)
89
+
90
+ ### 常用命令
91
+
92
+ ```powershell
93
+ # 交互 TUI(Textual):方向键选组/用例,快捷键见下
94
+ uv run apitest
95
+
96
+ | 键 | 作用 |
97
+ |----|------|
98
+ | `g` | 跑当前组 |
99
+ | `c` | 跑当前用例 |
100
+ | `a` | 跑全部组 |
101
+ | `e` | **选择环境**(列表点选,Enter 确认) |
102
+ | `n` | 下一环境(顺序轮换) |
103
+ | `s` / `b` | **返回 Suite 选择**(多个 suite 时;Esc 取消并回到当前 suite) |
104
+ | `q` | 退出(也可点底部 Footer 的 Quit) |
105
+
106
+ 日志区使用 RichLog 彩色输出;跑完用例后会输出报告路径与 `file:///.../report.html` 链接。
107
+
108
+ 启动时若存在多个 suite(如 `demo-ui`、`xsiam-credential`),会先弹出 Suite 列表;进入主界面后按 **`s`** 可退回 Suite 选择页重新选择(Esc 取消)。
109
+
110
+ # 只读冒烟
111
+ uv run apitest run --suite xsiam-credential --group readonly --env dev
112
+
113
+ # 写组(含 create-then-delete flow)
114
+ uv run apitest run --suite xsiam-credential --group write --env dev
115
+
116
+ # 全量(所有 group)
117
+ uv run apitest run --suite xsiam-credential --all --env dev
118
+ ```
119
+
120
+ 报告目录:`reports/{suite}/{timestamp}/`(已 gitignore)。
121
+
122
+ 报告文件:
123
+ - `report.html`:主报告(可直接浏览器打开)
124
+ - `summary.json`:机器可读摘要
125
+ - `index.json`:索引(含 `reportHtml` 指针)
126
+ - `failures/{caseId}.json`:失败或 `--verbose` 详情(脱敏)
127
+ - `summary.md`:仅兼容模式(`legacy_md=True`)生成
128
+
129
+ 非 TUI/菜单模式会同时打印:
130
+ - `Report: <本地路径>`
131
+ - `Report URL: file:///.../report.html`(多数终端可点击)
132
+
133
+ 可选 data root 切换:
134
+
135
+ ```powershell
136
+ # 使用用户空间 ~/.apitest
137
+ uv run apitest --user-data
138
+
139
+ # 指定自定义 data root
140
+ uv run apitest --data-dir "D:/apitest-data" run --suite xsiam-credential --all --env dev
141
+ ```
142
+
143
+ 默认 data root 为仓库根目录(`suites/`、`plugins/`、`.env` 所在目录)。
144
+
145
+ ### 新增接口(检查清单)
146
+
147
+ 1. 在 `suites/xsiam-credential/payloads/` 增加请求体或 query JSON
148
+ 2. 在 `suites/xsiam-credential/cases/` 增加 case(或 flow 步骤)
149
+ 3. 在 `groups/readonly/group.json` 或 `groups/write/group.json` 引用该 case
150
+ 4. `uv run pytest -q`
151
+
152
+ Agent 配置用例:`.cursor/skills/apitest-test-data/SKILL.md`(流程)+ `reference.md`(字段/示例)。
153
+
154
+ 表单上传:`bodyType` 为 `form-data` / `form-urlencoded`,见设计文档 §3.4.2。
155
+
156
+ ### 演示 Suite(看 TUI 交互)
157
+
158
+ 无需内网与 `HENGNAO_*` 密钥,走公网 [httpbin.org](https://httpbin.org):
159
+
160
+ ```powershell
161
+ uv run apitest
162
+ # 启动后若有两个 suite,选 demo-ui
163
+ ```
164
+
165
+ | 组 | 说明 |
166
+ |----|------|
167
+ | `readonly` | GET 连通、Query、2s 延迟 |
168
+ | `write` | POST JSON、form-urlencoded |
169
+ | `flows` | POST → extract → GET 变量传递 |
170
+
171
+ 环境:`demo-ui` 含 `httpbin` / `httpbin-delay` / `playground` / `sandbox`。TUI 内按 **`e`** 弹出列表选择;按 **`n`** 可顺序切换到下一环境。
172
+
173
+ ### 目录
174
+
175
+ ```text
176
+ core/ # 框架内核
177
+ suites/demo-ui/ # TUI 演示(httpbin)
178
+ suites/xsiam-credential/
179
+ suite.json
180
+ groups/readonly|write/group.json
181
+ cases/
182
+ payloads/
183
+ plugins/ # 项目级自定义 auth_*.py(可选)
184
+ reports/ # 执行产物
185
+ ```
186
+
187
+ ## 测试
188
+
189
+ ```powershell
190
+ uv run pytest -q
191
+ ```
192
+
193
+ ## 安全说明
194
+
195
+ - 日志与报告会对 `accessToken`、`refreshToken`、`clientSecret`、`appSecret`、`sign` 脱敏。
196
+ - 勿将 `.env` 提交到仓库。
@@ -0,0 +1,173 @@
1
+ # XSIAM Credential / apitest
2
+
3
+ ## 环境准备
4
+
5
+ ### 方式 A:从 PyPI / 内网 PyPI 运行(推荐分发)
6
+
7
+ PyPI **包名与命令**均为 **`fapitest`**(`uvx fapitest` 即可;公网 [apitest](https://pypi.org/project/apitest/) 为其他项目,勿混用)。
8
+
9
+ 需已安装 [uv](https://docs.astral.sh/uv/):
10
+
11
+ ```powershell
12
+ uvx fapitest --user-data init
13
+ uvx fapitest --user-data
14
+ uvx fapitest --user-data run --suite demo-ui --group readonly --env httpbin
15
+
16
+ # 内网 PyPI(示例)
17
+ # uvx --index-url https://pypi.your-company.com/simple/ fapitest --user-data init
18
+ ```
19
+
20
+ 持久安装:
21
+
22
+ ```powershell
23
+ uv tool install fapitest
24
+ fapitest --user-data init
25
+ fapitest --user-data
26
+ ```
27
+
28
+ 发布流程见 [`docs/publishing-pypi.md`](./docs/publishing-pypi.md)。
29
+
30
+ ### 方式 A′:`uvx --from` 本地或 Git(未上 PyPI 时)
31
+
32
+ ```powershell
33
+ uvx --from "C:/Programs/dbapp-workspace/apitest-standalone" apitest --user-data init
34
+ uvx --from "C:/Programs/dbapp-workspace/apitest-standalone" apitest --user-data
35
+ ```
36
+
37
+ 说明:
38
+
39
+ - `uvx` 会临时安装到 uv 缓存,不污染当前目录;
40
+ - 包内已内置 suite 模板;`init --user-data` 写入 `~/.apitest/`;
41
+ - 已 `init` 且存在 `~/.apitest/suites` 时,可省略每次的 `--user-data`;
42
+ - 在**已 clone 的仓库根目录**内开发时,默认使用仓库下的 `suites/`。
43
+
44
+ ### 方式 B:仓库内开发(仅 uv)
45
+
46
+ ```powershell
47
+ uv venv --python 3.12
48
+ .\.venv\Scripts\Activate.ps1
49
+ uv pip install -e .
50
+ uv pip install "pytest>=8.0.0" "responses>=0.25.0"
51
+ ```
52
+
53
+ `.env` 示例(勿提交仓库):
54
+
55
+ ```env
56
+ HENGNAO_APPKEY=your_key
57
+ HENGNAO_APPSECRET=your_secret
58
+ ```
59
+
60
+ ## apitest(推荐)
61
+
62
+ JSON 配置驱动的 OpenAPI 联调框架。日常在仓库内用 `uv run fapitest`;任意目录用 `uvx fapitest`(见上文方式 A)。
63
+
64
+ - **使用文档**:[`docs/2026-05-29-apitest-framework-usage.md`](./docs/2026-05-29-apitest-framework-usage.md)
65
+ - **设计文档**:[`docs/superpowers/specs/2026-05-29-apitest-framework-design.md`](./docs/superpowers/specs/2026-05-29-apitest-framework-design.md)
66
+
67
+ ### 常用命令
68
+
69
+ ```powershell
70
+ # 交互 TUI(Textual):方向键选组/用例,快捷键见下
71
+ uv run apitest
72
+
73
+ | 键 | 作用 |
74
+ |----|------|
75
+ | `g` | 跑当前组 |
76
+ | `c` | 跑当前用例 |
77
+ | `a` | 跑全部组 |
78
+ | `e` | **选择环境**(列表点选,Enter 确认) |
79
+ | `n` | 下一环境(顺序轮换) |
80
+ | `s` / `b` | **返回 Suite 选择**(多个 suite 时;Esc 取消并回到当前 suite) |
81
+ | `q` | 退出(也可点底部 Footer 的 Quit) |
82
+
83
+ 日志区使用 RichLog 彩色输出;跑完用例后会输出报告路径与 `file:///.../report.html` 链接。
84
+
85
+ 启动时若存在多个 suite(如 `demo-ui`、`xsiam-credential`),会先弹出 Suite 列表;进入主界面后按 **`s`** 可退回 Suite 选择页重新选择(Esc 取消)。
86
+
87
+ # 只读冒烟
88
+ uv run apitest run --suite xsiam-credential --group readonly --env dev
89
+
90
+ # 写组(含 create-then-delete flow)
91
+ uv run apitest run --suite xsiam-credential --group write --env dev
92
+
93
+ # 全量(所有 group)
94
+ uv run apitest run --suite xsiam-credential --all --env dev
95
+ ```
96
+
97
+ 报告目录:`reports/{suite}/{timestamp}/`(已 gitignore)。
98
+
99
+ 报告文件:
100
+ - `report.html`:主报告(可直接浏览器打开)
101
+ - `summary.json`:机器可读摘要
102
+ - `index.json`:索引(含 `reportHtml` 指针)
103
+ - `failures/{caseId}.json`:失败或 `--verbose` 详情(脱敏)
104
+ - `summary.md`:仅兼容模式(`legacy_md=True`)生成
105
+
106
+ 非 TUI/菜单模式会同时打印:
107
+ - `Report: <本地路径>`
108
+ - `Report URL: file:///.../report.html`(多数终端可点击)
109
+
110
+ 可选 data root 切换:
111
+
112
+ ```powershell
113
+ # 使用用户空间 ~/.apitest
114
+ uv run apitest --user-data
115
+
116
+ # 指定自定义 data root
117
+ uv run apitest --data-dir "D:/apitest-data" run --suite xsiam-credential --all --env dev
118
+ ```
119
+
120
+ 默认 data root 为仓库根目录(`suites/`、`plugins/`、`.env` 所在目录)。
121
+
122
+ ### 新增接口(检查清单)
123
+
124
+ 1. 在 `suites/xsiam-credential/payloads/` 增加请求体或 query JSON
125
+ 2. 在 `suites/xsiam-credential/cases/` 增加 case(或 flow 步骤)
126
+ 3. 在 `groups/readonly/group.json` 或 `groups/write/group.json` 引用该 case
127
+ 4. `uv run pytest -q`
128
+
129
+ Agent 配置用例:`.cursor/skills/apitest-test-data/SKILL.md`(流程)+ `reference.md`(字段/示例)。
130
+
131
+ 表单上传:`bodyType` 为 `form-data` / `form-urlencoded`,见设计文档 §3.4.2。
132
+
133
+ ### 演示 Suite(看 TUI 交互)
134
+
135
+ 无需内网与 `HENGNAO_*` 密钥,走公网 [httpbin.org](https://httpbin.org):
136
+
137
+ ```powershell
138
+ uv run apitest
139
+ # 启动后若有两个 suite,选 demo-ui
140
+ ```
141
+
142
+ | 组 | 说明 |
143
+ |----|------|
144
+ | `readonly` | GET 连通、Query、2s 延迟 |
145
+ | `write` | POST JSON、form-urlencoded |
146
+ | `flows` | POST → extract → GET 变量传递 |
147
+
148
+ 环境:`demo-ui` 含 `httpbin` / `httpbin-delay` / `playground` / `sandbox`。TUI 内按 **`e`** 弹出列表选择;按 **`n`** 可顺序切换到下一环境。
149
+
150
+ ### 目录
151
+
152
+ ```text
153
+ core/ # 框架内核
154
+ suites/demo-ui/ # TUI 演示(httpbin)
155
+ suites/xsiam-credential/
156
+ suite.json
157
+ groups/readonly|write/group.json
158
+ cases/
159
+ payloads/
160
+ plugins/ # 项目级自定义 auth_*.py(可选)
161
+ reports/ # 执行产物
162
+ ```
163
+
164
+ ## 测试
165
+
166
+ ```powershell
167
+ uv run pytest -q
168
+ ```
169
+
170
+ ## 安全说明
171
+
172
+ - 日志与报告会对 `accessToken`、`refreshToken`、`clientSecret`、`appSecret`、`sign` 脱敏。
173
+ - 勿将 `.env` 提交到仓库。
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2"
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ from core.errors import ConfigError
10
+ from core.init_cmd import init_user_data
11
+ from core.loader import discover_suites, find_suite_by_name
12
+ from core.menu import run_menu
13
+ from core.paths import resolve_data_root, script_root, set_runtime_data_root, suites_template_source
14
+ from core.report import write_run_report
15
+ from core.runner import run_all_groups, run_group
16
+
17
+
18
+ def _file_uri(path: Path) -> str:
19
+ return path.resolve().as_uri()
20
+
21
+
22
+ def _cmd_run(args: argparse.Namespace) -> int:
23
+ suite = find_suite_by_name(args.suite)
24
+ session_env = args.env or suite.default_env
25
+ if args.all:
26
+ result = run_all_groups(suite, session_env=session_env)
27
+ else:
28
+ if not args.group:
29
+ print("--group or --all is required", file=sys.stderr)
30
+ return 2
31
+ result = run_group(suite, args.group, session_env=session_env)
32
+ out = write_run_report(suite_name=suite.name, session_env=session_env, result=result)
33
+ report_html = (out / "report.html").resolve()
34
+ total = result.total
35
+ rate = (result.passed / total * 100) if total else 0
36
+ print(f"Summary: passed={result.passed} failed={result.failed} total={total} ({rate:.0f}%)")
37
+ print(f"Report: {report_html}")
38
+ print(f"Report URL: {_file_uri(report_html)}")
39
+ return 0 if result.failed == 0 else 1
40
+
41
+
42
+ def _cmd_init(args: argparse.Namespace) -> int:
43
+ source = suites_template_source()
44
+ target = resolve_data_root(user_data=args.user_data, data_dir=args.data_dir)
45
+ initialized = init_user_data(source_data_root=source, target_data_root=target, force=args.force)
46
+ print(f"Initialized data root: {initialized}")
47
+ return 0
48
+
49
+
50
+ def main(argv: list[str] | None = None) -> int:
51
+ parser = argparse.ArgumentParser(prog="fapitest")
52
+ parser.add_argument("--user-data", action="store_true", help="Use ~/.apitest as data root")
53
+ parser.add_argument("--data-dir", help="Custom data root directory")
54
+ sub = parser.add_subparsers(dest="command")
55
+
56
+ run_parser = sub.add_parser("run", help="Run suite groups non-interactively")
57
+ run_parser.add_argument("--suite", required=True)
58
+ run_parser.add_argument("--group")
59
+ run_parser.add_argument("--all", action="store_true")
60
+ run_parser.add_argument("--env")
61
+ run_parser.set_defaults(func=_cmd_run)
62
+
63
+ sub.add_parser("menu", help="Interactive menu")
64
+ init_parser = sub.add_parser("init", help="Initialize data root with suites template")
65
+ init_parser.add_argument("--force", action="store_true")
66
+ init_parser.set_defaults(func=_cmd_init)
67
+
68
+ args = parser.parse_args(argv)
69
+ root = resolve_data_root(user_data=args.user_data, data_dir=args.data_dir)
70
+ set_runtime_data_root(root)
71
+ # Keep compatibility with current repos; user-space .env should override script/.env.
72
+ load_dotenv(script_root() / ".env")
73
+ load_dotenv(root / ".env", override=True)
74
+
75
+ if args.command == "run":
76
+ try:
77
+ return _cmd_run(args)
78
+ except ConfigError as exc:
79
+ print(exc, file=sys.stderr)
80
+ return 1
81
+ if args.command == "init":
82
+ try:
83
+ return _cmd_init(args)
84
+ except ConfigError as exc:
85
+ print(exc, file=sys.stderr)
86
+ return 1
87
+
88
+ if args.command == "menu" or (args.command is None and sys.stdin.isatty()):
89
+ return run_menu()
90
+
91
+ if args.command is None:
92
+ parser.print_help()
93
+ return 0
94
+
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ raise SystemExit(main())
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def _json_subset(expected: Any, actual: Any) -> bool:
7
+ if isinstance(expected, dict):
8
+ if not isinstance(actual, dict):
9
+ return False
10
+ return all(k in actual and _json_subset(expected[k], actual[k]) for k in expected)
11
+ return expected == actual
12
+
13
+
14
+ def check_success(
15
+ http_status: int,
16
+ body: Any,
17
+ rules: dict[str, Any],
18
+ ) -> tuple[bool, list[str]]:
19
+ reasons: list[str] = []
20
+ expected_status = rules.get("httpStatus")
21
+ if expected_status is not None and http_status not in expected_status:
22
+ reasons.append(f"httpStatus expected {expected_status}, got {http_status}")
23
+
24
+ json_rule = rules.get("json")
25
+ if json_rule is not None:
26
+ if isinstance(body, dict):
27
+ if not _json_subset(json_rule, body):
28
+ reasons.append(f"json subset mismatch: expected {json_rule}, got {body}")
29
+ elif body is not None and not isinstance(body, dict):
30
+ pass
31
+ else:
32
+ reasons.append("json rule configured but response body is not a JSON object")
33
+
34
+ return (len(reasons) == 0, reasons)
@@ -0,0 +1 @@
1
+ """Bundled suite templates (populated at wheel build time)."""
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "post-then-get",
3
+ "type": "flow",
4
+ "name": "POST 写入变量再 GET",
5
+ "steps": [
6
+ {
7
+ "name": "post-seed",
8
+ "method": "POST",
9
+ "path": "/post",
10
+ "bodyFile": "payloads/post_seed.json",
11
+ "extract": { "echoToken": "$.json.echoToken" }
12
+ },
13
+ {
14
+ "name": "get-echo",
15
+ "method": "GET",
16
+ "path": "/get",
17
+ "query": {
18
+ "echoToken": "{{echoToken}}",
19
+ "tag": "{{demoTag}}"
20
+ }
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "get-basic",
3
+ "name": "GET 连通性",
4
+ "method": "GET",
5
+ "path": "/get"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "get-delay",
3
+ "name": "GET 延迟 2s(看进度)",
4
+ "method": "GET",
5
+ "path": "/delay/2"
6
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "get-query",
3
+ "name": "GET 带 Query",
4
+ "method": "GET",
5
+ "path": "/get",
6
+ "queryFile": "payloads/get_query.json"
7
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "post-form",
3
+ "name": "POST 表单",
4
+ "method": "POST",
5
+ "path": "/post",
6
+ "bodyType": "form-urlencoded",
7
+ "formFile": "forms/post_form.json"
8
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "post-json",
3
+ "name": "POST JSON",
4
+ "method": "POST",
5
+ "path": "/post",
6
+ "bodyFile": "payloads/post_json.json"
7
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "fields": {
3
+ "field1": "hello",
4
+ "tag": "{{demoTag}}"
5
+ }
6
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "flows",
3
+ "tags": ["flow", "demo"],
4
+ "env": "httpbin",
5
+ "inheritSuccess": true,
6
+ "cases": [
7
+ { "id": "post-then-get", "file": "cases/flows/post_then_get.json" }
8
+ ]
9
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "readonly",
3
+ "tags": ["read-only", "demo"],
4
+ "env": "httpbin",
5
+ "inheritSuccess": true,
6
+ "cases": [
7
+ { "id": "get-basic", "file": "cases/get-basic.json" },
8
+ { "id": "get-query", "file": "cases/get-query.json" },
9
+ { "id": "get-delay", "file": "cases/get-delay.json" }
10
+ ]
11
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "write",
3
+ "tags": ["write", "demo"],
4
+ "env": "httpbin",
5
+ "inheritSuccess": true,
6
+ "cases": [
7
+ { "id": "post-json", "file": "cases/post-json.json" },
8
+ { "id": "post-form", "file": "cases/post-form.json" }
9
+ ]
10
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "foo": "bar",
3
+ "tag": "{{demoTag}}"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "apitest-demo",
3
+ "tag": "{{demoTag}}"
4
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "echoToken": "token-{{demoTag}}-001"
3
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "demo-ui",
3
+ "version": "1",
4
+ "defaultEnv": "httpbin",
5
+ "plugins": {
6
+ "paths": ["plugins"]
7
+ },
8
+ "auth": { "plugin": "none" },
9
+ "environments": {
10
+ "httpbin": {
11
+ "baseUrl": "https://httpbin.org",
12
+ "variables": {
13
+ "demoTag": "apitest-tui"
14
+ }
15
+ },
16
+ "httpbin-delay": {
17
+ "baseUrl": "https://httpbin.org",
18
+ "variables": {
19
+ "demoTag": "apitest-slow"
20
+ }
21
+ },
22
+ "playground": {
23
+ "baseUrl": "https://httpbin.org",
24
+ "variables": {
25
+ "demoTag": "playground",
26
+ "note": "按 e 切到此环境后跑 get-query,可在响应 args 里看到 tag"
27
+ }
28
+ },
29
+ "sandbox": {
30
+ "baseUrl": "https://httpbin.org",
31
+ "variables": {
32
+ "demoTag": "sandbox",
33
+ "channel": "tui-round-robin"
34
+ }
35
+ }
36
+ },
37
+ "success": {
38
+ "httpStatus": [200]
39
+ },
40
+ "hooks": {
41
+ "module": "hooks.py",
42
+ "enabled": false
43
+ },
44
+ "http": {
45
+ "timeout": 15,
46
+ "retries": 1
47
+ }
48
+ }