"""HTTP contract tests for the Home market-data overview.""" from datetime import UTC, date, datetime from decimal import Decimal from fastapi.testclient import TestClient from zhixing_server.bootstrap.app import create_app from zhixing_server.modules.market_data.application.overview import ( MarketDataOverviewReader, ) from zhixing_server.modules.market_data.domain.overview import ( MarketDataOverview, MarketDataUpdate, OverviewBatchError, OverviewFailure, ) from zhixing_server.modules.market_data.presentation.home import ( get_market_data_overview_reader, ) TARGET_DATE = date(2026, 8, 6) class FakeOverviewReader: """Small in-memory read adapter used at the HTTP dependency seam.""" def __init__(self, overview: MarketDataOverview) -> None: self.overview = overview def read_overview(self) -> MarketDataOverview: return self.overview def get_client(overview: MarketDataOverview) -> TestClient: """Build the real application with only its read adapter overridden.""" application = create_app() reader = FakeOverviewReader(overview) def override_reader() -> MarketDataOverviewReader: return reader application.dependency_overrides[get_market_data_overview_reader] = override_reader return TestClient(application) def test_home_overview_returns_complete_success_contract() -> None: """A complete batch exposes active-pool counts and Shanghai completion time.""" response = get_client( MarketDataOverview( status="success", stock_pool_count=2, latest_update=MarketDataUpdate( trade_date=TARGET_DATE, completed_at=datetime(2026, 8, 7, 8, 30, tzinfo=UTC), status="success", target_count=2, successful_count=2, failed_count=0, valid_count=2, coverage_rate=Decimal("1"), failures=(), batch_errors=(), ), ) ).get("/api/v1/home/overview") assert response.status_code == 200 assert response.json() == { "status": "success", "stock_pool_count": 2, "latest_update": { "trade_date": "2026-08-06", "completed_at": "2026-08-07T16:30:00+08:00", "status": "success", "target_count": 2, "successful_count": 2, "failed_count": 0, "valid_count": 2, "coverage_rate": 1.0, "failures": [], "batch_errors": [], }, } def test_home_overview_keeps_stock_and_batch_failures_separate() -> None: """Partial results preserve failure categories and batch-level errors.""" response = get_client( MarketDataOverview( status="partial_success", stock_pool_count=3, latest_update=MarketDataUpdate( trade_date=TARGET_DATE, completed_at=datetime(2026, 8, 6, 16, 30), status="partial_success", target_count=3, successful_count=1, failed_count=2, valid_count=1, coverage_rate=Decimal("0.3333333333"), failures=( OverviewFailure( ts_code="000001.SZ", name="平安银行", failure_type="missing_daily_bar", reason="目标交易日缺少日线行情", trade_date=TARGET_DATE, ), OverviewFailure( ts_code="600000.SH", name="浦发银行", failure_type="recorded_stock_failure", reason="供应商请求失败", trade_date=TARGET_DATE, ), ), batch_errors=( OverviewBatchError( item_key="retention", error_type="database_error", reason="保留策略执行失败", ), ), ), ) ).get("/api/v1/home/overview") assert response.status_code == 200 payload = response.json() assert payload["latest_update"]["failed_count"] == 2 assert payload["latest_update"]["coverage_rate"] == 0.3333333333 assert payload["latest_update"]["failures"][0]["failure_type"] == "missing_daily_bar" assert payload["latest_update"]["batch_errors"] == [ { "item_key": "retention", "error_type": "database_error", "reason": "保留策略执行失败", } ] def test_home_overview_returns_updating_without_completion_time() -> None: """A running batch remains distinguishable from an absent batch.""" response = get_client( MarketDataOverview( status="updating", stock_pool_count=1, latest_update=MarketDataUpdate( trade_date=TARGET_DATE, completed_at=None, status="updating", target_count=1, successful_count=0, failed_count=1, valid_count=0, coverage_rate=Decimal("0"), failures=(), batch_errors=(), ), ) ).get("/api/v1/home/overview") assert response.status_code == 200 assert response.json()["status"] == "updating" assert response.json()["latest_update"]["completed_at"] is None def test_home_overview_returns_failed_batch_without_fabricating_stock_errors() -> None: """A failed batch keeps infrastructure errors separate from stock details.""" response = get_client( MarketDataOverview( status="failed", stock_pool_count=2, latest_update=MarketDataUpdate( trade_date=TARGET_DATE, completed_at=datetime(2026, 8, 6, 16, 30), status="failed", target_count=2, successful_count=0, failed_count=2, valid_count=0, coverage_rate=Decimal("0"), failures=(), batch_errors=( OverviewBatchError( item_key="retention", error_type="database_error", reason="保留策略执行失败", ), ), ), ) ).get("/api/v1/home/overview") assert response.status_code == 200 assert response.json()["status"] == "failed" assert response.json()["latest_update"]["failures"] == [] assert response.json()["latest_update"]["batch_errors"][0]["item_key"] == "retention" def test_home_overview_returns_no_data_without_fake_update() -> None: """No synchronization batch is represented by a null latest update.""" response = get_client( MarketDataOverview(status="no_data", stock_pool_count=7, latest_update=None) ).get("/api/v1/home/overview") assert response.status_code == 200 assert response.json() == { "status": "no_data", "stock_pool_count": 7, "latest_update": None, }