"""Tests for /api/meet-kevin/backtest/* routes (mocked DB).""" from __future__ import annotations import uuid from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock import pytest from fastapi.testclient import TestClient from services.api_gateway.auth.jwt import create_access_token from services.api_gateway.auth.middleware import get_config, get_current_user from services.api_gateway.config import ApiGatewayConfig from services.api_gateway.main import create_app @pytest.fixture def config() -> ApiGatewayConfig: return ApiGatewayConfig( jwt_secret_key="test-secret-for-routes-kevin-backtest", database_url="sqlite+aiosqlite:///:memory:", redis_url="redis://localhost:6379/0", ) @pytest.fixture def auth_headers(config: ApiGatewayConfig) -> dict[str, str]: token = create_access_token("user-test", "tester", config) return {"Authorization": f"Bearer {token}"} def _make_session() -> tuple[MagicMock, AsyncMock]: session = AsyncMock() session.__aenter__ = AsyncMock(return_value=session) session.__aexit__ = AsyncMock(return_value=False) factory = MagicMock() factory.return_value = session return factory, session def _make_execute_result(scalars: list, scalar_one_or_none=None): result = MagicMock() result.scalars.return_value.all.return_value = scalars result.scalar_one_or_none.return_value = scalar_one_or_none return result @pytest.fixture def app(config: ApiGatewayConfig): factory, session = _make_session() app = create_app(config) app.dependency_overrides[get_current_user] = lambda: { "sub": "user-test", "username": "tester", "type": "access", } app.dependency_overrides[get_config] = lambda: config app.state.db_session_factory = factory app.state.config = config app.state.redis = AsyncMock() app.state.db_engine = MagicMock() app._test_session = session # type: ignore[attr-defined] return app def test_list_runs_returns_empty_initially(app, auth_headers): session: AsyncMock = app._test_session # type: ignore[attr-defined] session.execute = AsyncMock(return_value=_make_execute_result([])) client = TestClient(app, raise_server_exceptions=False) resp = client.get("/api/meet-kevin/backtest/runs", headers=auth_headers) assert resp.status_code == 200 assert resp.json() == [] def test_get_run_by_uuid_returns_404_if_unknown(app, auth_headers): session: AsyncMock = app._test_session # type: ignore[attr-defined] session.execute = AsyncMock( return_value=_make_execute_result([], scalar_one_or_none=None) ) client = TestClient(app, raise_server_exceptions=False) unknown = uuid.uuid4() resp = client.get( f"/api/meet-kevin/backtest/runs/{unknown}", headers=auth_headers ) assert resp.status_code == 404 def test_list_runs_returns_persisted_row(app, auth_headers): """When the DB has a row, the route serialises it correctly.""" fake_row = MagicMock() fake_row.run_uuid = uuid.uuid4() fake_row.status = MagicMock(value="completed") fake_row.started_at = datetime.now(timezone.utc) fake_row.finished_at = datetime.now(timezone.utc) fake_row.metrics_json = {"total_return_pct": 3.5, "trade_count": 2} session: AsyncMock = app._test_session # type: ignore[attr-defined] session.execute = AsyncMock(return_value=_make_execute_result([fake_row])) client = TestClient(app, raise_server_exceptions=False) resp = client.get("/api/meet-kevin/backtest/runs", headers=auth_headers) assert resp.status_code == 200 rows = resp.json() assert len(rows) == 1 assert rows[0]["status"] == "completed" assert rows[0]["total_return_pct"] == 3.5 def test_latest_returns_404_if_no_completed(app, auth_headers): session: AsyncMock = app._test_session # type: ignore[attr-defined] session.execute = AsyncMock( return_value=_make_execute_result([], scalar_one_or_none=None) ) client = TestClient(app, raise_server_exceptions=False) resp = client.get("/api/meet-kevin/backtest/latest", headers=auth_headers) assert resp.status_code == 404 def test_run_backtest_returns_run_uuid(app, auth_headers): session: AsyncMock = app._test_session # type: ignore[attr-defined] session.commit = AsyncMock() session.add = MagicMock() client = TestClient(app, raise_server_exceptions=False) resp = client.post( "/api/meet-kevin/backtest/run", headers=auth_headers, json={"initial_capital": 100000.0, "slippage_pct": 0.0005}, ) assert resp.status_code == 202 data = resp.json() assert "run_uuid" in data assert data["status"] == "running"