File size: 8,363 Bytes
92bfe31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
"""
Route-level tests for the /api/admin/model-config endpoints.

Follows the auth mock pattern from test_api.py.
"""

import os
from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient

import main as main_module
from main import app
from services.inference_client import reset_runtime_overrides

main_module._firebase_ready = True
main_module._init_firebase_admin = lambda: None
main_module.firebase_firestore = None
main_module.firebase_auth = MagicMock()
main_module.firebase_auth.verify_id_token = MagicMock(return_value={
    "uid": "test-teacher-uid",
    "email": "teacher@example.com",
    "role": "teacher",
})

admin_client = TestClient(app, headers={"Authorization": "Bearer admin-token"})

_RESOLVED_KEYS = {
    "INFERENCE_MODEL_ID", "INFERENCE_CHAT_MODEL_ID",
    "HF_QUIZ_MODEL_ID", "HF_RAG_MODEL_ID", "INFERENCE_LOCK_MODEL_ID",
}
_KNOWN_PROFILES = {"dev", "budget", "prod"}
_BASE_CONFIG_KEYS = {"profile", "overrides", "resolved"}


@pytest.fixture(autouse=True)
def _mock_firestore():
    with patch("services.inference_client._save_runtime_config_to_firestore", side_effect=None):
        yield


@pytest.fixture(autouse=True)
def _reset_overrides():
    reset_runtime_overrides()
    yield
    reset_runtime_overrides()


# โ”€โ”€โ”€ Auth Enforcement โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestAuth:
    def test_get_rejects_bad_token(self):
        main_module.firebase_auth.verify_id_token = MagicMock(side_effect=Exception("bad"))
        c = TestClient(app, headers={"Authorization": "Bearer bad-token"})
        response = c.get("/api/admin/model-config")
        main_module.firebase_auth.verify_id_token = MagicMock(return_value={
            "uid": "admin-uid", "email": "admin@example.com", "role": "admin",
        })
        assert response.status_code in {401, 403}

    def test_get_rejects_student_role(self):
        main_module.firebase_auth.verify_id_token = MagicMock(return_value={
            "uid": "student-uid", "email": "s@example.com", "role": "student",
        })
        c = TestClient(app, headers={"Authorization": "Bearer student-token"})
        response = c.get("/api/admin/model-config")
        main_module.firebase_auth.verify_id_token = MagicMock(return_value={
            "uid": "admin-uid", "email": "admin@example.com", "role": "admin",
        })
        assert response.status_code == 403


# โ”€โ”€โ”€ GET Model Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestGetModelConfig:
    def test_returns_base_keys(self):
        response = admin_client.get("/api/admin/model-config")
        assert response.status_code == 200
        data = response.json()
        for key in _BASE_CONFIG_KEYS:
            assert key in data

    def test_resolved_contains_expected_keys(self):
        response = admin_client.get("/api/admin/model-config")
        data = response.json()
        resolved = data.get("resolved", {})
        for key in _RESOLVED_KEYS:
            assert key in resolved

    def test_available_profiles_present(self):
        response = admin_client.get("/api/admin/model-config")
        data = response.json()
        profiles = data.get("availableProfiles", [])
        for p in _KNOWN_PROFILES:
            assert p in profiles

    def test_profile_descriptions_present(self):
        response = admin_client.get("/api/admin/model-config")
        data = response.json()
        descriptions = data.get("profileDescriptions", {})
        for p in _KNOWN_PROFILES:
            assert p in descriptions

    def test_resolved_models_are_non_empty_strings(self):
        admin_client.post("/api/admin/model-config/profile", json={"profile": "dev"})
        response = admin_client.get("/api/admin/model-config")
        data = response.json()
        resolved = data.get("resolved", {})
        for key, value in resolved.items():
            assert isinstance(value, str), f"{key} is not a string: {value}"
            assert len(value) > 0, f"Resolved key {key} is empty"


# โ”€โ”€โ”€ POST Profile Switch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestPostProfileSwitch:
    def test_switch_to_dev_succeeds(self):
        response = admin_client.post("/api/admin/model-config/profile", json={"profile": "dev"})
        assert response.status_code == 200
        assert response.json()["success"] is True

    def test_switch_to_budget_succeeds(self):
        response = admin_client.post("/api/admin/model-config/profile", json={"profile": "budget"})
        assert response.status_code == 200
        data = response.json()
        assert data["success"] is True
        assert data["applied"]["profile"] == "budget"

    def test_switch_to_prod_succeeds(self):
        response = admin_client.post("/api/admin/model-config/profile", json={"profile": "prod"})
        assert response.status_code == 200
        data = response.json()
        assert data["success"] is True
        assert data["applied"]["profile"] == "prod"

    def test_switch_to_invalid_profile_returns_400(self):
        response = admin_client.post("/api/admin/model-config/profile", json={"profile": "nonexistent"})
        assert response.status_code == 400

    def test_switch_missing_profile_field(self):
        response = admin_client.post("/api/admin/model-config/profile", json={})
        assert response.status_code == 422


# โ”€โ”€โ”€ POST Override โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestPostOverride:
    def test_set_valid_override_key_succeeds(self):
        response = admin_client.post(
            "/api/admin/model-config/override",
            json={"key": "INFERENCE_MODEL_ID", "value": "test/override-model"},
        )
        assert response.status_code == 200
        assert response.json()["success"] is True

    def test_set_invalid_override_key_returns_400(self):
        response = admin_client.post(
            "/api/admin/model-config/override",
            json={"key": "EMBEDDING_MODEL", "value": "test/emb"},
        )
        assert response.status_code == 400

    def test_override_is_visible_in_subsequent_get(self):
        admin_client.post(
            "/api/admin/model-config/override",
            json={"key": "INFERENCE_MODEL_ID", "value": "custom/model-v2"},
        )
        response = admin_client.get("/api/admin/model-config")
        data = response.json()
        overrides = data.get("overrides", {})
        assert "INFERENCE_MODEL_ID" in overrides
        assert overrides["INFERENCE_MODEL_ID"] == "custom/model-v2"


# โ”€โ”€โ”€ DELETE Reset โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestDeleteReset:
    def test_reset_returns_success(self):
        response = admin_client.delete("/api/admin/model-config/reset")
        assert response.status_code == 200
        assert response.json()["success"] is True

    def test_reset_clears_override(self):
        admin_client.post(
            "/api/admin/model-config/override",
            json={"key": "INFERENCE_MODEL_ID", "value": "temp/model"},
        )
        response = admin_client.delete("/api/admin/model-config/reset")
        assert response.status_code == 200
        overrides = response.json()["current"]["overrides"]
        assert overrides == {}

    def test_reset_clears_profile(self):
        admin_client.post("/api/admin/model-config/profile", json={"profile": "budget"})
        response = admin_client.delete("/api/admin/model-config/reset")
        assert response.status_code == 200
        assert response.json()["current"]["profile"] == ""


# โ”€โ”€โ”€ Profile after switch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestProfileAfterSwitch:
    def test_switched_profile_visible_in_get(self):
        admin_client.post("/api/admin/model-config/profile", json={"profile": "dev"})
        response = admin_client.get("/api/admin/model-config")
        assert response.json()["profile"] == "dev"