PayrollManagementSystem/backend/tests/integration/test_schema_sync.py
liuwenxin 273ebf4657 chore: 初始化项目结构与登录鉴权模块
后端 FastAPI 六层结构(api/services/repositories/models/schemas/core),
统一响应信封、全局异常、日志脱敏;6 张表经 Alembic 迁移建库,SQL 留存
db/migration;登录鉴权含图形验证码(Redis GETDEL 防重放)、JWT、登录限流、
登出黑名单(TTL=令牌剩余有效期)、登录日志。

前端 Vue3 + TS + Element Plus 骨架,含登录页、路由守卫、请求封装与 Vite 代理。

工程配置:uv 锁定 Python 3.12、ruff 检查与格式化、editorconfig、gitattributes。

测试 184 项(单元 155 / 集成 29)全绿,浏览器端到端 8 项通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-09 18:46:36 +08:00

90 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""DDL 漂移检测。
结构定义同时存在于两处:
- `db/migration/V1__*.sql` —— 事实来源,生产迁移用
- `db/init/init-dev.sql` —— 开发环境重置脚本,Docker 首次启动唯一会自动执行的位置
两份 DDL 手工同步迟早会漂移,且后果隐蔽:开发库的表结构与迁移脚本不一致,
新人克隆下来建出的库和生产不一样,等到上线才发现。
本测试把「靠自觉」变成「可自动拦截」,成本极低。
注:本文件不需要容器,放在 integration 下是因为它校验的是交付产物而非应用代码。
"""
import re
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[3]
MIGRATION_FILE = PROJECT_ROOT / "db" / "migration" / "V1__init_rbac_and_login_logs.sql"
INIT_DEV_FILE = PROJECT_ROOT / "db" / "init" / "init-dev.sql"
_CREATE_TABLE = re.compile(r"CREATE TABLE\s+`?(\w+)`?\s*\((.*?)\n\)", re.DOTALL)
_COLUMN_LINE = re.compile(r"^\s{4}`?(\w+)`?\s", re.MULTILINE)
# 这些开头的是约束定义,不是列定义
_NON_COLUMN_KEYWORDS = {"CONSTRAINT", "PRIMARY", "UNIQUE", "FOREIGN", "KEY", "INDEX"}
def _parse_tables(sql: str) -> dict[str, set[str]]:
"""从 SQL 文本中提取 {表名: {列名集合}}。"""
tables: dict[str, set[str]] = {}
for table_name, body in _CREATE_TABLE.findall(sql):
tables[table_name] = {
column
for column in _COLUMN_LINE.findall(body)
if column.upper() not in _NON_COLUMN_KEYWORDS
}
return tables
@pytest.fixture(scope="module")
def migration_tables() -> dict[str, set[str]]:
return _parse_tables(MIGRATION_FILE.read_text(encoding="utf-8"))
@pytest.fixture(scope="module")
def init_dev_tables() -> dict[str, set[str]]:
return _parse_tables(INIT_DEV_FILE.read_text(encoding="utf-8"))
class TestSchemaSync:
"""两份 DDL 的结构一致性。"""
def test_文件存在(self) -> None:
assert MIGRATION_FILE.is_file(), f"缺少迁移脚本:{MIGRATION_FILE}"
assert INIT_DEV_FILE.is_file(), f"缺少开发初始化脚本:{INIT_DEV_FILE}"
def test_表集合一致(self, migration_tables: dict, init_dev_tables: dict) -> None:
assert set(init_dev_tables) == set(migration_tables), (
"两份 DDL 的表集合不一致 —— 表结构变更时需同时维护 "
"db/migration/V{n}__*.sql 与 db/init/init-dev.sql"
)
def test_每张表的列集合一致(self, migration_tables: dict, init_dev_tables: dict) -> None:
mismatched = {
table: {
"仅迁移脚本有": sorted(migration_tables[table] - init_dev_tables[table]),
"仅初始化脚本有": sorted(init_dev_tables[table] - migration_tables[table]),
}
for table in set(migration_tables) & set(init_dev_tables)
if migration_tables[table] != init_dev_tables[table]
}
assert not mismatched, f"以下表的列不一致:{mismatched}"
class TestSeedData:
"""字典数据在两处应保持同步。"""
@pytest.mark.parametrize("role_code", ["admin", "user"])
def test_角色初始化数据在两处都存在(self, role_code: str) -> None:
migration_sql = MIGRATION_FILE.read_text(encoding="utf-8")
init_sql = INIT_DEV_FILE.read_text(encoding="utf-8")
assert f"'{role_code}'" in migration_sql
assert f"'{role_code}'" in init_sql
def test_开发脚本标记了迁移版本(self) -> None:
"""不写 alembic_version 的话,在开发库上执行 upgrade head 会重复建表而失败。"""
init_sql = INIT_DEV_FILE.read_text(encoding="utf-8")
assert "INSERT INTO alembic_version" in init_sql