main
py 100 lines 5.12 KB
Raw
1 from uuid import uuid4
2
3 import httpx
4 import pytest
5
6 from app.international_fundamentals import InternationalFundamentalsResult, SecEdgarFundamentalProvider, EodhdFundamentalProvider
7 from app.models import CompanyResearchProfile
8 from app.portfolio_orchestration import PortfolioResearchOrchestrator
9 from app.repository import ResearchRepository
10 from app.settings import Settings
11
12
13 def profile(**changes):
14 value = dict(instrument_id=uuid4(), company_id=uuid4(), company_name="Example", ticker="MSFT", exchange="XNAS", mic="XNAS", country="US", currency="USD")
15 value.update(changes)
16 return CompanyResearchProfile(**value)
17
18
19 class Provider:
20 def __init__(self, result=None, error=None): self.result, self.error = result, error
21 async def collect(self, _profile):
22 if self.error: raise self.error
23 return self.result
24
25
26 @pytest.mark.asyncio
27 @pytest.mark.parametrize(("provider_id", "ticker"), [("SEC_CIK", "0000789019"), ("EODHD", "AIXA.F")])
28 async def test_verified_mapping_is_written_once_through_portfolio_boundary(monkeypatch, provider_id, ticker):
29 captured = []
30 async def handler(request):
31 captured.append(request)
32 return httpx.Response(200, json={})
33 p = profile()
34 client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
35 provider = Provider(InternationalFundamentalsResult([], {provider_id: ticker}))
36 monkeypatch.setattr("app.portfolio_orchestration.international_provider_for", lambda *_args, **_kwargs: provider)
37 orchestrator = PortfolioResearchOrchestrator(ResearchRepository(Settings()), Settings(), client=client)
38 await orchestrator.refresh_international_fundamentals(
39 p,
40 correlation_id="mapping-correlation",
41 identity_headers={"X-AIP-User-Id": "internal-research", "X-AIP-User-Roles": "ADMIN"},
42 )
43 assert len(captured) == 1
44 assert captured[0].method == "PUT"
45 assert captured[0].url.path == f"/api/v1/instruments/{p.instrument_id}/provider-mappings/verified"
46 assert captured[0].headers["x-aip-user-id"] == "internal-research"
47 assert captured[0].headers["x-aip-user-roles"] == "ADMIN"
48 assert captured[0].headers["x-correlation-id"] == "mapping-correlation"
49 payload = __import__("json").loads(captured[0].content)
50 assert payload["provider"] == provider_id and payload["providerInstrumentId"] == ticker
51 assert payload["resolutionSource"] == "RESEARCH_ENGINE_VERIFIED_FUNDAMENTALS" and payload["confidence"] == 0.90
52 await client.aclose()
53
54
55 @pytest.mark.asyncio
56 async def test_empty_or_failed_provider_never_writes_mapping(monkeypatch):
57 calls = []
58 async def handler(request): calls.append(request); return httpx.Response(200)
59 client = httpx.AsyncClient(transport=httpx.MockTransport(handler)); p = profile()
60 orchestrator = PortfolioResearchOrchestrator(ResearchRepository(Settings()), Settings(), client=client)
61 monkeypatch.setattr("app.portfolio_orchestration.international_provider_for", lambda *_args, **_kwargs: Provider(InternationalFundamentalsResult([], {})))
62 await orchestrator.refresh_international_fundamentals(p)
63 monkeypatch.setattr("app.portfolio_orchestration.international_provider_for", lambda *_args, **_kwargs: Provider(error=RuntimeError("verification failed")))
64 with pytest.raises(RuntimeError): await orchestrator.refresh_international_fundamentals(p)
65 assert calls == []
66 await client.aclose()
67
68
69 @pytest.mark.asyncio
70 async def test_trusted_sec_cik_bypasses_ticker_discovery(monkeypatch):
71 p = profile(provider_instrument_ids={"SEC_CIK": "0000789019"}); calls = []
72 async def forbidden(*_args): raise AssertionError("CIK discovery must not run")
73 def handler(request):
74 calls.append(request.url.path)
75 return httpx.Response(200, json={"facts": {"us-gaap": {}}})
76 provider = SecEdgarFundamentalProvider(Settings(), client=httpx.AsyncClient(transport=httpx.MockTransport(handler)))
77 monkeypatch.setattr(provider, "_resolve_cik", forbidden)
78 result = await provider.collect(p)
79 assert result.verified_provider_ids == {"SEC_CIK": "0000789019"}
80 assert calls == ["/api/xbrl/companyfacts/CIK0000789019.json"]
81
82
83 @pytest.mark.asyncio
84 async def test_trusted_eodhd_symbol_bypasses_ticker_fallback():
85 p = profile(provider_instrument_ids={"EODHD": "AIXA.F"}); paths = []
86 def handler(request):
87 paths.append(request.url.path)
88 return httpx.Response(200, json={"General": {"Exchange": "XNAS", "CurrencyCode": "USD"}, "Financials": {}})
89 provider = EodhdFundamentalProvider(Settings(eodhd_api_key="x", eodhd_base_url="https://eod.test"), client=httpx.AsyncClient(transport=httpx.MockTransport(handler)))
90 result = await provider.collect(p)
91 assert result.verified_provider_ids == {"EODHD": "AIXA.F"}
92 assert paths == ["/fundamentals/AIXA.F"]
93
94
95 @pytest.mark.asyncio
96 async def test_india_never_enters_mapping_boundary(monkeypatch):
97 p = profile(country="IN", exchange="NSE", mic="XNSE")
98 monkeypatch.setattr("app.portfolio_orchestration.international_provider_for", lambda *_args, **_kwargs: None)
99 orchestrator = PortfolioResearchOrchestrator(ResearchRepository(Settings()), Settings())
100 assert await orchestrator.refresh_international_fundamentals(p) is None