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:
@@ -30,6 +30,7 @@ from app.platform.db import (
|
||||
make_platform_engine,
|
||||
)
|
||||
from app.platform.registry import UnknownTenantError, TenantRegistry
|
||||
from app.platform.tenant_config import TenantConfig
|
||||
from app.platform.tenant_source import DbTenantSource, FileTenantSource
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -61,7 +62,7 @@ async def _make_engine(tmp_path):
|
||||
|
||||
|
||||
async def _add_jiancai(engine, prefix="ai-kefu-jc", version=1,
|
||||
config_json=None, with_version=True):
|
||||
config_json=None, with_version=True, es_status="active"):
|
||||
"""jiancai 租户 + main(JC_DOMAIN)+ es(brand_overlay)+ 版本行。"""
|
||||
async with AsyncSession(engine, expire_on_commit=False) as session:
|
||||
tenant = Tenant(slug="jiancai", name="HESUPIPE 建材", status="active")
|
||||
@@ -69,6 +70,7 @@ async def _add_jiancai(engine, prefix="ai-kefu-jc", version=1,
|
||||
await session.flush()
|
||||
main = Site(tenant_id=tenant.id, slug="main", name="main")
|
||||
es = Site(tenant_id=tenant.id, slug="es", name="es",
|
||||
status=es_status,
|
||||
brand_overlay={"title": "HESUPIPE Soporte"})
|
||||
session.add_all([main, es])
|
||||
await session.flush()
|
||||
@@ -223,6 +225,34 @@ async def test_db_source_missing_prompts_group_raises(tmp_path):
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def test_db_source_filters_disabled_sites(tmp_path):
|
||||
"""终审 I-1:site.status != active 的站点不进 LoadedTenant(域名不进
|
||||
registry 索引 → 该域名 404;site_by_slug 取不到)。es 停用后只剩
|
||||
main;全部停用 → 空 sites(零站点校验只对库表形状)。"""
|
||||
engine = await _make_engine(tmp_path)
|
||||
try:
|
||||
await _add_jiancai(engine, es_status="disabled")
|
||||
lt = await DbTenantSource(engine).load_tenant("jiancai")
|
||||
assert [s["slug"] for s in lt.sites] == ["main"] # es 被过滤
|
||||
main = lt.sites[0]
|
||||
assert main["status"] == "active"
|
||||
# TenantConfig 消费侧:Site 数据类带 status,域名索引只含 main
|
||||
config = TenantConfig.from_snapshot(
|
||||
lt.slug, lt.name, lt.snapshot, sites=lt.sites, status=lt.status)
|
||||
assert [s.slug for s in config.sites] == ["main"]
|
||||
assert config.sites[0].status == "active"
|
||||
|
||||
# 全部停用 = 合法运营状态(门面全下线),输出空 sites 不炸
|
||||
async with AsyncSession(engine, expire_on_commit=False) as session:
|
||||
await session.execute(text(
|
||||
"UPDATE sites SET status='disabled'"))
|
||||
await session.commit()
|
||||
lt2 = await DbTenantSource(engine).load_tenant("jiancai")
|
||||
assert lt2.sites == []
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
# ---- 文件源(直测;等价性由既有 test_registry 全绿证明)-----------------------
|
||||
|
||||
|
||||
@@ -270,6 +300,28 @@ def test_file_source_read_tenant_rereads_disk(tmp_path):
|
||||
assert src.read_tenant("t1").snapshot["push"]["prefix"] == "v2"
|
||||
|
||||
|
||||
def test_file_source_filters_disabled_sites(tmp_path):
|
||||
"""终审 I-1:tenant.json 站点带 status: "disabled" → 不进
|
||||
LoadedTenant(文件源同步行为);无 status 键 → 默认 active,既有
|
||||
种子零改动兼容。"""
|
||||
_write_min_seed(tmp_path, {"t1": {
|
||||
"slug": "t1", "name": "t1", "status": "active",
|
||||
"sites": [
|
||||
{"slug": "main", "domains": ["a.example.com"]}, # 无键=active
|
||||
{"slug": "es", "status": "active", "domains": []},
|
||||
{"slug": "down", "status": "disabled",
|
||||
"domains": ["down.example.com"], "brand": {"title": "x"}},
|
||||
],
|
||||
"config": {}}})
|
||||
lt = FileTenantSource(tmp_path / "seed").read_tenant("t1")
|
||||
assert [s["slug"] for s in lt.sites] == ["main", "es"] # down 被滤
|
||||
# 消费侧同构:from_snapshot 只见 active 站点
|
||||
config = TenantConfig.from_snapshot(
|
||||
lt.slug, lt.name, lt.snapshot, sites=lt.sites, status=lt.status)
|
||||
assert [s.slug for s in config.sites] == ["main", "es"]
|
||||
assert all(s.status == "active" for s in config.sites)
|
||||
|
||||
|
||||
# ---- registry × DB 源(reload / upsert)--------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user