275 lines
14 KiB
Python
275 lines
14 KiB
Python
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 (
|
|
_credential_line,
|
|
documentation_configuration_states,
|
|
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 test_mailbox_toolbar_contract_documents_context_refresh_and_read_only_bounds_in_both_languages(self) -> None:
|
|
from govoplan_mail.backend.manifest import manifest
|
|
topic = next(item for item in manifest.documentation if item.id == "mail.workflow.read-mailbox")
|
|
self.assertEqual(set(topic.documentation_types), {"user", "admin"})
|
|
self.assertTrue({"mail.mailbox.reload", "mail.mailbox.tools"}.issubset(topic.metadata["help_contexts"]))
|
|
for phrase in ("one right-aligned Reload", "Mailbox tools", "IMAP retains the current page", "JMAP starts a fresh cursor chain", "Failed refreshes preserve usable loaded data", "ignore late reads", "do not grant profile administration rights"):
|
|
self.assertIn(phrase, topic.body)
|
|
for phrase in ("genau einmal Neuladen rechts", "Postfachwerkzeuge", "IMAP behält die Seite", "JMAP beginnt", "Verspätete Antworten", "Fehlgeschlagene Aktualisierungen", "keine Profilverwaltungsrechte"):
|
|
self.assertIn(phrase, topic.translations["de"]["body"])
|
|
|
|
def test_imap_batch_contract_documents_bounds_and_per_message_safety_in_both_languages(self) -> None:
|
|
from govoplan_mail.backend.manifest import manifest
|
|
topic = next(item for item in manifest.documentation if item.id == "mail.reference.campaign-delivery-contract")
|
|
for body in (topic.body, topic.translations["de"]["body"]):
|
|
self.assertIn("campaign_imap_batch", body)
|
|
self.assertIn("GOVOPLAN_IMAP_BATCH_REUSE", body)
|
|
self.assertIn("MULTIAPPEND", body)
|
|
self.assertIn("100", body)
|
|
self.assertIn("300", body)
|
|
self.assertIn("permissions and recovery evidence are never cached", topic.body)
|
|
self.assertIn("no automatic APPEND replay", topic.body)
|
|
self.assertIn("Berechtigungen und Nachweise werden nicht zwischengespeichert", topic.translations["de"]["body"])
|
|
|
|
def test_campaign_contract_documents_protocol_scoped_runtime_and_complete_validation(self) -> None:
|
|
from govoplan_mail.backend.manifest import manifest
|
|
topic = next(item for item in manifest.documentation if item.id == "mail.reference.campaign-delivery-contract")
|
|
self.assertIn("Runtime credential-selection checks are protocol-scoped", topic.body)
|
|
self.assertIn("still check both configured protocols", topic.body)
|
|
self.assertIn("before credential decryption or provider contact", topic.body)
|
|
self.assertIn("protokollbezogen", topic.translations["de"]["body"])
|
|
self.assertIn("weiterhin beide konfigurierten Protokolle", topic.translations["de"]["body"])
|
|
|
|
def test_credential_policy_guidance_describes_explicit_mail_references_not_local_secrets(self) -> None:
|
|
text = _credential_line({"smtp_credentials": {"inherit": False}, "imap_credentials": {"inherit": True}})
|
|
self.assertIn("SMTP requires an explicit Mail credential", text)
|
|
self.assertIn("IMAP allows a profile default or explicit Mail credential", text)
|
|
self.assertIn("Secrets remain in Mail", text)
|
|
self.assertNotIn("local credentials", text)
|
|
self.assertNotIn("only for protocols that inherit", text)
|
|
|
|
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])
|
|
|
|
self_service = self.topics(
|
|
self.context(
|
|
{"mail:profile:read", "mail:profile:write_own"}
|
|
),
|
|
enabled,
|
|
)
|
|
self.assertIn("mail.workflow.create-custom-profile", self_service)
|
|
condition = self_service["mail.workflow.create-custom-profile"].conditions[0]
|
|
self.assertEqual(condition.required_scopes, ("mail:profile:read",))
|
|
self.assertEqual(
|
|
condition.any_scopes,
|
|
("mail:profile:write", "mail:profile:write_own"),
|
|
)
|
|
|
|
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),
|
|
({"mail:secret:manage_own"}, True, False, False),
|
|
)
|
|
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", "mail.mailbox", "mail.mailbox.reload", "mail.mailbox.tools"])
|
|
self.assertIn("mail.admin.profiles", topics["mail.profiles-and-policy"].metadata["help_contexts"])
|
|
self.assertIn("mail.bounce-processing", topics["mail.bounce-processing"].metadata["help_contexts"])
|
|
|
|
def test_configuration_provider_exposes_only_explicit_or_inherited_state(self) -> None:
|
|
inherited = EffectiveMailProfilePolicy(
|
|
source_policies=[
|
|
{"scope_type": "system", "applied_fields": ["defaults"]},
|
|
{"scope_type": "tenant", "applied_fields": []},
|
|
]
|
|
)
|
|
explicit = EffectiveMailProfilePolicy(
|
|
source_policies=[
|
|
{"scope_type": "system", "applied_fields": ["defaults"]},
|
|
{"scope_type": "tenant", "applied_fields": ["smtp_hosts"]},
|
|
]
|
|
)
|
|
context = self.context({"mail:profile:read"}, documentation_type="admin")
|
|
|
|
with patch(
|
|
"govoplan_mail.backend.documentation.effective_mail_profile_policy_for_scope",
|
|
return_value=inherited,
|
|
):
|
|
inherited_state = documentation_configuration_states(
|
|
context,
|
|
("mail_profile_policy",),
|
|
)
|
|
with patch(
|
|
"govoplan_mail.backend.documentation.effective_mail_profile_policy_for_scope",
|
|
return_value=explicit,
|
|
):
|
|
explicit_state = documentation_configuration_states(
|
|
context,
|
|
("mail_profile_policy",),
|
|
)
|
|
|
|
self.assertEqual(
|
|
inherited_state["mail_profile_policy"].state,
|
|
"inherited",
|
|
)
|
|
self.assertEqual(
|
|
explicit_state["mail_profile_policy"].state,
|
|
"enabled",
|
|
)
|
|
self.assertNotIn("smtp_hosts", repr(explicit_state))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|