Files
zhixing-system/zhixing-server/tests/test_home_overview_http.py
T
2026-08-07 13:29:31 +08:00

220 lines
7.2 KiB
Python

"""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,
}