feat(api): /api/meet-kevin/* routes (11 endpoints)
This commit is contained in:
parent
8f5ee8f1c3
commit
bfa7a503da
4 changed files with 840 additions and 0 deletions
0
tests/api_gateway/routes/__init__.py
Normal file
0
tests/api_gateway/routes/__init__.py
Normal file
173
tests/api_gateway/routes/test_meet_kevin.py
Normal file
173
tests/api_gateway/routes/test_meet_kevin.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
"""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": []}
|
||||
Loading…
Add table
Add a link
Reference in a new issue