feat(projects): automate project infrastructure provisioning
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services import project_provisioning
|
||||
|
||||
|
||||
class _FakeGeoServer:
|
||||
calls: list[tuple] = []
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return None
|
||||
|
||||
def check_ready(self):
|
||||
self.calls.append(("geoserver_ready",))
|
||||
|
||||
def workspace_exists(self, workspace):
|
||||
self.calls.append(("workspace_exists", workspace))
|
||||
return False
|
||||
|
||||
def create_project_workspace(self, **kwargs):
|
||||
self.calls.append(("create_workspace", kwargs))
|
||||
return ("junctions", "pipes")
|
||||
|
||||
def delete_workspace(self, workspace):
|
||||
self.calls.append(("delete_workspace", workspace))
|
||||
|
||||
|
||||
def _arrange_preflight(monkeypatch, calls):
|
||||
schema_template = project_provisioning.settings.WNDB_SCHEMA_TEMPLATE_DB_NAME
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"have_project",
|
||||
lambda name: name == schema_template,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"require_timescale_schema_template",
|
||||
lambda: calls.append(("require_timescale_template",)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"ensure_replication_worker_capacity",
|
||||
lambda: calls.append(("replication_capacity",)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"timescale_database_exists",
|
||||
lambda _name: False,
|
||||
)
|
||||
monkeypatch.setattr(project_provisioning, "GeoServerAdminClient", _FakeGeoServer)
|
||||
|
||||
|
||||
def test_provision_project_creates_resources_in_dependency_order(monkeypatch):
|
||||
calls: list[tuple] = []
|
||||
_FakeGeoServer.calls = calls
|
||||
_arrange_preflight(monkeypatch, calls)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"create_project",
|
||||
lambda name: calls.append(("create_business", name)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"network_update",
|
||||
lambda path, name: calls.append(("import_model", Path(path).name, name)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"get_project_map_bbox",
|
||||
lambda name: (1.0, 2.0, 3.0, 4.0),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"create_project_model_template",
|
||||
lambda name: calls.append(("create_model_template", name)) or "demo_template",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"create_timescale_database",
|
||||
lambda name: calls.append(("create_timescale", name)),
|
||||
)
|
||||
|
||||
result = project_provisioning.provision_project_infrastructure(
|
||||
code="demo",
|
||||
workspace="demo",
|
||||
inp_path="model.inp",
|
||||
)
|
||||
|
||||
ordered_actions = [call[0] for call in calls]
|
||||
assert ordered_actions.index("create_business") < ordered_actions.index("import_model")
|
||||
assert ordered_actions.index("import_model") < ordered_actions.index("create_model_template")
|
||||
assert ordered_actions.index("create_model_template") < ordered_actions.index("create_timescale")
|
||||
assert ordered_actions.index("create_timescale") < ordered_actions.index("create_workspace")
|
||||
assert result.model_template == "demo_template"
|
||||
assert result.map_bbox == (1.0, 2.0, 3.0, 4.0)
|
||||
|
||||
|
||||
def test_provision_project_rolls_back_created_resources_in_reverse_order(monkeypatch):
|
||||
calls: list[tuple] = []
|
||||
_FakeGeoServer.calls = calls
|
||||
_arrange_preflight(monkeypatch, calls)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"create_project",
|
||||
lambda name: calls.append(("create_business", name)),
|
||||
)
|
||||
monkeypatch.setattr(project_provisioning, "network_update", lambda *_args: None)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"get_project_map_bbox",
|
||||
lambda _name: (1.0, 2.0, 3.0, 4.0),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"create_project_model_template",
|
||||
lambda name: calls.append(("create_model_template", name)) or "demo_template",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"create_timescale_database",
|
||||
lambda _name: (_ for _ in ()).throw(RuntimeError("timescale unavailable")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"delete_project_model_template",
|
||||
lambda name: calls.append(("delete_model_template", name)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
project_provisioning,
|
||||
"delete_project",
|
||||
lambda name: calls.append(("delete_business", name)),
|
||||
)
|
||||
|
||||
with pytest.raises(project_provisioning.ProjectProvisioningError) as exc_info:
|
||||
project_provisioning.provision_project_infrastructure(
|
||||
code="demo",
|
||||
workspace="demo",
|
||||
inp_path="model.inp",
|
||||
)
|
||||
|
||||
assert exc_info.value.stage == "timescaledb"
|
||||
assert [call[0] for call in calls[-2:]] == [
|
||||
"delete_model_template",
|
||||
"delete_business",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
["Demo", "1demo", "demo-project", "demo_template", "postgres"],
|
||||
)
|
||||
def test_validate_project_code_rejects_unsafe_or_reserved_names(code):
|
||||
with pytest.raises(ValueError):
|
||||
project_provisioning.validate_project_code(code)
|
||||
Reference in New Issue
Block a user