feat(api): /api/meet-kevin/backtest/* routes
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled
This commit is contained in:
parent
db103df9b1
commit
886dbaec86
4 changed files with 478 additions and 0 deletions
0
tests/api_gateway/__init__.py
Normal file
0
tests/api_gateway/__init__.py
Normal file
137
tests/api_gateway/routes/test_meet_kevin_backtest.py
Normal file
137
tests/api_gateway/routes/test_meet_kevin_backtest.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue