from __future__ import annotations from pathlib import Path from dataclasses import replace import tempfile import unittest from sqlalchemy import create_engine, select from sqlalchemy.orm import Session, sessionmaker from govoplan_access.backend.db import models as access_models # noqa: F401 from govoplan_core.core.configuration_packages import ( ConfigurationPackageFragment, ConfigurationPreflightContext, ) from govoplan_core.admin.models import SystemSettings from govoplan_core.core.infrastructure_capabilities import ( infrastructure_capability_receipt_from_mapping, ) from govoplan_core.db.base import Base from govoplan_core.db.session import configure_database, reset_database from govoplan_core.security.credential_envelopes import CredentialEnvelope from govoplan_mail.backend.configuration_provider import ( MAIL_CONFIGURATION_CAPABILITY, SqlMailConfigurationProvider, ) from govoplan_mail.backend.db.models import ( MailProfilePolicy, MailServerCredentialBinding, MailServerEndpoint, MailServerProfile, ) from govoplan_mail.backend.manifest import manifest def _receipt(): return infrastructure_capability_receipt_from_mapping( { "schema_version": 1, "installation_id": "mail-provider-test", "profile": "evaluation", "capabilities": [ { "id": "mail.smtp", "label": "SMTP delivery", "state": "available_unconfigured", "source": "installer-managed-test", "detail": "GreenMail is available for profile binding.", "endpoint": { "scheme": "smtp", "host": "test-mail", "port": 3025, }, "secret_refs": [], "dependent_modules": ["mail"], } ], "post_install_tasks": [ { "id": "mail.smtp-profile", "resume_key": "mail-provider-test:mail.smtp-profile:v1", "capability_id": "mail.smtp", "state": "pending", "owner_module": "mail", "summary": "Create a Mail SMTP profile.", "required_inputs": ["credential envelope reference when required"], "secret_boundary": "credential-envelope-reference-only", } ], } ) def _external_receipt(*, credential_required: bool = False): return infrastructure_capability_receipt_from_mapping( { "schema_version": 1, "installation_id": "mail-provider-external", "profile": "production", "capabilities": [ { "id": "mail.smtp", "label": "SMTP delivery", "state": "available_unconfigured", "source": "operator-supplied", "detail": "An external relay needs reviewed Mail configuration.", "endpoint": {}, "secret_refs": ( ["env:SMTP_CREDENTIAL_ENVELOPE_REF"] if credential_required else [] ), "dependent_modules": ["mail"], } ], "post_install_tasks": [], } ) class MailConfigurationProviderTests(unittest.TestCase): def setUp(self) -> None: self.tempdir = tempfile.TemporaryDirectory(prefix="govoplan-mail-config-") self.addCleanup(self.tempdir.cleanup) database_path = Path(self.tempdir.name) / "mail.sqlite3" self.engine = create_engine(f"sqlite:///{database_path}") Base.metadata.create_all( self.engine, tables=( access_models.User.__table__, SystemSettings.__table__, CredentialEnvelope.__table__, MailServerProfile.__table__, MailServerEndpoint.__table__, MailServerCredentialBinding.__table__, MailProfilePolicy.__table__, ), ) configure_database( f"sqlite:///{database_path}", engine=self.engine, dispose_previous=True, ) self.SessionLocal = sessionmaker( bind=self.engine, class_=Session, expire_on_commit=False, ) with self.SessionLocal() as session: session.add( MailProfilePolicy( id="tenant-policy", tenant_id="tenant-1", scope_type="tenant", scope_id="tenant-1", policy={}, ) ) session.add( CredentialEnvelope( id="credential-1", tenant_id="tenant-1", scope_type="tenant", scope_id="tenant-1", name="SMTP credential", credential_kind="username_password", public_data={"username": "mailer"}, secret_data_encrypted="encrypted-outside-package", secret_keys=["password"], allowed_modules=["mail"], allowed_server_refs=[], inherit_to_lower_scopes=True, is_active=True, ) ) session.commit() self.addCleanup(self._cleanup_database) self.provider = SqlMailConfigurationProvider() self.context = ConfigurationPreflightContext( tenant_id="tenant-1", operator_user_id=None, infrastructure_receipt=_receipt(), ) def _cleanup_database(self) -> None: reset_database() self.engine.dispose() def test_provider_is_registered_and_describes_receipt_bound_fragment(self) -> None: self.assertIn(MAIL_CONFIGURATION_CAPABILITY, manifest.capability_factories) description = self.provider.describe() self.assertEqual(("smtp_profile",), description.fragment_types) def test_apply_is_idempotent_and_binds_existing_credential_envelope(self) -> None: fragment = ConfigurationPackageFragment( module_id="mail", fragment_type="smtp_profile", fragment_id="test-smtp", payload={"credential_envelope_id": "credential-1"}, ) first_plan = self.provider.preflight(fragment, self.context) self.assertEqual("create", first_plan.plan[0].action) self.assertFalse( any(item.severity == "blocker" for item in first_plan.diagnostics) ) first_apply = self.provider.apply(fragment, {}, self.context) self.assertIn("test-smtp", first_apply.created_refs) second_plan = self.provider.preflight(fragment, self.context) second_apply = self.provider.apply(fragment, {}, self.context) self.assertEqual("skip", second_plan.plan[0].action) self.assertEqual({}, second_apply.created_refs) self.assertEqual({}, second_apply.updated_refs) with self.SessionLocal() as session: profiles = session.scalars(select(MailServerProfile)).all() servers = session.scalars(select(MailServerEndpoint)).all() bindings = session.scalars(select(MailServerCredentialBinding)).all() self.assertEqual(1, len(profiles)) self.assertEqual("test-mail", profiles[0].smtp_config["host"]) self.assertEqual("plain", profiles[0].smtp_config["security"]) self.assertEqual(1, len(servers)) self.assertEqual("test-mail", servers[0].config["host"]) self.assertEqual(1, len(bindings)) self.assertEqual("credential-1", bindings[0].credential_id) def test_conflicting_existing_profile_is_preserved_by_default(self) -> None: fragment = ConfigurationPackageFragment( module_id="mail", fragment_type="smtp_profile", fragment_id="test-smtp", payload={}, ) self.provider.apply(fragment, {}, self.context) with self.SessionLocal() as session: server = session.scalar(select(MailServerEndpoint)) assert server is not None server.config = {**server.config, "host": "manually-changed.example.test"} session.commit() plan = self.provider.preflight(fragment, self.context) result = self.provider.apply(fragment, {}, self.context) self.assertEqual("blocked", plan.plan[0].action) self.assertIn( "mail_configuration_conflict", {item.code for item in result.diagnostics}, ) with self.SessionLocal() as session: server = session.scalar(select(MailServerEndpoint)) assert server is not None self.assertEqual("manually-changed.example.test", server.config["host"]) def test_explicit_conflict_update_reconciles_then_becomes_noop(self) -> None: fragment = ConfigurationPackageFragment( module_id="mail", fragment_type="smtp_profile", fragment_id="test-smtp", payload={}, ) self.provider.apply(fragment, {}, self.context) with self.SessionLocal() as session: server = session.scalar(select(MailServerEndpoint)) assert server is not None server.config = {**server.config, "host": "manually-changed.example.test"} session.commit() update_fragment = replace( fragment, payload={"on_conflict": "update"}, ) plan = self.provider.preflight(update_fragment, self.context) result = self.provider.apply(update_fragment, {}, self.context) settled = self.provider.preflight(update_fragment, self.context) self.assertEqual("update", plan.plan[0].action) self.assertIn("test-smtp", result.updated_refs) self.assertEqual("skip", settled.plan[0].action) with self.SessionLocal() as session: server = session.scalar(select(MailServerEndpoint)) assert server is not None self.assertEqual("test-mail", server.config["host"]) def test_inline_credentials_are_rejected(self) -> None: fragment = ConfigurationPackageFragment( module_id="mail", fragment_type="smtp_profile", payload={ "smtp": { "host": "test-mail", "port": 3025, "security": "plain", "password": "must-not-cross-boundary", } }, ) plan = self.provider.preflight(fragment, self.context) self.assertEqual("blocked", plan.plan[0].action) self.assertIn( "mail_configuration_secret_forbidden", {item.code for item in plan.diagnostics}, ) def test_external_relay_collects_missing_non_secret_inputs(self) -> None: fragment = ConfigurationPackageFragment( module_id="mail", fragment_type="smtp_profile", fragment_id="external-smtp", payload={}, ) missing = self.provider.preflight( fragment, replace(self.context, infrastructure_receipt=_external_receipt()), ) ready = self.provider.preflight( fragment, replace( self.context, infrastructure_receipt=_external_receipt(), supplied_data={ "mail.smtp.external-smtp.host": "smtp.example.test", "mail.smtp.external-smtp.port": 587, "mail.smtp.external-smtp.security": "starttls", }, ), ) self.assertEqual("blocked", missing.plan[0].action) self.assertEqual( { "mail.smtp.external-smtp.host", "mail.smtp.external-smtp.port", "mail.smtp.external-smtp.security", }, {item.key for item in missing.required_data if item.required}, ) self.assertEqual("create", ready.plan[0].action) def test_receipt_secret_reference_requires_credential_envelope_reference( self, ) -> None: fragment = ConfigurationPackageFragment( module_id="mail", fragment_type="smtp_profile", fragment_id="authenticated-smtp", payload={ "smtp": { "host": "smtp.example.test", "port": 587, "security": "starttls", } }, ) context = replace( self.context, infrastructure_receipt=_external_receipt(credential_required=True), ) missing = self.provider.preflight(fragment, context) ready = self.provider.preflight( ConfigurationPackageFragment( module_id="mail", fragment_type="smtp_profile", fragment_id="authenticated-smtp", payload={ **fragment.payload, "credential_envelope_id": "credential-1", }, ), context, ) self.assertEqual("blocked", missing.plan[0].action) self.assertIn( "mail.smtp.authenticated-smtp.credential_envelope_id", {item.key for item in missing.required_data if item.required}, ) self.assertEqual("create", ready.plan[0].action) def test_system_profile_requires_system_configuration_authority(self) -> None: fragment = ConfigurationPackageFragment( module_id="mail", fragment_type="smtp_profile", payload={"profile": {"scope_type": "system"}}, ) blocked = self.provider.preflight(fragment, self.context) ready = self.provider.preflight( fragment, replace( self.context, operator_scopes=frozenset({"system:settings:write"}), ), ) self.assertEqual("blocked", blocked.plan[0].action) self.assertIn( "system_configuration_authority_required", {item.code for item in blocked.diagnostics}, ) self.assertEqual("create", ready.plan[0].action) if __name__ == "__main__": unittest.main()