lty/qy_lty/.planning/phases/02-admin-rest/02-VERIFICATION.md
pmc 3cfd481f84 test(02-02): 端到端验收 8 条 success criteria 全 PASS
- Django test client 程序化跑 6 条验收点(GET 脱敏 / PUT 全字段覆写 + 响应脱敏 / PUT 空记录 get_or_create / 401 无 token / 403 user token GET / 403 user token PUT),共 28 项独立断言全部 PASS
- /swagger.json/ schema 校验:路径 /v1/admin/credential-slot/ + GET/PUT 两 method + access_token description 含脱敏 / 末 4 位 / 掩码 三个语义关键字
- 验收完毕主动还原 DB 探针态(app_id=probe_app, access_token=probe_secret_xxxx)
- token 明文不入仓库(仅记长度 + PASS 判定,Redis 30 天 TTL 攻击面)
- 临时脚本 _phase2_verify.py / _phase2_swagger_verify.py 已删(不入 commit)
- 验收点 #8 互引由 Task 2 落地后回写
2026-05-07 23:05:38 +08:00

200 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 2 Verification — 管理端通用凭据槽位 REST 接口端到端验收
**Verified**: 2026-05-07
**Phase**: 02-admin-rest
**Plan**: 02-02验收 + 互引)
**Coverage**: CRED-03 + CRED-04ROADMAP Phase 2 全部 4 条 success criteria
**验证方式**: Django 5.2 test client程序化+ drf-yasg `/swagger.json/` schema 校验
**未启动**: daphne / runserver — 全部走 `django.test.Client` 内存调用,避免端口占用与运行环境噪音
---
## 验收摘要
| # | 验收点 | 方法 | 结果 |
| --- | ------------------------------------------------------- | --------------------------------------------- | --------- |
| 1 | GET 携 admin token 返回脱敏壳层 | Django test client | ✓ PASS |
| 2 | PUT 携 admin token 全字段覆写 + 响应脱敏 | Django test client | ✓ PASS |
| 3 | PUT 在空记录场景自动 `get_or_create` | Django test client手动 delete + PUT | ✓ PASS |
| 4 | 无 `Authorization` 头 → 401 + 标准壳层 | Django test client | ✓ PASS |
| 5 | 携普通 user token GET → 403 + `message` 含 "管理员" | Django test client | ✓ PASS |
| 6 | PUT 携 user token → 403验证 PUT 也走 `_ensure_admin`| Django test client | ✓ PASS |
| 7 | `/swagger.json/` 含路径 + GET/PUT 两 method + 脱敏 description | Django test client命中 drf-yasg schema | ✓ PASS |
| 8 | 修改记录两端互引qy_lty + qy-lty-admin 各一条) | 文件 grep 双向命中 | ✓ PASS |
**Total: 8 / 8 PASS** — Phase 2 全部 success criteria 已覆盖。
---
## ROADMAP Phase 2 Success Criteria 映射
ROADMAP.md Phase 2 的 4 条 success criteria 全部映射到本验证表的具体验收点:
| ROADMAP SC | 内容 | 对应验收点 |
| ---------- | ------------------------------- | ------------------- |
| SC#1 | GET 脱敏admin token | #1 |
| SC#2 | PUT 全字段覆写 + `get_or_create`| #2 + #3 |
| SC#3 | 鉴权拒绝矩阵(无 token / user token | #4 + #5 + #6 |
| SC#4 | Swagger / ReDoc schema 一致 | #7 |
---
## Step 1测试 token 准备(不黏贴明文 token
```python
# 在 _phase2_verify.py 内(已删)
admin_user = ParadiseUser.objects.filter(is_staff=True).first() # 或临时创建
admin_token = generate_token(admin_user.id, is_admin=True) # 写 Redis admin_token:{token} key
user = ParadiseUser.objects.filter(is_staff=False).first() # 或临时创建
user_token = generate_token(user.id, is_admin=False) # 写 Redis token:{token} key
```
执行输出(**token 明文已脱敏**,不入仓库):
```
PREP admin_user_id=11 (created=False)
PREP user_id=16 (created=False)
PREP admin_token=<redacted, length=36>
PREP user_token=<redacted, length=36>
```
验证完毕脚本自动 `cache.delete(f"admin_token:{admin_token}")` + `cache.delete(f"token:{user_token}")` 清理两个 Redis key非临时创建的 user 不动)。
---
## Step 2Django test client 程序化验收(验收点 #1 ~ #6
`_phase2_verify.py` 真实执行输出(共 28 项独立断言全部 PASS原始日志token 明文已脱敏):
```
#1 PASS GET admin token -> 200 status=200
#1 PASS GET success=True success=True
#1 PASS GET code=200 code=200
#1 PASS GET data 字段集 keys=['app_id', 'access_token', 'updated_at']
#1 PASS GET access_token 脱敏 got='*************xxxx' expected='*************xxxx'
#2 PASS PUT admin token -> 200 status=200
#2 PASS PUT success=True success=True
#2 PASS PUT DB 全字段覆写 app_id db.app_id='phase2_app'
#2 PASS PUT DB 全字段覆写 access_token (明文) db.access_token starts with='sk-pha'...
#2 PASS PUT 响应 access_token 脱敏 resp='****************************1234' expected='****************************1234'
#2 PASS PUT 响应末 4 位 = 1234 tail=1234
#2 PASS PUT 响应前缀以 * 开头 prefix='**'
#3 PASS DB 已清空 delete().exists()=False
#3 PASS PUT 空记录 -> 200 status=200
#3 PASS PUT 空记录后 DB 已创建并写入 app_id db.app_id='after_delete'
#3 PASS PUT 空记录后 DB 已创建并写入 access_token db.access_token='tok-XYZ9'
#3 PASS PUT 空记录后 pk=1单例 pk=1
#4 PASS 无 token -> 401 status=401
#4 PASS 无 token success=False success=False
#4 PASS 无 token code=401 code=401
#4 PASS 无 token 含 message message='身份认证信息未提供。'
#5 PASS user token GET -> 403 status=403
#5 PASS user token success=False success=False
#5 PASS user token code=403 code=403
#5 PASS user token message 含 '管理员' message='需要管理员权限'
#6 PASS user token PUT -> 403 status=403
#6 PASS user token PUT success=False success=False
#6 PASS user token PUT 不影响 DB db.app_id='after_delete' (仍为 #3 写入的值)
========== 全部 6 大验收点28 项断言)通过 28/28 ==========
```
**关键发现**
- 验收点 #4 中间件兜底 message 是 `'身份认证信息未提供。'`DRF 默认中文 NotAuthenticated与 Plan 假设的 "至少有 message 字段" 一致;标准壳层 `success=False / code=401`
- 验收点 #5 view 内 `_ensure_admin` 返回的 message 精确为 `'需要管理员权限'`(与 plan acceptance criteria 完全一致)
- 验收点 #2 写入 `sk-phase2_verify_secret_ABCD1234`32 字节)后响应 access_token = `'****************************1234'`28 个 `*` + `1234`,长度 32 = 原 token 长度,符合 `mask_token` 实现)
---
## Step 3DB 探针态还原
测试结束后脚本主动还原 DB 探针态(与 Phase 1 留下的契约一致):
```python
slot = CredentialSlot.get_solo()
slot.app_id = 'probe_app'
slot.access_token = 'probe_secret_xxxx'
slot.save()
```
输出:
```
RESTORE DB 已还原探针态app_id='probe_app' access_token_masked=*************xxxx
CLEANUP 已删除 Redis admin_token / user_token key
```
校验:`mask_token('probe_secret_xxxx')` = `'*************xxxx'`13 个 `*` + 末 4 位 `xxxx`)— 与 Phase 1 探针完全一致。
---
## Step 4drf-yasg Swagger schema 验收(验收点 #7
`_phase2_swagger_verify.py` 通过 `Client.get('/swagger.json/')` 拉取 OpenAPI schema 进行校验。
**关键发现**:本仓库 `StandardResponseMiddleware` 也会把 drf-yasg 的 JSON schema 包进 `{success, code, message, data}` 壳层;真正的 OpenAPI 在 `data` 字段内(`basePath = '/api'`),所以 swagger paths 里的 key 是去掉 `/api` 前缀的形式 `/v1/admin/credential-slot/`
实际执行输出PASS
```
/swagger.json/ status=200 content-type=application/json
schema 在 standard response 壳层 'data' 字段内basePath=/api
共 92 条 path
#7 PASS matched path key = /v1/admin/credential-slot/
#7 PASS paths['/v1/admin/credential-slot/'] 含 GET + PUT 两 method
#7 PASS access_token description 含脱敏掩码语义关键字: ['脱敏', '末 4 位', '掩码']
========== Swagger 验收点 #7 PASS ==========
```
匹配到的 path`/v1/admin/credential-slot/`(拼上 `basePath=/api` 即完整 URL `/api/v1/admin/credential-slot/`GET + PUT 两 method 完整暴露access_token 字段 description 同时命中 `脱敏` / `末 4 位` / `掩码` 三个语义关键字(来自 `_credential_slot_data_schema``description='Access Token 末 4 位脱敏掩码(如 "*********1234",前缀字符数 = 原长 - 4'`)。
---
## Step 5修改记录两端互引验收点 #8
由 02-02 Task 2 落地:
```
qy_lty/docs/修改记录.md 顶部新增 ### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口GET 脱敏 / PUT 覆写)
跨项目联动 → 引用 qy-lty-admin/docs/修改记录.md 同期条目
qy-lty-admin/docs/修改记录.md 顶部新增 ### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)
服务端联动 → 引用 ../qy_lty/docs/修改记录.md 同期条目
```
互引校验grep 双向命中):
```
$ grep "qy-lty-admin/docs/修改记录" qy_lty/docs/修改记录.md # ≥ 1 hit
$ grep "qy_lty/docs/修改记录" qy-lty-admin/docs/修改记录.md # ≥ 1 hit
```
闭环已建立CLAUDE.md「跨项目联动两端各写一条互相引用」规则在本 phase 首次落地。
---
## 边界与限制说明
- **token 明文不入仓库**:本文件仅记录 token 长度(`<redacted, length=36>`+ PASS 判定,绝不黏贴 UUID 字串。Redis 30 天 TTL 期内任何泄露的 token 都仍可用,是新增的攻击面;设计动机见 02-02-PLAN.md `<threat_model>` T-02P2-01。
- **不启 daphne / runserver**Django test client 是 in-process 调用,不经 ASGI / WSGI handler优势是无端口占用 + 快速可重复;劣势是不会触发任何 ASGI middleware 链。本仓库的鉴权 / 标准壳层 middleware 都是 Django MIDDLEWARE 而非 ASGI 层,所以 test client 路径与生产路径在本验收范围内功能等价。
- **临时验收脚本已删除**`_phase2_verify.py``_phase2_swagger_verify.py` 仅作 02-02 Task 1 的一次性证据生成,验收完毕后从仓库根目录删除(不入 commit。如需复跑可参考本文件 Step 2 / Step 4 的脚本模板。
---
## DB 终态记录
| 字段 | 值 |
| -------------- | ----------------------------------------------- |
| pk | 1单例 |
| app_id | `probe_app` |
| access_token | `probe_secret_xxxx`(明文存 DB/ `*************xxxx`(脱敏返回)|
| updated_at | 2026-05-07验收脚本最后一次 `slot.save()` 触发)|
供 Phase 3CRED-05 客户端读取 + CRED-06 阿里云日志脱敏)以此为起点。
---
*由 02-02-PLAN.md Task 1 / Task 2 联合生成Plan 02-02 Task 2 末尾再次 Edit 把 #8 从 ⏳ 改为 ✓*