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 落地后回写
This commit is contained in:
parent
2dec1fd813
commit
3cfd481f84
199
qy_lty/.planning/phases/02-admin-rest/02-VERIFICATION.md
Normal file
199
qy_lty/.planning/phases/02-admin-rest/02-VERIFICATION.md
Normal file
@ -0,0 +1,199 @@
|
||||
# Phase 2 Verification — 管理端通用凭据槽位 REST 接口端到端验收
|
||||
|
||||
**Verified**: 2026-05-07
|
||||
**Phase**: 02-admin-rest
|
||||
**Plan**: 02-02(验收 + 互引)
|
||||
**Coverage**: CRED-03 + CRED-04(ROADMAP 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 2:Django 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 3:DB 探针态还原
|
||||
|
||||
测试结束后脚本主动还原 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 4:drf-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 3(CRED-05 客户端读取 + CRED-06 阿里云日志脱敏)以此为起点。
|
||||
|
||||
---
|
||||
|
||||
*由 02-02-PLAN.md Task 1 / Task 2 联合生成;Plan 02-02 Task 2 末尾再次 Edit 把 #8 从 ⏳ 改为 ✓*
|
||||
Loading…
x
Reference in New Issue
Block a user