mathpulse-api-v3test / tests /test_admin_model_routes.py
github-actions[bot]
๐Ÿš€ Auto-deploy backend from GitHub (93e7c2a)
92bfe31
"""
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"