Files
TJWaterServerBinary/tests/unit/test_wndb_projects.py
T
jiang 90b02057bc
Generic Container CI/CD / test-build-publish (push) Successful in 1m13s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m13s
feat(projects): automate project infrastructure provisioning
2026-09-11 10:57:51 +08:00

349 lines
11 KiB
Python

from contextlib import contextmanager
import pytest
from app.infra.db.project_routing import ActiveProjectRouting, activate_project_routing
from app.native.wndb.core import projects
class _FakeCursor:
def __init__(self, *, rows=None, current_database: str = "postgres") -> None:
self.rows = rows or []
self.current_database = current_database
self.calls: list[tuple[object, object]] = []
self._last_statement = None
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def __iter__(self):
return iter(self.rows)
def execute(self, statement, params=None):
self.calls.append((statement, params))
self._last_statement = statement
return self
def fetchone(self):
if isinstance(self._last_statement, str) and self._last_statement.startswith(
"select datallowconn"
):
return {"datallowconn": False}
return {"current_database": self.current_database}
class _FakeConnection:
def __init__(self, cursor: _FakeCursor) -> None:
self._cursor = cursor
def cursor(self, **_kwargs):
return self._cursor
class _SequenceCursor(_FakeCursor):
def __init__(self, rows: list[dict]) -> None:
super().__init__()
self._fetch_rows = iter(rows)
def fetchone(self):
return next(self._fetch_rows)
def _admin_connection(cursor: _FakeCursor):
@contextmanager
def connection():
yield _FakeConnection(cursor)
return connection
def _project_connection(cursor: _FakeCursor):
@contextmanager
def connection(_name):
yield _FakeConnection(cursor)
return connection
@pytest.mark.parametrize(
"name",
[
"postgres",
"system_hub",
"SYSTEM_HUB",
"tjwater_v2_template",
"tjwater_v2_schema_template",
"another_template",
],
)
def test_delete_project_rejects_protected_database_before_side_effects(
monkeypatch, name
) -> None:
closed: list[str] = []
monkeypatch.setattr(projects, "close_project_pool", closed.append)
monkeypatch.setattr(
projects,
"admin_connection",
lambda: pytest.fail("protected database opened an administration connection"),
)
with pytest.raises(ValueError, match="protected"):
projects.delete_project(name)
assert closed == []
def test_copy_project_rejects_metadata_source(monkeypatch) -> None:
monkeypatch.setattr(
projects,
"admin_connection",
lambda: pytest.fail("protected database opened an administration connection"),
)
with pytest.raises(ValueError, match="protected"):
projects.copy_project("system_hub", "copy")
def test_copy_project_rejects_unconfigured_template_source(monkeypatch) -> None:
monkeypatch.setattr(
projects,
"admin_connection",
lambda: pytest.fail("unconfigured template opened an administration connection"),
)
with pytest.raises(ValueError, match="protected"):
projects.copy_project("tjwater_next_template", "copy")
def test_create_project_allows_the_protected_template_as_source(monkeypatch) -> None:
cursor = _FakeCursor()
closed: list[str] = []
monkeypatch.setattr(projects, "admin_connection", _admin_connection(cursor))
monkeypatch.setattr(projects, "close_project_pool", closed.append)
projects.create_project("project_a")
assert closed == ["tjwater_v2_schema_template", "project_a"]
assert any(call[1] == ("tjwater_v2_schema_template",) for call in cursor.calls)
assert any(
isinstance(call[0], str)
and call[0].startswith("select pg_terminate_backend")
and call[1] == ("tjwater_v2_schema_template",)
for call in cursor.calls
)
assert any("create database" in str(call[0]).lower() for call in cursor.calls)
def test_list_project_excludes_metadata_database(monkeypatch) -> None:
cursor = _FakeCursor(rows=[{"datname": "project_a"}])
monkeypatch.setattr(projects, "admin_connection", _admin_connection(cursor))
assert projects.list_project() == ["project_a"]
excluded = cursor.calls[0][1][0]
assert "system_hub" in excluded
assert "tjwater_v2_schema_template" in excluded
def test_delete_project_uses_routed_physical_database_name(monkeypatch) -> None:
cursor = _FakeCursor()
closed: list[str] = []
monkeypatch.setattr(projects, "admin_connection", _admin_connection(cursor))
monkeypatch.setattr(projects, "close_project_pool", closed.append)
routing = ActiveProjectRouting(
project_code="logical-project",
business_dsn="postgresql://user:password@db.example/tjwater_v2",
)
with activate_project_routing(routing):
projects.delete_project("logical-project")
assert closed == ["logical-project"]
assert any(call[1] == ("tjwater_v2",) for call in cursor.calls)
def test_temporary_project_names_are_unique_and_postgres_safe() -> None:
first = projects.temporary_project_name(
"TJWater V2 / Production With A Very Long Project Name",
"Burst Analysis",
)
second = projects.temporary_project_name(
"TJWater V2 / Production With A Very Long Project Name",
"Burst Analysis",
)
assert first != second
assert len(first.encode("utf-8")) <= 63
assert first.startswith("tjw_tmp_burst_analysis_tjwat")
def test_temporary_database_capacity_rejects_creation_at_limit(
monkeypatch,
) -> None:
cursor = _FakeCursor()
monkeypatch.setattr(projects.settings, "WNDB_TEMP_DB_MAX_COUNT", 2)
cursor.fetchone = lambda: {"count": 2}
with pytest.raises(RuntimeError, match="limit reached"):
with projects._temporary_database_capacity(cursor, "tjw_tmp_analysis_123"):
pytest.fail("capacity guard yielded after reaching the limit")
assert any("pg_advisory_unlock" in str(statement) for statement, _ in cursor.calls)
def test_project_model_template_requires_ready_active_subscription(monkeypatch) -> None:
cursor = _SequenceCursor(
[
{
"subscriptions": 1,
"enabled_subscriptions": 1,
"active_workers": 1,
},
{"relations": 32, "pending_relations": 0},
]
)
closed: list[str] = []
monkeypatch.setattr(projects, "project_connection", _project_connection(cursor))
monkeypatch.setattr(projects, "close_project_pool", closed.append)
projects._ensure_project_model_template_ready("project_a_template")
assert closed == ["project_a_template"]
@pytest.mark.parametrize(
("status", "relations", "message"),
[
(
{
"subscriptions": 1,
"enabled_subscriptions": 1,
"active_workers": 0,
},
{"relations": 32, "pending_relations": 0},
"subscription is not active",
),
(
{
"subscriptions": 1,
"enabled_subscriptions": 1,
"active_workers": 1,
},
{"relations": 32, "pending_relations": 1},
"still synchronizing",
),
],
)
def test_project_model_template_rejects_unready_subscription(
monkeypatch, status, relations, message
) -> None:
cursor = _SequenceCursor([status, relations])
closed: list[str] = []
monkeypatch.setattr(projects, "project_connection", _project_connection(cursor))
monkeypatch.setattr(projects, "close_project_pool", closed.append)
with pytest.raises(RuntimeError, match=message):
projects._ensure_project_model_template_ready("project_a_template")
assert closed == ["project_a_template"]
def test_temporary_project_database_cleans_up_after_failure(monkeypatch) -> None:
calls: list[tuple[str, ...]] = []
monkeypatch.setattr(
projects,
"temporary_project_name",
lambda project, purpose: "isolated_run",
)
monkeypatch.setattr(
projects,
"copy_project",
lambda source, target, **kwargs: calls.append(
("copy", source, target, str(kwargs.get("allow_template_source")))
),
)
monkeypatch.setattr(projects, "have_project", lambda name: True)
monkeypatch.setattr(
projects,
"delete_project",
lambda name: calls.append(("delete", name)),
)
with pytest.raises(RuntimeError, match="analysis failed"):
with projects.temporary_project_database("project_a", "age") as name:
assert name == "isolated_run"
raise RuntimeError("analysis failed")
assert calls == [
("copy", "project_a_template", "isolated_run", "True"),
("delete", "isolated_run"),
]
def test_temporary_template_database_does_not_clone_a_project(monkeypatch) -> None:
calls: list[tuple[str, ...]] = []
monkeypatch.setattr(
projects,
"temporary_project_name",
lambda project, purpose: "empty_conversion",
)
monkeypatch.setattr(
projects,
"copy_project",
lambda source, target, **kwargs: calls.append(
("copy", source, target, str(kwargs.get("allow_template_source")))
),
)
monkeypatch.setattr(projects, "have_project", lambda name: True)
monkeypatch.setattr(
projects,
"delete_project",
lambda name: calls.append(("delete", name)),
)
with projects.temporary_template_database("conversion", "v3_to_v2") as name:
assert name == "empty_conversion"
assert calls == [
(
"copy",
"tjwater_v2_schema_template",
"empty_conversion",
"True",
),
("delete", "empty_conversion"),
]
def test_clean_project_deletes_only_explicit_unique_targets(monkeypatch) -> None:
cursor = _FakeCursor()
closed: list[str] = []
monkeypatch.setattr(projects, "admin_connection", _admin_connection(cursor))
monkeypatch.setattr(projects, "close_project_pool", closed.append)
projects.clean_project(["temp_a", "temp_b", "temp_a"])
assert closed == ["temp_a", "temp_b"]
termination_targets = [
params[0]
for statement, params in cursor.calls
if isinstance(statement, str) and statement.startswith("select pg_terminate_backend")
]
assert termination_targets == ["temp_a", "temp_b"]
def test_clean_project_validates_all_targets_before_deleting(monkeypatch) -> None:
closed: list[str] = []
monkeypatch.setattr(projects, "close_project_pool", closed.append)
monkeypatch.setattr(
projects,
"admin_connection",
lambda: pytest.fail("invalid targets opened an administration connection"),
)
with pytest.raises(ValueError, match="protected"):
projects.clean_project(["temp_a", "system_hub"])
assert closed == []