Spaces:
Running
Running
| """ | |
| 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"} | |
| def _mock_firestore(): | |
| with patch("services.inference_client._save_runtime_config_to_firestore", side_effect=None): | |
| yield | |
| 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" |