mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-20 22:28:05 +00:00
285 lines
9.3 KiB
Python
285 lines
9.3 KiB
Python
"""Smoke tests that keep the FastAPI shell safe during refactors."""
|
|
|
|
from datetime import date, timedelta
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from server.core.providers.base import DataUnavailable
|
|
from server.core.data_gateway import Fundamentals, HoldersData, OHLCV, OHLCVBar, Profile, Quote
|
|
from server.main import app
|
|
|
|
|
|
EXPECTED_PREFIXES = [
|
|
"/api/analysis",
|
|
"/api/chat",
|
|
"/api/calendar",
|
|
"/api/crypto",
|
|
"/api/copilot",
|
|
"/api/credentials",
|
|
"/api/dart",
|
|
"/api/earnings",
|
|
"/api/edgar",
|
|
"/api/edinet",
|
|
"/api/estimates",
|
|
"/api/financials",
|
|
"/api/fmp",
|
|
"/api/fx",
|
|
"/api/insider",
|
|
"/api/macro",
|
|
"/api/market",
|
|
"/api/markets",
|
|
"/api/news",
|
|
"/api/portfolio",
|
|
"/api/research",
|
|
"/api/screener",
|
|
"/api/technical",
|
|
"/api/tax",
|
|
"/api/valuation",
|
|
]
|
|
|
|
|
|
def test_health_endpoint_returns_ok() -> None:
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/health")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "ok"
|
|
|
|
|
|
def test_openapi_contains_all_router_prefixes() -> None:
|
|
with TestClient(app) as client:
|
|
response = client.get("/openapi.json")
|
|
|
|
assert response.status_code == 200
|
|
paths = response.json()["paths"].keys()
|
|
for prefix in EXPECTED_PREFIXES:
|
|
assert any(path.startswith(prefix) for path in paths), prefix
|
|
|
|
|
|
def test_lightweight_asset_type_endpoint(monkeypatch) -> None:
|
|
from server.routers import market_data
|
|
|
|
monkeypatch.setattr(market_data, "detect_asset_type", lambda ticker: market_data.AssetType.EQUITY)
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/market/asset-type/AAPL")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"ticker": "AAPL", "asset_type": "equity"}
|
|
|
|
|
|
def test_quote_endpoint_can_use_gateway_flag(monkeypatch) -> None:
|
|
from server.routers import market_data
|
|
|
|
class FakeGateway:
|
|
async def quote(self, ticker: str) -> Quote:
|
|
return Quote(symbol=ticker.upper(), price=123.45, change_pct=1.234, source="fake")
|
|
|
|
monkeypatch.setattr(market_data.core_flags, "new_data_gateway_enabled", lambda: True)
|
|
monkeypatch.setattr(market_data, "get_data_gateway", lambda: FakeGateway())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/market/quote/AAPL")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"ticker": "AAPL", "current_price": 123.45, "change_pct": 1.23}
|
|
|
|
|
|
def test_quote_endpoint_gateway_failure_degrades(monkeypatch) -> None:
|
|
from server.routers import market_data
|
|
|
|
class FailingGateway:
|
|
async def quote(self, ticker: str) -> Quote:
|
|
raise DataUnavailable(ticker, "quote")
|
|
|
|
monkeypatch.setattr(market_data.core_flags, "new_data_gateway_enabled", lambda: True)
|
|
monkeypatch.setattr(market_data, "get_data_gateway", lambda: FailingGateway())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/market/quote/AAPL")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"ticker": "AAPL", "current_price": None, "change_pct": None}
|
|
|
|
|
|
def test_peer_endpoint_returns_gateway_matrix(monkeypatch) -> None:
|
|
from server.routers import market_data
|
|
|
|
class FakeGateway:
|
|
async def profile(self, ticker: str) -> Profile:
|
|
return Profile(symbol=ticker.upper(), sector="Technology", industry="Semiconductors", source="fake")
|
|
|
|
async def peers(self, ticker: str) -> list[str]:
|
|
return ["AMD", "NVDA"]
|
|
|
|
async def fundamentals(self, ticker: str, period: str = "ttm") -> Fundamentals:
|
|
rows = {
|
|
"NVDA": Fundamentals(
|
|
symbol="NVDA",
|
|
period=period,
|
|
name="NVIDIA",
|
|
market_cap=3_000_000_000_000,
|
|
pe=40.0,
|
|
ev_ebitda=32.0,
|
|
roic=0.45,
|
|
gross_margin=0.72,
|
|
revenue_growth=0.6,
|
|
source="fake",
|
|
),
|
|
"AMD": Fundamentals(
|
|
symbol="AMD",
|
|
period=period,
|
|
name="AMD",
|
|
market_cap=250_000_000_000,
|
|
pe=35.0,
|
|
ev_ebitda=25.0,
|
|
roic=0.12,
|
|
gross_margin=0.5,
|
|
revenue_growth=0.1,
|
|
source="fake",
|
|
),
|
|
}
|
|
return rows[ticker.upper()]
|
|
|
|
monkeypatch.setattr(market_data, "get_data_gateway", lambda: FakeGateway())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/market/peers/NVDA?metrics=pe,ev_ebitda,roic,gross_margin")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["primary"] == "NVDA"
|
|
assert data["peer_symbols"] == ["AMD"]
|
|
assert data["metrics"] == ["pe", "ev_ebitda", "roic", "gross_margin"]
|
|
assert [row["ticker"] for row in data["matrix"]] == ["NVDA", "AMD"]
|
|
assert data["averages"]["pe"] == 37.5
|
|
|
|
|
|
def test_transcript_delta_degrades_without_fmp_key(monkeypatch) -> None:
|
|
monkeypatch.delenv("FMP_API_KEY", raising=False)
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/earnings/NVDA/transcript-delta")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["available"] is False
|
|
|
|
|
|
def test_calendar_degrades_without_fmp_key(monkeypatch) -> None:
|
|
monkeypatch.delenv("FMP_API_KEY", raising=False)
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/calendar/economic")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["available"] is False
|
|
assert data["grouped"] == {}
|
|
|
|
|
|
def test_financial_statement_table_uses_gateway(monkeypatch) -> None:
|
|
from server.routers import financials
|
|
|
|
class FakeGateway:
|
|
async def financials(self, ticker: str, statement: str = "income", period: str = "annual") -> dict:
|
|
return {
|
|
"ticker": ticker.upper(),
|
|
"statement": statement,
|
|
"period": period,
|
|
"source": "fake",
|
|
"periods": ["2025", "2024"],
|
|
"line_items": {"revenue": [120.0, 100.0]},
|
|
}
|
|
|
|
monkeypatch.setattr(financials, "get_data_gateway", lambda: FakeGateway())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/financials/AAPL/table?statement=income&period=annual")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["line_items"]["revenue"] == [120.0, 100.0]
|
|
|
|
|
|
def test_ownership_endpoint_normalizes_gateway_rows(monkeypatch) -> None:
|
|
from server.routers import market_data
|
|
|
|
class FakeGateway:
|
|
async def holders(self, ticker: str) -> HoldersData:
|
|
return HoldersData(
|
|
symbol=ticker.upper(),
|
|
institutions=[
|
|
{"Holder": "Vanguard", "Shares": 1000, "pctHeld": 0.12, "Value": 250000, "Change": 25},
|
|
],
|
|
insiders=[
|
|
{"Name": "CEO Example", "Shares Owned Directly": 100, "change": -5},
|
|
],
|
|
source="fake",
|
|
)
|
|
|
|
monkeypatch.setattr(market_data, "get_data_gateway", lambda: FakeGateway())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/market/ownership/NVDA")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["available"] is True
|
|
assert data["institutional_pct"] == 12.0
|
|
assert data["institutions"][0]["name"] == "Vanguard"
|
|
assert data["insiders"][0]["name"] == "CEO Example"
|
|
|
|
|
|
def test_portfolio_correlation_uses_gateway_history(monkeypatch) -> None:
|
|
from server.routers import portfolio
|
|
|
|
monkeypatch.setattr(
|
|
portfolio,
|
|
"_load_positions",
|
|
lambda: [
|
|
{"ticker": "AAA", "quantity": 1, "avg_price": 10},
|
|
{"ticker": "BBB", "quantity": 1, "avg_price": 10},
|
|
],
|
|
)
|
|
|
|
class FakeGateway:
|
|
async def history(self, ticker: str, range_key: str = "3mo") -> OHLCV:
|
|
base = 10 if ticker == "AAA" else 20
|
|
bars = [
|
|
OHLCVBar(date=date(2026, 1, 1) + timedelta(days=idx), open=None, high=None, low=None, close=base + idx)
|
|
for idx in range(5)
|
|
]
|
|
return OHLCV(symbol=ticker, range=range_key, bars=bars, source="fake")
|
|
|
|
monkeypatch.setattr(portfolio, "get_data_gateway", lambda: FakeGateway())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/portfolio/correlation?window=90")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["available"] is True
|
|
assert data["tickers"] == ["AAA", "BBB"]
|
|
assert data["matrix"][0][0] == 1.0
|
|
|
|
|
|
def test_uk_cgt_calculator_uses_portfolio_positions(monkeypatch) -> None:
|
|
from server.routers import tax
|
|
|
|
monkeypatch.setattr(
|
|
tax,
|
|
"_load_positions",
|
|
lambda: [
|
|
{"ticker": "AAPL", "quantity": 10, "avg_price": 100, "currency": "USD"},
|
|
],
|
|
)
|
|
monkeypatch.setattr(tax, "_get_current_quote", lambda ticker, exchange="": {"price": 200, "currency": "USD"})
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/tax/uk/cgt/local?income_band=higher")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total_unrealized_gain_gbp"] == 800.0
|
|
assert data["tax_if_sold_all"] == 0.0
|
|
assert data["positions"][0]["ticker"] == "AAPL"
|