Files
govoplan-core/tests/test_change_sequence.py
Albrecht Degering c61abe154c
Some checks failed
Dependency Audit / dependency-audit (push) Has been cancelled
Module Matrix / module-matrix (push) Has been cancelled
Add module event producer coverage
2026-07-10 18:40:38 +02:00

163 lines
7.3 KiB
Python

from __future__ import annotations
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from govoplan_core.core.change_sequence import (
ChangeSequenceEntry,
ChangeSequenceRetentionFloor,
decode_sequence_watermark,
encode_sequence_watermark,
max_sequence_id,
min_sequence_id,
prune_sequence_entries,
record_change,
retained_sequence_floor,
sequence_entries_since,
sequence_watermark_is_expired,
)
from govoplan_core.core.events import EventActorRef, EventBus, EventObjectRef, EventTenantRef, PlatformEvent, event_bus_context
from govoplan_core.db.base import Base
class ChangeSequenceTests(unittest.TestCase):
def test_records_changes_and_returns_entries_after_watermark(self) -> None:
engine = create_engine("sqlite:///:memory:")
SessionLocal = sessionmaker(bind=engine)
try:
Base.metadata.create_all(bind=engine, tables=[ChangeSequenceEntry.__table__, ChangeSequenceRetentionFloor.__table__])
with SessionLocal() as session:
first = record_change(
session,
tenant_id="tenant-1",
module_id="files",
collection="files.assets",
resource_type="file",
resource_id="file-1",
operation="created",
)
second = record_change(
session,
tenant_id="tenant-1",
module_id="files",
collection="files.assets",
resource_type="file",
resource_id="file-1",
operation="updated",
)
session.commit()
self.assertEqual(encode_sequence_watermark(first.id), "seq:1")
self.assertEqual(decode_sequence_watermark("seq:1"), 1)
self.assertEqual(max_sequence_id(session, tenant_id="tenant-1", module_id="files"), second.id)
self.assertEqual(min_sequence_id(session, tenant_id="tenant-1", module_id="files"), first.id)
self.assertFalse(sequence_watermark_is_expired(session, tenant_id="tenant-1", module_id="files", since=first.id - 1))
self.assertEqual(
[entry.operation for entry in sequence_entries_since(session, tenant_id="tenant-1", module_id="files", since=first.id)],
["updated"],
)
session.delete(first)
session.commit()
self.assertFalse(sequence_watermark_is_expired(session, tenant_id="tenant-1", module_id="files", since=0))
finally:
engine.dispose()
def test_record_change_publishes_module_change_event(self) -> None:
engine = create_engine("sqlite:///:memory:")
SessionLocal = sessionmaker(bind=engine)
try:
Base.metadata.create_all(bind=engine, tables=[ChangeSequenceEntry.__table__, ChangeSequenceRetentionFloor.__table__])
seen: list[PlatformEvent] = []
bus = EventBus()
bus.subscribe("*", seen.append)
cases = (
("files", "files.assets", "file", "file-1", "created", "files.file.created"),
("mail", "mail.profiles", "mail_profile", "profile-1", "updated", "mail.profile.updated"),
("campaigns", "campaigns.jobs", "campaign_job", "job-1", "updated", "campaigns.campaign.job.updated"),
)
with SessionLocal() as session:
with event_bus_context(bus):
for module_id, collection, resource_type, resource_id, operation, _event_type in cases:
record_change(
session,
tenant_id="tenant-1",
module_id=module_id,
collection=collection,
resource_type=resource_type,
resource_id=resource_id,
operation=operation,
actor_type="user",
actor_id="user-1",
payload={"scope_type": "tenant"},
)
self.assertEqual([case[-1] for case in cases], [event.type for event in seen])
event = seen[1]
self.assertEqual("mail", event.module_id)
self.assertEqual(EventActorRef(type="user", id="user-1"), event.actor)
self.assertEqual(EventTenantRef(id="tenant-1"), event.tenant)
self.assertEqual(EventObjectRef(type="mail_profile", id="profile-1"), event.resource)
self.assertEqual("mail.profiles", event.payload["collection"])
self.assertEqual({"scope_type": "tenant"}, event.payload["payload"])
finally:
engine.dispose()
def test_pruning_records_retention_floor_for_stale_watermarks(self) -> None:
engine = create_engine("sqlite:///:memory:")
SessionLocal = sessionmaker(bind=engine)
try:
Base.metadata.create_all(bind=engine, tables=[ChangeSequenceEntry.__table__, ChangeSequenceRetentionFloor.__table__])
with SessionLocal() as session:
first = record_change(
session,
tenant_id="tenant-1",
module_id="files",
collection="files.assets",
resource_type="file",
resource_id="file-1",
operation="created",
)
second = record_change(
session,
tenant_id="tenant-1",
module_id="files",
collection="files.assets",
resource_type="file",
resource_id="file-2",
operation="created",
)
session.commit()
first_id = first.id
second_resource_id = second.resource_id
deleted = prune_sequence_entries(
session,
tenant_id="tenant-1",
module_id="files",
collections=("files.assets",),
before_or_equal_id=first_id,
)
session.commit()
self.assertEqual(deleted, 1)
self.assertEqual(retained_sequence_floor(session, tenant_id="tenant-1", module_id="files", collections=("files.assets",)), first_id)
self.assertTrue(sequence_watermark_is_expired(session, tenant_id="tenant-1", module_id="files", collections=("files.assets",), since=0))
self.assertFalse(sequence_watermark_is_expired(session, tenant_id="tenant-1", module_id="files", collections=("files.assets",), since=first_id))
self.assertEqual(
[entry.resource_id for entry in sequence_entries_since(session, tenant_id="tenant-1", module_id="files", collections=("files.assets",), since=first_id)],
[second_resource_id],
)
finally:
engine.dispose()
def test_rejects_invalid_watermark(self) -> None:
with self.assertRaises(ValueError):
decode_sequence_watermark("seq:not-a-number")
if __name__ == "__main__":
unittest.main()