"""Tests for /api/dreams/analyze endpoint.""" from __future__ import annotations import json from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient from app.routes.dreams import _rate_store @pytest.fixture(autouse=True) def _reset_rate_limiter(): _rate_store.clear() yield _rate_store.clear() def _mock_gemini_response(**overrides: object) -> MagicMock: defaults = { "emotions": ["fear", "wonder"], "themes": ["being chased"], "symbols": ["water"], "interpretation": "This reflects anxiety about a real-life situation.", "lucidityIndicator": False, } defaults.update(overrides) response = MagicMock() response.text = json.dumps(defaults) return response def test_analyze_valid_dream_returns_structured_result(client: TestClient) -> None: mock_client = MagicMock() mock_client.models.generate_content.return_value = _mock_gemini_response() with patch("app.services.dream_analyzer._get_client", return_value=mock_client): response = client.post( "/api/dreams/analyze", json={"dreamText": "I was being chased through a dark forest and found a river.", "language": "English"}, ) assert response.status_code == 200 body = response.json() assert body["emotions"] == ["fear", "wonder"] assert body["themes"] == ["being chased"] assert body["lucidityIndicator"] is False def test_analyze_rejects_too_short_dream(client: TestClient) -> None: response = client.post("/api/dreams/analyze", json={"dreamText": "hi"}) assert response.status_code == 400 assert response.json()["detail"]["code"] == "dream_too_short" def test_analyze_rejects_empty_dream_text(client: TestClient) -> None: response = client.post("/api/dreams/analyze", json={"dreamText": ""}) assert response.status_code == 422 def test_analyze_rejects_missing_dream_text(client: TestClient) -> None: response = client.post("/api/dreams/analyze", json={}) assert response.status_code == 422 def test_analyze_handles_missing_api_key(client: TestClient) -> None: with patch("app.services.dream_analyzer._get_client", side_effect=ValueError("DREAMS_GEMINI_API_KEY required")): response = client.post( "/api/dreams/analyze", json={"dreamText": "A perfectly normal dream about flying over mountains."}, ) assert response.status_code == 503 assert response.json()["detail"]["code"] == "service_not_configured" def test_analyze_handles_malformed_gemini_output(client: TestClient) -> None: mock_client = MagicMock() bad_response = MagicMock() bad_response.text = "I refuse to analyze this." mock_client.models.generate_content.return_value = bad_response with patch("app.services.dream_analyzer._get_client", return_value=mock_client): response = client.post( "/api/dreams/analyze", json={"dreamText": "A perfectly normal dream about flying over mountains."}, ) assert response.status_code == 502 assert response.json()["detail"]["code"] == "analysis_failed" def test_analyze_filters_invalid_emotions_from_gemini(client: TestClient) -> None: """Gemini could hallucinate an emotion outside the allowed set — the service must filter it rather than pass it straight through.""" mock_client = MagicMock() mock_client.models.generate_content.return_value = _mock_gemini_response( emotions=["fear", "not_a_real_emotion", "joy"] ) with patch("app.services.dream_analyzer._get_client", return_value=mock_client): response = client.post( "/api/dreams/analyze", json={"dreamText": "A perfectly normal dream about flying over mountains."}, ) assert response.status_code == 200 assert "not_a_real_emotion" not in response.json()["emotions"] def test_analyze_enforces_rate_limit(client: TestClient) -> None: mock_client = MagicMock() mock_client.models.generate_content.return_value = _mock_gemini_response() with patch("app.services.dream_analyzer._get_client", return_value=mock_client): last_status = 200 for _ in range(10): r = client.post( "/api/dreams/analyze", json={"dreamText": "A perfectly normal dream about flying over mountains."}, ) last_status = r.status_code assert last_status == 429