173 lines
5.7 KiB
Python
173 lines
5.7 KiB
Python
"""Smoke tests for /api/meet-kevin/* routes.
|
|
|
|
Three behaviors covered:
|
|
1. GET /api/meet-kevin/health → 200 with counts_by_status, daily_cost_usd, daily_cost_cap_usd
|
|
2. GET /api/meet-kevin/videos (empty DB) → {"videos": [], "total": 0, "page": 1, "per_page": 20}
|
|
3. GET /api/meet-kevin/stocks (empty DB) → {"stocks": []}
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from services.api_gateway.auth.middleware import get_current_user
|
|
from services.api_gateway.config import ApiGatewayConfig
|
|
from services.api_gateway.main import create_app
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture()
|
|
def config() -> ApiGatewayConfig:
|
|
return ApiGatewayConfig(
|
|
jwt_secret_key="test-secret-meet-kevin",
|
|
database_url="sqlite+aiosqlite:///:memory:",
|
|
redis_url="redis://localhost:6379/0",
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_user() -> dict:
|
|
return {"sub": "user-test", "username": "tester", "type": "access"}
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_redis() -> AsyncMock:
|
|
redis = AsyncMock()
|
|
redis.get = AsyncMock(return_value=None)
|
|
redis.set = AsyncMock()
|
|
return redis
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_session():
|
|
"""Async context-manager session mock."""
|
|
session = AsyncMock()
|
|
session.__aenter__ = AsyncMock(return_value=session)
|
|
session.__aexit__ = AsyncMock(return_value=False)
|
|
return session
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_factory(mock_session):
|
|
factory = MagicMock(return_value=mock_session)
|
|
return factory
|
|
|
|
|
|
def _scalar_result(value):
|
|
"""Mock execute() result that returns a scalar."""
|
|
result = MagicMock()
|
|
result.scalar.return_value = value
|
|
result.scalar_one_or_none.return_value = value
|
|
result.scalars.return_value.all.return_value = []
|
|
result.all.return_value = []
|
|
return result
|
|
|
|
|
|
def _rows_result(rows):
|
|
"""Mock execute() result that returns rows."""
|
|
result = MagicMock()
|
|
result.all.return_value = rows
|
|
result.scalars.return_value.all.return_value = rows
|
|
result.scalar.return_value = len(rows)
|
|
result.scalar_one_or_none.return_value = rows[0] if rows else None
|
|
return result
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(config, mock_user, mock_redis, mock_factory) -> TestClient:
|
|
app = create_app(config)
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
app.state.redis = mock_redis
|
|
app.state.db_session_factory = mock_factory
|
|
app.state.db_engine = MagicMock()
|
|
app.state.config = config
|
|
return TestClient(app, raise_server_exceptions=False)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test: GET /api/meet-kevin/health
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestMeetKevinHealth:
|
|
def test_health_returns_required_keys(
|
|
self, client: TestClient, mock_session: AsyncMock
|
|
) -> None:
|
|
"""health returns counts_by_status, daily_cost_usd, daily_cost_cap_usd."""
|
|
# Health calls: count queries by status + daily cost sum + last_polled_at
|
|
# Return Nones/zeros for empty DB — any 4 sequential scalar calls
|
|
mock_session.execute = AsyncMock(
|
|
side_effect=[
|
|
_scalar_result(0), # discovered count
|
|
_scalar_result(0), # captioned count
|
|
_scalar_result(0), # analyzed count
|
|
_scalar_result(0), # failed count
|
|
_scalar_result(0), # skipped count
|
|
_scalar_result(None), # daily cost sum
|
|
_scalar_result(None), # last_polled_at
|
|
_scalar_result(None), # daily_cost_cap_usd
|
|
]
|
|
)
|
|
|
|
resp = client.get("/api/meet-kevin/health")
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
assert "counts_by_status" in data
|
|
assert "daily_cost_usd" in data
|
|
assert "daily_cost_cap_usd" in data
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test: GET /api/meet-kevin/videos (empty DB)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestMeetKevinVideos:
|
|
def test_videos_empty_db_returns_empty_page(
|
|
self, client: TestClient, mock_session: AsyncMock
|
|
) -> None:
|
|
"""GET /api/meet-kevin/videos on empty DB returns empty paginated response."""
|
|
# Two execute calls: count query + data query
|
|
mock_session.execute = AsyncMock(
|
|
side_effect=[
|
|
_scalar_result(0), # count
|
|
_rows_result([]), # data rows
|
|
]
|
|
)
|
|
|
|
resp = client.get("/api/meet-kevin/videos")
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
assert data["videos"] == []
|
|
assert data["total"] == 0
|
|
assert data["page"] == 1
|
|
assert data["per_page"] == 20
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test: GET /api/meet-kevin/stocks (empty DB)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestMeetKevinStocks:
|
|
def test_stocks_empty_db_returns_empty_list(
|
|
self, client: TestClient, mock_session: AsyncMock
|
|
) -> None:
|
|
"""GET /api/meet-kevin/stocks on empty DB returns {"stocks": []}."""
|
|
mock_session.execute = AsyncMock(
|
|
side_effect=[
|
|
_rows_result([]), # aggregate query
|
|
]
|
|
)
|
|
|
|
resp = client.get("/api/meet-kevin/stocks")
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
assert data == {"stocks": []}
|