- 为后端Admin新增migration_compat工具,支持离线迁移时的基线资源检查,重构全部alembic迁移脚本适配离线模式,避免重复创建已存在的数据库表和字段 - 完善初始数据库迁移脚本,添加默认部门种子数据,新增ConciergeAdvisor详情字段从JSON到JSONB的类型转换迁移 - 小程序端新增图片资源解析工具resolveNetworkImage,实现本地静态资源到CDN的自动映射,添加图片加载容错降级机制 - 完善HeroSlide类型定义,新增actionLabel字段,优化首页轮播组件的文本渲染与错误处理逻辑 - 新增对应测试用例验证迁移路径正确性与前端数据处理逻辑 - 新增临时 brainstorm UI对比草稿与开发临时状态文件
216 lines
6.5 KiB
Python
216 lines
6.5 KiB
Python
from importlib.util import module_from_spec, spec_from_file_location
|
|
import subprocess
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
VERSIONS_DIR = Path(__file__).parents[1] / "alembic" / "versions"
|
|
|
|
|
|
def load_migration(filename: str):
|
|
path = VERSIONS_DIR / filename
|
|
spec = spec_from_file_location(f"clean_database_{path.stem}", path)
|
|
assert spec and spec.loader
|
|
migration = module_from_spec(spec)
|
|
spec.loader.exec_module(migration)
|
|
return migration
|
|
|
|
|
|
class ExistingRows:
|
|
def first(self):
|
|
return ("existing",)
|
|
|
|
|
|
class EmptyRows:
|
|
def first(self):
|
|
return None
|
|
|
|
|
|
class ExistingBind:
|
|
def execute(self, _statement):
|
|
return ExistingRows()
|
|
|
|
|
|
class EmptyBind:
|
|
def execute(self, _statement):
|
|
return EmptyRows()
|
|
|
|
|
|
class CurrentSchemaInspector:
|
|
tables = {
|
|
"AdminDepartment",
|
|
"AdminMenu",
|
|
"AdminRole",
|
|
"AdminRoleDepartment",
|
|
"AdminRoleMenu",
|
|
"AdminUser",
|
|
"AdminUserDepartment",
|
|
"AdminUserRole",
|
|
"AuditLog",
|
|
"ConciergeAdvisor",
|
|
"Customer",
|
|
"DetailRecord",
|
|
"HeroSlide",
|
|
"HomeTeamBuilding",
|
|
"HomeWanfaRecommendation",
|
|
"HomeWildArchive",
|
|
"Lead",
|
|
"LeadFollowup",
|
|
"MediaAsset",
|
|
"Order",
|
|
"VehicleOption",
|
|
"WanfaCategory",
|
|
"WanfaRoute",
|
|
}
|
|
|
|
def get_table_names(self):
|
|
return sorted(self.tables)
|
|
|
|
def get_columns(self, table_name):
|
|
if table_name not in self.tables:
|
|
return []
|
|
return [{"name": "deptId"}, {"name": "createdById"}]
|
|
|
|
def get_indexes(self, table_name):
|
|
if table_name not in self.tables:
|
|
return []
|
|
return [
|
|
{"name": f"ix_{table_name}_deptId"},
|
|
{"name": f"ix_{table_name}_createdById"},
|
|
]
|
|
|
|
def get_foreign_keys(self, table_name):
|
|
if table_name not in self.tables:
|
|
return []
|
|
return [
|
|
{
|
|
"name": f"fk_{table_name}_deptId",
|
|
"constrained_columns": ["deptId"],
|
|
"referred_table": "AdminDepartment",
|
|
"referred_columns": ["id"],
|
|
},
|
|
{
|
|
"name": f"fk_{table_name}_createdById",
|
|
"constrained_columns": ["createdById"],
|
|
"referred_table": "AdminUser",
|
|
"referred_columns": ["id"],
|
|
},
|
|
]
|
|
|
|
|
|
def test_admin_rbac_does_not_recreate_tables_already_created_by_initial_baseline(monkeypatch):
|
|
migration = load_migration("0025_admin_rbac.py")
|
|
inspector = CurrentSchemaInspector()
|
|
calls = []
|
|
|
|
monkeypatch.setattr(migration, "is_offline_mode", lambda: False, raising=False)
|
|
monkeypatch.setattr(migration, "inspect", lambda _bind: inspector, raising=False)
|
|
monkeypatch.setattr(migration.op, "get_bind", lambda: ExistingBind())
|
|
monkeypatch.setattr(
|
|
migration.op,
|
|
"create_table",
|
|
lambda *args, **_kwargs: calls.append(("create_table", args[0])),
|
|
)
|
|
monkeypatch.setattr(
|
|
migration.op,
|
|
"bulk_insert",
|
|
lambda *args, **_kwargs: calls.append(("bulk_insert", args[0].name)),
|
|
)
|
|
monkeypatch.setattr(
|
|
migration.op,
|
|
"execute",
|
|
lambda *_args, **_kwargs: calls.append(("execute",)),
|
|
)
|
|
|
|
migration.upgrade()
|
|
|
|
assert [call for call in calls if call[0] in {"create_table", "bulk_insert"}] == []
|
|
|
|
|
|
def test_initial_baseline_seeds_default_department_before_owned_data(monkeypatch):
|
|
migration = load_migration("0001_initial_schema.py")
|
|
inspector = CurrentSchemaInspector()
|
|
calls = []
|
|
|
|
monkeypatch.setattr(migration.context, "is_offline_mode", lambda: False)
|
|
monkeypatch.setattr(migration, "inspect", lambda _bind: inspector, raising=False)
|
|
monkeypatch.setattr(migration.op, "get_bind", lambda: EmptyBind())
|
|
monkeypatch.setattr(
|
|
migration.op,
|
|
"bulk_insert",
|
|
lambda table, rows: calls.append((table.name, rows)),
|
|
)
|
|
|
|
migration.upgrade()
|
|
|
|
assert [table_name for table_name, _rows in calls] == ["AdminDepartment"]
|
|
assert calls[0][1][0]["id"] == migration.DEFAULT_DEPT_ID
|
|
|
|
|
|
def test_concierge_details_migration_converts_json_to_jsonb(monkeypatch):
|
|
migration_path = VERSIONS_DIR / "0035_concierge_details_jsonb.py"
|
|
if not migration_path.exists():
|
|
pytest.fail("0035_concierge_details_jsonb.py is required to align the model type")
|
|
migration = load_migration("0035_concierge_details_jsonb.py")
|
|
calls = []
|
|
|
|
monkeypatch.setattr(
|
|
migration.op,
|
|
"alter_column",
|
|
lambda *args, **kwargs: calls.append((args, kwargs)),
|
|
)
|
|
|
|
migration.upgrade()
|
|
|
|
assert migration.down_revision == "0034_media_assets"
|
|
assert calls[0][0][:2] == ("ConciergeAdvisor", "details")
|
|
assert calls[0][1]["postgresql_using"] == '"details"::jsonb'
|
|
|
|
|
|
def test_offline_upgrade_generates_sql_for_a_clean_database():
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "alembic", "upgrade", "head", "--sql"],
|
|
cwd=VERSIONS_DIR.parents[1],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr[-4000:]
|
|
assert "0035_concierge_details_jsonb" in result.stdout
|
|
assert result.stdout.count('CREATE TABLE "AdminRole"') == 1
|
|
assert result.stdout.count('CREATE TABLE "MediaAsset"') == 1
|
|
assert 'ALTER TABLE "HomeWildArchive" ADD COLUMN images' not in result.stdout
|
|
assert 'ALTER TABLE "HomeTeamBuilding" ADD COLUMN "detailSubtitle"' not in result.stdout
|
|
assert 'ALTER TABLE "DetailRecord" ADD COLUMN "conciergeAdvisorId"' not in result.stdout
|
|
assert 'ALTER TABLE "Lead" ADD COLUMN "leadType"' not in result.stdout
|
|
|
|
|
|
def test_admin_ownership_skips_missing_and_already_owned_tables(monkeypatch):
|
|
migration = load_migration("0026_admin_ownership.py")
|
|
inspector = CurrentSchemaInspector()
|
|
calls = []
|
|
|
|
monkeypatch.setattr(migration, "is_offline_mode", lambda: False, raising=False)
|
|
monkeypatch.setattr(migration, "inspect", lambda _bind: inspector, raising=False)
|
|
monkeypatch.setattr(migration.op, "get_bind", lambda: ExistingBind())
|
|
for operation in (
|
|
"add_column",
|
|
"create_index",
|
|
"create_foreign_key",
|
|
"execute",
|
|
"alter_column",
|
|
):
|
|
monkeypatch.setattr(
|
|
migration.op,
|
|
operation,
|
|
lambda *args, _operation=operation, **_kwargs: calls.append(
|
|
(_operation, args[0] if args else None)
|
|
),
|
|
)
|
|
|
|
migration.upgrade()
|
|
|
|
assert calls == []
|