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 覆盖,供幂等用例携带自定义键。
74 lines
3.1 KiB
Python
74 lines
3.1 KiB
Python
"""E2E 自检守卫:fixture 只能使用 seed 脚本真正创建的运营账号。
|
||
|
||
为什么需要它(本轮发现的真实缺陷):
|
||
|
||
库中 operator 账号**没有任何迁移种子**,`DemoDataSeeder` 也不创建运营账号
|
||
(只统计数量)。因此全新库(CI)里唯一的运营账号来源是
|
||
`tests/e2e/sql/seed_e2e_operators.sql`。
|
||
|
||
而 `conftest.py` 的 `admin_token` 曾写死 `username="admin"` —— 那是某台
|
||
开发机上手工创建的遗留账号。本机有它,所以本地跑绿;CI 全新库没有它,
|
||
该 fixture 必然 401 → **整个 E2E job 从未真正通过过**。
|
||
|
||
这类"依赖本机手工数据"的写法没有任何运行期提示,只能靠机械检查拦住。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from pathlib import Path
|
||
|
||
E2E_DIR = Path(__file__).resolve().parent
|
||
CONFTEST = E2E_DIR / "conftest.py"
|
||
SEED_SQL = E2E_DIR / "sql" / "seed_e2e_operators.sql"
|
||
MIGRATION_DIR = E2E_DIR.parent.parent / "backend" / "src" / "main" / "resources" / "db" / "migration"
|
||
|
||
|
||
def _seeded_usernames() -> set[str]:
|
||
sql = SEED_SQL.read_text(encoding="utf-8")
|
||
# seed 用的是 VALUES 行:('uuid', '名称', 'username', crypt(...), 'active')
|
||
return set(re.findall(r"'([A-Za-z0-9_]+)',\s+crypt\(", sql))
|
||
|
||
|
||
def _usernames_used_in_conftest() -> set[str]:
|
||
src = CONFTEST.read_text(encoding="utf-8")
|
||
used = set(re.findall(r'"username"\s*:\s*"([A-Za-z0-9_]+)"', src))
|
||
used |= set(re.findall(r'ADMIN_USERNAME\s*=\s*"([A-Za-z0-9_]+)"', src))
|
||
return used
|
||
|
||
|
||
def test_conftest_only_logs_in_with_seeded_accounts() -> None:
|
||
seeded = _seeded_usernames()
|
||
assert seeded, f"未能从 {SEED_SQL.name} 解析出任何账号,守卫已失效"
|
||
|
||
used = _usernames_used_in_conftest()
|
||
assert used, f"未能从 {CONFTEST.name} 解析出任何登录用户名,守卫已失效"
|
||
|
||
unknown = sorted(used - seeded)
|
||
assert not unknown, (
|
||
"conftest 使用了 seed 脚本未创建的运营账号:"
|
||
f"{unknown}(seed 现有:{sorted(seeded)})。\n"
|
||
"全新库(CI)里这些账号不存在,对应 fixture 会 401,E2E 必然失败。"
|
||
"请改用 seed 中的账号,或在 seed 脚本中补上该账号。"
|
||
)
|
||
|
||
|
||
def test_no_operator_account_is_seeded_by_migrations() -> None:
|
||
"""锁住上面那条推理的前提:迁移确实没有 operator 种子。
|
||
|
||
若将来有人在迁移里加了 admin 种子,本用例会变红——那时应重新评估
|
||
`admin_token` 的选择(迁移种子同样是自举的,可作为合法来源),
|
||
而不是继续凭本机的遗留账号。
|
||
"""
|
||
hits: list[str] = []
|
||
for sql_file in MIGRATION_DIR.glob("*.sql"):
|
||
text = sql_file.read_text(encoding="utf-8", errors="replace")
|
||
if re.search(r"INSERT\s+INTO\s+operators\b", text, re.IGNORECASE):
|
||
hits.append(sql_file.name)
|
||
|
||
assert not hits, (
|
||
f"迁移中出现了 operator 种子:{hits}。"
|
||
"本次 E2E fixture 的选型前提('运营账号只能来自 seed 脚本')已改变,"
|
||
"请重新评估 conftest 的 ADMIN_USERNAME 与本文档说明。"
|
||
)
|