from __future__ import annotations import unittest from datetime import datetime, timedelta from backend.data import ( DataPolicyError, DataQualityError, DataSourcePolicy, QualityEvidence, build_data_gateway, ) from backend.data.quality import market_timezone class DataGatewayTests(unittest.TestCase): def test_policy_allows_registered_calculation_source(self) -> None: policy = DataSourcePolicy.load() contract = policy.assert_allowed( "market.stock_daily", "tushare", "calculation" ) self.assertEqual(contract.primary, "tushare") def test_policy_rejects_public_web_source_for_calculation(self) -> None: policy = DataSourcePolicy.load() with self.assertRaises(DataPolicyError): policy.assert_allowed( "observation.realtime_indices", "eastmoney", "calculation" ) def test_policy_rejects_blocked_dataset(self) -> None: policy = DataSourcePolicy.load() with self.assertRaises(DataPolicyError): policy.assert_allowed("market.level2", "unresolved", "display") def test_gateway_uses_live_token_supplier_and_shared_ifind(self) -> None: token = {"value": "first"} gateway = build_data_gateway( {"ifind_refresh_token": "refresh", "ifind_access_token": "access"}, lambda: token["value"], ) self.assertEqual(gateway.tushare().token, "first") token["value"] = "second" self.assertEqual(gateway.tushare().token, "second") self.assertIs(gateway.chart_data.ifind, gateway.ifind) def test_server_has_no_direct_runtime_tushare_construction(self) -> None: from pathlib import Path source = (Path(__file__).resolve().parents[1] / "server.py").read_text(encoding="utf-8") self.assertEqual(source.count("TushareClient(self.token)"), 1) self.assertIn("return gateway.tushare()", source) def test_quality_gate_accepts_matching_daily_evidence(self) -> None: timezone = market_timezone() now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone) gateway = build_data_gateway({}) report = gateway.require_quality( QualityEvidence( dataset_id="market.stock_daily", provider_id="tushare", data_time="2026-07-29", observed_at=now, actual_count=5000, expected_count=5000, adjustment="current-unadjusted", units={ "open": "CNY/share", "high": "CNY/share", "low": "CNY/share", "close": "CNY/share", "pct_chg": "percent", "volume_shares": "share", "amount_yuan": "CNY", }, ), "calculation", now, ) self.assertTrue(report.accepted) self.assertEqual(report.coverage_ratio, 1.0) def test_quality_gate_rejects_stale_dynamic_auction(self) -> None: timezone = market_timezone() now = datetime(2026, 7, 29, 9, 24, tzinfo=timezone) gateway = build_data_gateway({}) with self.assertRaises(DataQualityError): gateway.require_quality( QualityEvidence( dataset_id="market.auction_dynamic", provider_id="ifind", data_time=now - timedelta(seconds=30), observed_at=now - timedelta(seconds=29), units={ "price": "CNY/share", "volume_shares": "share", "amount_yuan": "CNY", "pre_close": "CNY/share", "turnover_rate_pct": "percent", "volume_ratio": "ratio", "float_share": "share", }, ), "calculation", now, ) def test_quality_gate_rejects_low_coverage_and_wrong_adjustment(self) -> None: timezone = market_timezone() now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone) gateway = build_data_gateway({}) report = gateway.quality.evaluate( QualityEvidence( dataset_id="market.stock_daily", provider_id="tushare", data_time="2026-07-29", observed_at=now, actual_count=4000, expected_count=5000, adjustment="forward1", ), "calculation", now, ) self.assertFalse(report.accepted) self.assertTrue(any("Coverage" in issue for issue in report.issues)) self.assertTrue(any("Adjustment" in issue for issue in report.issues)) def test_quality_gate_enforces_financial_point_in_time(self) -> None: timezone = market_timezone() now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone) gateway = build_data_gateway({}) report = gateway.quality.evaluate( QualityEvidence( dataset_id="market.fundamentals", provider_id="tushare", data_time="2026-06-30", observed_at=now, available_at="2026-08-15", ), "calculation", now, ) self.assertFalse(report.accepted) self.assertTrue(any("not available" in issue for issue in report.issues)) def test_provider_chain_never_silently_promotes_display_fallback(self) -> None: gateway = build_data_gateway({}) self.assertEqual( gateway.provider_chain("chart.intraday", "display"), ("ifind", "eastmoney"), ) with self.assertRaises(RuntimeError): gateway.provider_chain("chart.intraday", "calculation") if __name__ == "__main__": unittest.main()