from contextlib import asynccontextmanager from datetime import datetime, timezone from types import SimpleNamespace from uuid import uuid4 import pytest from fastapi import HTTPException from app.api.v1.endpoints import workbench_events as endpoint from app.auth.project_dependencies import ProjectContext from app.infra.db.project_routing import ActiveProjectRouting @pytest.fixture def anyio_backend(): return "asyncio" class FakeHub: generation = 0 async def oldest_cursor(self): return 5 async def current_cursor(self): return 8 class FakeHubManager: @asynccontextmanager async def subscribe(self, _project_id, _dsn): yield FakeHub() class FakeMetadataRepository: def __init__(self, routings): self.routings = routings self.session = SimpleNamespace(closed=False, close=self.close) async def close(self): self.session.closed = True async def list_active_project_db_routings(self, _role): return self.routings class FakeSnapshotConnection: def __init__(self): self.statements = [] @asynccontextmanager async def transaction(self): yield async def execute(self, statement): self.statements.append(statement) class FakeEventRepository: def __init__(self, _conn, _project_id): pass async def current_cursor(self): return 12 class FakeOperationalTaskService: async def list(self, **_kwargs): return [], None async def list_notifications(self, **_kwargs): return [], None @pytest.mark.anyio async def test_sync_snapshot_starts_psycopg_read_only_repeatable_read_transaction(monkeypatch): project_id = uuid4() conn = FakeSnapshotConnection() now = datetime.now(timezone.utc) async def fake_runtime(**_kwargs): return { "availability": "not_configured", "source": "none", "scenario_availability": "not_configured", "artifact_availability": "not_configured", "schedule_availability": "not_configured", "work_order_availability": "not_configured", "alert_availability": "not_configured", "generated_at": now, "conditions": [], "alerts": [], "scenarios": [], "latest_analysis": None, "work_orders": [], } monkeypatch.setattr(endpoint, "WorkbenchEventRepository", FakeEventRepository) monkeypatch.setattr(endpoint, "workbench_runtime", fake_runtime) monkeypatch.setattr(endpoint, "_service", lambda _ctx, _conn: FakeOperationalTaskService()) snapshot = await endpoint.workbench_sync_snapshot( ctx=ProjectContext( project_id=project_id, project_code="project-a", user_id=uuid4(), project_role="member", ), conn=conn, ) assert snapshot.event_cursor == 12 assert conn.statements == [ "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY" ] @pytest.mark.anyio async def test_expired_event_cursor_requires_snapshot_reset(monkeypatch): project_id = uuid4() dsn = "postgresql://test@db.example:5432/project_a" routing = ActiveProjectRouting("project-a", dsn, project_id=project_id) metadata = FakeMetadataRepository([ SimpleNamespace(project_id=project_id, dsn=dsn) ]) monkeypatch.setattr(endpoint, "workbench_event_hubs", FakeHubManager()) response = await endpoint.workbench_events( after=1, last_event_id=None, ctx=ProjectContext( project_id=project_id, project_code="project-a", user_id=uuid4(), project_role="member", ), routing=routing, metadata_repo=metadata, ) first = await anext(response.body_iterator) assert metadata.session.closed is True assert "event: reset_required" in first assert '"cursor":1' in first @pytest.mark.anyio async def test_shared_business_database_disables_event_stream(): project_id = uuid4() dsn = "postgresql://user:secret@db.example:5432/shared" metadata = FakeMetadataRepository([ SimpleNamespace(project_id=project_id, dsn=dsn), SimpleNamespace(project_id=uuid4(), dsn="postgresql://other:other@db.example/shared"), ]) with pytest.raises(HTTPException) as exc: await endpoint.workbench_events( after=0, last_event_id=None, ctx=ProjectContext( project_id=project_id, project_code="project-a", user_id=uuid4(), project_role="member", ), routing=ActiveProjectRouting("project-a", dsn, project_id=project_id), metadata_repo=metadata, ) assert exc.value.status_code == 503