docs: add adaptive Campaign composition guide
This commit is contained in:
158
tests/test_documentation.py
Normal file
158
tests/test_documentation.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from govoplan_campaign.backend.documentation import documentation_topics
|
||||
from govoplan_campaign.backend.manifest import get_manifest
|
||||
from govoplan_core.core.modules import DocumentationContext
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Principal:
|
||||
scopes: frozenset[str]
|
||||
|
||||
def has(self, scope: str) -> bool:
|
||||
namespace, resource, _action = scope.split(":", 2)
|
||||
return scope in self.scopes or f"{namespace}:{resource}:*" in self.scopes or f"{namespace}:*" in self.scopes or "*:*" in self.scopes
|
||||
|
||||
|
||||
class _Registry:
|
||||
def __init__(self, integrations: set[str], *, interface_only: set[str] | None = None, capability_only: set[str] | None = None) -> None:
|
||||
interfaces = integrations | (interface_only or set())
|
||||
self._capabilities = integrations | (capability_only or set())
|
||||
self._manifests = (
|
||||
SimpleNamespace(
|
||||
provides_interfaces=tuple(SimpleNamespace(name=name) for name in sorted(interfaces)),
|
||||
),
|
||||
)
|
||||
|
||||
def manifests(self):
|
||||
return self._manifests
|
||||
|
||||
def has_capability(self, name: str) -> bool:
|
||||
return name in self._capabilities
|
||||
|
||||
|
||||
def _topics(scopes: set[str], integrations: set[str] | None = None, *, documentation_type: str = "user"):
|
||||
return documentation_topics(
|
||||
DocumentationContext(
|
||||
registry=_Registry(integrations or set()),
|
||||
principal=_Principal(frozenset(scopes)),
|
||||
documentation_type=documentation_type, # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_campaign_runtime_documentation_provider_is_registered() -> None:
|
||||
assert documentation_topics in get_manifest().documentation_providers
|
||||
|
||||
|
||||
def test_runtime_documentation_is_user_only_and_requires_a_campaign_task() -> None:
|
||||
assert _topics({"docs:documentation:read"}) == ()
|
||||
assert _topics({"campaigns:campaign:read"}, documentation_type="admin") == ()
|
||||
|
||||
|
||||
def test_runtime_documentation_reflects_actor_authority_without_exposing_scopes() -> None:
|
||||
topic = _topics(
|
||||
{
|
||||
"campaigns:campaign:read",
|
||||
"campaigns:campaign:create",
|
||||
"campaigns:campaign:update",
|
||||
"campaigns:campaign:validate",
|
||||
"campaigns:campaign:build",
|
||||
"campaigns:recipient:read",
|
||||
"campaigns:recipient:write",
|
||||
"campaigns:recipient:import",
|
||||
}
|
||||
)[0]
|
||||
|
||||
configuration = topic.metadata["current_configuration"]
|
||||
assert "Create new campaigns." in configuration
|
||||
assert "Build exact recipient messages for review." in configuration
|
||||
assert not any("queue" in item.lower() for item in configuration)
|
||||
assert not any("campaigns:" in item or "files:" in item or "mail:" in item for item in configuration)
|
||||
|
||||
|
||||
def test_runtime_documentation_requires_both_interface_and_capability() -> None:
|
||||
integration = "mail.campaign_delivery"
|
||||
scopes = {"campaigns:campaign:read", "mail:profile:use"}
|
||||
|
||||
for registry in (
|
||||
_Registry(set(), interface_only={integration}),
|
||||
_Registry(set(), capability_only={integration}),
|
||||
):
|
||||
topic = documentation_topics(
|
||||
DocumentationContext(registry=registry, principal=_Principal(frozenset(scopes)), documentation_type="user")
|
||||
)[0]
|
||||
assert not any("selection is available" in item for item in topic.metadata["current_configuration"])
|
||||
|
||||
topic = _topics(scopes, {integration})[0]
|
||||
assert any("Mail profile selection is available" in item for item in topic.metadata["current_configuration"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("integrations", "scopes", "expected"),
|
||||
[
|
||||
(
|
||||
{"files.campaign_attachments"},
|
||||
{"campaigns:campaign:read", "campaigns:recipient:read", "files:file:read"},
|
||||
"Managed campaign attachments can be previewed",
|
||||
),
|
||||
(
|
||||
{"addresses.lookup"},
|
||||
{"campaigns:campaign:read", "campaigns:recipient:read"},
|
||||
"Address records can be looked up",
|
||||
),
|
||||
(
|
||||
{"addresses.recipient_source"},
|
||||
{"campaigns:campaign:read", "campaigns:recipient:import"},
|
||||
"traceable recipient snapshot",
|
||||
),
|
||||
(
|
||||
{"notifications.dispatch"},
|
||||
{"campaigns:campaign:read"},
|
||||
"status changes",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_runtime_documentation_reflects_optional_composition_permutations(
|
||||
integrations: set[str],
|
||||
scopes: set[str],
|
||||
expected: str,
|
||||
) -> None:
|
||||
topic = _topics(scopes, integrations)[0]
|
||||
assert any(expected in item for item in topic.metadata["current_configuration"])
|
||||
|
||||
|
||||
def test_runtime_documentation_full_composition_uses_only_user_facing_names() -> None:
|
||||
integrations = {
|
||||
"mail.campaign_delivery",
|
||||
"files.campaign_attachments",
|
||||
"addresses.lookup",
|
||||
"addresses.recipient_source",
|
||||
"notifications.dispatch",
|
||||
}
|
||||
topic = _topics({"*:*"}, integrations)[0]
|
||||
rendered = "\n".join(
|
||||
(
|
||||
topic.title,
|
||||
topic.summary,
|
||||
topic.body,
|
||||
*topic.metadata["current_configuration"],
|
||||
*topic.metadata["limitations"],
|
||||
)
|
||||
)
|
||||
|
||||
assert "Mail profile selection" in rendered
|
||||
assert "Managed file versions" in rendered
|
||||
assert "Address records" in rendered
|
||||
assert "In-app notifications" in rendered
|
||||
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()
|
||||
Reference in New Issue
Block a user