Files
xiaobai-review/tests/test_data_gateway.py
T
8a7d1f3698 fix(HEL-494): 网站市场客户端改为纯中枢 Facade,并迁移 iFinD 凭据到中枢
生产 gateway 不再读取 Tushare token 或实例化 TushareProvider/TushareClient;问财凭据经带鉴权的中枢接口加密入库,避免发版后 iFinD 未配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-08 23:37:53 +08:00

205 lines
8.1 KiB
Python

from __future__ import annotations
import ast
import unittest
from datetime import datetime, timedelta
from pathlib import Path
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_hub_facade_and_proxies(self) -> None:
gateway = build_data_gateway(
{"ifind_refresh_token": "refresh", "ifind_access_token": "access"},
lambda: "must-not-be-used",
)
client = gateway.tushare()
self.assertEqual(client.token, "datahub")
self.assertIsNone(client.realtime_aggregator)
self.assertFalse(hasattr(client, "_legacy"))
self.assertIs(gateway.ifind, gateway.ifind_provider.client)
self.assertIs(gateway.chart_data.datahub, gateway.datahub)
self.assertIsNone(gateway.chart_data.ifind)
from backend.data.datahub.bridge import DatahubAwareTushareClient
from backend.data.datahub.ifind_proxy import HubIfindProxy
from backend.data.datahub.realtime_proxy import HubRealtimeProxy
self.assertIsInstance(client, DatahubAwareTushareClient)
self.assertIsInstance(gateway.ifind, HubIfindProxy)
self.assertIsInstance(gateway.realtime_observer, HubRealtimeProxy)
def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
source = (
Path(__file__).resolve().parents[1]
/ "backend"
/ "features"
/ "market"
/ "service.py"
).read_text(encoding="utf-8")
self.assertEqual(source.count("TushareClient(self.token)"), 1)
self.assertIn("return gateway.tushare()", source)
def test_provider_construction_has_unique_declared_owners(self) -> None:
root = Path(__file__).resolve().parents[1]
owners = {
"IfindProvider": {"backend/data/gateway.py"},
"MarketChartClient": {"backend/data/gateway.py"},
"TushareClient": {"backend/features/market/service.py"},
"DatahubClient": {"backend/data/gateway.py"},
"DatahubAwareTushareClient": {"backend/data/gateway.py"},
"DatahubBridge": {"backend/data/gateway.py"},
"HubIfindProxy": {"backend/data/gateway.py"},
"HubRealtimeProxy": {"backend/data/gateway.py"},
}
found = {name: set() for name in owners}
forbidden = {
"IfindHttpClient": set(),
"EastmoneyChartClient": set(),
"WebRealtimeAggregator": set(),
"TushareProvider": set(),
}
found_forbidden = {name: set() for name in forbidden}
for path in (root / "backend").rglob("*.py"):
relative = path.relative_to(root).as_posix()
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
name = getattr(node.func, "id", None) or getattr(node.func, "attr", None)
if name in found:
found[name].add(relative)
if name in found_forbidden:
found_forbidden[name].add(relative)
self.assertEqual(found, owners)
self.assertEqual(found_forbidden, forbidden)
provider_source = (root / "backend/data/providers/tushare.py").read_text(
encoding="utf-8"
)
self.assertIn("client_factory: Callable[[str], TushareClient] = TushareClient", provider_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()