main
py 131 lines 6.11 KB
Raw
1 import pytest
2 from datetime import datetime, timedelta, timezone
3 from decimal import Decimal
4 from types import SimpleNamespace
5 from uuid import uuid4
6 import app.main as main
7
8 @pytest.mark.asyncio
9 async def test_sector_route_joins_only_active_universe_and_skips_missing_inputs(monkeypatch):
10 valid, missing, research_only = uuid4(), uuid4(), uuid4()
11 class Fact: value = "Technology"
12 class Snapshot: facts = {"sector": Fact()}
13 class Record: snapshot = Snapshot()
14 class Score: overall_score = 80; category_evidence = {}
15 observed = {}
16 async def active_global_equities(**kwargs):
17 observed.update(kwargs)
18 return [
19 {"globalInstrumentId": str(valid), "canonicalName": "Valid", "ticker": "VAL", "exchange": "XNAS", "country": "US", "currency": "USD"},
20 {"globalInstrumentId": str(missing), "canonicalName": "Missing", "ticker": "MISS", "exchange": "XNAS", "country": "US", "currency": "USD"},
21 ]
22 monkeypatch.setattr(main.portfolio_orchestrator, "active_global_equities", active_global_equities)
23 monkeypatch.setattr(main.repository, "profile", lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("leaderboard must not require a process-local profile")))
24 monkeypatch.setattr(main.repository, "summary", lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("leaderboard must not use profile-bound summary")))
25 monkeypatch.setattr(main.repository, "persisted_canonical_read_model_score", lambda key: Score() if key == valid else None)
26 monkeypatch.setattr(main.repository, "structured_market_snapshots_for", lambda _ids: {valid: [Record()]})
27 result = await main.sector_leaderboard(x_correlation_id="test-correlation", x_aip_user_id="test-user")
28 assert result["sectors"][0]["stocks"][0]["globalInstrumentId"] == str(valid)
29 assert str(research_only) not in str(result)
30 assert observed["correlation_id"] == "test-correlation"
31 assert observed["identity_headers"]["X-AIP-User-Id"] == "test-user"
32
33 async def _async(value): return value
34
35
36 @pytest.mark.asyncio
37 async def test_sector_performance_route_uses_authenticated_universe_and_persisted_prices_only(monkeypatch):
38 instrument_id = uuid4()
39 class Fact: value = "Technology"
40 class Snapshot: facts = {"sector": Fact()}
41 class Record: snapshot = Snapshot()
42 observed = {}
43 async def active_global_equities(**kwargs):
44 observed.update(kwargs)
45 return [{"globalInstrumentId": str(instrument_id), "canonicalName": "Persisted", "ticker": "PST", "exchange": "XETR", "country": "DE", "currency": "EUR"}]
46 now = datetime(2026, 9, 7, tzinfo=timezone.utc)
47 prices = [
48 SimpleNamespace(observed_at=now - timedelta(days=8), price=Decimal("100"), currency="EUR"),
49 SimpleNamespace(observed_at=now, price=Decimal("110"), currency="EUR"),
50 ]
51 monkeypatch.setattr(main.portfolio_orchestrator, "active_global_equities", active_global_equities)
52 monkeypatch.setattr(main.repository, "structured_market_snapshots_for", lambda ids: {instrument_id: [Record()]})
53 monkeypatch.setattr(main.repository, "market_price_observations_for", lambda ids: {instrument_id: prices})
54 monkeypatch.setattr(main.repository, "persisted_canonical_read_model_score", lambda *_args: (_ for _ in ()).throw(AssertionError("performance must not use research score")))
55 result = await main.sector_performance(region="EUROPE", sector="Technology", period="WEEK", limit=5, x_correlation_id="performance-test", x_aip_user_id="test-user")
56 assert result["bestPerformers"][0]["globalInstrumentId"] == str(instrument_id)
57 assert result["bestPerformers"][0]["performancePct"] == Decimal("10")
58 assert result["worstPerformers"][0]["globalInstrumentId"] == str(instrument_id)
59 assert observed["identity_headers"]["X-AIP-User-Id"] == "test-user"
60
61
62 @pytest.mark.asyncio
63 @pytest.mark.parametrize("sector", [
64 "Communication Services",
65 "Consumer Discretionary",
66 "Consumer Staples",
67 "Energy",
68 "Financials",
69 "Healthcare",
70 "Industrials",
71 "Materials",
72 "Real Estate",
73 "Technology",
74 "Utilities",
75 ])
76 async def test_every_india_canonical_sector_ranks_full_week_universe_before_top_and_worst_limit(
77 monkeypatch, sector
78 ):
79 now = datetime(2026, 9, 8, tzinfo=timezone.utc)
80 instrument_ids = [uuid4() for _ in range(7)]
81 payloads = [
82 {
83 "globalInstrumentId": str(instrument_id),
84 "canonicalName": f"{sector} {index}",
85 "ticker": f"S{index}",
86 "exchange": "NSE",
87 "country": "IN",
88 "currency": "INR",
89 "canonicalSector": sector,
90 }
91 for index, instrument_id in enumerate(instrument_ids)
92 ]
93 listings = [SimpleNamespace(as_payload=lambda value=value: value) for value in payloads]
94 prices = {
95 instrument_id: [
96 SimpleNamespace(observed_at=now - timedelta(days=8), price=Decimal("100"), currency="INR"),
97 SimpleNamespace(observed_at=now, price=Decimal(str(101 + index)), currency="INR"),
98 ]
99 for index, instrument_id in enumerate(instrument_ids)
100 }
101
102 async def durable_india_universe(**_kwargs):
103 return listings
104
105 monkeypatch.setattr(main.india_market_universe_provider, "listings", durable_india_universe)
106 monkeypatch.setattr(main.repository, "market_price_observations_for", lambda ids: {
107 instrument_id: prices[instrument_id] for instrument_id in ids
108 })
109 monkeypatch.setattr(
110 main.repository,
111 "structured_market_snapshots_for",
112 lambda _ids: (_ for _ in ()).throw(AssertionError("India sector join is durable upstream")),
113 )
114
115 result = await main.sector_performance(
116 region="INDIA",
117 sector=sector,
118 period="WEEK",
119 limit=5,
120 x_aip_user_id="test-user",
121 )
122
123 assert result["sector"] == sector
124 assert len(result["bestPerformers"]) == 5
125 assert len(result["worstPerformers"]) == 5
126 assert [row["globalInstrumentId"] for row in result["bestPerformers"]] == [
127 str(value) for value in reversed(instrument_ids[2:])
128 ]
129 assert [row["globalInstrumentId"] for row in result["worstPerformers"]] == [
130 str(value) for value in instrument_ids[:5]
131 ]