mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Fourteen real conflicts plus the 66 generated READMEs.
utils/cache/files.py and its test: both sides added a skip-dir rule, resolved
as a union so the root `build/` prune and the per-root walk key coexist.
plugins/lookup/database.py and postgres_libraries.py: main's added lookups kept,
with get_entity_name renamed to entity_name as this branch renamed it.
roles/web-app-{mailu,matrix}/meta/services.yml: main's `covers: [email]` and its
MAIL_PROVIDER-gated email entry, plus this branch's gitlab/jira/n8n/jitsi
consumers, which the merge base carried on neither side.
group_vars/all/17_resource.yml: this branch's lookup('resource', ...) refactor;
main's single added literal is already covered by _POSTGRES_MAX_LOCKS_PER_TRANSACTION.
docs/requirements/035-mcp-proxy-expansion.md: this branch moved the design into
ADR 0003, so the requirement keeps only the user story and the criteria. Main's
delta inside the moved block was two new roles, carried into the ADR audit:
web-app-stalwart as adapter_candidate, svc-net-tor-smtp as privileged_control_plane.
The no_surface total said 99 while its own subcategories summed to 112.
inventories/development/default.yml: biber keeps no explicit email. Both sides fixed
the same CI failure; this branch fixed it in _apply_account_defaults, which renders
biber@{{ DOMAIN_PRIMARY }} (utils/cache/users/__init__.py:68) — byte-identical to
the address main hardcoded, so the inventory line is a second copy of one fact.
roles/web-app-{gitlab,keycloak}/files/playwright/playwright.spec.js and
roles/web-app-gitlab/templates/playwright.env.j2: union of both import sets, with
requireDotenvValue kept over decodeDotenvQuotedValue for CANONICAL_DOMAIN because
an empty value must fail rather than match every substring.
The 66 role READMEs take this side: aceb5295ef moved Cosmos, Quick Setup and
Credits to the documentation build, and test_readme.py now rejects them in a
committed README. Main's prose edits inside them are carried over by hand (the
Mailu deprecation banner, Mailu -> Stalwart in erpnext and zammad), and the two
READMEs main added fresh are stripped of the three generated sections.
roles/web-app-sphinx/README.md stays deleted; 6eb9a59d14 merged that role into
web-app-docs. The root README's generated role table was regenerated from the
merged metadata.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
165 lines
5.7 KiB
Python
165 lines
5.7 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 . import PROJECT_ROOT
|
|
|
|
RELAY = "mail.infinito.test"
|
|
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",
|
|
"IS_COMPOSE_MODE": False,
|
|
"IS_SWARM_MODE": True,
|
|
"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",
|
|
"IS_COMPOSE_MODE": False,
|
|
"IS_SWARM_MODE": True,
|
|
"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",
|
|
"IS_COMPOSE_MODE": False,
|
|
"IS_SWARM_MODE": True,
|
|
"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",
|
|
"IS_COMPOSE_MODE": False,
|
|
"IS_SWARM_MODE": True,
|
|
"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)
|