test: 补 §19 端点编号守卫与端点权限判定的 HTTP 层用例 #8

Closed
Zsy_0626 wants to merge 0 commits from ZSY_test_guard into qyqy_develop
Owner

背景

这两道守卫对应 PR #7 评审暴露出来的两个静默盲区。都不是新功能,是补"能被自动发现"的能力。

一、docs/05 §19 端点编号唯一性 —— tools/check_docs_endpoint_ids.py

tools/check_authoritative_docs.py 只校验 docs/ 的文件名编号,不校验 §19 的端点编号。
两条线各自新增端点时都占了 A034/A035,合并后 §19 同时存在两个 A034 与两个 A035,
而文档守卫照样通过 —— 编号复用比"编号不够"更麻烦,而且静默遗留。

口径要点:不枚举前缀白名单。 同一件事上还踩过一次"扫描正则写成 [AMKCS],
漏掉 O/R 两段,把 62 个端点报成 55 个"——漏掉的号段一旦将来被复用,脚本仍会报"重复 0"。
所以覆盖报告会把"扫到哪几个号段、各多少条"一并打印:

§19 覆盖:62 个端点编号 / 6 个号段(A×40、C×7、K×4、M×4、O×3、R×4)
docs/05 §19 端点编号无重复

首列不是 XXX### 形状的行会被显式列出而不是静默跳过。只读、不依赖任何环境变量(纯解析文档),
裸检出环境可直接跑。测试见 tests/unit/tools/test_docs_endpoint_ids.py(7 例,含"空扫也算通过"的反例)。

二、端点权限判定的 HTTP 层用例 —— tests/unit/api/test_permission_enforcement.py

既有用例(含 tests/integration/test_customer_service_handover_admin_mysql.py)都通过
build_request_context 覆写注入已经带好权限的上下文,只覆盖"有权限能通",
覆盖不到"缺权限必须被拒"。而"权限码在库里根本不存在"这类环境数据问题恰恰只会在这一层暴露:

权限判定发生在身份解析之后(app/api/dependencies/auth.py → IdentityService.resolve),
服务层测试是自己构造 RequestContext 的、权限字段由测试塞入,所以全绿也照样漏。

覆盖客服二期三个权限码对应的 5 个端点,正反双向断言:

方向 断言
反向 权限集里没有该权限 → 必须 403 且错误码 AGENT_PERMISSION_DENIED
正向 把该权限放进去 → 不能再是 403(把端点要求的权限码钉住,改动即红)

只替换两处边界:build_request_context(跳过 JWT 与身份库)与 AuthorizationService 的审计落库
(内存替身);真实路由、真实权限判定、真实 403 信封。

已做反向验证:给一个不存在的权限码时,5 个端点全部 403 —— 确认正向断言不是空过。

验证(隔离 worktree,基线 origin/qyqy_develop = 4d8edb4)

  • pytest tests/unit tests/contract -q -p no:cacheprovider → 1294 passed, 2 skipped, 0 failed
  • ruff check app tests tools alembic → All checks passed
  • mypy app → 244 个源文件 0 错
  • tools/check_authoritative_docs.py → 50 份文档无撞号;tools/check_docs_endpoint_ids.py → 无重复

顺带两个实测观察(不在本 PR 范围内,供你判断)

  1. 有 6 个权限码被代码 require(...),但既不在 seed_test_rbac.py、也不在 §19 的权限列:
    financial:nl2sql:read、probe:read、risk:alert:read、risk:alert:scan、risk:alert:write、
    risk:report:mail。它们属风险线与工具线,不是我们的改动,所以我没有动种子(避免跨线改号段)。
    如果这不是有意为之,那就是同一类"环境数据缺失 → 403"的隐患;要不要补,你定。
  2. tools/check_rbac_seed_consistency.py 单独执行时依赖完整环境变量(它 import 种子与 grant 脚本
    → 触发 Settings();我这边无 .env 时会报 neo4j_uri/jwt_audience/redis_url 缺失),
    而它的 pytest 用例在同一环境下通过 —— 两者行为不一致。如果希望门禁在裸检出环境也能跑,
    可以考虑在脚本里给 Settings 注入占位值,或改成不 import 运行期模块只解析常量。
