"""Tests for /api/meet-kevin/strategy/* + /positions/{symbol}/close routes.""" from __future__ import annotations from datetime import datetime, timezone from decimal import Decimal 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-strategy", 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(): session = AsyncMock() session.__aenter__ = AsyncMock(return_value=session) session.__aexit__ = AsyncMock(return_value=False) factory = MagicMock() factory.return_value = session return factory, session def _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.redis.get = AsyncMock(return_value=None) app.state.redis.set = AsyncMock() app.state.db_engine = MagicMock() app._test_session = session # type: ignore[attr-defined] return app def test_tickers_returns_empty_when_no_mentions(app, auth_headers): session = app._test_session # type: ignore[attr-defined] session.execute = AsyncMock(return_value=_result([])) client = TestClient(app, raise_server_exceptions=False) resp = client.get("/api/meet-kevin/strategy/tickers", headers=auth_headers) assert resp.status_code == 200 assert resp.json() == [] def test_tickers_joins_mention_audit_trade(app, auth_headers): mention = MagicMock( id=1, symbol="NVDA", action=MagicMock(value="buy"), conviction=Decimal("0.7"), time_horizon=MagicMock(value="weeks"), created_at=datetime.now(timezone.utc), ) audit = MagicMock( mention_id=1, bridge_status=MagicMock(value="emitted"), effective_conviction=Decimal("0.75"), ) session = app._test_session # type: ignore[attr-defined] session.execute = AsyncMock( side_effect=[ _result([mention]), _result([audit]), _result([]), # no trades ] ) client = TestClient(app, raise_server_exceptions=False) resp = client.get("/api/meet-kevin/strategy/tickers", headers=auth_headers) assert resp.status_code == 200 rows = resp.json() assert len(rows) == 1 assert rows[0]["symbol"] == "NVDA" assert rows[0]["bridge_status"] == "emitted" assert rows[0]["effective_conviction"] == 0.75 assert rows[0]["has_open_trade"] is False def test_performance_returns_aggregates(app, auth_headers): closed_win = MagicMock(pnl=100.0) closed_loss = MagicMock(pnl=-50.0) session = app._test_session # type: ignore[attr-defined] session.execute = AsyncMock( return_value=_result([closed_win, closed_loss]) ) client = TestClient(app, raise_server_exceptions=False) resp = client.get("/api/meet-kevin/strategy/performance", headers=auth_headers) assert resp.status_code == 200 data = resp.json() assert data["closed_trade_count"] == 2 assert data["wins"] == 1 assert data["losses"] == 1 def test_close_position_writes_redis_flag(app, auth_headers): client = TestClient(app, raise_server_exceptions=False) resp = client.post( "/api/meet-kevin/positions/nvda/close", headers=auth_headers ) assert resp.status_code == 200 assert resp.json()["symbol"] == "NVDA" redis = app.state.redis redis.set.assert_awaited() def test_equity_curve_returns_kevin_series(app, auth_headers): snap = MagicMock( timestamp=datetime(2026, 5, 1, tzinfo=timezone.utc), total_value=100000.0, ) session = app._test_session # type: ignore[attr-defined] session.execute = AsyncMock(return_value=_result([snap])) client = TestClient(app, raise_server_exceptions=False) resp = client.get( "/api/meet-kevin/strategy/equity-curve", headers=auth_headers ) assert resp.status_code == 200 data = resp.json() assert "kevin_equity_curve" in data assert data["kevin_equity_curve"][0][1] == 100000.0