test(e2e): 运营登录只用 seed 账号;补幂等 E2E 与两处机械守卫

1) 修复一处从未暴露的 CI 缺陷

库中 operator 账号没有任何迁移种子,DemoDataSeeder 也不创建运营账号
(只统计数量),全新库唯一的账号来源是 seed_e2e_operators.sql。
而 conftest 的 admin_token 写死 username="admin" —— 那是某台开发机上
手工创建的遗留账号:本机有它所以本地一直跑绿,CI 全新库没有它 →
该 fixture 必然 401 → e2e job 从未真正通过过。
同一文件里「外加迁移 seed 的 admin」这句注释也是纯粹的虚构。

改用 seed 创建的 e2e_admin(已绑定 super_admin,语义等价)。

2) fixture_account_guard_test.py(2 用例)

机械拦住「fixture 依赖本机手工数据」:从 seed SQL 解析账号清单,
扫描 conftest 的所有登录用户名,任何一个不在 seed 内即变红;
另一条锁住推理前提(迁移里确实没有 operator 种子)。
两条都带「解析失败即失败」断言 —— 守卫自身静默失效是本项目反复踩过的坑。

3) idempotency_test.py(7 用例)

同键重放响应逐字节相同、同键重复提交只产生一条记录、body 字段幂等未回归、
不同键相互独立、同键作用域按 actor 隔离、5 线程并发同键业务恰好落库一次、
无键重复 Like 不得返回 500。

4) conftest.ApiClient.call 支持 headers 覆盖,供幂等用例携带自定义键。
This commit is contained in:
WorkBuddy
2026-09-18 16:37:33 +08:00
parent 8dee67040c
commit af5412646b
3 changed files with 321 additions and 7 deletions
+29 -7
View File
@@ -49,12 +49,16 @@ class ApiClient:
token: Optional[str] = None,
json: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
) -> requests.Response:
headers = {"Authorization": f"Bearer {token}"} if token else {}
merged = {"Authorization": f"Bearer {token}"} if token else {}
if headers:
# 显式传入的头覆盖默认头(幂等键等按用例定制)
merged.update(headers)
return self.session.request(
method,
f"{self.base_url}{path}",
headers=headers,
headers=merged,
json=json,
params=params,
timeout=self.timeout,
@@ -112,15 +116,29 @@ def operator_password() -> str:
return pwd
ADMIN_USERNAME = "e2e_admin"
@pytest.fixture(scope="session")
def admin_token(api: ApiClient, operator_password: str) -> str:
"""超级管理员 token。
必须使用 **seed 脚本创建的账号**,而不是 `admin`。
原因(本轮发现):库中 operator 账号**没有任何迁移种子**,`DemoDataSeeder`
也不创建运营账号(只统计),运营账号只能由运行时服务或本 seed 脚本产生。
此前该 fixture 写的是 `username="admin"` —— 那是某台开发机上手工创建的遗留
账号,全新库(CI)里根本不存在 → 该 fixture 在 CI 上必然 401,
**整个 E2E job 从未真正通过过**,只是本机因为有遗留账号而看不出来。
`e2e_admin` 由 seed 绑定 super_admin 角色,语义等价且可自举。
"""
data = api.ok(
"POST",
"/auth/operator/login",
json={"username": "admin", "password": operator_password},
json={"username": ADMIN_USERNAME, "password": operator_password},
)
token = data.get("accessToken")
assert token, "admin 登录未返回 accessToken"
assert token, f"{ADMIN_USERNAME} 登录未返回 accessToken"
return token
@@ -130,9 +148,13 @@ def operator_token(
) -> Callable[[str], str]:
"""按用户名取运营 token(会话内缓存)。
可用账号见 tests/e2e/sql/seed_e2e_operators.sql:
e2e_admin / e2e_platform / e2e_counselor / e2e_station / e2e_matchmaker / e2e_content,
外加迁移 seed 的 admin。用法:`operator_token("e2e_counselor")`。
可用账号**仅限** tests/e2e/sql/seed_e2e_operators.sql 中创建的 6 个:
e2e_admin / e2e_platform / e2e_counselor / e2e_station / e2e_matchmaker / e2e_content。
迁移里**没有任何 operator 种子**(原文"外加迁移 seed 的 admin"是错的,
已纠正——全新库里没有 admin 这个账号)。用法:`operator_token("e2e_counselor")`。
传入 seed 之外的用户名等于依赖某台机器上的手工数据,CI 上必然 401;
`fixture_account_guard_test.py` 会机械拦住这类写法。
"""
cache: Dict[str, str] = {}