docs: surface custom Mail profile task
This commit is contained in:
@@ -114,6 +114,10 @@ non-production provider and mailbox first. A successful connection test does
|
||||
not prove policy authorization for a later Campaign context, deliverability,
|
||||
recipient acceptance, SPF/DKIM/DMARC alignment, or future availability.
|
||||
|
||||
Testing a saved profile requires both `mail:profile:test` and
|
||||
`mail:profile:use`, and the profile must be active. Profile creation or test
|
||||
authority alone is not enough.
|
||||
|
||||
Raw settings test endpoints accept new settings only for actors who may both
|
||||
test profiles and manage secrets. They are an administration aid, not a way for
|
||||
ordinary consumers to bypass reusable profiles.
|
||||
@@ -149,6 +153,14 @@ profile metadata administrators not to know or replace credentials.
|
||||
|
||||
### Create or change a profile
|
||||
|
||||
The configured Help Center exposes **Create a custom Mail profile** only when
|
||||
the current actor has profile-write authority and the effective user-scope
|
||||
policy permits user profiles. It states the active SMTP/IMAP hostname
|
||||
allow-list groups and deny rules, plus the actor's separate credential, test,
|
||||
use, and approval requirements. The Settings task creates in the current
|
||||
account's user scope; `mail:profile:write` itself remains broad profile
|
||||
administration authority, not an API-enforced self-only permission.
|
||||
|
||||
1. Choose the narrowest suitable scope and a stable, descriptive name/slug.
|
||||
2. Configure SMTP, optional IMAP, TLS mode, account identity, Sent-folder
|
||||
behavior, and timeouts. Sender/envelope/recipient constraints belong to
|
||||
|
||||
@@ -4,15 +4,32 @@ from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.modules import DocumentationContext, DocumentationLink, DocumentationTopic
|
||||
from govoplan_mail.backend.mail_profiles import MailProfileError, effective_mail_profile_policy_for_scope
|
||||
from govoplan_core.core.modules import DocumentationCondition, DocumentationContext, DocumentationLink, DocumentationTopic
|
||||
from govoplan_mail.backend.mail_profiles import (
|
||||
EffectiveMailProfilePolicy,
|
||||
MailProfileError,
|
||||
effective_mail_profile_policy_for_scope,
|
||||
)
|
||||
|
||||
MAIL_POLICY_DOC_SCOPES = ("mail:profile:read", "admin:policies:read", "system:settings:read")
|
||||
MAIL_PROFILE_READ_SCOPE = "mail:profile:read"
|
||||
MAIL_PROFILE_WRITE_SCOPE = "mail:profile:write"
|
||||
MAIL_SECRET_MANAGE_SCOPE = "mail:secret:manage"
|
||||
MAIL_PROFILE_TEST_SCOPE = "mail:profile:test"
|
||||
MAIL_PROFILE_USE_SCOPE = "mail:profile:use"
|
||||
_HOST_POLICY_FIELDS = (("SMTP", "smtp_hosts"), ("IMAP", "imap_hosts"))
|
||||
|
||||
|
||||
def documentation_topics(context: DocumentationContext) -> tuple[DocumentationTopic, ...]:
|
||||
topic = _tenant_mail_policy_topic(context)
|
||||
return (topic,) if topic is not None else ()
|
||||
topics = [
|
||||
topic
|
||||
for topic in (
|
||||
_tenant_mail_policy_topic(context),
|
||||
_custom_mail_profile_topic(context),
|
||||
)
|
||||
if topic is not None
|
||||
]
|
||||
return tuple(topics)
|
||||
|
||||
|
||||
def _tenant_mail_policy_topic(context: DocumentationContext) -> DocumentationTopic | None:
|
||||
@@ -29,17 +46,26 @@ def _tenant_mail_policy_topic(context: DocumentationContext) -> DocumentationTop
|
||||
try:
|
||||
policy = effective_mail_profile_policy_for_scope(session, tenant_id=tenant_id, scope_type="tenant")
|
||||
except MailProfileError as exc:
|
||||
user_documentation = context.documentation_type == "user"
|
||||
return DocumentationTopic(
|
||||
id="mail.tenant-profile-policy-unavailable",
|
||||
title="Mail server policy could not be evaluated",
|
||||
summary="Mail profile documentation is installed, but the tenant-level effective policy could not be read for this request.",
|
||||
body=str(exc),
|
||||
body=(
|
||||
"The current Mail policy could not be loaded. Try again or ask a Mail administrator for help."
|
||||
if user_documentation
|
||||
else str(exc)
|
||||
),
|
||||
layer="available",
|
||||
documentation_types=(context.documentation_type,),
|
||||
source_module_id="mail",
|
||||
order=41,
|
||||
links=(_mail_policy_api_link(),),
|
||||
metadata={"error_type": type(exc).__name__},
|
||||
links=(
|
||||
(DocumentationLink(label="Public mail help", href="https://govplan.add-ideas.de/modules/mail", kind="public"),)
|
||||
if user_documentation
|
||||
else (_mail_policy_api_link(),)
|
||||
),
|
||||
metadata={} if user_documentation else {"error_type": type(exc).__name__},
|
||||
)
|
||||
|
||||
effective = policy.as_dict()
|
||||
@@ -90,6 +116,237 @@ def _tenant_mail_policy_topic(context: DocumentationContext) -> DocumentationTop
|
||||
)
|
||||
|
||||
|
||||
def _custom_mail_profile_topic(context: DocumentationContext) -> DocumentationTopic | None:
|
||||
if context.documentation_type != "user":
|
||||
return None
|
||||
principal = context.principal
|
||||
if not _has_all_scopes(principal, (MAIL_PROFILE_READ_SCOPE, MAIL_PROFILE_WRITE_SCOPE)):
|
||||
return None
|
||||
tenant_id = str(getattr(principal, "tenant_id", "") or "")
|
||||
user_id = str(getattr(getattr(principal, "user", None), "id", "") or "")
|
||||
session = context.session
|
||||
if not tenant_id or not user_id or not isinstance(session, Session):
|
||||
return None
|
||||
|
||||
try:
|
||||
policy = effective_mail_profile_policy_for_scope(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
scope_type="user",
|
||||
scope_id=user_id,
|
||||
)
|
||||
except MailProfileError:
|
||||
# A user-facing runtime topic must not turn internal policy resolution
|
||||
# details into documentation output. The task simply remains absent
|
||||
# until its effective policy can be proven.
|
||||
return None
|
||||
if not policy.allow_user_profiles:
|
||||
return None
|
||||
|
||||
can_manage_credentials = _has_any_scope(principal, (MAIL_SECRET_MANAGE_SCOPE,))
|
||||
can_test_profile = _has_all_scopes(principal, (MAIL_PROFILE_TEST_SCOPE, MAIL_PROFILE_USE_SCOPE))
|
||||
can_use_profile = _has_any_scope(principal, (MAIL_PROFILE_USE_SCOPE,))
|
||||
approval_required = bool(policy.allowed_profile_id_sets)
|
||||
constraints = _host_policy_constraint_records(policy)
|
||||
authority_lines = _custom_profile_authority_lines(
|
||||
can_manage_credentials=can_manage_credentials,
|
||||
can_test_profile=can_test_profile,
|
||||
can_use_profile=can_use_profile,
|
||||
approval_required=approval_required,
|
||||
)
|
||||
steps = _custom_profile_steps(
|
||||
can_manage_credentials=can_manage_credentials,
|
||||
can_test_profile=can_test_profile,
|
||||
can_use_profile=can_use_profile,
|
||||
approval_required=approval_required,
|
||||
)
|
||||
return DocumentationTopic(
|
||||
id="mail.workflow.create-custom-profile",
|
||||
title="Create a custom Mail profile",
|
||||
summary=(
|
||||
"Create a reusable profile in the current account's user-scoped Settings view, "
|
||||
"within the active SMTP and IMAP hostname policy."
|
||||
),
|
||||
body="\n".join(authority_lines),
|
||||
layer="configured",
|
||||
documentation_types=("user",),
|
||||
audience=("mail_profile_author", "campaign_manager"),
|
||||
order=41,
|
||||
conditions=(
|
||||
DocumentationCondition(
|
||||
required_modules=("mail",),
|
||||
required_scopes=(MAIL_PROFILE_READ_SCOPE, MAIL_PROFILE_WRITE_SCOPE),
|
||||
configuration_keys=("mail_profile_policy",),
|
||||
),
|
||||
),
|
||||
links=(
|
||||
DocumentationLink(label="My Mail profiles", href="/settings?section=mail-profiles", kind="runtime"),
|
||||
DocumentationLink(label="Public mail help", href="https://govplan.add-ideas.de/modules/mail", kind="public"),
|
||||
),
|
||||
related_modules=("campaigns",),
|
||||
unlocks=("A custom Mail-owned transport definition that authorized tasks can reference after all policy checks pass.",),
|
||||
configuration_keys=("mail_profile_policy",),
|
||||
i18n_key="mail.topic.create_custom_profile",
|
||||
source_module_id="mail",
|
||||
metadata={
|
||||
"kind": "workflow",
|
||||
"route": "/settings?section=mail-profiles",
|
||||
"screen": "My Mail profiles",
|
||||
"help_contexts": ["mail.profiles", "app.settings"],
|
||||
"prerequisites": [
|
||||
"The Mail profile editor is available in Settings and opens in the current account's user scope.",
|
||||
"The effective Mail policy permits user-scoped profiles.",
|
||||
],
|
||||
"steps": list(steps),
|
||||
"outcome": "A user-scoped custom Mail profile is saved without copying its credentials into a consuming module.",
|
||||
"current_configuration": list(authority_lines),
|
||||
"constraints": list(constraints),
|
||||
"verification": _custom_profile_verification(
|
||||
can_test_profile=can_test_profile,
|
||||
can_use_profile=can_use_profile,
|
||||
approval_required=approval_required,
|
||||
),
|
||||
"can_manage_credentials": can_manage_credentials,
|
||||
"can_test_profile": can_test_profile,
|
||||
"can_use_profile": can_use_profile,
|
||||
"approval_required_before_use": approval_required,
|
||||
"related_topic_ids": [
|
||||
"mail.workflow.choose-and-test-profile",
|
||||
"mail.profile-ownership-and-consumers",
|
||||
"mail.profiles-and-policy",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _host_policy_constraint_records(policy: EffectiveMailProfilePolicy) -> tuple[dict[str, Any], ...]:
|
||||
constraints: list[dict[str, Any]] = []
|
||||
for label, key in _HOST_POLICY_FIELDS:
|
||||
prefix = label.casefold()
|
||||
denied = _display_patterns(policy.blacklist_patterns.get(key, []))
|
||||
constraints.append({
|
||||
"id": f"{prefix}-host-deny",
|
||||
"label": f"{label} denied hosts",
|
||||
"description": (
|
||||
"Deny rules are checked first. The hostname must not match any listed pattern."
|
||||
if denied
|
||||
else "Deny rules are checked first. No hostname deny pattern is active."
|
||||
),
|
||||
**({"values": list(denied)} if denied else {}),
|
||||
})
|
||||
allowed_groups = tuple(
|
||||
group
|
||||
for group in (_display_patterns(items) for items in policy.whitelist_groups.get(key, []))
|
||||
if group
|
||||
)
|
||||
if not allowed_groups:
|
||||
constraints.append({
|
||||
"id": f"{prefix}-host-allow",
|
||||
"label": f"{label} allowed hosts",
|
||||
"description": "After deny checks, no hostname allow-list group is active.",
|
||||
})
|
||||
continue
|
||||
for index, group in enumerate(allowed_groups, start=1):
|
||||
constraints.append({
|
||||
"id": f"{prefix}-host-allow-{index}",
|
||||
"label": f"{label} allowed hosts — group {index}",
|
||||
"description": (
|
||||
"After deny checks, the hostname must match at least one pattern in this group. "
|
||||
"It must satisfy every active allow-list group shown for this protocol."
|
||||
),
|
||||
"values": list(group),
|
||||
})
|
||||
return tuple(constraints)
|
||||
|
||||
|
||||
def _display_patterns(patterns: list[str]) -> tuple[str, ...]:
|
||||
result: list[str] = []
|
||||
for pattern in patterns:
|
||||
value = str(pattern).strip()
|
||||
if value and value not in result:
|
||||
result.append(value)
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def _custom_profile_authority_lines(
|
||||
*,
|
||||
can_manage_credentials: bool,
|
||||
can_test_profile: bool,
|
||||
can_use_profile: bool,
|
||||
approval_required: bool,
|
||||
) -> tuple[str, ...]:
|
||||
credentials = (
|
||||
"Credential authority: you may save or replace Mail-owned SMTP/IMAP passwords."
|
||||
if can_manage_credentials
|
||||
else "Credential authority: you may define the profile, but you cannot save or replace passwords; an actor with both profile-write and secret-management authority must do that when authentication requires one."
|
||||
)
|
||||
testing = (
|
||||
"Test authority: you may run the profile's SMTP/IMAP connection tests after saving it as active."
|
||||
if can_test_profile
|
||||
else "Test authority: creating the profile does not let you run connection tests; ask an actor with both profile-test and profile-use authority to verify an active profile."
|
||||
)
|
||||
use = (
|
||||
"Use authority: you may select the profile in an authorized task after its contextual policy checks pass."
|
||||
if can_use_profile
|
||||
else "Use authority: creating the profile does not let you select or use it; separate Mail profile use authority is required."
|
||||
)
|
||||
approval = (
|
||||
"Approval: an approved-profile list is active. A newly generated profile reference must be approved before the profile can be selected or used."
|
||||
if approval_required
|
||||
else "Approval: no approved-profile list currently blocks a newly created profile, but each consuming task still rechecks its contextual policy."
|
||||
)
|
||||
return credentials, testing, use, approval
|
||||
|
||||
|
||||
def _custom_profile_steps(
|
||||
*,
|
||||
can_manage_credentials: bool,
|
||||
can_test_profile: bool,
|
||||
can_use_profile: bool,
|
||||
approval_required: bool,
|
||||
) -> tuple[str, ...]:
|
||||
steps = [
|
||||
"Open Settings, choose Mail profiles, and select Add profile in the current account's user-scoped view.",
|
||||
"Enter a stable name and configure SMTP plus optional IMAP hostnames that satisfy every host-policy statement shown above.",
|
||||
]
|
||||
steps.append(
|
||||
"Enter the required SMTP/IMAP credentials in the dedicated password fields."
|
||||
if can_manage_credentials
|
||||
else "Save the non-secret profile definition, then ask an actor with both profile-write and secret-management authority to add credentials if the server requires authentication."
|
||||
)
|
||||
steps.append("Save the profile; Mail validates the effective user-scope host policy again on the server.")
|
||||
steps.append(
|
||||
"Save the profile as active, then run the available SMTP and IMAP connection tests."
|
||||
if can_test_profile
|
||||
else "Ask an actor with both profile-test and profile-use authority to run the SMTP and IMAP connection tests after the profile is active."
|
||||
)
|
||||
if approval_required:
|
||||
steps.append("Ask a Mail administrator to add the new profile to the active approved-profile list.")
|
||||
steps.append(
|
||||
"Select the profile from the consuming task's picker and complete that task's contextual validation."
|
||||
if can_use_profile
|
||||
else "Ask an actor with Mail profile use authority to select it in the consuming task after approval and testing."
|
||||
)
|
||||
return tuple(steps)
|
||||
|
||||
|
||||
def _custom_profile_verification(*, can_test_profile: bool, can_use_profile: bool, approval_required: bool) -> str:
|
||||
checks = ["Reopen My Mail profiles and confirm the saved profile remains in the current account's user-scoped view."]
|
||||
checks.append(
|
||||
"Confirm the authorized SMTP/IMAP tests succeed."
|
||||
if can_test_profile
|
||||
else "Have an authorized tester confirm the SMTP/IMAP tests succeed."
|
||||
)
|
||||
if approval_required:
|
||||
checks.append("Confirm an administrator approved the generated profile reference before expecting it in a picker.")
|
||||
checks.append(
|
||||
"Confirm the intended task can select it and passes its own policy validation."
|
||||
if can_use_profile
|
||||
else "Confirm a separately authorized user can select it and passes the consuming task's policy validation."
|
||||
)
|
||||
return " ".join(checks)
|
||||
|
||||
|
||||
def _mail_policy_admin_text(policy: dict[str, Any], *, source_count: int) -> tuple[str, str]:
|
||||
allowed_profile_ids = policy.get("allowed_profile_ids")
|
||||
lower_scopes = _allowed_lower_scopes(policy)
|
||||
@@ -259,5 +516,16 @@ def _has_any_scope(principal: object | None, scopes: tuple[str, ...]) -> bool:
|
||||
return "*" in principal_scopes or any(scope in principal_scopes for scope in scopes)
|
||||
|
||||
|
||||
def _has_all_scopes(principal: object | None, scopes: tuple[str, ...]) -> bool:
|
||||
has = getattr(principal, "has", None)
|
||||
if callable(has):
|
||||
try:
|
||||
return all(bool(has(scope)) for scope in scopes)
|
||||
except Exception:
|
||||
return False
|
||||
principal_scopes = set(getattr(principal, "scopes", ()) or ())
|
||||
return "*" in principal_scopes or all(scope in principal_scopes for scope in scopes)
|
||||
|
||||
|
||||
def _mail_policy_api_link() -> DocumentationLink:
|
||||
return DocumentationLink(label="Tenant mail policy API", href="/api/v1/mail/policies/tenant", kind="api")
|
||||
|
||||
@@ -279,6 +279,7 @@ manifest = ModuleManifest(
|
||||
"kind": "workflow",
|
||||
"route": "/settings?section=mail-profiles",
|
||||
"screen": "Mail profiles",
|
||||
"help_contexts": ["mail.profiles", "app.settings"],
|
||||
"prerequisites": [
|
||||
"Mail is installed and you may read, use, and test profiles visible in the current context.",
|
||||
"A profile administrator has configured credentials and effective policy.",
|
||||
@@ -321,6 +322,7 @@ manifest = ModuleManifest(
|
||||
"kind": "workflow",
|
||||
"route": "/mail",
|
||||
"screen": "Mail",
|
||||
"help_contexts": ["mail.list"],
|
||||
"prerequisites": [
|
||||
"An active visible profile has IMAP configured.",
|
||||
"You may both use that profile and read its mailbox.",
|
||||
|
||||
171
tests/test_documentation.py
Normal file
171
tests/test_documentation.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.modules import DocumentationContext
|
||||
from govoplan_mail.backend.documentation import documentation_topics
|
||||
from govoplan_mail.backend.mail_profiles import EffectiveMailProfilePolicy, MailProfileError
|
||||
|
||||
|
||||
class _Principal:
|
||||
tenant_id = "tenant-1"
|
||||
user = SimpleNamespace(id="user-1")
|
||||
|
||||
def __init__(self, scopes: set[str]) -> None:
|
||||
self.scopes = frozenset(scopes)
|
||||
|
||||
def has(self, scope: str) -> bool:
|
||||
return scope in self.scopes
|
||||
|
||||
|
||||
class MailRuntimeDocumentationTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.session = Session()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.session.close()
|
||||
|
||||
def context(self, scopes: set[str], *, documentation_type: str = "user") -> DocumentationContext:
|
||||
return DocumentationContext(
|
||||
registry=object(),
|
||||
principal=_Principal(scopes),
|
||||
settings=None,
|
||||
session=self.session,
|
||||
documentation_type=documentation_type, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
def topics(self, context: DocumentationContext, user_policy: EffectiveMailProfilePolicy) -> dict[str, object]:
|
||||
tenant_policy = EffectiveMailProfilePolicy()
|
||||
|
||||
def effective_policy(_session, *, scope_type, **_kwargs):
|
||||
return user_policy if scope_type == "user" else tenant_policy
|
||||
|
||||
with patch(
|
||||
"govoplan_mail.backend.documentation.effective_mail_profile_policy_for_scope",
|
||||
side_effect=effective_policy,
|
||||
):
|
||||
return {topic.id: topic for topic in documentation_topics(context)}
|
||||
|
||||
def test_custom_profile_task_requires_write_authority_and_enabled_user_profiles(self) -> None:
|
||||
enabled = EffectiveMailProfilePolicy(allow_user_profiles=True)
|
||||
without_write = self.topics(self.context({"mail:profile:read"}), enabled)
|
||||
self.assertNotIn("mail.workflow.create-custom-profile", without_write)
|
||||
|
||||
without_read = self.topics(self.context({"mail:profile:write"}), enabled)
|
||||
self.assertNotIn("mail.workflow.create-custom-profile", without_read)
|
||||
|
||||
disabled = EffectiveMailProfilePolicy(allow_user_profiles=False)
|
||||
blocked = self.topics(self.context({"mail:profile:read", "mail:profile:write"}), disabled)
|
||||
self.assertNotIn("mail.workflow.create-custom-profile", blocked)
|
||||
|
||||
available = self.topics(self.context({"mail:profile:read", "mail:profile:write"}), enabled)
|
||||
self.assertIn("mail.workflow.create-custom-profile", available)
|
||||
task = available["mail.workflow.create-custom-profile"]
|
||||
self.assertEqual(task.title, "Create a custom Mail profile")
|
||||
self.assertEqual(task.metadata["route"], "/settings?section=mail-profiles")
|
||||
self.assertEqual(task.metadata["help_contexts"], ["mail.profiles", "app.settings"])
|
||||
self.assertIn("current account's user scope", task.metadata["prerequisites"][0])
|
||||
|
||||
def test_custom_profile_task_distinguishes_secret_test_and_use_authority(self) -> None:
|
||||
policy = EffectiveMailProfilePolicy()
|
||||
cases = (
|
||||
(set(), False, False, False),
|
||||
({"mail:secret:manage"}, True, False, False),
|
||||
({"mail:profile:test"}, False, False, False),
|
||||
({"mail:profile:use"}, False, False, True),
|
||||
({"mail:profile:test", "mail:profile:use"}, False, True, True),
|
||||
({"mail:secret:manage", "mail:profile:test", "mail:profile:use"}, True, True, True),
|
||||
)
|
||||
for extra_scopes, credentials, testing, use in cases:
|
||||
with self.subTest(extra_scopes=extra_scopes):
|
||||
topics = self.topics(self.context({"mail:profile:read", "mail:profile:write", *extra_scopes}), policy)
|
||||
task = topics["mail.workflow.create-custom-profile"]
|
||||
self.assertEqual(task.metadata["can_manage_credentials"], credentials)
|
||||
self.assertEqual(task.metadata["can_test_profile"], testing)
|
||||
self.assertEqual(task.metadata["can_use_profile"], use)
|
||||
self.assertIn(
|
||||
"you may save or replace" if credentials else "you cannot save or replace passwords",
|
||||
task.body,
|
||||
)
|
||||
self.assertIn(
|
||||
"you may run" if testing else "does not let you run connection tests",
|
||||
task.body,
|
||||
)
|
||||
self.assertIn(
|
||||
"you may select" if use else "does not let you select or use it",
|
||||
task.body,
|
||||
)
|
||||
|
||||
def test_custom_profile_task_preserves_host_allow_group_and_deny_precedence(self) -> None:
|
||||
policy = EffectiveMailProfilePolicy(
|
||||
whitelist_groups={
|
||||
"smtp_hosts": [
|
||||
["*.example.edu", "smtp.shared.test"],
|
||||
["smtp-*.example.edu"],
|
||||
],
|
||||
"imap_hosts": [["imap.example.edu"]],
|
||||
},
|
||||
blacklist_patterns={
|
||||
"smtp_hosts": ["smtp-blocked.example.edu"],
|
||||
"imap_hosts": ["imap-legacy.example.edu"],
|
||||
},
|
||||
allowed_profile_id_sets=[{"private-profile-id"}],
|
||||
)
|
||||
topics = self.topics(
|
||||
self.context({"mail:profile:read", "mail:profile:write", "mail:secret:manage", "mail:profile:test", "mail:profile:use"}),
|
||||
policy,
|
||||
)
|
||||
task = topics["mail.workflow.create-custom-profile"]
|
||||
constraints = {item["id"]: item for item in task.metadata["constraints"]}
|
||||
constraints_text = " ".join(item["description"] for item in task.metadata["constraints"])
|
||||
|
||||
self.assertIn("Deny rules are checked first", constraints_text)
|
||||
self.assertEqual(constraints["smtp-host-deny"]["values"], ["smtp-blocked.example.edu"])
|
||||
self.assertEqual(constraints["smtp-host-allow-1"]["values"], ["*.example.edu", "smtp.shared.test"])
|
||||
self.assertEqual(constraints["smtp-host-allow-2"]["values"], ["smtp-*.example.edu"])
|
||||
self.assertIn("every active allow-list group", constraints_text)
|
||||
self.assertEqual(constraints["imap-host-allow-1"]["values"], ["imap.example.edu"])
|
||||
self.assertTrue(task.metadata["approval_required_before_use"])
|
||||
self.assertIn("must be approved before the profile can be selected or used", task.body)
|
||||
self.assertNotIn("private-profile-id", repr(task))
|
||||
self.assertIn("every active allow-list group", constraints["smtp-host-allow-1"]["description"])
|
||||
|
||||
def test_custom_profile_task_explains_when_no_approval_list_is_active(self) -> None:
|
||||
topics = self.topics(
|
||||
self.context({"mail:profile:read", "mail:profile:write", "mail:profile:use"}),
|
||||
EffectiveMailProfilePolicy(),
|
||||
)
|
||||
task = topics["mail.workflow.create-custom-profile"]
|
||||
self.assertFalse(task.metadata["approval_required_before_use"])
|
||||
self.assertIn("no approved-profile list currently blocks", task.body)
|
||||
|
||||
def test_user_policy_errors_are_generic_and_never_create_a_task(self) -> None:
|
||||
context = self.context({"mail:profile:read", "mail:profile:write"})
|
||||
with patch(
|
||||
"govoplan_mail.backend.documentation.effective_mail_profile_policy_for_scope",
|
||||
side_effect=MailProfileError("sensitive source-id profile-id username secret"),
|
||||
):
|
||||
topics = {topic.id: topic for topic in documentation_topics(context)}
|
||||
|
||||
self.assertNotIn("mail.workflow.create-custom-profile", topics)
|
||||
unavailable = topics["mail.tenant-profile-policy-unavailable"]
|
||||
self.assertEqual(
|
||||
unavailable.body,
|
||||
"The current Mail policy could not be loaded. Try again or ask a Mail administrator for help.",
|
||||
)
|
||||
self.assertNotIn("sensitive", repr(unavailable))
|
||||
|
||||
def test_manifest_workflows_declare_contextual_help_targets(self) -> None:
|
||||
from govoplan_mail.backend.manifest import get_manifest
|
||||
|
||||
topics = {topic.id: topic for topic in get_manifest().documentation}
|
||||
self.assertEqual(topics["mail.workflow.choose-and-test-profile"].metadata["help_contexts"], ["mail.profiles", "app.settings"])
|
||||
self.assertEqual(topics["mail.workflow.read-mailbox"].metadata["help_contexts"], ["mail.list"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user