## 背景 这两道守卫对应 PR #7 评审暴露出来的两个**静默盲区**。都不是新功能,是补"能被自动发现"的能力。 ### 一、`docs/05` §19 端点编号唯一性 —— `tools/check_docs_endpoint_ids.py` `tools/check_authoritative_docs.py` 只校验 `docs/` 的**文件名编号**,不校验 §19 的**端点编号**。 两条线各自新增端点时都占了 `A034`/`A035`,合并后 §19 同时存在两个 `A034` 与两个 `A035`, 而文档守卫照样通过 —— 编号复用比"编号不够"更麻烦,而且**静默遗留**。 **口径要点:不枚举前缀白名单。** 同一件事上还踩过一次"扫描正则写成 `[AMKCS]`, 漏掉 `O`/`R` 两段,把 62 个端点报成 55 个"——漏掉的号段一旦将来被复用,脚本仍会报"重复 0"。 所以覆盖报告会把"扫到哪几个号段、各多少条"一并打印: ``` §19 覆盖:62 个端点编号 / 6 个号段(A×40、C×7、K×4、M×4、O×3、R×4) docs/05 §19 端点编号无重复 ``` 首列不是 `XXX###` 形状的行会被**显式列出**而不是静默跳过。只读、**不依赖任何环境变量**(纯解析文档), 裸检出环境可直接跑。测试见 `tests/unit/tools/test_docs_endpoint_ids.py`(7 例,含"空扫也算通过"的反例)。 ### 二、端点权限判定的 HTTP 层用例 —— `tests/unit/api/test_permission_enforcement.py` 既有用例(含 `tests/integration/test_customer_service_handover_admin_mysql.py`)都通过 `build_request_context` 覆写注入**已经带好权限**的上下文,只覆盖"有权限能通", **覆盖不到"缺权限必须被拒"**。而"权限码在库里根本不存在"这类环境数据问题恰恰只会在这一层暴露: > 权限判定发生在身份解析之后(`app/api/dependencies/auth.py` → `IdentityService.resolve`), > 服务层测试是自己构造 `RequestContext` 的、权限字段由测试塞入,所以全绿也照样漏。 覆盖客服二期三个权限码对应的 5 个端点,**正反双向断言**: | 方向 | 断言 | |---|---| | 反向 | 权限集里没有该权限 → 必须 `403` 且错误码 `AGENT_PERMISSION_DENIED` | | 正向 | 把该权限放进去 → **不能**再是 `403`(把端点要求的权限码钉住,改动即红) | 只替换两处边界:`build_request_context`(跳过 JWT 与身份库)与 `AuthorizationService` 的审计落库 (内存替身);真实路由、真实权限判定、真实 403 信封。 **已做反向验证**:给一个不存在的权限码时,5 个端点全部 403 —— 确认正向断言不是空过。 ## 验证(隔离 worktree,基线 `origin/qyqy_develop` = `4d8edb4`) - `pytest tests/unit tests/contract -q -p no:cacheprovider` → **1294 passed, 2 skipped, 0 failed** - `ruff check app tests tools alembic` → All checks passed - `mypy app` → 244 个源文件 0 错 - `tools/check_authoritative_docs.py` → 50 份文档无撞号;`tools/check_docs_endpoint_ids.py` → 无重复 ## 顺带两个实测观察(不在本 PR 范围内,供你判断) 1. **有 6 个权限码被代码 `require(...)`,但既不在 `seed_test_rbac.py`、也不在 §19 的权限列**: `financial:nl2sql:read`、`probe:read`、`risk:alert:read`、`risk:alert:scan`、`risk:alert:write`、 `risk:report:mail`。它们属风险线与工具线,**不是我们的改动**,所以我没有动种子(避免跨线改号段)。 如果这不是有意为之,那就是同一类"环境数据缺失 → 403"的隐患;要不要补,你定。 2. `tools/check_rbac_seed_consistency.py` **单独执行**时依赖完整环境变量(它 import 种子与 grant 脚本 → 触发 `Settings()`;我这边无 `.env` 时会报 `neo4j_uri`/`jwt_audience`/`redis_url` 缺失), 而它的 pytest 用例在同一环境下通过 —— 两者行为不一致。如果希望门禁在裸检出环境也能跑, 可以考虑在脚本里给 Settings 注入占位值,或改成不 import 运行期模块只解析常量。
Zsy_0626 added 1 commit 2026-09-12 12:58:08 +08:00
补两道守卫,覆盖 2026-09-12 那次合并评审暴露出来的两个盲区。

一、`docs/05` §19 端点编号唯一性(新增 `tools/check_docs_endpoint_ids.py`)
- 背景:`tools/check_authoritative_docs.py` 只校验 `docs/` 的**文件名编号**,不校验
  §19 的**端点编号**。两条线各自新增端点时都占了 `A034`/`A035`,合并后 §19 同时
  存在两个 `A034` 与两个 `A035`,而文档守卫照样通过 —— 编号复用会静默遗留。
- 口径要点:**不枚举前缀白名单**,前缀从数据里归纳后连同数量一起输出。同一件事上
  还踩过一次"扫描正则写成 `[AMKCS]`,漏掉 `O`/`R` 两段,把 62 个端点报成 55 个"——
  漏掉的号段一旦被复用,脚本仍会报"重复 0"。所以覆盖报告会打印
   `62 个端点编号 / 6 个号段(A×40、C×7、K×4、M×4、O×3、R×4)`,让漏扫本身可见。
