docs: surface custom Mail profile task

This commit is contained in:
2026-07-21 18:43:46 +02:00
parent e6a3617a94
commit 193898b00d
4 changed files with 460 additions and 7 deletions

171
tests/test_documentation.py Normal file
View 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()