fix: 终审修复——站点停用接线/prefix查重补创建reset/默认凭据警示+取舍补录
- I-1 站点停用全链接线: Site 数据类加 status 字段; DB 源 _load_one 与 文件源 read_tenant 均只输出 active 站点(停用站点域名不进 registry 索引 → 该域名 404;全停用=合法运营状态输出空 sites;文件源无 status 键默认 active 兼容)。E2E: PATCH 站点 disabled → 其域名 404、复启用 恢复;源级直测两源过滤。 - I-3 prefix 跨租户查重补两口: db.other_push_prefixes 共用助手 (MAX(version) 逐租户当前前缀,排除自身);创建向导(出厂档案前缀被占 → 400 提示改 blank/改占用方)与 reset push(重置回出厂撞车 → 400 提示 走 PUT)接入;PUT 原有查重改用同一助手。rollback 维持豁免(历史不可变 语义优先,取舍清单记档)。既有测试第二租户改 blank profile 避撞。 - I-4 默认凭据警示: lifespan 启动检测 ADMIN_PASSWORD 仍为 Settings 字段 默认值(非硬编码第二份,防双源漂移)→ logger.warning 醒目提示; CLAUDE.md Phase 4 部署记档补 ADMIN_PASSWORD 生产要求。 - I-2/M-2 取舍补录: 密钥掩码未实现(GET config/settings 明文返回 api_key/smtp 密码,Phase 3 前须定 __SET__ 哨兵方案);站点管理权限现为 仅超管(Phase 3 对接时决策放开或改 spec);取舍清单第 2 条同步刷新 (rollback 现为唯一豁免口)。 全量回归: 571 passed(基线 566 + 新增 5)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -249,8 +249,10 @@ def test_tenant_admin_cannot_create_tenant(super_client):
|
||||
"name": "越权租户", "slug": "evil", "profile": "jiancai",
|
||||
"admin_username": "evil-admin", "admin_password": "evil-pass-1"})
|
||||
assert r.status_code == 403
|
||||
# 对照: 超管可建
|
||||
assert _create(super_client, slug="ok1").status_code == 201
|
||||
# 对照: 超管可建(blank profile——jiancai 的出厂 prefix 已被 demo 占用,
|
||||
# 见 test_create_second_same_profile_prefix_conflict)
|
||||
assert _create(super_client, slug="ok1",
|
||||
profile="blank").status_code == 201
|
||||
|
||||
|
||||
# ---- _blankify 单测 --------------------------------------------------------------
|
||||
@@ -306,9 +308,27 @@ def test_create_auto_profile(super_client, tmp_path):
|
||||
assert r.json()["title"] == "HESU Support" # auto 档案身份
|
||||
|
||||
|
||||
def test_create_second_same_profile_prefix_conflict(super_client, tmp_path):
|
||||
"""终审 I-3:创建向导补 push.prefix 跨租户查重——同 profile 第二个租户
|
||||
的出厂前缀必撞(jiancai=ai-kefu-jc),spec E1 前缀全局唯一创建期就拦;
|
||||
blank 出厂前缀为空串,不参战。"""
|
||||
assert _create(super_client, slug="jc1", profile="jiancai").status_code == 201
|
||||
r = _create(super_client, slug="jc2", profile="jiancai")
|
||||
assert r.status_code == 400
|
||||
detail = r.json()["detail"]
|
||||
assert "ai-kefu-jc" in detail and "push.prefix" in detail
|
||||
# auto 前缀不同,不撞
|
||||
assert _create(super_client, slug="au1", profile="auto").status_code == 201
|
||||
# blank 出厂前缀为空 → 可建(创建后经配置 API 配前缀)
|
||||
assert _create(super_client, slug="bl1", profile="blank").status_code == 201
|
||||
# jc2 没落库(整事务未提交)
|
||||
rows = _db_fetch(tmp_path, select(Tenant.slug).where(Tenant.slug == "jc2"))
|
||||
assert rows == []
|
||||
|
||||
|
||||
def test_domain_collision_reports_owner(super_client):
|
||||
_create(super_client, slug="demo", domains=["demo.example.com"])
|
||||
_create(super_client, slug="other")
|
||||
_create(super_client, slug="other", profile="blank")
|
||||
# 大写归一后同域 → 400 带现有归属(tenant + site)
|
||||
r = super_client.post(f"{TENANTS}/other/sites/main/domains",
|
||||
headers=_auth(super_client),
|
||||
@@ -316,8 +336,9 @@ def test_domain_collision_reports_owner(super_client):
|
||||
assert r.status_code == 400
|
||||
detail = r.json()["detail"]
|
||||
assert "demo" in detail and "main" in detail
|
||||
# 创建向导的初始域名同样预检
|
||||
r = _create(super_client, slug="third", domains=["demo.example.com"])
|
||||
# 创建向导的初始域名同样预检(blank profile——排除 prefix 查重的干扰)
|
||||
r = _create(super_client, slug="third", profile="blank",
|
||||
domains=["demo.example.com"])
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
@@ -384,7 +405,9 @@ def test_site_create_patch_overlay(super_client, tmp_path):
|
||||
f"{TENANTS}/demo/sites/ghost", headers=_auth(super_client),
|
||||
json={"name": "x"}).status_code == 404
|
||||
|
||||
# 站点停用持久化(解析级语义后续任务接线,此处钉字段落库+reload 不炸)
|
||||
# 站点停用全链路(终审 I-1):字段落库 + reload 后数据源过滤停用站点——
|
||||
# 其域名不进索引(经中间件 404,与租户停用的 503 语义差:门面下线≠服务
|
||||
# 暂停),同租户其他站点照常;?site= 覆盖也取不到(回退默认门面)。
|
||||
r = super_client.patch(f"{TENANTS}/demo/sites/es",
|
||||
headers=_auth(super_client),
|
||||
json={"status": "disabled"})
|
||||
@@ -393,6 +416,27 @@ def test_site_create_patch_overlay(super_client, tmp_path):
|
||||
.join(Tenant, Site.tenant_id == Tenant.id)
|
||||
.where(Tenant.slug == "demo", Site.slug == "es"))
|
||||
assert rows[0][0].status == "disabled"
|
||||
r = super_client.get("/api/public/brand",
|
||||
headers={"host": "es.demo.example.com"})
|
||||
assert r.status_code == 404 # 停用站点域名 404
|
||||
r = super_client.get("/api/public/brand",
|
||||
headers={"host": "demo.example.com"})
|
||||
assert r.status_code == 200 # main 站点不受影响
|
||||
assert r.json()["title"] == "HESUPIPE Support" # 租户级门面(jiancai 档案)
|
||||
# ?site=es 覆盖取不到停用站点 → 回退 main 门面(title 是租户级,非 ES v2)
|
||||
r = super_client.get("/api/public/brand?site=es",
|
||||
headers={"host": "demo.example.com"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["title"] != "Demo ES v2"
|
||||
|
||||
# 复启用 → 域名恢复服务
|
||||
r = super_client.patch(f"{TENANTS}/demo/sites/es",
|
||||
headers=_auth(super_client),
|
||||
json={"status": "active"})
|
||||
assert r.status_code == 200
|
||||
assert super_client.get(
|
||||
"/api/public/brand",
|
||||
headers={"host": "es.demo.example.com"}).status_code == 200
|
||||
|
||||
|
||||
def test_patch_tenant_name_and_validation(super_client, tmp_path):
|
||||
@@ -417,7 +461,7 @@ def test_patch_tenant_name_and_validation(super_client, tmp_path):
|
||||
def test_list_tenants(super_client):
|
||||
_create(super_client, slug="demo",
|
||||
domains=["a.example.com", "b.example.com"])
|
||||
_create(super_client, slug="other")
|
||||
_create(super_client, slug="other", profile="blank")
|
||||
r = super_client.get(TENANTS, headers=_auth(super_client))
|
||||
assert r.status_code == 200
|
||||
items = {i["slug"]: i for i in r.json()["items"]}
|
||||
@@ -433,7 +477,7 @@ def test_list_tenants(super_client):
|
||||
def test_detail_shape_and_scope(super_client):
|
||||
_create(super_client, slug="demo", admin="demo-admin",
|
||||
password="demo-pass-1", domains=["demo.example.com"])
|
||||
_create(super_client, slug="other")
|
||||
_create(super_client, slug="other", profile="blank")
|
||||
r = super_client.get(f"{TENANTS}/demo", headers=_auth(super_client))
|
||||
assert r.status_code == 200
|
||||
d = r.json()
|
||||
|
||||
Reference in New Issue
Block a user