summaryrefslogtreecommitdiff
path: root/backend/tests/test_api.py
blob: 70b67a72b841ffe57076a02068ea7aa5be0a4dee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import pandas as pd
import pytest
from fastapi import HTTPException

from app import main
from app.services import data_service


def test_health() -> None:
    assert main.health() == {"status": "ok"}


def test_search_smoke(monkeypatch) -> None:
    monkeypatch.setattr(main.data_service, "search_tickers", lambda q: [{"symbol": "AAPL", "name": "Apple Inc.", "exchange": "NASDAQ"}])
    assert main.search("apple")[0]["symbol"] == "AAPL"


def test_watchlist_smoke(tmp_path, monkeypatch) -> None:
    monkeypatch.setattr(main, "DB_PATH", tmp_path / "prism.db")
    monkeypatch.setattr(main.data_service, "get_company_info", lambda symbol: {"currentPrice": 100.0, "previousClose": 95.0})
    res = main.add_watchlist_symbol("aapl")
    assert res["items"][0]["symbol"] == "AAPL"


def test_mocked_ticker_overview(monkeypatch) -> None:
    monkeypatch.setattr(
        main.data_service,
        "get_ticker_overview",
        lambda symbol: {
            "profile": {"symbol": "AAPL", "name": "Apple Inc.", "sector": None, "industry": None, "exchange": "NASDAQ", "website": None, "summary": None},
            "quote": {"price": 100.0, "prev_close": 98.0, "change": 2.0, "change_pct": 0.0204},
            "signals": [],
            "stats": {"market_cap": None, "trailing_pe": None, "trailing_eps": None, "volume": None, "average_volume": None, "beta": None},
            "range_52w": {"low": None, "high": None, "price": 100.0},
            "short_interest": {"short_percent_of_float": None, "short_ratio": None, "shares_short": None, "shares_short_prior_month": None, "shares_short_delta_pct": None},
            "meta": {"status": "partial", "is_partial": True, "field_availability": {}, "sources": {}},
        },
    )
    assert main.ticker_overview("AAPL")["profile"]["symbol"] == "AAPL"


def test_service_overview_prefers_info_fields(monkeypatch) -> None:
    monkeypatch.setattr(
        data_service,
        "get_company_info",
        lambda symbol: {
            "longName": "Apple Inc.",
            "exchange": "NMS",
            "currentPrice": 190.0,
            "previousClose": 188.0,
            "marketCap": 2_900_000_000_000,
            "trailingPE": 31.2,
            "trailingEps": 6.08,
            "volume": 50_000_000,
            "averageVolume": 60_000_000,
            "beta": 1.18,
            "fiftyTwoWeekHigh": 199.0,
            "fiftyTwoWeekLow": 164.0,
        },
    )
    monkeypatch.setattr(data_service, "get_fast_info", lambda symbol: {"lastPrice": 1.0, "exchange": "NYQ"})
    monkeypatch.setattr(data_service, "get_price_history", lambda symbol, period="1m": [])
    monkeypatch.setattr(data_service, "_pick_search_match", lambda symbol: {"symbol": "AAPL", "name": "Wrong", "exchange": "NYSE"})
    monkeypatch.setattr(data_service, "get_profile_enrichment", lambda symbol: {})

    overview = data_service.get_ticker_overview("AAPL")
    assert overview is not None
    assert overview["profile"]["name"] == "Apple Inc."
    assert overview["profile"]["exchange"] == "NASDAQ"
    assert overview["quote"]["price"] == 190.0
    assert overview["stats"]["market_cap"] == 2_900_000_000_000
    assert overview["meta"]["sources"]["profile.name"] == "info"
    assert overview["meta"]["sources"]["quote.price"] == "info"


def test_service_overview_falls_back_to_fast_info(monkeypatch) -> None:
    monkeypatch.setattr(data_service, "get_company_info", lambda symbol: {})
    monkeypatch.setattr(
        data_service,
        "get_fast_info",
        lambda symbol: {
            "lastPrice": 100.0,
            "previousClose": 98.0,
            "marketCap": 2_000_000_000,
            "lastVolume": 1_500_000,
            "threeMonthAverageVolume": 1_250_000,
            "yearHigh": 130.0,
            "yearLow": 90.0,
            "exchange": "NMS",
        },
    )
    monkeypatch.setattr(data_service, "get_price_history", lambda symbol, period="1m": [])
    monkeypatch.setattr(data_service, "_pick_search_match", lambda symbol: {"symbol": "AAPL", "name": "Apple Inc.", "exchange": "NASDAQ"})
    monkeypatch.setattr(data_service, "get_profile_enrichment", lambda symbol: {})

    overview = data_service.get_ticker_overview("AAPL")
    assert overview is not None
    assert overview["quote"]["price"] == 100.0
    assert overview["quote"]["prev_close"] == 98.0
    assert overview["stats"]["average_volume"] == 1_250_000
    assert overview["meta"]["sources"]["quote.price"] == "fast_info"
    assert overview["meta"]["sources"]["range_52w.high"] == "fast_info"


