# 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 从 ⏳ 改为 ✓*
