File size: 6,042 Bytes
e391a84 d852d80 e391a84 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """
tests/integration/test_api.py
βββββββββββββββββββββββββββββββ
Integration tests for FastAPI endpoints.
Uses:
β’ httpx.AsyncClient β async HTTP client for FastAPI TestClient
β’ SQLite in-memory database (aiosqlite) β no PostgreSQL needed
β’ MockModelService + InMemoryBroker β no RabbitMQ needed
These tests exercise the full request/response cycle through all layers
(Interface β Application β Domain β Infrastructure) using real SQLite.
"""
from __future__ import annotations
import os
import pytest
import pytest_asyncio
# ββ Override settings BEFORE importing anything from src βββββββββββββββββββββ
os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///./test_bp_monitoring_clean.db"
os.environ["RABBITMQ_URL"] = "amqp://guest:guest@localhost:5672/"
os.environ["USE_MOCK_MODEL"] = "true"
os.environ["DEBUG"] = "true"
# SQLite compatibility compilers are now globally registered in connection.py
from httpx import ASGITransport, AsyncClient
from src.infrastructure.database.connection import create_all_tables, dispose_engine
from src.interface.api.app import create_app
from src.shared.config import get_settings
from src.domain.interfaces.services.message_broker import MessageBroker
from src.interface.api.dependencies import get_broker
class MockBroker(MessageBroker):
async def connect(self) -> None: pass
async def disconnect(self) -> None: pass
async def is_connected(self) -> bool: return True
async def publish(self, queue_name: str, message: dict, **kwargs) -> None: pass
async def consume(self, queue_name: str, handler, **kwargs) -> None: pass
@pytest_asyncio.fixture(scope="module")
async def app():
"""Create a test FastAPI app with SQLite DB."""
# Clear cached settings so env overrides take effect
get_settings.cache_clear()
test_app = create_app()
test_app.dependency_overrides[get_broker] = lambda: MockBroker()
await create_all_tables()
yield test_app
await dispose_engine()
# Clean up test DB file
if os.path.exists("./test_bp_monitoring_clean.db"):
os.remove("./test_bp_monitoring_clean.db")
@pytest_asyncio.fixture(scope="module")
async def client(app):
"""Async HTTP client for the test app."""
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as c:
yield c
# ββ Health Check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestHealthEndpoint:
async def test_health_returns_ok(self, client: AsyncClient) -> None:
response = await client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
async def test_root_endpoint(self, client: AsyncClient) -> None:
response = await client.get("/")
assert response.status_code == 200
assert "BP Monitoring Pipeline" in response.json()["service"]
# ββ PPG Ingestion βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
VALID_PPG_PAYLOAD = {
"device_id": "sensor-001",
"user_id": "test-user-integration",
"sampling_rate": 125.0,
"ppg_values": [0.1, 0.2, 0.5, 0.9, 0.8, 0.4, 0.15] * 180, # ~10s at 125 Hz
"duration_seconds": 10.0,
}
class TestPPGIngestEndpoint:
async def test_ingest_valid_payload_returns_201(self, client: AsyncClient) -> None:
response = await client.post("/api/v1/ppg/ingest", json=VALID_PPG_PAYLOAD)
assert response.status_code == 201
data = response.json()
assert "signal_id" in data
assert data["user_id"] == "test-user-integration"
assert data["device_id"] == "sensor-001"
assert data["num_samples"] > 0
async def test_ingest_empty_ppg_values_returns_422(self, client: AsyncClient) -> None:
payload = {**VALID_PPG_PAYLOAD, "ppg_values": []}
response = await client.post("/api/v1/ppg/ingest", json=payload)
assert response.status_code == 422 # Pydantic validation error
async def test_ingest_invalid_sampling_rate_returns_422(self, client: AsyncClient) -> None:
payload = {**VALID_PPG_PAYLOAD, "sampling_rate": -10.0}
response = await client.post("/api/v1/ppg/ingest", json=payload)
assert response.status_code == 422
async def test_ingest_missing_required_field_returns_422(self, client: AsyncClient) -> None:
payload = {k: v for k, v in VALID_PPG_PAYLOAD.items() if k != "device_id"}
response = await client.post("/api/v1/ppg/ingest", json=payload)
assert response.status_code == 422
# ββ Predictions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestPredictionEndpoints:
async def test_get_predictions_empty_user_returns_200(self, client: AsyncClient) -> None:
response = await client.get("/api/v1/predictions/nonexistent-user")
assert response.status_code == 200
data = response.json()
assert data["user_id"] == "nonexistent-user"
assert data["total"] == 0
assert data["predictions"] == []
async def test_get_predictions_with_limit(self, client: AsyncClient) -> None:
response = await client.get("/api/v1/predictions/test-user-integration?limit=5")
assert response.status_code == 200
async def test_get_prediction_by_signal_not_found_returns_404(
self, client: AsyncClient
) -> None:
response = await client.get(
"/api/v1/predictions/test-user/signal/nonexistent-signal-id"
)
assert response.status_code == 404
|