Files
next-tjwater-server/tests/unit/test_workbench_events.py
T
jiang ed06bc2bfc fix(workbench): configure psycopg snapshot transaction
Open the psycopg transaction without unsupported SQLAlchemy-style options, then set the PostgreSQL isolation and read-only characteristics before the first query. Cover the exact snapshot endpoint path with a regression test.
2026-09-24 09:49:01 +08:00

167 lines
4.7 KiB
Python

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