"""Tests for the FastAPI service layer.""" from typing import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi.testclient import TestClient # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_mock_state() -> MagicMock: """Return a fully-configured mock ServiceState.""" state = MagicMock() state.is_ready = True state.campaign_count.return_value = 0 state.simulations_executed.return_value = 0 state.initialize = AsyncMock() state.shutdown = AsyncMock() state.governor = MagicMock() return state # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def client() -> Generator[TestClient, None, None]: """Provide a TestClient with mocked ServiceState initialization.""" mock_state = _make_mock_state() with patch("synthra.api.app.ServiceState.get_instance", return_value=mock_state): from synthra.api.app import create_app application = create_app() with TestClient(application, raise_server_exceptions=True) as tc: yield tc # --------------------------------------------------------------------------- # Root # --------------------------------------------------------------------------- def test_root_returns_service_identity(client: TestClient) -> None: """GET /api returns service name, version, and running status.""" response = client.get("/api") assert response.status_code == 200 body = response.json() assert body["service"] == "Synthra" assert body["version"] == "0.1.0" assert body["status"] == "running" # --------------------------------------------------------------------------- # Health # --------------------------------------------------------------------------- def test_health_returns_healthy(client: TestClient) -> None: """GET /api/health returns status=healthy.""" response = client.get("/api/health") assert response.status_code == 200 assert response.json() == {"status": "healthy"} def test_health_is_fast(client: TestClient) -> None: """GET /api/health must complete without touching state or DB.""" import time start = time.perf_counter() response = client.get("/api/health") elapsed = time.perf_counter() - start assert response.status_code == 200 # Must complete within 200 ms even on slow CI assert elapsed < 0.2 # --------------------------------------------------------------------------- # Status # --------------------------------------------------------------------------- def test_status_returns_expected_schema(client: TestClient) -> None: """GET /api/status returns version, uptime_seconds, and memory sub-object.""" response = client.get("/api/status") assert response.status_code == 200 body = response.json() assert body["version"] == "0.1.0" assert isinstance(body["uptime_seconds"], float) assert body["uptime_seconds"] >= 0 mem = body["memory"] assert mem["backend"] == "sqlite" assert isinstance(mem["campaign_count"], int) assert isinstance(mem["simulations_executed"], int) # --------------------------------------------------------------------------- # Campaigns (stub) # --------------------------------------------------------------------------- def test_campaigns_list_returns_empty(client: TestClient) -> None: """GET /api/campaigns returns empty list and total.""" response = client.get("/api/campaigns") assert response.status_code == 200 body = response.json() assert body["campaigns"] == [] assert body["total"] == 0 # --------------------------------------------------------------------------- # Import sanity # --------------------------------------------------------------------------- def test_app_module_importable() -> None: """The app module must be importable from the expected path.""" from synthra.api import app as app_module # noqa: F401 assert hasattr(app_module, "app") def test_state_module_importable() -> None: """ServiceState must be importable from synthra.api.state.""" from synthra.api.state import ServiceState # noqa: F401 assert ServiceState is not None def test_run_campaign_endpoint(client: TestClient) -> None: """POST /api/campaigns/{campaign_id}/run calls governor.run_campaign.""" from synthra.api.app import ServiceState mock_state = ServiceState.get_instance() response = client.post("/api/campaigns/CMP-0001/run") assert response.status_code == 200 assert response.json() == { "status": "success", "message": "Campaign CMP-0001 queued for execution", } mock_state.governor.run_campaign.assert_called_once_with("CMP-0001") def test_run_campaign_endpoint_error(client: TestClient) -> None: """POST /api/campaigns/{campaign_id}/run handles errors properly.""" from synthra.api.app import ServiceState mock_state = ServiceState.get_instance() mock_state.governor.run_campaign.side_effect = ValueError("Some campaign error") response = client.post("/api/campaigns/CMP-0001/run") assert response.status_code == 200 assert response.json() == { "status": "error", "message": "Some campaign error", }