main
py 74 lines 5.58 KB
Raw
1 from uuid import uuid4
2 from unittest.mock import AsyncMock
3 import httpx
4 import pytest
5 from fastapi.testclient import TestClient
6 from app import main
7 from app.models import StructuredInstrumentResolution
8 from app.structured_market import StructuredProviderError
9 from test_structured_market import provider, response, instrument
10
11 @pytest.mark.asyncio
12 @pytest.mark.parametrize('symbol,isin,name', [('ALPHA','INE111A01010','Alpha Components Limited'),('BETA','INE222A01010','Beta Engineering Limited'),('GAMMA','INE333A01010','Gamma Services Limited')])
13 async def test_trusted_official_identity_resolves_generically_without_research_acquisition(symbol,isin,name):
14 calls=[]
15 def handler(request):
16 calls.append(request.url.path)
17 assert request.url.params['newsCount']=='0'
18 return response({'quotes':[{'symbol':symbol+'.NS','exchange':'NSI','quoteType':'EQUITY','longname':name,'isin':isin}]})
19 resolved=await provider(handler).resolve_instrument(instrument(canonicalName=name,isin=isin,structuredNseCandidateTicker=symbol+'.NS',structuredNseCandidateSource='VERIFIED_NSE'))
20 assert resolved.provider_ticker==symbol+'.NS'
21 assert resolved.status=='VERIFIED_NSE_CANDIDATE'
22 assert calls==['/v1/finance/search']
23
24 @pytest.mark.asyncio
25 @pytest.mark.parametrize('case',['mismatched_isin','ambiguous','unavailable'])
26 async def test_mapping_validation_fails_closed(case):
27 quote={'symbol':'ALPHA.NS','exchange':'NSI','quoteType':'EQUITY','longname':'Alpha Components Limited','isin':'INE111A01010'}
28 if case=='mismatched_isin':quote['isin']='INE222A01010'
29 def handler(request):return response({'quotes':[quote,quote] if case=='ambiguous' else [quote]},503 if case=='unavailable' else 200)
30 with pytest.raises(StructuredProviderError,match='UNAVAILABLE' if case=='unavailable' else 'COMPANY_NOT_RESOLVED'):
31 await provider(handler).resolve_instrument(instrument(canonicalName='Alpha Components Limited',isin='INE111A01010',structuredNseCandidateTicker='ALPHA.NS',structuredNseCandidateSource='VERIFIED_NSE'))
32
33 def test_internal_identity_route_never_collects_snapshot(monkeypatch):
34 from datetime import datetime, timezone
35 resolver=AsyncMock(return_value=StructuredInstrumentResolution(instrument_id=uuid4(),provider='YAHOO_FINANCE',provider_ticker='ALPHA.NS',company_name='Alpha Components Limited',confidence=.95,resolved_at=datetime.now(timezone.utc),status='VERIFIED_NSE_CANDIDATE'))
36 collect=AsyncMock(side_effect=AssertionError('Research acquisition forbidden'))
37 monkeypatch.setattr(main.portfolio_orchestrator.structured_provider,'resolve_instrument',resolver)
38 monkeypatch.setattr(main.portfolio_orchestrator.structured_provider,'collect',collect)
39 payload={'assetType':'EQUITY','isin':'INE111A01010','structuredNseCandidateSource':'VERIFIED_NSE','structuredNseCandidateTicker':'ALPHA.NS'}
40 client=TestClient(main.app)
41 assert client.post('/internal/v1/research/instruments/resolve-provider',json=payload).status_code==403
42 assert client.post('/internal/v1/research/instruments/resolve-provider',json=payload,headers={'X-AIP-Service-Identity':'portfolio-service'}).status_code==200
43 collect.assert_not_called()
44
45 @pytest.mark.asyncio
46 async def test_targeted_ensure_uses_mapping_added_after_initial_canonical_read():
47 from app.repository import ResearchRepository
48 from app.persistence import SqliteResearchPersistence
49 from app.settings import Settings
50 from app.portfolio_orchestration import PortfolioResearchOrchestrator
51 from app.research_readiness_runtime import ResearchReadinessRuntime, RepositoryResearchReadinessAdapter
52 from app.yahoo_mcp_acquisition import McpFirstResearchCapabilityExecutor
53 from test_yahoo_mcp_acquisition import INSTRUMENT_ID, FakeGateway, FakeLegacy
54 repository=ResearchRepository(settings=Settings(research_live_enabled=False,research_demo_enabled=False),persistence=SqliteResearchPersistence())
55 orchestrator=PortfolioResearchOrchestrator(repository,repository.settings)
56 metadata={'globalInstrumentId':str(INSTRUMENT_ID),'canonicalName':'Ready Limited','isin':'INE111A01010','assetType':'EQUITY','currency':'INR','country':'IN','primaryExchange':'NSE','primarySymbol':'READY','status':'ACTIVE','providerMappings':[{'provider':'NSE','providerSymbol':'READY','status':'VERIFIED','exchange':'NSE','currency':'INR'}]}
57 assert orchestrator.register_global_profile_metadata(INSTRUMENT_ID,metadata)
58 assert 'YAHOO_FINANCE' not in repository.profile(INSTRUMENT_ID).provider_instrument_ids
59 metadata['providerMappings'].append({'provider':'YAHOO_FINANCE','providerSymbol':'READY.NS','status':'VERIFIED','exchange':'NSE','currency':'INR','resolutionSource':'YAHOO_FROM_VERIFIED_NSE'})
60 assert orchestrator.register_global_profile_metadata(INSTRUMENT_ID,metadata)
61 class VerifiedGateway(FakeGateway):
62 async def acquire_requirement(self,profile,**kwargs):
63 assert profile.instrument_id==INSTRUMENT_ID
64 assert profile.provider_instrument_ids['YAHOO_FINANCE']=='READY.NS'
65 return await super().acquire_requirement(profile,**kwargs)
66 gateway=VerifiedGateway()
67 legacy=FakeLegacy()
68 executor=McpFirstResearchCapabilityExecutor(legacy,repository,gateway,enabled=True)
69 runtime=ResearchReadinessRuntime(repository,RepositoryResearchReadinessAdapter(repository),executor)
70 result=await runtime.ensure(INSTRUMENT_ID,jurisdiction='INDIA',requirement_ids=['LATEST_PRICE'])
71 assert 'VERIFIED_YAHOO_MAPPING_REQUIRED' not in str(result.failures)
72 assert gateway.calls
73 assert repository.market_price_observations_for({INSTRUMENT_ID})[INSTRUMENT_ID]
74 assert repository.financial_facts_for(INSTRUMENT_ID)==[]