from __future__ import annotations import unittest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from govoplan_audit.backend.commands import AuditCommand, CommandBus from govoplan_audit.backend.db.models import AuditOutboxEvent from govoplan_audit.backend.outbox import SqlAuditOutbox from govoplan_core.core.events import EventActorRef, PlatformEvent from govoplan_core.db.base import Base class AuditCommandBusTests(unittest.TestCase): def test_command_bus_dispatches_commands_separately_from_events(self) -> None: bus = CommandBus() seen: list[AuditCommand] = [] wildcard: list[AuditCommand] = [] bus.subscribe("retention.run", seen.append) bus.subscribe("*", wildcard.append) command = AuditCommand(type="retention.run", module_id="policy", payload={"dry_run": True}) bus.dispatch(command) self.assertEqual([command], seen) self.assertEqual([command], wildcard) self.assertEqual("retention.run", command.to_dict()["type"]) class AuditOutboxTests(unittest.TestCase): def test_outbox_enqueues_governed_event_and_dispatches_pending_rows(self) -> None: engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(bind=engine, tables=[AuditOutboxEvent.__table__]) Session = sessionmaker(bind=engine) outbox = SqlAuditOutbox() seen: list[PlatformEvent] = [] with Session() as session: event = PlatformEvent( type="tenant.created", module_id="tenancy", payload={"tenant_id": "tenant-1"}, actor=EventActorRef(type="user", id="user-1"), ) row = outbox.enqueue(session, event) self.assertEqual("pending", row.status) self.assertEqual("tenant.created", row.event_type) self.assertEqual(event.event_id, row.event_id) self.assertEqual(event.event_id, row.correlation_id) self.assertEqual("user-1", row.payload["actor"]["id"]) counts = outbox.dispatch_pending(session, dispatcher=seen.append) self.assertEqual({"selected": 1, "dispatched": 1, "failed": 0}, counts) self.assertEqual(1, len(seen)) self.assertEqual("tenant.created", seen[0].type) self.assertEqual("dispatched", row.status) self.assertEqual(1, row.attempts) self.assertIsNotNone(row.dispatched_at) def test_outbox_records_failed_dispatch_for_retry(self) -> None: engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(bind=engine, tables=[AuditOutboxEvent.__table__]) Session = sessionmaker(bind=engine) outbox = SqlAuditOutbox() with Session() as session: row = outbox.enqueue(session, PlatformEvent(type="demo.failed", module_id="audit")) counts = outbox.dispatch_pending(session, dispatcher=lambda event: (_ for _ in ()).throw(RuntimeError("offline"))) self.assertEqual({"selected": 1, "dispatched": 0, "failed": 1}, counts) self.assertEqual("failed", row.status) self.assertEqual(1, row.attempts) self.assertEqual("offline", row.last_error) self.assertIsNotNone(row.next_attempt_at) if __name__ == "__main__": unittest.main()