Spaces:
Running
Running
| """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" | |
| 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 | |
| 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"] | |