File size: 4,476 Bytes
e49c006
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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