main
py 142 lines 4.83 KB
Raw
1 from __future__ import annotations
2
3 from datetime import datetime, timezone
4 from typing import Any
5 from uuid import UUID
6
7 import pytest
8
9 from app.contracts import McpAuthContext, McpAuthenticationType
10 from app.server import McpGatewayContainer, create_container
11 from app.settings import McpGatewaySettings
12
13
14 INSTRUMENT_ID = UUID("11111111-1111-4111-8111-111111111111")
15 WATCHLIST_ID = UUID("22222222-2222-4222-8222-222222222222")
16 USER_ID = UUID("33333333-3333-4333-8333-333333333333")
17 NOW = datetime(2026, 9, 11, 10, 0, tzinfo=timezone.utc)
18
19
20 class FakeApplicationReader:
21 def __init__(self) -> None:
22 self.calls: list[tuple[str, dict[str, Any], McpAuthContext, str]] = []
23 self.provider_calls = 0
24 self.closed = False
25 self.raise_error: Exception | None = None
26
27 async def invoke(
28 self,
29 tool: str,
30 arguments: dict[str, Any],
31 auth: McpAuthContext,
32 request_id: str,
33 ) -> dict[str, Any]:
34 self.calls.append((tool, arguments, auth, request_id))
35 if self.raise_error is not None:
36 raise self.raise_error
37 instrument_id = arguments.get("globalInstrumentId")
38 responses: dict[str, dict[str, Any]] = {
39 "get_research_readiness": {
40 "globalInstrumentId": instrument_id,
41 "overallStatus": "READY",
42 "requirements": [{"requirementId": "QUARTERLY_FINANCIALS", "status": "READY_FRESH"}],
43 "generatedAt": NOW.isoformat(),
44 },
45 "get_company_analysis": {
46 "globalInstrumentId": instrument_id,
47 "ruleEngineVersion": "STOCK_RULE_ENGINE_V1",
48 "score": 78,
49 "decision": "RESEARCH_READY",
50 "generatedAt": NOW.isoformat(),
51 },
52 "get_financial_facts": {
53 "globalInstrumentId": instrument_id,
54 "financialResultHistory": [{"period": "2026-Q2", "revenue": 100}],
55 },
56 "get_quarterly_results": {
57 "globalInstrumentId": instrument_id,
58 "latestQuarterlyResult": {"period": "2026-Q2", "revenue": 100},
59 },
60 "get_shareholding": {
61 "globalInstrumentId": instrument_id,
62 "shareholdingSnapshots": [{"period": "2026-Q2", "promoterPercent": 51.0}],
63 },
64 "get_recent_news": {
65 "globalInstrumentId": instrument_id,
66 "days": arguments.get("days", 30),
67 "news": [
68 {
69 "title": "Quarterly results published",
70 "publicationDate": NOW.isoformat(),
71 "source": {"type": "EXCHANGE_FILING", "url": "https://example.test/filing"},
72 }
73 ],
74 },
75 "get_sector_performance": {
76 "region": arguments.get("region"),
77 "sector": arguments.get("sector"),
78 "period": arguments.get("period"),
79 "leaders": [{"globalInstrumentId": str(INSTRUMENT_ID), "returnPercent": 3.1}],
80 "asOf": NOW.isoformat(),
81 },
82 "search_research_evidence": {
83 "globalInstrumentId": instrument_id,
84 "query": arguments.get("query"),
85 "matches": [{"documentId": "doc-1", "sourceName": "NSE"}],
86 },
87 "get_watchlist": {
88 "watchlistId": arguments.get("watchlistId"),
89 "userId": str(auth.user_id) if auth.user_id else None,
90 "instruments": [{"globalInstrumentId": str(INSTRUMENT_ID)}],
91 },
92 }
93 return responses[tool]
94
95 async def close(self) -> None:
96 self.closed = True
97
98
99 class RecordingAudit:
100 def __init__(self) -> None:
101 self.events: list[dict[str, Any]] = []
102
103 def emit(self, event: str, **kwargs: Any) -> None:
104 self.events.append({"event": event, **kwargs})
105
106
107 @pytest.fixture
108 def settings() -> McpGatewaySettings:
109 return McpGatewaySettings(
110 AIP_ENVIRONMENT="TEST",
111 AIP_MCP_SERVICE_IDENTITY="mcp-test-service",
112 AIP_MCP_AUTHENTICATION_TYPE="TEST",
113 AIP_MCP_SCOPES="mcp:read,watchlist:read",
114 AIP_MCP_LOCAL_USER_ID=str(USER_ID),
115 AIP_MCP_INVOCATION_TIMEOUT_SECONDS="0.1",
116 )
117
118
119 @pytest.fixture
120 def auth() -> McpAuthContext:
121 return McpAuthContext(
122 userId=USER_ID,
123 serviceIdentity="mcp-test-service",
124 roles=("MCP_READER",),
125 scopes=("mcp:read", "watchlist:read"),
126 authenticationType=McpAuthenticationType.TEST,
127 )
128
129
130 @pytest.fixture
131 def reader() -> FakeApplicationReader:
132 return FakeApplicationReader()
133
134
135 @pytest.fixture
136 def audit() -> RecordingAudit:
137 return RecordingAudit()
138
139
140 @pytest.fixture
141 def container(settings, reader, audit) -> McpGatewayContainer:
142 return create_container(settings, reader=reader, audit=audit)