stock-data-api / tests /test_routes.py
fromozuzhouzzz
feat: add niuone US market + X timeline sources on HF tip
b2ca3f2
Raw
History Blame Contribute Delete
9.54 kB
"""Tests for API routes."""
import pytest
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["ok"] is True
def test_catalog(client, auth_headers):
response = client.get("/api/v1/catalog", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert "groups" in data
def test_quote(client, auth_headers):
"""Test real-time quote endpoint (requires network)."""
response = client.get("/api/v1/stocks/600519/quote", headers=auth_headers)
assert response.status_code in (200, 503) # 503 if upstream down
data = response.json()
assert "ok" in data
def test_financial_params(client, auth_headers):
"""Test financial endpoint accepts start_year/end_year params."""
response = client.get(
"/api/v1/stocks/600519/financial?kind=abstract&start_year=2023&end_year=2025&limit=50",
headers=auth_headers,
)
assert response.status_code in (200, 503) # 503 if upstream down
data = response.json()
assert "ok" in data
def test_catalog_includes_fleur_gap_endpoints(client, auth_headers):
response = client.get("/api/v1/catalog", headers=auth_headers)
assert response.status_code == 200
groups = response.json()["groups"]
assert "/stocks/{stock_code}/cashflow" in groups["stocks"]
assert "/stocks/{stock_code}/dividends" in groups["stocks"]
assert "/stocks/{stock_code}/equity-history" in groups["stocks"]
assert "/stocks/{stock_code}/freeholders" in groups["stocks"]
assert "/market/trade-calendar" in groups["market"]
assert "/macro/chinabond/yield-curve" in groups["macro"]
assert "/etfs/premium" in groups["etfs"]
assert "/etfs/{fund_code}/premium" in groups["etfs"]
assert "/hk/stocks/{stock_code}/short-selling" in groups["hk"]
def test_fleur_gap_routes_are_wired(client, auth_headers, monkeypatch):
from app.api import routes
def ok_payload(name):
return {"ok": True, "data": {"name": name}, "meta": {"source": "test"}}
monkeypatch.setattr(routes.market_data_service, "stock_cashflow", lambda stock_code, limit, kind: ok_payload("cashflow"))
monkeypatch.setattr(routes.market_data_service, "stock_dividends", lambda stock_code, limit, kind: ok_payload("dividends"))
monkeypatch.setattr(routes.market_data_service, "stock_equity_history", lambda stock_code, limit: ok_payload("equity"))
monkeypatch.setattr(routes.market_data_service, "stock_freeholders", lambda stock_code, limit, end_date: ok_payload("freeholders"))
monkeypatch.setattr(routes.market_data_service, "trade_calendar", lambda start_date, end_date, limit: ok_payload("calendar"))
monkeypatch.setattr(routes.market_data_service, "etf_premium", lambda limit, sort: ok_payload("etf_premium"))
monkeypatch.setattr(routes.market_data_service, "etf_premium_detail", lambda fund_code: ok_payload("etf_premium_detail"))
monkeypatch.setattr(routes.market_data_service, "hk_short_selling", lambda stock_code, limit, pages: ok_payload("hk_short_selling"))
monkeypatch.setattr(
routes.market_data_service,
"china_bond_yield_curve",
lambda start_date, end_date, days, limit: ok_payload("bond"),
)
paths = [
"/api/v1/stocks/600519/cashflow?kind=quarterly&limit=2",
"/api/v1/stocks/600519/dividends?kind=allotment&limit=2",
"/api/v1/stocks/600519/equity-history?limit=2",
"/api/v1/stocks/600519/freeholders?limit=2",
"/api/v1/market/trade-calendar?start_date=2026-01-01&end_date=2026-01-31",
"/api/v1/etfs/premium?sort=premium&limit=2",
"/api/v1/etfs/510300/premium",
"/api/v1/hk/stocks/00700/short-selling?limit=2&pages=1",
"/api/v1/macro/chinabond/yield-curve?start_date=2026-01-01&end_date=2026-01-31",
]
for path in paths:
response = client.get(path, headers=auth_headers)
assert response.status_code == 200
assert response.json()["ok"] is True
def test_search_get_prefers_ai_search_hub_by_default(client, auth_headers, monkeypatch):
from app.api import routes
called = {}
def fail_model_search(**kwargs):
raise AssertionError("model search should not be used for default GET search")
def fake_ai_search(**kwargs):
called.update(kwargs)
return {
"ok": True,
"data": {"query": kwargs["query"], "sites_succeeded": ["qwen"]},
"meta": {"source": "ai_search_hub.multi_platform"},
}
monkeypatch.setattr(routes.search_service, "search", fail_model_search)
monkeypatch.setattr(routes.ai_search_hub_service, "search", fake_ai_search)
response = client.get("/api/v1/search?q=test", headers=auth_headers)
data = response.json()
assert response.status_code == 200
assert data["ok"] is True
assert called["query"] == "test"
assert called["sites"] == routes.GET_SEARCH_FAST_SITES
assert called["timeout"] == routes.GET_SEARCH_DEFAULT_TIMEOUT_SECONDS
assert data["meta"]["source"] == "ai_search_hub.multi_platform.fast_get"
assert data["meta"]["routing"]["mode"] == "fast_get"
def test_search_get_falls_back_to_ai_search_hub(client, auth_headers, monkeypatch):
from app.api import routes
fallback_called = {}
def fake_search(**kwargs):
raise routes.SearchUpstreamFailed(
"primary failed",
[{"attempt": 1, "ok": False, "elapsed_ms": 60000, "error": "ReadTimeout"}],
)
def fake_ai_search(**kwargs):
fallback_called.update(kwargs)
return {
"ok": True,
"data": {"query": kwargs["query"], "results": {}},
"meta": {"source": "ai_search_hub.multi_platform"},
}
monkeypatch.setattr(routes.search_service, "search", fake_search)
monkeypatch.setattr(routes.ai_search_hub_service, "search", fake_ai_search)
response = client.get("/api/v1/search?q=test&max_tokens=1024", headers=auth_headers)
data = response.json()
assert response.status_code == 200
assert data["ok"] is True
assert fallback_called["query"] == "test"
assert fallback_called["timeout"] == routes.GET_SEARCH_DEFAULT_TIMEOUT_SECONDS
assert data["meta"]["source"] == "ai_search_hub.multi_platform.fallback"
assert data["meta"]["fallback"]["from"] == "openai_compatible.search_model"
assert data["meta"]["attempts"][0]["ok"] is False
assert data["meta"]["attempts"][-1]["source"] == "ai_search_hub.multi_platform"
@pytest.mark.network
def test_fund_flow(client, auth_headers):
"""Test fund flow endpoint with multiple days (requires network)."""
response = client.get("/api/v1/stocks/600519/fund-flow?days=10", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["ok"] is True
assert "data" in data
@pytest.mark.network
def test_research_reports(client, auth_headers):
"""Test research reports endpoint (requires network)."""
response = client.get("/api/v1/stocks/600519/research-reports?limit=5", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["ok"] is True
assert "data" in data
def test_niuone_endpoints_are_wired(client, auth_headers, monkeypatch):
from app.api import routes
def ok_payload(name):
return {"ok": True, "data": {"name": name}, "meta": {"source": "test"}}
monkeypatch.setattr(routes.market_data_service, "us_indices", lambda limit: ok_payload("us_indices"))
monkeypatch.setattr(routes.market_data_service, "us_sectors", lambda limit: ok_payload("us_sectors"))
monkeypatch.setattr(routes.market_data_service, "us_market_summary", lambda: ok_payload("us_market_summary"))
monkeypatch.setattr(routes.market_data_service, "us_stock_quote", lambda symbol: ok_payload("us_stock_quote"))
monkeypatch.setattr(routes.market_data_service, "us_stock_daily", lambda symbol, days: ok_payload("us_stock_daily"))
monkeypatch.setattr(
routes.market_data_service,
"x_timeline",
lambda accounts, limit, hydrate: ok_payload("x_timeline"),
)
paths = [
"/api/v1/us/indices?limit=3",
"/api/v1/us/sectors?limit=3",
"/api/v1/us/market-summary",
"/api/v1/us/stocks/AAPL/quote",
"/api/v1/us/stocks/AAPL/daily?days=5",
"/api/v1/social/x/timeline?accounts=elonmusk&limit=3",
]
for path in paths:
response = client.get(path, headers=auth_headers)
assert response.status_code == 200
assert response.json()["ok"] is True
def test_x_timeline_requires_accounts(client, auth_headers):
response = client.get("/api/v1/social/x/timeline", headers=auth_headers)
assert response.status_code == 422
def test_catalog_includes_niuone_endpoints(client, auth_headers):
response = client.get("/api/v1/catalog", headers=auth_headers)
assert response.status_code == 200
groups = response.json()["groups"]
assert "/us/indices" in groups["us"]
assert "/us/sectors" in groups["us"]
assert "/us/market-summary" in groups["us"]
assert "/us/stocks/{symbol}/quote" in groups["us"]
assert "/us/stocks/{symbol}/daily" in groups["us"]
assert "/social/x/timeline" in groups["social"]