Enforce tenant module entitlements beyond requests
This commit is contained in:
@@ -426,6 +426,10 @@ class _FakeCampaignPolicyContextProvider:
|
||||
|
||||
|
||||
class _FakeCampaignDeliveryTaskProvider:
|
||||
def tenant_id_for_job(self, session: object, *, job_id: str):
|
||||
del session, job_id
|
||||
return "tenant-1"
|
||||
|
||||
def send_campaign_job(self, session: object, *, job_id: str, enqueue_imap_task: bool = True):
|
||||
del session
|
||||
return {"job_id": job_id, "enqueue_imap_task": enqueue_imap_task}
|
||||
|
||||
@@ -4,6 +4,7 @@ import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from govoplan_core.celery_app import celery, dispatch_calendar_outbox
|
||||
from tests.worker_test_support import allowed_worker_admissions
|
||||
|
||||
|
||||
class CalendarOutboxWorkerTests(unittest.TestCase):
|
||||
@@ -22,6 +23,10 @@ class CalendarOutboxWorkerTests(unittest.TestCase):
|
||||
|
||||
with (
|
||||
patch("govoplan_core.celery_app._calendar_outbox", return_value=provider),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
patch("govoplan_core.db.session.get_database", return_value=database),
|
||||
):
|
||||
result = dispatch_calendar_outbox.run("tenant-1", 25)
|
||||
|
||||
@@ -8,6 +8,7 @@ from govoplan_core.celery_app import (
|
||||
dispatch_dataflow_runs,
|
||||
purge_dataflow_runs,
|
||||
)
|
||||
from tests.worker_test_support import allowed_worker_admissions
|
||||
|
||||
|
||||
class DataflowRunWorkerTests(unittest.TestCase):
|
||||
@@ -30,11 +31,16 @@ class DataflowRunWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.db.session.get_database",
|
||||
return_value=database,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = dispatch_dataflow_runs.run(7)
|
||||
|
||||
provider.dispatch_pending.assert_called_once_with(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
limit=7,
|
||||
worker_id=ANY,
|
||||
)
|
||||
@@ -57,10 +63,18 @@ class DataflowRunWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.db.session.get_database",
|
||||
return_value=database,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = purge_dataflow_runs.run(25)
|
||||
|
||||
provider.purge_expired.assert_called_once_with(session, limit=25)
|
||||
provider.purge_expired.assert_called_once_with(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
limit=25,
|
||||
)
|
||||
session.commit.assert_called_once_with()
|
||||
self.assertEqual(2, result["purged"])
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from govoplan_core.celery_app import celery, dispatch_dataflow_triggers
|
||||
from tests.worker_test_support import allowed_worker_admissions
|
||||
|
||||
|
||||
class DataflowTriggerWorkerTests(unittest.TestCase):
|
||||
@@ -30,10 +31,18 @@ class DataflowTriggerWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.db.session.get_database",
|
||||
return_value=database,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = dispatch_dataflow_triggers.run(25)
|
||||
|
||||
provider.dispatch_due.assert_called_once_with(session, limit=25)
|
||||
provider.dispatch_due.assert_called_once_with(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
limit=25,
|
||||
)
|
||||
session.commit.assert_called_once_with()
|
||||
self.assertEqual(result["succeeded"], 1)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from govoplan_core.core.idm import (
|
||||
)
|
||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from tests.worker_test_support import allowed_worker_admissions
|
||||
|
||||
|
||||
class _Lifecycle:
|
||||
@@ -70,6 +71,10 @@ class IdmAssignmentLifecycleWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.db.session.get_database",
|
||||
return_value=database,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = expire_idm_assignments.run("tenant-1", 25)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from govoplan_core.celery_app import (
|
||||
dispatch_mail_outbox,
|
||||
purge_mail_outbox,
|
||||
)
|
||||
from tests.worker_test_support import allowed_worker_admissions
|
||||
|
||||
|
||||
class _Provider:
|
||||
@@ -35,6 +36,10 @@ class MailDeliveryWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.celery_app._mail_delivery_outbox",
|
||||
return_value=_Provider(),
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = dispatch_mail_outbox.run("tenant-1", 7)
|
||||
|
||||
@@ -56,10 +61,15 @@ class MailDeliveryWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.celery_app._mail_delivery_outbox",
|
||||
return_value=_Provider(),
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = purge_mail_outbox.run(19)
|
||||
|
||||
self.assertIs(result["session"], session)
|
||||
self.assertEqual(result["tenant_id"], "tenant-1")
|
||||
self.assertEqual(result["limit"], 19)
|
||||
|
||||
def test_worker_routes_and_schedules_are_declared(self) -> None:
|
||||
|
||||
@@ -3,22 +3,27 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi import APIRouter, Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal
|
||||
from govoplan_core.celery_app import _run_tenant_worker_batches
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.lifecycle import require_module_active
|
||||
from govoplan_core.core.module_entitlements import (
|
||||
ModuleEntitlementConflict,
|
||||
ModuleEntitlementError,
|
||||
TenantModuleEntitlementResolver,
|
||||
TenantModuleOperatorActionRequired,
|
||||
TenantModuleUnavailable,
|
||||
tenant_module_entitlement_state,
|
||||
update_system_tenant_module_policy,
|
||||
update_tenant_module_selection,
|
||||
)
|
||||
from govoplan_core.core.modules import ModuleManifest
|
||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.db.session import configure_database, get_database
|
||||
from govoplan_core.server.platform import create_platform_router
|
||||
@@ -177,6 +182,75 @@ class TenantModuleEntitlementTests(unittest.TestCase):
|
||||
self.assertEqual({"access", "admin"}, set(state.effective_modules))
|
||||
self.assertTrue(state.diagnostics)
|
||||
|
||||
def test_resolver_caches_and_invalidates_tenant_state(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
for manifest in self.manifests.values():
|
||||
registry.register(manifest)
|
||||
tenant = SimpleNamespace(id="tenant-1", is_active=True, settings={})
|
||||
|
||||
class CountingSession:
|
||||
calls = 0
|
||||
|
||||
def get(self, _model, _tenant_id):
|
||||
self.calls += 1
|
||||
return tenant
|
||||
|
||||
session = CountingSession()
|
||||
resolver = TenantModuleEntitlementResolver(
|
||||
registry,
|
||||
ttl_seconds=60,
|
||||
max_entries=2,
|
||||
)
|
||||
|
||||
resolver.resolve(session, "tenant-1")
|
||||
resolver.resolve(session, "tenant-1")
|
||||
self.assertEqual(1, session.calls)
|
||||
|
||||
resolver.invalidate("tenant-1")
|
||||
resolver.resolve(session, "tenant-1")
|
||||
self.assertEqual(2, session.calls)
|
||||
|
||||
def test_new_and_accepted_work_have_distinct_disable_semantics(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
for manifest in self.manifests.values():
|
||||
registry.register(manifest)
|
||||
settings, _state = update_system_tenant_module_policy(
|
||||
{},
|
||||
self.manifests,
|
||||
available_modules=(),
|
||||
forced_modules=(),
|
||||
enabled_modules=(),
|
||||
expected_revision=0,
|
||||
)
|
||||
tenant = SimpleNamespace(
|
||||
id="tenant-1",
|
||||
is_active=True,
|
||||
settings=settings,
|
||||
)
|
||||
session = SimpleNamespace(get=lambda _model, _tenant_id: tenant)
|
||||
resolver = TenantModuleEntitlementResolver(registry, ttl_seconds=0)
|
||||
|
||||
with self.assertRaises(TenantModuleUnavailable) as rejected:
|
||||
resolver.require(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
module_id="files",
|
||||
work_state="new",
|
||||
)
|
||||
self.assertEqual("rejected", rejected.exception.admission.disposition)
|
||||
|
||||
with self.assertRaises(TenantModuleOperatorActionRequired) as preserved:
|
||||
resolver.require(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
module_id="files",
|
||||
work_state="accepted",
|
||||
)
|
||||
self.assertEqual(
|
||||
"operator_action_required",
|
||||
preserved.exception.admission.disposition,
|
||||
)
|
||||
|
||||
|
||||
class TenantModuleEntitlementRouteTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
@@ -288,6 +362,144 @@ class TenantModuleEntitlementRouteTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(200, response.status_code, response.text)
|
||||
|
||||
def test_public_tenant_route_enforces_module_entitlement(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
for manifest in self.manifests:
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id=manifest.id,
|
||||
name=manifest.name,
|
||||
version=manifest.version,
|
||||
dependencies=manifest.dependencies,
|
||||
public_tenant_resolver=(
|
||||
(lambda _request, _session: "tenant-1")
|
||||
if manifest.id == "files"
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
app = FastAPI()
|
||||
app.state.govoplan_registry = registry
|
||||
guarded = APIRouter(dependencies=[Depends(require_module_active("files"))])
|
||||
|
||||
@guarded.get("/public-files/{token}")
|
||||
def public_files_route(token: str):
|
||||
return {"token": token}
|
||||
|
||||
app.include_router(guarded)
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/public-files/example")
|
||||
|
||||
self.assertEqual(404, response.status_code, response.text)
|
||||
self.assertEqual(
|
||||
"Module is unavailable in the active tenant: files",
|
||||
response.json()["detail"],
|
||||
)
|
||||
|
||||
def test_tenant_capability_rejects_unavailable_provider(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
for manifest in self.manifests:
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id=manifest.id,
|
||||
name=manifest.name,
|
||||
version=manifest.version,
|
||||
dependencies=manifest.dependencies,
|
||||
capability_factories=(
|
||||
{"files.example": lambda _context: object()}
|
||||
if manifest.id == "files"
|
||||
else {}
|
||||
),
|
||||
)
|
||||
)
|
||||
registry.configure_capability_context(
|
||||
ModuleContext(registry=registry, settings=SimpleNamespace())
|
||||
)
|
||||
with get_database().session() as session:
|
||||
with self.assertRaises(TenantModuleUnavailable):
|
||||
registry.require_tenant_capability(
|
||||
"files.example",
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
)
|
||||
|
||||
def test_request_context_treats_unavailable_optional_capability_as_absent(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
for manifest in self.manifests:
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id=manifest.id,
|
||||
name=manifest.name,
|
||||
version=manifest.version,
|
||||
dependencies=manifest.dependencies,
|
||||
capability_factories=(
|
||||
{"files.example": lambda _context: object()}
|
||||
if manifest.id == "files"
|
||||
else {}
|
||||
),
|
||||
)
|
||||
)
|
||||
registry.configure_capability_context(
|
||||
ModuleContext(registry=registry, settings=SimpleNamespace())
|
||||
)
|
||||
app = FastAPI()
|
||||
app.state.govoplan_registry = registry
|
||||
guarded = APIRouter(dependencies=[Depends(require_module_active("admin"))])
|
||||
|
||||
@guarded.get("/admin-capability")
|
||||
def admin_capability_route():
|
||||
return {"files_available": registry.capability("files.example") is not None}
|
||||
|
||||
app.include_router(guarded)
|
||||
with patch(
|
||||
"govoplan_core.core.lifecycle.get_api_principal",
|
||||
return_value=self.principal,
|
||||
), TestClient(app) as client:
|
||||
response = client.get(
|
||||
"/admin-capability",
|
||||
headers={"Authorization": "Bearer test"},
|
||||
)
|
||||
|
||||
self.assertEqual(200, response.status_code, response.text)
|
||||
self.assertFalse(response.json()["files_available"])
|
||||
|
||||
def test_worker_preserves_accepted_work_for_operator_when_disabled(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
for manifest in self.manifests:
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id=manifest.id,
|
||||
name=manifest.name,
|
||||
version=manifest.version,
|
||||
dependencies=manifest.dependencies,
|
||||
capability_factories=(
|
||||
{"files.worker": lambda _context: object()}
|
||||
if manifest.id == "files"
|
||||
else {}
|
||||
),
|
||||
)
|
||||
)
|
||||
registry.configure_capability_context(
|
||||
ModuleContext(registry=registry, settings=SimpleNamespace())
|
||||
)
|
||||
invoked: list[str] = []
|
||||
with get_database().session() as session:
|
||||
result = _run_tenant_worker_batches(
|
||||
registry,
|
||||
session,
|
||||
capability_name="files.worker",
|
||||
tenant_id="tenant-1",
|
||||
operation=lambda tenant_id: invoked.append(tenant_id) or {},
|
||||
defaults={"processed": 0},
|
||||
)
|
||||
|
||||
self.assertEqual([], invoked)
|
||||
self.assertEqual(1, result["operator_action_required"])
|
||||
self.assertEqual(
|
||||
"operator_action_required",
|
||||
result["operator_actions"][0]["disposition"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -520,6 +520,7 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
id="example",
|
||||
name="Example",
|
||||
version="test",
|
||||
public_tenant_resolver=lambda _request, _session: "tenant-1",
|
||||
frontend=FrontendModule(
|
||||
module_id="example",
|
||||
package_name="@govoplan/example-webui",
|
||||
@@ -536,6 +537,7 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
id="example",
|
||||
name="Example",
|
||||
version="test",
|
||||
public_tenant_resolver=lambda _request, _session: "tenant-1",
|
||||
frontend=FrontendModule(
|
||||
module_id="example",
|
||||
package_name="@govoplan/example-webui",
|
||||
@@ -584,6 +586,7 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
id=module_id,
|
||||
name=module_id.title(),
|
||||
version="test",
|
||||
public_tenant_resolver=lambda _request, _session: "tenant-1",
|
||||
frontend=FrontendModule(
|
||||
module_id=module_id,
|
||||
public_routes=(
|
||||
|
||||
@@ -10,6 +10,10 @@ from govoplan_core.celery_app import (
|
||||
purge_platform_events,
|
||||
)
|
||||
from govoplan_core.core.events import PlatformEvent
|
||||
from govoplan_core.core.dataflows import CAPABILITY_DATAFLOW_TRIGGER_DISPATCHER
|
||||
from govoplan_core.core.events import CAPABILITY_PLATFORM_EVENT_OUTBOX
|
||||
from govoplan_core.core.search import CAPABILITY_SEARCH_INDEX_WRITER
|
||||
from tests.worker_test_support import allowed_worker_admissions
|
||||
|
||||
|
||||
class PlatformEventWorkerTests(unittest.TestCase):
|
||||
@@ -18,16 +22,30 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
database = MagicMock()
|
||||
database.SessionLocal.return_value.__enter__.return_value = session
|
||||
outbox = MagicMock()
|
||||
outbox.dispatch_pending.return_value = {
|
||||
"selected": 1,
|
||||
"delivered": 1,
|
||||
"retrying": 0,
|
||||
"quarantined": 0,
|
||||
"dispatched": 1,
|
||||
"observer_failed": 0,
|
||||
}
|
||||
outbox.dispatch_pending.side_effect = (
|
||||
{
|
||||
"selected": 1,
|
||||
"delivered": 1,
|
||||
"retrying": 0,
|
||||
"quarantined": 0,
|
||||
"dispatched": 1,
|
||||
"observer_failed": 0,
|
||||
},
|
||||
{
|
||||
"selected": 0,
|
||||
"delivered": 0,
|
||||
"retrying": 0,
|
||||
"quarantined": 0,
|
||||
"dispatched": 0,
|
||||
"observer_failed": 0,
|
||||
},
|
||||
)
|
||||
dataflow = MagicMock()
|
||||
registry = MagicMock()
|
||||
registry.has_capability.side_effect = lambda name: name in {
|
||||
CAPABILITY_PLATFORM_EVENT_OUTBOX,
|
||||
CAPABILITY_DATAFLOW_TRIGGER_DISPATCHER,
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -54,12 +72,21 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.db.session.get_database",
|
||||
return_value=database,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = dispatch_platform_events.run(25)
|
||||
|
||||
call = outbox.dispatch_pending.call_args
|
||||
self.assertEqual(2, outbox.dispatch_pending.call_count)
|
||||
call = outbox.dispatch_pending.call_args_list[0]
|
||||
self.assertEqual(session, call.args[0])
|
||||
self.assertEqual(25, call.kwargs["limit"])
|
||||
self.assertEqual("tenant-1", call.kwargs["tenant_id"])
|
||||
system_call = outbox.dispatch_pending.call_args_list[1]
|
||||
self.assertTrue(system_call.kwargs["tenantless_only"])
|
||||
self.assertIsNone(system_call.kwargs["tenant_id"])
|
||||
consumer = call.kwargs["consumers"][0]
|
||||
self.assertEqual(
|
||||
"dataflow.event-triggers.v1",
|
||||
@@ -86,14 +113,24 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
database = MagicMock()
|
||||
database.SessionLocal.return_value.__enter__.return_value = session
|
||||
outbox = MagicMock()
|
||||
outbox.dispatch_pending.return_value = {
|
||||
"selected": 1,
|
||||
"delivered": 1,
|
||||
"retrying": 0,
|
||||
"quarantined": 0,
|
||||
"dispatched": 1,
|
||||
"observer_failed": 0,
|
||||
}
|
||||
outbox.dispatch_pending.side_effect = (
|
||||
{
|
||||
"selected": 1,
|
||||
"delivered": 1,
|
||||
"retrying": 0,
|
||||
"quarantined": 0,
|
||||
"dispatched": 1,
|
||||
"observer_failed": 0,
|
||||
},
|
||||
{
|
||||
"selected": 0,
|
||||
"delivered": 0,
|
||||
"retrying": 0,
|
||||
"quarantined": 0,
|
||||
"dispatched": 0,
|
||||
"observer_failed": 0,
|
||||
},
|
||||
)
|
||||
search = MagicMock()
|
||||
search.process_changes.return_value = {
|
||||
"selected": 1,
|
||||
@@ -101,11 +138,16 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
"retrying": 0,
|
||||
"quarantined": 0,
|
||||
}
|
||||
registry = MagicMock()
|
||||
registry.has_capability.side_effect = lambda name: name in {
|
||||
CAPABILITY_PLATFORM_EVENT_OUTBOX,
|
||||
CAPABILITY_SEARCH_INDEX_WRITER,
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"govoplan_core.celery_app._platform_registry",
|
||||
return_value=MagicMock(),
|
||||
return_value=registry,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._platform_event_outbox",
|
||||
@@ -127,10 +169,16 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.db.session.get_database",
|
||||
return_value=database,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = dispatch_platform_events.run(25)
|
||||
|
||||
consumer = outbox.dispatch_pending.call_args.kwargs["consumers"][0]
|
||||
consumer = outbox.dispatch_pending.call_args_list[0].kwargs[
|
||||
"consumers"
|
||||
][0]
|
||||
self.assertEqual("search.indexing.v1", consumer.consumer_id)
|
||||
self.assertEqual(frozenset({"*"}), consumer.event_types)
|
||||
event = PlatformEvent(type="files.file.updated", module_id="files")
|
||||
@@ -141,7 +189,11 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
event=event,
|
||||
delivery_key=delivery_key,
|
||||
)
|
||||
search.process_changes.assert_called_once_with(session, limit=25)
|
||||
search.process_changes.assert_called_once_with(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
limit=25,
|
||||
)
|
||||
self.assertEqual(1, result["search_changes"]["applied"])
|
||||
session.commit.assert_called_once_with()
|
||||
|
||||
@@ -150,9 +202,20 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
database = MagicMock()
|
||||
database.SessionLocal.return_value.__enter__.return_value = session
|
||||
outbox = MagicMock()
|
||||
outbox.purge_terminal.return_value = {"deleted": 2}
|
||||
outbox.purge_terminal.side_effect = (
|
||||
{"deleted": 2},
|
||||
{"deleted": 1},
|
||||
)
|
||||
registry = MagicMock()
|
||||
registry.has_capability.side_effect = lambda name: (
|
||||
name == CAPABILITY_PLATFORM_EVENT_OUTBOX
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"govoplan_core.celery_app._platform_registry",
|
||||
return_value=registry,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._platform_event_outbox",
|
||||
return_value=outbox,
|
||||
@@ -166,17 +229,25 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
"platform_event_outbox_terminal_retention_days",
|
||||
30,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = purge_platform_events.run(75)
|
||||
|
||||
call = outbox.purge_terminal.call_args
|
||||
self.assertEqual(2, outbox.purge_terminal.call_count)
|
||||
call = outbox.purge_terminal.call_args_list[0]
|
||||
self.assertEqual(session, call.args[0])
|
||||
self.assertEqual(75, call.kwargs["limit"])
|
||||
self.assertEqual("tenant-1", call.kwargs["tenant_id"])
|
||||
system_call = outbox.purge_terminal.call_args_list[1]
|
||||
self.assertTrue(system_call.kwargs["tenantless_only"])
|
||||
before = call.kwargs["before"]
|
||||
self.assertIsInstance(before, datetime)
|
||||
self.assertEqual(timezone.utc, before.tzinfo)
|
||||
session.commit.assert_called_once_with()
|
||||
self.assertEqual({"deleted": 2}, result)
|
||||
self.assertEqual(3, result["deleted"])
|
||||
|
||||
def test_worker_routes_and_periodic_tasks_are_registered(self) -> None:
|
||||
self.assertEqual(
|
||||
|
||||
@@ -18,6 +18,9 @@ class _CompleteGateway:
|
||||
def resolve_participation(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def resolve_public_invitation(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def submit_governed_response(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from govoplan_core.core.postbox import (
|
||||
postbox_routing_provider,
|
||||
)
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from tests.worker_test_support import allowed_worker_admissions
|
||||
|
||||
|
||||
class _RoutingProvider:
|
||||
@@ -68,6 +69,10 @@ class PostboxRoutingWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.db.session.get_database",
|
||||
return_value=database,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = dispatch_postbox_routes.run("tenant-1", 25)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from govoplan_core.core.workflows import (
|
||||
workflow_runtime_worker,
|
||||
workflow_trigger_dispatcher,
|
||||
)
|
||||
from tests.worker_test_support import allowed_worker_admissions
|
||||
|
||||
|
||||
class _Worker:
|
||||
@@ -141,10 +142,18 @@ class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.db.session.get_database",
|
||||
return_value=database,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._worker_admissions",
|
||||
side_effect=allowed_worker_admissions,
|
||||
),
|
||||
):
|
||||
result = reconcile_workflow_instances.run(25)
|
||||
|
||||
worker.reconcile_pending.assert_called_once_with(session, limit=25)
|
||||
worker.reconcile_pending.assert_called_once_with(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
limit=25,
|
||||
)
|
||||
session.commit.assert_called_once_with()
|
||||
self.assertEqual(1, result["advanced"])
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from govoplan_core.core.module_entitlements import (
|
||||
TenantModuleAdmission,
|
||||
TenantWorkState,
|
||||
)
|
||||
|
||||
|
||||
def allowed_worker_admissions(
|
||||
_registry,
|
||||
_session,
|
||||
*,
|
||||
capability_name: str,
|
||||
tenant_id: str | None,
|
||||
work_state: TenantWorkState = "accepted",
|
||||
) -> tuple[TenantModuleAdmission, ...]:
|
||||
return (
|
||||
TenantModuleAdmission(
|
||||
tenant_id=tenant_id or "tenant-1",
|
||||
module_id=capability_name.split(".", 1)[0],
|
||||
revision=1,
|
||||
work_state=work_state,
|
||||
allowed=True,
|
||||
disposition="allowed",
|
||||
reason="Test tenant permits the worker capability.",
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user