def test_service_overview_falls_back_to_search_and_history(monkeypatch) -> None:
    month_history = [
        {"date": "2026-01-01", "close": 98.0, "volume": 1000.0},
        {"date": "2026-01-02", "close": 100.0, "volume": 1200.0},
    ]
    year_history = month_history + [{"date": "2026-04-02", "close": 120.0, "volume": 900.0}]
    monkeypatch.setattr(data_service, "get_company_info", lambda symbol: {})
    monkeypatch.setattr(data_service, "get_fast_info", lambda symbol: {})
    monkeypatch.setattr(data_service, "_pick_search_match", lambda symbol: {"symbol": "AAPL", "name": "Apple Inc.", "exchange": "NMS"})
    monkeypatch.setattr(data_service, "get_profile_enrichment", lambda symbol: {})
    monkeypatch.setattr(data_service, "get_price_history", lambda symbol, period="1m": month_history if period == "1m" else year_history)

    overview = data_service.get_ticker_overview("AAPL")
    assert overview is not None
    assert overview["profile"]["name"] == "Apple Inc."
    assert overview["quote"]["price"] == 100.0
    assert overview["range_52w"]["high"] == 120.0
    assert overview["meta"]["sources"]["profile.name"] == "search"
    assert overview["meta"]["sources"]["quote.price"] == "history_recent"


def test_service_overview_invalid_symbol(monkeypatch) -> None:
    monkeypatch.setattr(data_service, "get_company_info", lambda symbol: {})
    monkeypatch.setattr(data_service, "get_fast_info", lambda symbol: {})
    monkeypatch.setattr(data_service, "_pick_search_match", lambda symbol: {})
    monkeypatch.setattr(data_service, "get_profile_enrichment", lambda symbol: {})
    monkeypatch.setattr(data_service, "get_price_history", lambda symbol, period="1m": [])
    assert data_service.get_ticker_overview("BAD") is None


def test_ticker_overview_404(monkeypatch) -> None:
    monkeypatch.setattr(main.data_service, "get_ticker_overview", lambda symbol: None)
    with pytest.raises(HTTPException) as exc:
        main.ticker_overview("INVALID")
    assert exc.value.status_code == 404
    assert exc.value.detail == "ticker data unavailable"


def test_ticker_overview_partial_response(monkeypatch) -> None:
    monkeypatch.setattr(
        main.data_service,
        "get_ticker_overview",
        lambda symbol: {
            "profile": {"symbol": "AAPL", "name": "Apple Inc.", "exchange": "NASDAQ", "sector": None, "industry": None, "website": None, "summary": None},
            "quote": {"price": 100.0, "prev_close": 98.0, "change": 2.0, "change_pct": 0.0204},
            "signals": [],
            "stats": {"market_cap": None, "trailing_pe": None, "trailing_eps": None, "volume": 1000.0, "average_volume": None, "beta": None},
            "range_52w": {"low": None, "high": None, "price": 100.0},
            "short_interest": {"short_percent_of_float": None, "short_ratio": None, "shares_short": None, "shares_short_prior_month": None, "shares_short_delta_pct": None},
            "meta": {"status": "partial", "is_partial": True, "field_availability": {"stats.market_cap": False}, "sources": {"profile.name": "search"}},
        },
    )
    body = main.ticker_overview("AAPL")
    assert body["meta"]["is_partial"] is True
    assert body["profile"]["name"] == "Apple Inc."


def test_ticker_history_period_mapping(monkeypatch) -> None:
    data_service.HISTORY_CACHE.clear()
    captured: list[str] = []

    class DummyTicker:
        def __init__(self, symbol: str) -> None:
            self.symbol = symbol

        def history(self, period: str):
            captured.append(period)
            return pd.DataFrame(
                [{"Open": 1.0, "High": 1.0, "Low": 1.0, "Close": 1.0, "Volume": 1.0}],
                index=[pd.Timestamp("2026-01-01")],
            )

    monkeypatch.setattr(data_service.yf, "Ticker", DummyTicker)
    assert len(data_service.get_price_history("AAPL", period="1m")) == 1
    assert len(data_service.get_price_history("AAPL", period="3m")) == 1
    assert len(data_service.get_price_history("AAPL", period="6m")) == 1
    assert captured == ["1mo", "3mo", "6mo"]