Add module event producer coverage
This commit is contained in:
@@ -18,6 +18,7 @@ from govoplan_core.core.change_sequence import (
|
||||
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
|
||||
|
||||
|
||||
@@ -64,6 +65,46 @@ class ChangeSequenceTests(unittest.TestCase):
|
||||
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)
|
||||
|
||||
@@ -16,6 +16,7 @@ from govoplan_core.core.events import (
|
||||
EventTenantRef,
|
||||
PlatformEvent,
|
||||
current_event_trace,
|
||||
event_bus_context,
|
||||
event_context,
|
||||
normalize_trace_id,
|
||||
)
|
||||
@@ -149,6 +150,83 @@ class CoreEventTests(unittest.TestCase):
|
||||
finally:
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
def test_audit_event_publishes_governed_platform_event(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-audit-event-"))
|
||||
try:
|
||||
with temporary_database(f"sqlite:///{root / 'audit.db'}") as database:
|
||||
from govoplan_access.backend.db import models as access_models # noqa: F401
|
||||
from govoplan_admin.backend.db import models as admin_models # noqa: F401
|
||||
from govoplan_audit.backend.db import models as audit_models # noqa: F401
|
||||
from govoplan_tenancy.backend.db import models as tenancy_models # noqa: F401
|
||||
|
||||
Base.metadata.create_all(bind=database.engine)
|
||||
seen: list[PlatformEvent] = []
|
||||
bus = EventBus()
|
||||
bus.subscribe("*", seen.append)
|
||||
with database.session() as session:
|
||||
with event_bus_context(bus), event_context(correlation_id="corr-1"):
|
||||
item = audit_event(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
scope="tenant",
|
||||
action="user.updated",
|
||||
object_type="user",
|
||||
object_id="user-2",
|
||||
details={"password": "secret", "field": "display_name"},
|
||||
)
|
||||
|
||||
action_events = [event for event in seen if event.type == "user.updated"]
|
||||
self.assertEqual(1, len(action_events))
|
||||
event = action_events[0]
|
||||
self.assertEqual("access", event.module_id)
|
||||
self.assertEqual("corr-1", event.correlation_id)
|
||||
self.assertEqual(EventActorRef(type="user", id="user-1"), event.actor)
|
||||
self.assertEqual(EventTenantRef(id="tenant-1"), event.tenant)
|
||||
self.assertEqual(EventObjectRef(type="user", id="user-2"), event.resource)
|
||||
self.assertEqual(item.id, event.payload["audit_log_id"])
|
||||
self.assertEqual("<redacted>", event.payload["details"]["password"])
|
||||
finally:
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
def test_audit_events_map_existing_module_action_prefixes(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-audit-module-events-"))
|
||||
try:
|
||||
with temporary_database(f"sqlite:///{root / 'audit.db'}") as database:
|
||||
from govoplan_access.backend.db import models as access_models # noqa: F401
|
||||
from govoplan_admin.backend.db import models as admin_models # noqa: F401
|
||||
from govoplan_audit.backend.db import models as audit_models # noqa: F401
|
||||
from govoplan_tenancy.backend.db import models as tenancy_models # noqa: F401
|
||||
|
||||
Base.metadata.create_all(bind=database.engine)
|
||||
seen: list[PlatformEvent] = []
|
||||
bus = EventBus()
|
||||
bus.subscribe("*", seen.append)
|
||||
cases = {
|
||||
"user.created": "access",
|
||||
"tenant.created": "tenancy",
|
||||
"privacy_retention.policy_updated": "policy",
|
||||
"files.connector.imported": "files",
|
||||
"campaign.created": "campaigns",
|
||||
"report.email_sent": "campaigns",
|
||||
}
|
||||
with database.session() as session:
|
||||
with event_bus_context(bus):
|
||||
for action in cases:
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
action=action,
|
||||
object_type="demo",
|
||||
object_id=action,
|
||||
)
|
||||
|
||||
by_type = {event.type: event.module_id for event in seen if event.type in cases}
|
||||
self.assertEqual(cases, by_type)
|
||||
finally:
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
def test_audit_operation_context_keeps_lifecycle_ids_and_sanitizes_details(self) -> None:
|
||||
payload = audit_operation_context(
|
||||
module_id="mail",
|
||||
|
||||
@@ -100,6 +100,7 @@ from govoplan_core.core.modules import MigrationSpec, ModuleManifest
|
||||
from govoplan_core.core.registry import PlatformRegistry, RegistryError
|
||||
from govoplan_core.admin.models import SystemSettings
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.db.bootstrap import bootstrap_dev_data
|
||||
from govoplan_core.db.session import configure_database as configure_database_handle, get_database, reset_database
|
||||
from govoplan_core.security.module_permissions import scopes_grant_compatible
|
||||
from govoplan_core.security.permissions import scope_grants
|
||||
@@ -107,6 +108,7 @@ from govoplan_core.server.app import create_app
|
||||
from govoplan_core.server.config import GovoplanServerConfig
|
||||
from govoplan_core.server.registry import available_module_manifests, build_platform_registry
|
||||
from govoplan_core.server.route_validation import RouteCollisionError
|
||||
from govoplan_core.tenancy.scope import create_scope_tables
|
||||
from govoplan_files.backend.storage.backends import LocalFilesystemStorageBackend, StorageBackendError
|
||||
|
||||
_MANIFEST_PROJECT_MODULES = {
|
||||
@@ -530,6 +532,52 @@ finally:
|
||||
self.assertEqual(files_enabled, registry.integration_enabled("campaigns", "files"))
|
||||
self.assertEqual(mail_enabled, registry.integration_enabled("campaigns", "mail"))
|
||||
|
||||
def test_tenant_lifecycle_with_product_modules_installed_and_absent(self) -> None:
|
||||
cases = (
|
||||
("tenancy_only", ()),
|
||||
("files_only", ("files",)),
|
||||
("mail_only", ("mail",)),
|
||||
("campaign_only", ("campaigns",)),
|
||||
("full_product", ("files", "mail", "campaigns")),
|
||||
)
|
||||
for name, product_modules in cases:
|
||||
with self.subTest(name=name):
|
||||
app, _settings_obj = self._app_for_modules(("tenancy", *product_modules))
|
||||
database = get_database()
|
||||
create_scope_tables(database.engine)
|
||||
Base.metadata.create_all(bind=database.engine)
|
||||
with database.session() as session:
|
||||
bootstrap_dev_data(session, user_password="test-admin")
|
||||
|
||||
with TestClient(app) as client:
|
||||
login = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "admin@example.local", "password": "test-admin"},
|
||||
)
|
||||
self.assertEqual(200, login.status_code, login.text)
|
||||
headers = {"Authorization": f"Bearer {login.json()['access_token']}"}
|
||||
|
||||
created = client.post(
|
||||
"/api/v1/admin/tenants",
|
||||
headers=headers,
|
||||
json={
|
||||
"slug": f"lifecycle-{name}",
|
||||
"name": f"Lifecycle {name}",
|
||||
"settings": {},
|
||||
},
|
||||
)
|
||||
self.assertEqual(201, created.status_code, created.text)
|
||||
tenant_id = created.json()["id"]
|
||||
|
||||
updated = client.patch(
|
||||
f"/api/v1/admin/tenants/{tenant_id}",
|
||||
headers=headers,
|
||||
json={"name": f"Lifecycle {name} Updated", "is_active": False},
|
||||
)
|
||||
self.assertEqual(200, updated.status_code, updated.text)
|
||||
self.assertEqual(f"Lifecycle {name} Updated", updated.json()["name"])
|
||||
self.assertFalse(updated.json()["is_active"])
|
||||
|
||||
def test_registry_rejects_missing_required_capability(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
registry.register(ModuleManifest(
|
||||
|
||||
Reference in New Issue
Block a user