Files
easy_tdx_max/tests/unit/test_web_api.py
T
Justin Gu eb8a7a5675 feat(web): add FastAPI app factory, all routers, CLI serve command, and tests
- App factory with lifespan management and CORS middleware
- Market router: security list, quotes, market stat, fund-flow
- Bars router: kline, index kline, minute, transaction
- Finance router: xdxr, finance, company info, financial records
- Block router: block file parsing
- Chanlun router: POST /chanlun/analyze
- Realtime router: WebSocket /ws/realtime/{symbol}
- CLI: easy-tdx serve command
- 16 unit tests, all passing offline (no network)
2026-06-12 03:08:12 +08:00

250 lines
7.9 KiB
Python

"""Web API tests (offline, no network).
Covers: schemas, error handling, app factory, DI, all routers,
CLI serve command, OpenAPI schema generation.
"""
from __future__ import annotations
import pytest
# ---------------------------------------------------------------------------
# Task 2: Schemas & Error Handling
# ---------------------------------------------------------------------------
def test_market_enum_values():
"""MarketEnum should map string names to int values matching Market enum."""
pytest.importorskip("fastapi")
from easy_tdx.web.schemas import MarketEnum
assert MarketEnum.SZ == 0
assert MarketEnum.SH == 1
assert MarketEnum.BJ == 2
def test_kline_category_enum():
"""KlineCategoryEnum should map string names to int values."""
pytest.importorskip("fastapi")
from easy_tdx.web.schemas import KlineCategoryEnum
assert KlineCategoryEnum.MIN_5 == 0
assert KlineCategoryEnum.DAY == 4
assert KlineCategoryEnum.WEEK == 5
def test_quote_request_validation():
"""QuoteRequest should validate stocks list."""
pytest.importorskip("fastapi")
from easy_tdx.web.schemas import QuoteRequest
req = QuoteRequest(stocks=[{"market": "SZ", "code": "000001"}])
assert len(req.stocks) == 1
assert req.stocks[0].market == "SZ"
assert req.stocks[0].code == "000001"
def test_chanlun_request_defaults():
"""ChanlunRequest should have sensible defaults."""
pytest.importorskip("fastapi")
from easy_tdx.web.schemas import ChanlunRequest
req = ChanlunRequest(market="SZ", code="000001")
assert req.category == "DAY"
assert req.count == 800
def test_api_error_response():
"""ApiErrorResponse should serialize correctly."""
pytest.importorskip("fastapi")
from easy_tdx.web.errors import ApiErrorResponse
err = ApiErrorResponse(error="test error", detail="some detail")
d = err.model_dump()
assert d["error"] == "test error"
assert d["detail"] == "some detail"
# ---------------------------------------------------------------------------
# Task 3: App Factory & Dependency Injection
# ---------------------------------------------------------------------------
def test_create_app_returns_fastapi_instance():
"""create_app should return a FastAPI app with routers mounted."""
pytest.importorskip("fastapi")
from easy_tdx.web import create_app
app = create_app()
assert app.title == "easy-tdx API"
# Check routers are mounted
routes = [r.path for r in app.routes]
assert any("/api/v1/security" in r for r in routes)
assert any("/api/v1/bars" in r for r in routes)
assert any("/api/v1/chanlun" in r for r in routes)
assert any("/ws/realtime" in r for r in routes)
def test_deps_get_client_type():
"""get_client should be callable (actual client creation needs network)."""
pytest.importorskip("fastapi")
from easy_tdx.web.deps import get_client
assert callable(get_client)
# ---------------------------------------------------------------------------
# Task 4: Market Router
# ---------------------------------------------------------------------------
def test_market_router_endpoints():
"""Market router should define all expected endpoints."""
pytest.importorskip("fastapi")
from easy_tdx.web.routers.market import router
paths = [r.path for r in router.routes]
assert "/security/count" in paths
assert "/security/list" in paths
assert "/security/list-all" in paths
assert "/quotes" in paths
assert "/market/stat" in paths
assert "/fund-flow" in paths
assert "/fund-flow/history" in paths
# ---------------------------------------------------------------------------
# Task 5: Bars Router
# ---------------------------------------------------------------------------
def test_bars_router_endpoints():
"""Bars router should define all expected endpoints."""
pytest.importorskip("fastapi")
from easy_tdx.web.routers.bars import router
paths = [r.path for r in router.routes]
assert "/bars" in paths
assert "/bars/index" in paths
assert "/minute" in paths
assert "/minute/history" in paths
assert "/transaction" in paths
assert "/transaction/history" in paths
# ---------------------------------------------------------------------------
# Task 6: Finance Router
# ---------------------------------------------------------------------------
def test_finance_router_endpoints():
"""Finance router should define all expected endpoints."""
pytest.importorskip("fastapi")
from easy_tdx.web.routers.finance import router
paths = [r.path for r in router.routes]
assert "/xdxr" in paths
assert "/finance" in paths
assert "/company/category" in paths
assert "/company/content" in paths
assert "/financial/file-list" in paths
assert "/financial/records" in paths
# ---------------------------------------------------------------------------
# Task 7: Block Router
# ---------------------------------------------------------------------------
def test_block_router_endpoints():
"""Block router should define expected endpoints."""
pytest.importorskip("fastapi")
from easy_tdx.web.routers.block import router
paths = [r.path for r in router.routes]
assert "/block" in paths
# ---------------------------------------------------------------------------
# Task 8: Chanlun Router
# ---------------------------------------------------------------------------
def test_chanlun_router_endpoints():
"""Chanlun router should define the analyze endpoint."""
pytest.importorskip("fastapi")
from easy_tdx.web.routers.chanlun import router
paths = [r.path for r in router.routes]
assert "/chanlun/analyze" in paths
# ---------------------------------------------------------------------------
# Task 9: Realtime Router
# ---------------------------------------------------------------------------
def test_realtime_router_endpoints():
"""Realtime router should define the WebSocket endpoint."""
pytest.importorskip("fastapi")
from easy_tdx.web.routers.realtime import router
paths = [r.path for r in router.routes]
assert any("realtime" in p for p in paths)
# ---------------------------------------------------------------------------
# Task 10: CLI serve command
# ---------------------------------------------------------------------------
def test_serve_command_exists():
"""CLI should have a serve command registered."""
pytest.importorskip("fastapi")
from easy_tdx.cli import cli
assert "serve" in cli.commands
# ---------------------------------------------------------------------------
# Task 11: Integration — route registration & OpenAPI
# ---------------------------------------------------------------------------
def test_full_app_routes_registered():
"""All routers should be mounted and accessible."""
pytest.importorskip("fastapi")
from easy_tdx.web import create_app
app = create_app()
all_paths = [r.path for r in app.routes]
expected_prefixes = [
"/api/v1/security",
"/api/v1/bars",
"/api/v1/xdxr",
"/api/v1/block",
"/api/v1/chanlun",
"/ws/realtime",
]
for prefix in expected_prefixes:
matched = any(prefix in p for p in all_paths)
assert matched, f"Expected route with prefix '{prefix}' not found in {all_paths}"
def test_openapi_schema_generated():
"""OpenAPI schema should be auto-generated and contain key paths."""
pytest.importorskip("fastapi")
from easy_tdx.web import create_app
app = create_app()
schema = app.openapi()
assert schema["info"]["title"] == "easy-tdx API"
assert "/api/v1/security/count" in schema["paths"]
assert "/api/v1/bars" in schema["paths"]
assert "/api/v1/chanlun/analyze" in schema["paths"]
# WebSocket routes are NOT included in OpenAPI schema by default;
# they are verified in test_full_app_routes_registered instead.
# Just ensure REST paths are present.
assert "/api/v1/fund-flow" in schema["paths"]