feat(projects): automate project infrastructure provisioning
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -191,3 +192,114 @@ def test_committed_refresh_failure_is_not_wrapped_as_retryable_500(
|
||||
asyncio.run(model_import._apply_model_update(b"[TITLE]\n", "demo"))
|
||||
|
||||
assert exc_info.value is error
|
||||
|
||||
|
||||
def test_project_provision_creates_metadata_only_after_infrastructure(monkeypatch):
|
||||
project_id = uuid4()
|
||||
now = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
project = SimpleNamespace(
|
||||
id=project_id,
|
||||
name="Demo",
|
||||
code="demo",
|
||||
description=None,
|
||||
gs_workspace="demo",
|
||||
map_extent={"bbox": [1.0, 2.0, 3.0, 4.0], "zoom": 15},
|
||||
status="active",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
infrastructure = SimpleNamespace(
|
||||
map_bbox=(1.0, 2.0, 3.0, 4.0),
|
||||
business_dsn="postgresql://business/demo",
|
||||
timescale_dsn="postgresql://timescale/demo",
|
||||
model_template="demo_template",
|
||||
layers=("junctions", "pipes"),
|
||||
)
|
||||
repo = SimpleNamespace(
|
||||
session=SimpleNamespace(rollback=AsyncMock()),
|
||||
get_project_by_code=AsyncMock(return_value=None),
|
||||
create_provisioned_project=AsyncMock(return_value=project),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
model_import,
|
||||
"_run_uploaded_inp",
|
||||
AsyncMock(return_value='{"simulation_result":"successful"}'),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
model_import,
|
||||
"is_database_encryption_configured",
|
||||
lambda: True,
|
||||
)
|
||||
|
||||
async def fake_threadpool(function, *args, **kwargs):
|
||||
assert function is model_import._provision_from_inp_sync
|
||||
return infrastructure
|
||||
|
||||
monkeypatch.setattr(model_import, "run_in_threadpool", fake_threadpool)
|
||||
monkeypatch.setattr(model_import, "log_audit_event", AsyncMock())
|
||||
client = _client(
|
||||
admin=SimpleNamespace(id=uuid4(), role="admin", is_superuser=True),
|
||||
repo=repo,
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/admin/project-provisions",
|
||||
data={"name": "Demo", "code": "demo", "map_zoom": "15"},
|
||||
files={"file": ("model.inp", VALID_INP)},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json()["model_template_database"] == "demo_template"
|
||||
repo.create_provisioned_project.assert_awaited_once()
|
||||
assert repo.create_provisioned_project.await_args.kwargs["business_dsn"] == infrastructure.business_dsn
|
||||
model_import.log_audit_event.assert_awaited_once()
|
||||
|
||||
|
||||
def test_project_provision_cleans_infrastructure_when_metadata_commit_fails(monkeypatch):
|
||||
cleanup = Mock(return_value=[])
|
||||
infrastructure = SimpleNamespace(
|
||||
map_bbox=(1.0, 2.0, 3.0, 4.0),
|
||||
business_dsn="postgresql://business/demo",
|
||||
timescale_dsn="postgresql://timescale/demo",
|
||||
model_template="demo_template",
|
||||
layers=("junctions", "pipes"),
|
||||
cleanup=cleanup,
|
||||
)
|
||||
repo = SimpleNamespace(
|
||||
session=SimpleNamespace(rollback=AsyncMock()),
|
||||
get_project_by_code=AsyncMock(return_value=None),
|
||||
create_provisioned_project=AsyncMock(side_effect=RuntimeError("metadata down")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
model_import,
|
||||
"_run_uploaded_inp",
|
||||
AsyncMock(return_value='{"simulation_result":"successful"}'),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
model_import,
|
||||
"is_database_encryption_configured",
|
||||
lambda: True,
|
||||
)
|
||||
|
||||
async def fake_threadpool(function, *args, **kwargs):
|
||||
if function is model_import._provision_from_inp_sync:
|
||||
return infrastructure
|
||||
return function(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(model_import, "run_in_threadpool", fake_threadpool)
|
||||
monkeypatch.setattr(model_import, "log_audit_event", AsyncMock())
|
||||
client = _client(
|
||||
admin=SimpleNamespace(id=uuid4(), role="admin", is_superuser=True),
|
||||
repo=repo,
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/admin/project-provisions",
|
||||
data={"name": "Demo", "code": "demo"},
|
||||
files={"file": ("model.inp", VALID_INP)},
|
||||
)
|
||||
|
||||
assert response.status_code == 503
|
||||
repo.session.rollback.assert_awaited_once()
|
||||
cleanup.assert_called_once_with()
|
||||
model_import.log_audit_event.assert_not_awaited()
|
||||
|
||||
Reference in New Issue
Block a user