"""PostgreSQL integration coverage for append-only audit events.""" from __future__ import annotations from datetime import UTC, datetime from typing import cast from uuid import uuid4 import pytest import pytest_asyncio from sqlalchemy import text from sqlalchemy.exc import DBAPIError from ragcore.application.audit import AuditEventService from ragcore.config import Settings from ragcore.domain.audit import AuditEvent, AuditEventType from ragcore.domain.control import Workspace from ragcore.infrastructure.database import ( create_database_engine, create_session_factory, session_scope, ) from ragcore.infrastructure.database.audit import AuditEventRepository from ragcore.infrastructure.database.control import ControlPlaneRepository @pytest_asyncio.fixture async def audit_context() -> tuple[AuditEventRepository, Workspace, Workspace]: engine = create_database_engine(Settings(env="test")) factory = create_session_factory(engine) async with session_scope(factory) as session: suffix = uuid4().hex[:8] first = Workspace(uuid4(), "First", f"first-{suffix}") second = Workspace(uuid4(), "Second", f"second-{suffix}") control = ControlPlaneRepository(session) await control.create_workspace(first) await control.create_workspace(second) await session.commit() yield AuditEventRepository(session), first, second await session.rollback() await engine.dispose() @pytest.mark.integration async def test_audit_events_redact_content_and_preserve_trace_correlation( audit_context: tuple[AuditEventRepository, Workspace, Workspace], ) -> None: repository, workspace, _ = audit_context service = AuditEventService(repository, clock=lambda: datetime(2026, 8, 1, tzinfo=UTC)) event = await service.record( workspace_id=workspace.id, event_type=AuditEventType.SECURITY_FAILURE, trace_id="trace-123", correlation_id="request-456", details={ "reason": "denied", "token": "rc_sa_secret", "query": "private text", "nested": {"password": "secret", "safe": "kept"}, }, ) await repository._session.commit() stored = await service.list_events(workspace.id) assert event.trace_id == "trace-123" assert stored[0].correlation_id == "request-456" assert stored[0].details == {"reason": "denied", "nested": {"safe": "kept"}} @pytest.mark.integration async def test_repository_resanitizes_mutated_event_details( audit_context: tuple[AuditEventRepository, Workspace, Workspace], ) -> None: repository, workspace, _ = audit_context event = AuditEvent( id=uuid4(), workspace_id=workspace.id, event_type=AuditEventType.ADMIN_ACTION, occurred_at=datetime.now(UTC), trace_id="mutable-details", details={"nested": {}}, ) nested = cast(dict[str, object], event.details["nested"]) nested["token"] = "must-not-persist" await repository.append(event) await repository._session.commit() stored = await repository.list_events(workspace.id) assert stored[0].details == {"nested": {}} @pytest.mark.integration async def test_audit_reads_are_workspace_scoped( audit_context: tuple[AuditEventRepository, Workspace, Workspace], ) -> None: repository, first, second = audit_context service = AuditEventService(repository) await service.record( workspace_id=first.id, event_type=AuditEventType.ADMIN_ACTION, trace_id="first-trace", ) await service.record( workspace_id=second.id, event_type=AuditEventType.ADMIN_ACTION, trace_id="second-trace", ) await repository._session.commit() assert [event.trace_id for event in await service.list_events(first.id)] == ["first-trace"] assert [event.trace_id for event in await service.list_events(second.id)] == ["second-trace"] @pytest.mark.integration async def test_database_rejects_audit_update_delete_and_truncate( audit_context: tuple[AuditEventRepository, Workspace, Workspace], ) -> None: repository, workspace, _ = audit_context service = AuditEventService(repository) event = await service.record( workspace_id=workspace.id, event_type=AuditEventType.ADMIN_ACTION, trace_id="immutable-trace", ) await repository._session.commit() with pytest.raises(DBAPIError, match="append-only"): await repository._session.execute( text("UPDATE audit.audit_events SET trace_id = 'changed' WHERE id = :id"), {"id": event.id}, ) await repository._session.rollback() with pytest.raises(DBAPIError, match="append-only"): await repository._session.execute( text("DELETE FROM audit.audit_events WHERE id = :id"), {"id": event.id} ) await repository._session.rollback() with pytest.raises(DBAPIError, match="append-only"): await repository._session.execute(text("TRUNCATE audit.audit_events")) await repository._session.rollback() assert (await service.list_events(workspace.id))[0].trace_id == "immutable-trace"