from types import SimpleNamespace from unittest.mock import AsyncMock from uuid import uuid4 import pytest from sqlalchemy.dialects import postgresql from app.infra.db.metadb.repositories import metadata_repository from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository from app.domain.schemas.project_settings import ProjectSettings @pytest.fixture def anyio_backend(): return "asyncio" class _ProjectListResult: def __init__(self, rows): self._rows = rows def all(self): return self._rows @pytest.mark.anyio async def test_list_projects_for_user_only_queries_active_projects(): user_id = uuid4() project = SimpleNamespace( id=uuid4(), name="Active Project", code="active-project", description=None, gs_workspace="active-project", map_extent=None, status="active", ) session = SimpleNamespace( execute=AsyncMock(return_value=_ProjectListResult([(project, "member")])) ) projects = await MetadataRepository(session).list_projects_for_user(user_id) statement = session.execute.await_args.args[0] sql = str( statement.compile( dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True}, ) ) assert "user_project_membership.user_id" in sql assert "projects.status = 'active'" in sql assert [item.code for item in projects] == ["active-project"] @pytest.mark.anyio async def test_create_provisioned_project_commits_project_routes_and_membership_once( monkeypatch, ): added = [] session = SimpleNamespace( add=added.append, commit=AsyncMock(), refresh=AsyncMock(), ) monkeypatch.setattr( metadata_repository, "_encrypt_database_secret", lambda value: f"encrypted::{value}", ) user_id = uuid4() project = await MetadataRepository(session).create_provisioned_project( name="Demo", code="demo", description=None, gs_workspace="demo", map_extent={"bbox": [1, 2, 3, 4], "zoom": 14}, creator_user_id=user_id, business_dsn="postgresql://business/demo", timescale_dsn="postgresql://timescale/demo", ) assert project.status == "active" assert [record.db_role for record in added[1:3]] == ["biz_data", "iot_data"] assert added[3].user_id == user_id session.commit.assert_awaited_once() session.refresh.assert_awaited_once_with(project) @pytest.mark.anyio async def test_update_map_config_preserves_legacy_settings_and_other_projects(): project = SimpleNamespace(map_config={'legend': 'original', 'coordinate_reference': {'source_srid': 990001}}) session = SimpleNamespace(commit=AsyncMock(), refresh=AsyncMock()) repository = MetadataRepository(session) repository.get_project_by_id = AsyncMock(return_value=project) project_id = uuid4() search = {'enabled': True, 'provider': 'tianditu', 'admin_code': '341126', 'region': '凤阳县'} await repository.update_project(project_id, updates={'map_config': {'place_search': search}}) repository.get_project_by_id.assert_awaited_once_with(project_id) assert project.map_config == {'legend': 'original', 'coordinate_reference': {'source_srid': 990001}, 'place_search': search} session.commit.assert_awaited_once() @pytest.mark.anyio async def test_update_project_settings_preserves_unmentioned_parameters(): original = {"scada_monitoring_interval_seconds": 300, "future_option": {"enabled": True}} project = SimpleNamespace(settings=original) session = SimpleNamespace(commit=AsyncMock(), refresh=AsyncMock()) repository = MetadataRepository(session) repository.get_project_by_id = AsyncMock(return_value=project) project_id = uuid4() await repository.update_project( project_id, updates={"settings": {"scada_monitoring_interval_seconds": 900}}, ) assert project.settings == {"scada_monitoring_interval_seconds": 900, "future_option": {"enabled": True}} assert original["scada_monitoring_interval_seconds"] == 300 session.commit.assert_awaited_once() @pytest.mark.anyio async def test_empty_settings_update_preserves_values_and_explicit_null_clears_interval(): project = SimpleNamespace(settings={"scada_monitoring_interval_seconds": 900, "other": True}) session = SimpleNamespace(commit=AsyncMock(), refresh=AsyncMock()) repository = MetadataRepository(session) repository.get_project_by_id = AsyncMock(return_value=project) project_id = uuid4() await repository.update_project(project_id, updates={"settings": {}}) assert project.settings == {"scada_monitoring_interval_seconds": 900, "other": True} await repository.update_project( project_id, updates={"settings": {"scada_monitoring_interval_seconds": None}} ) assert project.settings == {"scada_monitoring_interval_seconds": None, "other": True} def test_scada_monitoring_roles_are_typed_inside_extensible_project_settings(): settings = ProjectSettings.model_validate({ "scada_monitoring_interval_seconds": 900, "scada_monitoring": { "rule_version": "burst-monitor-v1", "pressure_device_ids": ["P1", "P2"], "outlet_flow_device_ids": ["F1"], }, "future_option": True, }) assert settings.scada_monitoring.pressure_device_ids == ["P1", "P2"] assert settings.model_dump()["future_option"] is True with pytest.raises(ValueError): ProjectSettings.model_validate({ "scada_monitoring": { "pressure_device_ids": ["D1"], "outlet_flow_device_ids": ["D1"], } })