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 def _admin_connection(cursor: _FakeCursor): @contextmanager def connection(): yield _FakeConnection(cursor) return connection @pytest.mark.parametrize( "name", [ "postgres", "project", "system_hub", "SYSTEM_HUB", "tjwater_v2_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_template", "project_a"] assert any(call[1] == ("tjwater_v2_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_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 "project" in excluded assert "tjwater_v2_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_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: calls.append(("copy", source, target)), ) monkeypatch.setattr(projects, "have_project", lambda name: True) monkeypatch.setattr( projects, "delete_project", lambda name: calls.append(("delete", name)), ) monkeypatch.setattr( "app.native.wndb.core.model_replace.replace_project_model", lambda target, source, *, copy_source_scada: calls.append( ("clone", target, source, str(copy_source_scada)) ), ) monkeypatch.setattr( "app.native.wndb.core.database.refresh_materialized_views_after_commit", lambda name: calls.append(("refresh", 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", "tjwater_v2_template", "isolated_run"), ("clone", "isolated_run", "project_a", "True"), ("refresh", "isolated_run"), ("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: calls.append(("copy", source, target)), ) 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_template", "empty_conversion"), ("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 == []