- 只读、不依赖任何环境变量(纯解析文档),可在裸检出环境直接跑。

二、端点权限判定的 HTTP 层用例(新增 `tests/unit/api/test_permission_enforcement.py`)
- 背景:既有用例都通过 `build_request_context` 覆写注入**已经带好权限**的上下文,
  只覆盖"有权限能通",覆盖不到"缺权限必须被拒"。而"权限码在库里根本不存在"这类
  环境数据问题(本次三个新权限码)恰恰只会在这一层暴露:权限判定发生在身份解析之后,
  服务层测试自己构造 `RequestContext`,权限字段由测试塞入,所以全绿也照样漏。
- 覆盖客服二期三个权限码对应的 5 个端点:
  `handover:read`(工单列表/详情)、`memory:candidate:review`(候选列表/审核)、
  `memory:candidate:confirm`(用户确认)。
- 正反双向断言:缺权限 → 必须 403 且错误码为 `AGENT_PERMISSION_DENIED`;
  带权限 → **不能**再是 403(这一条把端点要求的权限码钉住,改动即红)。
- 只替换两处边界:`build_request_context`(跳过 JWT 与身份库)与
  `AuthorizationService` 的审计落库(内存替身);真实路由、真实权限判定与真实 403 信封。
- 已做反向验证:给一个不存在的权限码时,5 个端点全部被拒(403),确认正向断言非空过。

验证(隔离 worktree,基线 `origin/qyqy_develop` = 4d8edb4):
pytest tests/unit tests/contract -> 1294 passed, 2 skipped, 0 failed;
ruff check app tests tools alembic 全通过;mypy app 244 源文件 0 错;
check_authoritative_docs(50 份文档无撞号)与本脚本均通过。
Author
Owner

已直接推送至 qyqy_develop(快进,未使用合并提交)

按要求改为直接推送,不再走 PR 分流:origin/qyqy_develop 由 4d8edb4 快进到 c5adf01。

推送坐标

  • 提交:c5adf01(test: 补端点编号守卫与权限判定的 HTTP 层用例,3 文件 +450)
  • 源:ZSY_test_guard(与 qyqy_develop 推送后同一提交)
  • 前置校验:4d8edb4 是 c5adf01 的祖先(快进),合并预演无冲突

内容(详见 PR 描述)

  1. tools/check_docs_endpoint_ids.py + 7 例测试 —— 补 check_authoritative_docs.py 不校验 §19 端点编号的盲区;带覆盖自检输出(62 个端点编号 / 6 个号段(A×40、C×7、K×4、M×4、O×3、R×4)),不枚举前缀白名单。
  2. tests/unit/api/test_permission_enforcement.py + 11 例 —— 补"缺权限必须被拒"的盲区,覆盖 5 个端点 / 3 个权限码,正反双向断言。

本地验证:pytest tests/unit tests/contract → 1294 passed, 2 skipped, 0 failed;Ruff 全通过;Mypy 244 文件 0 错;两个文档守卫通过。

此处按「已推送」关闭 PR,仅为看板整洁;分支 ZSY_test_guard 内容已全部在 qyqy_develop 中。

## 已直接推送至 `qyqy_develop`(快进,未使用合并提交) 按要求改为**直接推送**,不再走 PR 分流:`origin/qyqy_develop` 由 `4d8edb4` 快进到 **`c5adf01`**。 **推送坐标** - 提交:`c5adf01`(`test: 补端点编号守卫与权限判定的 HTTP 层用例`,3 文件 +450) - 源:`ZSY_test_guard`(与 `qyqy_develop` 推送后同一提交) - 前置校验:`4d8edb4` 是 `c5adf01` 的祖先(快进),合并预演无冲突 **内容**(详见 PR 描述) 1. `tools/check_docs_endpoint_ids.py` + 7 例测试 —— 补 `check_authoritative_docs.py` 不校验 §19 端点编号的盲区;带覆盖自检输出(`62 个端点编号 / 6 个号段(A×40、C×7、K×4、M×4、O×3、R×4)`),不枚举前缀白名单。 2. `tests/unit/api/test_permission_enforcement.py` + 11 例 —— 补"缺权限必须被拒"的盲区,覆盖 5 个端点 / 3 个权限码,正反双向断言。 **本地验证**:`pytest tests/unit tests/contract` → 1294 passed, 2 skipped, 0 failed;Ruff 全通过;Mypy 244 文件 0 错;两个文档守卫通过。 此处按「已推送」关闭 PR,仅为看板整洁;分支 `ZSY_test_guard` 内容已全部在 `qyqy_develop` 中。
Zsy_0626 closed this pull request 2026-09-12 13:02:01 +08:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.