from types import SimpleNamespace from unittest.mock import AsyncMock from uuid import uuid4 from fastapi import HTTPException from fastapi.testclient import TestClient from app.api.v1.endpoints import model_import from app.auth.metadata_dependencies import ( get_current_metadata_admin, get_metadata_repository, ) from app.infra.db.metadb.repositories.metadata_repository import ProjectDbRouting from app.infra.db.project_routing import get_project_pgconn_string from tests.conftest import build_test_app VALID_INP = b"[TITLE]\nDesktop model\n[JUNCTIONS]\n;ID Elev Demand\n" def _client(*, admin=None, repo=None) -> TestClient: app = build_test_app(model_import.router, "/api/v1") if admin is not None: app.dependency_overrides[get_current_metadata_admin] = lambda: admin if repo is not None: app.dependency_overrides[get_metadata_repository] = lambda: repo return TestClient(app) def test_system_admin_can_import_model_without_project_membership( monkeypatch, ): project_id = uuid4() project = SimpleNamespace(id=project_id, code="demo", status="active") repo = SimpleNamespace( session=object(), get_project_by_id=AsyncMock(return_value=project), ) admin = SimpleNamespace(id=uuid4(), role="admin", is_superuser=False) monkeypatch.setattr( model_import, "_run_uploaded_inp", AsyncMock(return_value="imported"), ) monkeypatch.setattr(model_import, "log_audit_event", AsyncMock()) client = _client(admin=admin, repo=repo) response = client.post( f"/api/v1/admin/projects/{project_id}/model-imports", files={"file": ("desktop-model.inp", VALID_INP)}, ) assert response.status_code == 200 assert response.json()["project_id"] == str(project_id) assert response.json()["result"] == "imported" repo.get_project_by_id.assert_awaited_once_with(project_id) model_import.log_audit_event.assert_awaited_once() def test_non_admin_is_denied_model_import(): def deny_admin(): raise HTTPException(status_code=403, detail="Admin access required") app = build_test_app(model_import.router, "/api/v1") app.dependency_overrides[get_current_metadata_admin] = deny_admin client = TestClient(app) response = client.post( f"/api/v1/admin/projects/{uuid4()}/model-imports", files={"file": ("desktop-model.inp", VALID_INP)}, ) assert response.status_code == 403 assert response.json()["detail"] == "Admin access required" def test_model_import_rejects_non_inp_file(monkeypatch): project_id = uuid4() repo = SimpleNamespace( session=object(), get_project_by_id=AsyncMock( return_value=SimpleNamespace( id=project_id, code="demo", status="active", ) ), ) monkeypatch.setattr(model_import, "log_audit_event", AsyncMock()) client = _client( admin=SimpleNamespace(id=uuid4(), role="admin", is_superuser=False), repo=repo, ) response = client.post( f"/api/v1/admin/projects/{project_id}/model-imports", files={"file": ("desktop-model.txt", VALID_INP)}, ) assert response.status_code == 400 assert response.json()["detail"] == "Only .inp model files are accepted" model_import.log_audit_event.assert_not_awaited() def test_model_update_uses_project_business_routing(monkeypatch): project_id = uuid4() project = SimpleNamespace(id=project_id, code="demo", status="active") repo = SimpleNamespace( session=object(), get_project_by_id=AsyncMock(return_value=project), get_project_db_routing=AsyncMock( return_value=ProjectDbRouting( project_id=project_id, db_role="biz_data", db_type="postgresql", dsn="postgresql://user:password@biz.example/routed_business", pool_min_size=1, pool_max_size=5, ) ), ) captured: dict[str, str] = {} async def fake_apply_model_update(content: bytes, project_code: str) -> None: assert content == VALID_INP captured["project_code"] = project_code captured["dsn"] = get_project_pgconn_string(project_code) monkeypatch.setattr(model_import, "_apply_model_update", fake_apply_model_update) monkeypatch.setattr(model_import, "log_audit_event", AsyncMock()) client = _client( admin=SimpleNamespace(id=uuid4(), role="admin", is_superuser=False), repo=repo, ) response = client.patch( f"/api/v1/admin/projects/{project_id}/model-imports", files={"file": ("desktop-model.inp", VALID_INP)}, ) assert response.status_code == 200 assert captured == { "project_code": "demo", "dsn": "postgresql://user:password@biz.example/routed_business", } repo.get_project_db_routing.assert_awaited_once_with(project_id, "biz_data")