""" Integration tests for the FastAPI endpoints. Uses httpx.AsyncClient with a mocked transcriber so GPU hardware is not required. """ from __future__ import annotations import io from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio def _make_mock_app(): """Create a test FastAPI app with mocked model state.""" from fastapi import FastAPI from fastapi.testclient import TestClient from src.api.routes import health, iot, transcribe from src.engine.transcriber import TranscriptionResult app = FastAPI() app.include_router(health.router, prefix="/api/v1") app.include_router(transcribe.router, prefix="/api/v1") app.include_router(iot.router, prefix="/api/v1") # Mock adapter manager mock_adapter_manager = MagicMock() mock_adapter_manager.get_active.return_value = "bam" mock_adapter_manager.list_available.return_value = ["bam", "ful"] mock_adapter_manager.list_loaded.return_value = ["bam"] # Mock transcriber mock_transcriber = MagicMock() mock_transcriber.transcribe_file.return_value = TranscriptionResult( text="bunding nɔgɔ foro", language="bam", duration_s=3.0, processing_time_ms=250, ) # Mock sensor bridge mock_sensor_bridge = MagicMock() from src.iot.sensor_bridge import SensorData from datetime import datetime mock_sensor_bridge.fetch = AsyncMock( return_value=SensorData( sensor_type="soil", values={"moisture_pct": 42.0, "ph": 6.5}, timestamp=datetime.utcnow().isoformat(), ) ) app.state.adapter_manager = mock_adapter_manager app.state.transcriber = mock_transcriber app.state.sensor_bridge = mock_sensor_bridge return app, TestClient(app) class TestHealthEndpoint: def setup_method(self): self.app, self.client = _make_mock_app() def test_health_returns_200(self): response = self.client.get("/api/v1/health") assert response.status_code == 200 def test_health_response_structure(self): data = self.client.get("/api/v1/health").json() assert "status" in data assert "model_loaded" in data assert "adapters_available" in data def test_health_model_loaded_true(self): data = self.client.get("/api/v1/health").json() assert data["model_loaded"] is True def test_health_active_adapter(self): data = self.client.get("/api/v1/health").json() assert data["active_adapter"] == "bam" class TestTranscribeEndpoint: def setup_method(self): self.app, self.client = _make_mock_app() def _wav_bytes(self) -> bytes: """Minimal valid WAV file bytes for testing.""" import wave import struct buf = io.BytesIO() with wave.open(buf, "wb") as wf: wf.setnchannels(1) wf.setsampwidth(2) wf.setframerate(16000) wf.writeframes(struct.pack("<" + "h" * 160, *([0] * 160))) return buf.getvalue() def test_transcribe_returns_200(self): response = self.client.post( "/api/v1/transcribe", data={"language": "bam"}, files={"audio_file": ("test.wav", self._wav_bytes(), "audio/wav")}, ) assert response.status_code == 200 def test_transcribe_response_has_text(self): data = self.client.post( "/api/v1/transcribe", data={"language": "bam"}, files={"audio_file": ("test.wav", self._wav_bytes(), "audio/wav")}, ).json() assert "text" in data assert isinstance(data["text"], str) def test_invalid_language_returns_422(self): response = self.client.post( "/api/v1/transcribe", data={"language": "xyz"}, files={"audio_file": ("test.wav", self._wav_bytes(), "audio/wav")}, ) assert response.status_code == 422 def test_unsupported_file_type_returns_422(self): response = self.client.post( "/api/v1/transcribe", data={"language": "bam"}, files={"audio_file": ("test.txt", b"not audio", "text/plain")}, ) assert response.status_code == 422 class TestIoTQueryEndpoint: def setup_method(self): self.app, self.client = _make_mock_app() def _wav_bytes(self) -> bytes: import io import struct import wave buf = io.BytesIO() with wave.open(buf, "wb") as wf: wf.setnchannels(1) wf.setsampwidth(2) wf.setframerate(16000) wf.writeframes(struct.pack("<" + "h" * 160, *([0] * 160))) return buf.getvalue() def test_query_returns_200(self): response = self.client.post( "/api/v1/query", data={"language": "bam", "field_id": "field_001"}, files={"audio_file": ("test.wav", self._wav_bytes(), "audio/wav")}, ) assert response.status_code == 200 def test_query_response_structure(self): data = self.client.post( "/api/v1/query", data={"language": "bam"}, files={"audio_file": ("test.wav", self._wav_bytes(), "audio/wav")}, ).json() assert "transcription" in data assert "intent" in data assert "sensor_data" in data assert "voice_response" in data def test_query_voice_response_is_french(self): data = self.client.post( "/api/v1/query", data={"language": "bam"}, files={"audio_file": ("test.wav", self._wav_bytes(), "audio/wav")}, ).json() # French response should contain at least one French word response_text = data["voice_response"] french_indicators = ["du", "de", "le", "la", "les", "et", "Humidité", "sol", "pH"] assert any(word in response_text for word in french_indicators)