Files
computer-playbook/tests/integration/test_mailu_service_dependency.py
T
kevinveenbirkenbachandClaude Sonnet 4.6 be341234bb feat: register web-app-mailu as 'email' service and enforce dependency
- Add 'email' key (role: web-app-mailu, type: frontend) to SERVICE_REGISTRY
- Declare compose.services.email with enabled: true and shared: true in all
  33 app roles that reference web-app-mailu or use SYSTEM_EMAIL_* variables
- Add integration test test_mailu_service_dependency.py that enforces this
  constraint for any future role additions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 01:13:56 +02:00

108 lines
3.5 KiB
Python

import re
import unittest
from pathlib import Path
import yaml
_MAILU_ROLE = "web-app-mailu"
_EMAIL_SERVICE_KEY = "email"
# Patterns that indicate an email dependency on Mailu
_MAILU_REF_RE = re.compile(r"web-app-mailu")
_SYSTEM_EMAIL_RE = re.compile(r"SYSTEM_EMAIL_")
# File extensions to scan within a role
_SCAN_EXTENSIONS = {".yml", ".yaml", ".j2", ".py", ".sh", ".conf", ".env"}
def _scan_role(role_path: Path) -> tuple[bool, bool]:
"""Return (refs_mailu, refs_system_email) for all scannable files in *role_path*."""
refs_mailu = False
refs_email = False
for path in role_path.rglob("*"):
if not path.is_file() or path.suffix not in _SCAN_EXTENSIONS:
continue
try:
text = path.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
if not refs_mailu and _MAILU_REF_RE.search(text):
refs_mailu = True
if not refs_email and _SYSTEM_EMAIL_RE.search(text):
refs_email = True
if refs_mailu and refs_email:
break
return refs_mailu, refs_email
class TestMailuServiceDependency(unittest.TestCase):
"""Every role that references 'web-app-mailu' or uses SYSTEM_EMAIL_* variables
must declare compose.services.email with enabled: true and shared: true
in its config/main.yml."""
PROJECT_ROOT = Path(__file__).resolve().parents[2]
def setUp(self):
self.roles_root = self.PROJECT_ROOT / "roles"
self.assertTrue(
self.roles_root.is_dir(),
f"Roles directory not found: {self.roles_root}",
)
def _email_service_conf(self, config_path: Path) -> dict:
if not config_path.is_file():
return {}
content = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
return (
content.get("compose", {}).get("services", {}).get(_EMAIL_SERVICE_KEY, {})
)
def test_mailu_dependents_declare_email_service(self):
errors = []
for role_path in sorted(self.roles_root.iterdir()):
if not role_path.is_dir():
continue
role_name = role_path.name
if role_name == _MAILU_ROLE:
continue
config_path = role_path / "config" / "main.yml"
if not config_path.is_file():
continue
refs_mailu, refs_system_email = _scan_role(role_path)
if not refs_mailu and not refs_system_email:
continue
reasons = []
if refs_mailu:
reasons.append("references 'web-app-mailu'")
if refs_system_email:
reasons.append("uses SYSTEM_EMAIL_* variables")
reason_str = " and ".join(reasons)
email_svc = self._email_service_conf(config_path)
rel = config_path.relative_to(self.PROJECT_ROOT)
if not email_svc.get("enabled"):
errors.append(
f"[{role_name}] {reason_str} but "
f"compose.services.email.enabled is not true in {rel}"
)
if not email_svc.get("shared"):
errors.append(
f"[{role_name}] {reason_str} but "
f"compose.services.email.shared is not true in {rel}"
)
if errors:
self.fail(
"Roles that depend on Mailu must declare "
"compose.services.email with enabled: true and shared: true:\n\n"
+ "\n".join(errors)
)
if __name__ == "__main__":
unittest.main()