Files
computer-playbook/tests/unit/python/plugins/lookup/test_smtp_host.py
T
kevinveenbirkenbachandClaude Opus 5.5 7fa3400cc6 Merge remote-tracking branch 'origin/main' into feature/network/multi-network-serving
Conflicts resolved by keeping both sides' intent:

- call-test-deploy.yml: main's re-enabled deploy concurrency group, keyed
  on the network axis instead of the removed tor axis.
- sys-svc-proxy: main's shared headers/forwarded.conf.j2 now carries the
  branch's per-vhost semantics (X-Forwarded-Host, TLS port and state read
  from the vhost domain), so html, ws, upload and the Prometheus probes
  all answer per network sibling.
- plugins/lookup/email.py: main's provider-agnostic mail lookup asks the
  network registry instead of testing the .onion suffix, as the branch
  does everywhere; container_extra_hosts.py likewise. The provider host's
  NETWORK_MODE now decides whether it is served onion-first, since a Tor
  node defaults to multi and keeps clearnet canonical.
- workspace common.sh: main's INFINITO_DOMAIN fallback without the
  duplicated no-op line.
- README.md and requirement 035: both sides' rows (Wazuh plus main's
  Pi-hole and Stalwart entries); adapter_candidate count 39.

Adapted to main where the branch's rules now reach main's code:

- six tests from main read the primary domain from default.env instead
  of writing it out, as the hardcoded-domain lint requires;
- the chutney worker-check test passes ONION_SUFFIX from the network
  registry, which the branch's svc-net-tor test script requires;
- web-app-wazuh moves its local http ports to 8096/8097 (8094/8095 now
  belong to web-app-stalwart) and sizes the indexer heap through
  service_memory_mb, the same fixes #414 carries;
- web-app-wazuh's README cosmos follows Stalwart as the mail provider.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 00:14:42 +02:00

160 lines
5.5 KiB
Python

"""Unit tests for the smtp_host lookup plugin.
Pins the SMTP endpoint matrix msmtp relays through: the configured relay in
production, loopback on a host that carries the stack, and the swarm manager's
routing mesh on a bare infra node inside the containerised test rig.
"""
from __future__ import annotations
import importlib.util
import unittest
from ansible.errors import AnsibleError
from utils.domains.default_primary import default_domain_primary
from . import PROJECT_ROOT
DOMAIN = default_domain_primary()
RELAY = f"mail.{DOMAIN}"
MANAGER = "swarm-manager-01"
def _load_lookup():
spec = importlib.util.spec_from_file_location(
"lookup_smtp_host",
str(PROJECT_ROOT / "plugins/lookup/smtp_host.py"),
)
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
return mod.LookupModule
class _DummyTemplar:
def __init__(self, available_variables=None):
self.available_variables = available_variables or {}
def template(self, value):
return value
class _RenderingTemplar:
"""A templar that actually resolves expressions, like the real one.
``_DummyTemplar`` returns its input unchanged, so it cannot expose a value
that only misbehaves while still an unrendered template.
"""
def __init__(self, available_variables=None, rendered=None):
self.available_variables = available_variables or {}
self._rendered = rendered or {}
def template(self, value):
return self._rendered.get(value, value)
def _email(external=True, host=RELAY):
return {"external": external, "host": host}
class TestSmtpHostLookup(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.LookupModule = _load_lookup()
def _resolve(self, variables, email=None):
lm = self.LookupModule()
lm._templar = _DummyTemplar(variables)
lm._loader = None
return lm.run([email if email is not None else _email()], variables=variables)[
0
]
def test_production_keeps_the_configured_relay(self):
vars_ = {"DOCKER_IN_CONTAINER": False, "DEPLOYMENT_MODE": "swarm"}
self.assertEqual(self._resolve(vars_), RELAY)
def test_non_external_mail_keeps_the_configured_relay(self):
vars_ = {"DOCKER_IN_CONTAINER": True, "DEPLOYMENT_MODE": "swarm"}
email = _email(external=False, host="localhost")
self.assertEqual(self._resolve(vars_, email), "localhost")
def test_compose_rig_uses_loopback(self):
vars_ = {"DOCKER_IN_CONTAINER": True, "DEPLOYMENT_MODE": "compose"}
self.assertEqual(self._resolve(vars_), "127.0.0.1")
def test_an_unrendered_group_var_still_reaches_the_rig_loopback(self):
"""DOCKER_IN_CONTAINER is a group_vars expression, and a lookup receives it
raw. Read unrendered it is neither true nor false, so the rig kept the
configured relay instead of loopback — and on an onion node that relay is a
.onion, which msmtp cannot route (EX_UNAVAILABLE, 'proxy failure')."""
raw = "{{ (ansible_facts['env']['container'] | default('')) != '' }}"
vars_ = {"DOCKER_IN_CONTAINER": raw, "DEPLOYMENT_MODE": "compose"}
lm = self.LookupModule()
lm._templar = _RenderingTemplar(vars_, {raw: True})
lm._loader = None
self.assertEqual(lm.run([_email()], variables=vars_)[0], "127.0.0.1")
def test_swarm_node_uses_loopback(self):
vars_ = {
"DOCKER_IN_CONTAINER": True,
"DEPLOYMENT_MODE": "swarm",
"group_names": ["svc-swarm-node"],
}
self.assertEqual(self._resolve(vars_), "127.0.0.1")
def test_infra_node_enters_through_the_manager(self):
vars_ = {
"DOCKER_IN_CONTAINER": True,
"DEPLOYMENT_MODE": "swarm",
"group_names": ["svc-nfs-server"],
"groups": {"svc-swarm-manager": [MANAGER, "swarm-manager-02"]},
}
self.assertEqual(self._resolve(vars_), MANAGER)
def test_infra_node_without_manager_falls_back_to_the_relay(self):
vars_ = {
"DOCKER_IN_CONTAINER": True,
"DEPLOYMENT_MODE": "swarm",
"group_names": ["svc-nfs-server"],
"groups": {"svc-swarm-manager": []},
}
self.assertEqual(self._resolve(vars_), RELAY)
def test_string_booleans_are_coerced(self):
vars_ = {"DOCKER_IN_CONTAINER": "True", "DEPLOYMENT_MODE": "compose"}
self.assertEqual(self._resolve(vars_, _email(external="yes")), "127.0.0.1")
def test_swarm_node_wins_over_a_populated_manager_group(self):
vars_ = {
"DOCKER_IN_CONTAINER": True,
"DEPLOYMENT_MODE": "swarm",
"group_names": ["svc-swarm-node"],
"groups": {"svc-swarm-manager": [MANAGER]},
}
self.assertEqual(self._resolve(vars_), "127.0.0.1")
def test_missing_terms_raise(self):
lm = self.LookupModule()
lm._templar = _DummyTemplar({})
with self.assertRaises(AnsibleError):
lm.run([], variables={})
def test_too_many_terms_raise(self):
lm = self.LookupModule()
lm._templar = _DummyTemplar({})
with self.assertRaises(AnsibleError):
lm.run([_email(), _email()], variables={})
def test_non_mapping_term_raises(self):
lm = self.LookupModule()
lm._templar = _DummyTemplar({})
with self.assertRaises(AnsibleError):
lm.run([RELAY], variables={})
if __name__ == "__main__":
unittest.main(verbosity=2)