"""数据质量检查回归测试。 背景(P0-02 遗留): MatchStats 主键改为 match_id 后,质量检查查询仍引用 MatchStats.id → AttributeError → POST /admin/data-quality/run 必然 500, 前端显示「运行失败」;且 handler 无日志,未捕获异常走 uvicorn.error (propagate=False)不进内存缓冲/文件日志,排障时无据可查。 """ from __future__ import annotations from unittest.mock import AsyncMock, MagicMock from src.api.routes import admin_quality def _fake_db(scalars: list[int]) -> MagicMock: """按顺序返回 scalar() 计数的假 AsyncSession。""" db = MagicMock() results = [] for v in scalars: r = MagicMock() r.scalar.return_value = v results.append(r) db.execute = AsyncMock(side_effect=results) db.add = MagicMock() db.commit = AsyncMock() return db async def test_run_data_quality_check_no_attribute_error(): """检查查询不得引用 MatchStats.id(P0-02 后该属性不存在)。 修复前: run_data_quality_check 抛 AttributeError → 500。 """ db = _fake_db([3, 1]) out = await admin_quality.run_data_quality_check(db) assert out["ok"] is True assert out["checks"] == [ {"name": "finished_without_stats", "passed": False}, {"name": "league_without_standings", "passed": False}, ] # 检查结果落库(2 条 DataQualityCheck) assert db.add.call_count == 2 db.commit.assert_awaited_once() async def test_run_data_quality_check_all_passed(): db = _fake_db([0, 0]) out = await admin_quality.run_data_quality_check(db) assert out["ok"] is True assert all(c["passed"] for c in out["checks"]) async def test_run_data_quality_check_logs_summary(caplog): """成功路径必须留日志:否则线上无从得知检查何时跑过、结果如何。""" import logging db = _fake_db([0, 0]) with caplog.at_level(logging.INFO, logger="src.api.routes.admin_quality"): await admin_quality.run_data_quality_check(db) assert any("数据质量检查" in r.message for r in caplog.records)