"""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"]