fix(e2e,pc): 不再产生跨租户同名的登录账号
账号唯一性是 (tenant_id, username),跨租户重名本身是合法行为(登录靠租户码区分);但默认值与 e2e 残留会让它变成默认状态:不带租户码登录会撞上 40012(该账号存在于多个单位),平台侧看账号也分不清哪个 admin 属于哪所学校。三处一起收口 —— 1) scripts/e2e-m0.py 跑完不再留租户:以前每跑一次就留下一个 e2e###### 租户 + 里面叫 admin 的管理员 + demo 下 3 个测试组织,库里那两个孤儿租户就是这么来的。现在按本次跑出的 id 自清(崩溃路径也兜住),不越界成 LIKE 'e2e%' 全清。 2) scripts/e2e-m1-tenant-domain.py 自己造、自己清「另一个租户里也有 admin」的夹具:它原本靠第 1 条的残留凑出 40012 这个前提,残留一被清掉就会静默反转成 8 条 FAIL。顺带修掉同族的第二处隐式依赖 —— 它假定 t14.localhost 是个真实租户,而那个租户早就不存在了。 3) PC 端「开通租户」的登录账号默认值不再写死 admin,改为跟随租户编码生成 admin_<编码>;运营一旦手改就不再被编码覆盖。 验证:全量 python scripts/verify-all.py --with-e2e 通过(37 脚本 / 1597 断言 / 0 失败),跑完库里仍只有 1 个租户、0 处跨租户重名;PC 端 pnpm typecheck 0 错、pnpm lint 干净、check-wiring 未接线 0 条,浏览器实演默认值跟随与手改不回退。
This commit is contained in:
+94
-2
@@ -16,7 +16,14 @@ M0 验收测试。
|
||||
|
||||
退出码:0 = 全部通过;1 = 有失败项(可直接用于 CI)。
|
||||
|
||||
依赖:cryptography
|
||||
依赖:cryptography;且需要能 `docker exec xt-pg`(**清理本脚本造出的测试租户与测试组织**)。
|
||||
|
||||
⚠️ 关于"跑完要清干净"(2026-10 补)
|
||||
本脚本以前**完全不清理**:每跑一次就留下一个 `e2e######` 租户(里面有一个叫 `admin`
|
||||
的管理员)+ demo 租户下的 3 个测试组织。跑两轮之后,库里同一时刻就存在两个
|
||||
`admin` —— 而"账号跨单位重名"这条保护(40012)一旦被别的东西当作前提,
|
||||
清理残留这一个动作就会把那个脚本**静默反转**(见 `e2e-m1-tenant-domain.py` 文件头)。
|
||||
所以:跑完(含中途崩溃)必须把自己造的东西删干净,且**只删自己建的**。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -24,6 +31,7 @@ import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
@@ -35,9 +43,18 @@ if hasattr(sys.stdout, "reconfigure"):
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PUB_KEY = os.path.join(ROOT, "deploy", "rsa-public-key-dev.pem")
|
||||
|
||||
PG_CONTAINER = os.environ.get("XT_PG_CONTAINER", "xt-pg")
|
||||
PG_USER = os.environ.get("PG_USER", "fill")
|
||||
PG_DB = os.environ.get("PG_DB", "filldb")
|
||||
PG_PWD = os.environ.get("PG_PASSWORD", "fill_dev_pwd")
|
||||
|
||||
PASSED: list[str] = []
|
||||
FAILED: list[tuple[str, str]] = []
|
||||
|
||||
#: 本脚本造出来的东西。放模块级 —— 脚本中途抛异常时也要能被清掉。
|
||||
CREATED_TENANT_IDS: list[int] = []
|
||||
CREATED_ORG_IDS: list[int] = []
|
||||
|
||||
|
||||
# ── HTTP ──────────────────────────────────────────────
|
||||
|
||||
@@ -129,6 +146,58 @@ def finish() -> int:
|
||||
return 0 if not FAILED else 1
|
||||
|
||||
|
||||
# ── 清理 ──────────────────────────────────────────────
|
||||
|
||||
def sql(stmt: str) -> tuple[bool, str]:
|
||||
"""直接执行 SQL —— 只用于清理测试夹具。"""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["docker", "exec", "-e", f"PGPASSWORD={PG_PWD}", PG_CONTAINER,
|
||||
"psql", "-U", PG_USER, "-d", PG_DB, "-t", "-A", "-c", stmt],
|
||||
capture_output=True, text=True, timeout=40)
|
||||
return r.returncode == 0, (r.stdout or r.stderr).strip()
|
||||
except Exception as e: # noqa: BLE001
|
||||
return False, str(e)
|
||||
|
||||
|
||||
_TENANT_TABLES: list[str] | None = None
|
||||
|
||||
|
||||
def tables_with_tenant_id() -> list[str]:
|
||||
"""
|
||||
带 `tenant_id` 的表 —— **动态枚举**,不手抄表名。
|
||||
|
||||
手抄的清单在"新增了一张业务表"那天就开始漏,而漏掉的那张表恰好很难被注意到
|
||||
(租户没了、行还在)。`dev-clean-e2e.py` 是同一口径。
|
||||
"""
|
||||
global _TENANT_TABLES
|
||||
if _TENANT_TABLES is None:
|
||||
_, out = sql("SELECT table_name FROM information_schema.columns "
|
||||
"WHERE table_schema='public' AND column_name='tenant_id'")
|
||||
_TENANT_TABLES = [t.strip() for t in out.splitlines() if t.strip()]
|
||||
return _TENANT_TABLES
|
||||
|
||||
|
||||
def cleanup_all() -> None:
|
||||
"""
|
||||
删掉本脚本造出来的数据。**幂等**,可以重复调用。
|
||||
|
||||
⚠️ 只删**自己建的**(按本次跑出来的 id):这里刻意不做
|
||||
`tenant_code LIKE 'e2e%'` 那种"顺手全清" —— 那是 `dev-clean-e2e.py` 的活。
|
||||
测试脚本越界删别人建的东西,就会让"跑一个脚本把另一个脚本的在途数据删了"
|
||||
这种事变得可能。
|
||||
"""
|
||||
for oid in list(CREATED_ORG_IDS):
|
||||
sql(f"DELETE FROM sys_org WHERE id = {oid}")
|
||||
CREATED_ORG_IDS.remove(oid)
|
||||
|
||||
for tid in list(CREATED_TENANT_IDS):
|
||||
for table in tables_with_tenant_id():
|
||||
sql(f"DELETE FROM {table} WHERE tenant_id = {tid}")
|
||||
sql(f"DELETE FROM sys_tenant WHERE id = {tid}")
|
||||
CREATED_TENANT_IDS.remove(tid)
|
||||
|
||||
|
||||
# ── 用例 ──────────────────────────────────────────────
|
||||
|
||||
def main() -> int:
|
||||
@@ -216,6 +285,8 @@ def main() -> int:
|
||||
expect_code("B2 平台运营开通租户成功", b, 0)
|
||||
created = data_of(b)
|
||||
new_tenant_id = created.get("id")
|
||||
if isinstance(new_tenant_id, int):
|
||||
CREATED_TENANT_IDS.append(new_tenant_id) # 登记,收尾与崩溃路径都要清它
|
||||
new_pwd = created.get("adminInitialPassword")
|
||||
check("B3 返回初始密码(仅此一次)", bool(new_pwd), json.dumps(created, ensure_ascii=False)[:200])
|
||||
check("B4 返回小程序码参数", created.get("miniProgramScene") == f"t{new_tenant_id}",
|
||||
@@ -262,6 +333,10 @@ def main() -> int:
|
||||
b = mk_org(demo_token, dept_id, f"E2E 24数控1班 {stamp}", 3, f"CLS{stamp}")
|
||||
expect_code("C3 建班级节点", b, 0)
|
||||
class_id = b.get("data")
|
||||
# 登记(C1/C2/C3 若失败,对应的 data 是 None,不能登记成 0/None)
|
||||
for oid in (school_id, dept_id, class_id):
|
||||
if isinstance(oid, int):
|
||||
CREATED_ORG_IDS.append(oid)
|
||||
|
||||
expect_code("C4 同级重复组织编码被拒(40001)",
|
||||
mk_org(demo_token, school_id, "重复编码", 2, f"DEPT{stamp}"), 40001)
|
||||
@@ -321,8 +396,25 @@ def main() -> int:
|
||||
check("E4 参数校验失败返回 40001 且带字段级信息", b.get("code") == 40001,
|
||||
f"code={b.get('code')}")
|
||||
|
||||
# ── F. 清理 ───────────────────────────────────────
|
||||
print("\n[F] 清理本次造出的测试数据")
|
||||
n_tenant, n_org = len(CREATED_TENANT_IDS), len(CREATED_ORG_IDS)
|
||||
cleanup_all()
|
||||
print(f" 已清理 {n_tenant} 个测试租户(连它的角色与账号)"
|
||||
f"、{n_org} 个挂在 demo 下的测试组织")
|
||||
check("F1 测试租户与测试组织都删掉了", not CREATED_TENANT_IDS and not CREATED_ORG_IDS,
|
||||
f"tenant={CREATED_TENANT_IDS} org={CREATED_ORG_IDS}")
|
||||
|
||||
return finish()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
# 崩溃也要清:否则每次失败的运行都往库里丢一个 e2e 租户和一个叫 admin 的账号
|
||||
# (跨租户同名账号就是这么攒出来的,见文件头 ⚠️)
|
||||
try:
|
||||
_code = main()
|
||||
except BaseException:
|
||||
print("\n ⚠ 脚本异常退出,正在清理已造出的测试数据…")
|
||||
cleanup_all()
|
||||
raise
|
||||
raise SystemExit(_code)
|
||||
|
||||
Reference in New Issue
Block a user