docs: surface permission-gated Campaign tasks

This commit is contained in:
2026-07-21 18:53:07 +02:00
parent 99ef25b08f
commit c05bb8e474
3 changed files with 624 additions and 15 deletions

View File

@@ -5,8 +5,7 @@ from types import SimpleNamespace
import pytest
from govoplan_campaign.backend.documentation import documentation_topics
from govoplan_campaign.backend.manifest import get_manifest
from govoplan_campaign.backend.documentation import CAMPAIGN_USER_DOCUMENTATION, documentation_topics
from govoplan_core.core.modules import DocumentationContext
@@ -47,6 +46,8 @@ def _topics(scopes: set[str], integrations: set[str] | None = None, *, documenta
def test_campaign_runtime_documentation_provider_is_registered() -> None:
from govoplan_campaign.backend.manifest import get_manifest
assert documentation_topics in get_manifest().documentation_providers
@@ -150,9 +151,201 @@ def test_runtime_documentation_full_composition_uses_only_user_facing_names() ->
assert "Managed file versions" in rendered
assert "Address records" in rendered
assert "In-app notifications" in rendered
assert topic.metadata["help_contexts"] == ["campaigns.list", "campaign.overview"]
for technical_name in integrations:
assert technical_name not in rendered
assert "0.1." not in rendered
assert "0.2." not in rendered
assert "hostname" not in rendered.lower()
assert "secret" not in rendered.lower()
def _visible_static_topics(
scopes: set[str],
*,
modules: set[str] | None = None,
capabilities: set[str] | None = None,
) -> set[str]:
installed = modules or {"campaigns"}
available_capabilities = capabilities or set()
principal = _Principal(frozenset(scopes))
visible: set[str] = set()
for topic in CAMPAIGN_USER_DOCUMENTATION:
condition = topic.conditions[0]
if not set(condition.required_modules).issubset(installed):
continue
if not set(condition.required_capabilities).issubset(available_capabilities):
continue
if not all(principal.has(scope) for scope in condition.required_scopes):
continue
visible.add(topic.id)
return visible
def test_campaign_manager_sees_only_authoring_tasks_from_the_static_handbook() -> None:
visible = _visible_static_topics(
{
"campaigns:campaign:read",
"campaigns:campaign:create",
"campaigns:campaign:update",
"campaigns:campaign:copy",
"campaigns:campaign:validate",
"campaigns:campaign:build",
"campaigns:recipient:read",
"campaigns:recipient:write",
"campaigns:recipient:import",
"campaigns:report:read",
}
)
assert {
"campaigns.workflow.create-campaign",
"campaigns.workflow.create-editable-successor",
"campaigns.workflow.import-recipients",
"campaigns.workflow.view-delivery-report",
}.issubset(visible)
assert "campaigns.workflow.queue-delivery" not in visible
assert "campaigns.workflow.send-small-controlled-run" not in visible
assert "campaigns.workflow.export-delivery-report" not in visible
assert "campaigns.workflow.share-campaign" not in visible
assert "campaigns.workflow.archive-campaign" not in visible
assert "campaigns.workflow.delete-untouched-draft" not in visible
def test_sender_sees_queue_and_send_only_with_the_mail_contract_and_profile_authority() -> None:
scopes = {
"campaigns:campaign:read",
"campaigns:campaign:queue",
"campaigns:campaign:send",
"campaigns:recipient:read",
"campaigns:report:read",
"mail:profile:use",
}
without_mail = _visible_static_topics(scopes)
with_mail = _visible_static_topics(
scopes,
modules={"campaigns", "mail"},
capabilities={"mail.campaign_delivery"},
)
assert "campaigns.workflow.queue-delivery" not in without_mail
assert "campaigns.workflow.send-small-controlled-run" not in without_mail
assert "campaigns.workflow.queue-delivery" in with_mail
assert "campaigns.workflow.send-small-controlled-run" in with_mail
def test_connected_authoring_tasks_require_their_declared_contracts_and_permissions() -> None:
attachment_scopes = {
"campaigns:campaign:read",
"campaigns:campaign:update",
"campaigns:campaign:validate",
"campaigns:recipient:read",
"files:file:read",
"files:file:share",
}
source_scopes = {
"campaigns:campaign:read",
"campaigns:campaign:update",
"campaigns:recipient:read",
"campaigns:recipient:write",
"campaigns:recipient:import",
}
files_visible = _visible_static_topics(
attachment_scopes,
modules={"campaigns", "files"},
capabilities={"files.campaign_attachments"},
)
addresses_visible = _visible_static_topics(
source_scopes,
modules={"campaigns", "addresses"},
capabilities={"addresses.recipient_source"},
)
assert "campaigns.workflow.use-managed-attachments" in files_visible
assert "campaigns.workflow.import-address-source" in addresses_visible
assert "campaigns.workflow.use-managed-attachments" not in _visible_static_topics(attachment_scopes)
assert "campaigns.workflow.import-address-source" not in _visible_static_topics(source_scopes)
def test_report_export_and_lifecycle_tasks_are_independently_permission_gated() -> None:
exporter = _visible_static_topics(
{
"campaigns:campaign:read",
"campaigns:report:export",
"campaigns:recipient:export",
}
)
custodian = _visible_static_topics(
{
"campaigns:campaign:read",
"campaigns:campaign:share",
"campaigns:campaign:archive",
"campaigns:campaign:delete",
}
)
assert "campaigns.workflow.export-delivery-report" in exporter
assert "campaigns.workflow.view-delivery-report" not in exporter
assert "campaigns.workflow.share-campaign" in custodian
assert "campaigns.workflow.archive-campaign" in custodian
assert "campaigns.workflow.delete-untouched-draft" in custodian
assert "campaigns.workflow.export-delivery-report" not in custodian
def test_static_campaign_handbook_has_unique_ids_help_contexts_and_no_planned_resend_claim() -> None:
from govoplan_campaign.backend.manifest import get_manifest
topics = get_manifest().documentation
ids = [topic.id for topic in topics]
rendered_static = "\n".join(
(topic.title + "\n" + topic.summary + "\n" + topic.body).lower()
for topic in CAMPAIGN_USER_DOCUMENTATION
)
known_help_contexts = {
"campaigns.list",
"campaign.overview",
"campaign.settings",
"campaign.fields",
"campaign.template",
"campaign.attachments",
"campaign.recipients",
"campaign.recipient-data",
"campaign.server-settings",
"campaign.global-settings",
"campaign.review-send",
"campaign.report",
"campaign.audit",
"campaign.json",
}
assert len(ids) == len(set(ids))
assert "single resend" not in rendered_static
for topic in CAMPAIGN_USER_DOCUMENTATION:
assert topic.metadata["kind"] == "workflow"
assert topic.metadata["prerequisites"]
assert topic.metadata["steps"]
assert topic.metadata["outcome"]
assert topic.metadata["verification"]
assert set(topic.metadata["help_contexts"]).issubset(known_help_contexts)
existing_user_workflows = {
topic.id: topic
for topic in topics
if topic.id
in {
"campaigns.mail-profile-user-journey",
"campaigns.workflow.prepare-validate-and-build",
"campaigns.workflow.complete-review",
"campaigns.workflow.retry-and-reconcile",
}
}
assert set(existing_user_workflows) == {
"campaigns.mail-profile-user-journey",
"campaigns.workflow.prepare-validate-and-build",
"campaigns.workflow.complete-review",
"campaigns.workflow.retry-and-reconcile",
}
for topic in existing_user_workflows.values():
assert topic.metadata["help_contexts"]