Files
computer-playbook/tests/unit/python/plugins/lookup/test_docker_cards.py
T
kevinveenbirkenbachandClaude Opus 5.5 afb82f57a1 feat(web-app-dashboard): let a role name the canonical domain its card opens
The dashboard card of every app opened its primary domain, so Matrix led to the Synapse status page and Stalwart to its admin UI. A role can now declare services.dashboard.card: <key of meta/domains.yml canonical>; docker_cards builds the card URL with lookup('canonical_url', app, key), the Apps menu reuses it, and roles_with_service('dashboard') hands the same domain to the tile e2e spec. Matrix sets card: element, Stalwart card: webmail. Without card nothing changes, and the prometheus/matomo target lists are untouched. A lint requires the key to resolve in the role's own domains.canonical.

The Matrix ansible flavor also sets Synapse's web_client_location to Element through matrix_synapse_configuration_extension_yaml, as the compose flavor already does, so matrix.<domain>/ redirects there. Merge note: feature/iam-profile-rbac defines the same extension variable inside the OIDC block; the next merge must combine both into one block (agreed with that session).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:06 +01:00

365 lines
12 KiB
Python

import shutil
import sys
import tempfile
import unittest
import unittest.mock as mock
from pathlib import Path
from ansible.errors import AnsibleError
from jinja2 import Environment, StrictUndefined, select_autoescape
sys.path.insert(
0,
str(
Path(str(Path(__file__).parent))
/ "../../../../../roles/web-app-dashboard/lookup_plugins"
),
)
import docker_cards as docker_cards_module
from docker_cards import LookupModule
from utils.roles.mapping import (
ROLE_FILE_META_INFO,
ROLE_FILE_META_MAIN,
ROLE_FILE_VARS_MAIN,
)
def _ansible_bool(value):
"""
Minimal Ansible-like bool filter for unit tests.
Mirrors common Ansible truthy/falsey handling for strings.
"""
if isinstance(value, bool):
return value
if value is None:
return False
if isinstance(value, (int, float)):
return value != 0
if isinstance(value, str):
v = value.strip().lower()
if v in {"y", "yes", "true", "on", "1"}:
return True
if v in {"n", "no", "false", "off", "0", ""}:
return False
return bool(value)
class DummyTemplar:
"""
Small, deterministic templating stub for unit tests.
It is intentionally minimal: only supports rendering Jinja strings
and provides an Ansible-like `bool` filter.
"""
def __init__(self, variables):
self._vars = variables
self._env = Environment(
undefined=StrictUndefined, autoescape=select_autoescape()
)
self._env.filters["bool"] = _ansible_bool
def template(self, value):
if value is None:
return value
if not isinstance(value, str):
return value
if "{{" in value and "}}" in value:
tmpl = self._env.from_string(value)
return tmpl.render(**self._vars)
return value
class DummyTlsResolveLookup:
"""
Deterministic tls stub.
Emulates: lookup('tls', app_id, 'url.base') -> 'http(s)://domain/'
"""
def __init__(self, templar):
self._templar = templar
def run(self, terms, variables=None, **kwargs):
variables = variables or {}
if not terms or len(terms) not in (1, 2):
raise AnsibleError(
"dummy tls: terms must be [app_id] or [app_id, want_path]"
)
app_id = str(terms[0]).strip()
if not app_id:
raise AnsibleError("dummy tls: empty term")
want = ""
if len(terms) == 2:
want = str(terms[1]).strip()
domains = variables.get("domains", {})
if app_id not in domains:
raise AnsibleError(f"dummy tls: app_id '{app_id}' not in domains")
domain_val = domains[app_id]
if isinstance(domain_val, list):
domain = domain_val[0] if domain_val else ""
elif isinstance(domain_val, dict):
domain = next(iter(domain_val.values()), "")
else:
domain = domain_val
domain = self._templar.template(domain).strip() if domain else ""
if not domain:
raise AnsibleError(f"dummy tls: empty domain for '{app_id}'")
tls_enabled = _ansible_bool(variables.get("TLS_ENABLED", True))
scheme = "https" if tls_enabled else "http"
base_url = f"{scheme}://{domain}/"
if want and want != "url.base":
raise AnsibleError(f"dummy tls: unsupported want_path='{want}'")
return [base_url]
class DummyCanonicalUrlLookup:
"""
Deterministic canonical_url stub.
Emulates: lookup('canonical_url', app_id, key) -> 'http(s)://<domains[app_id][key]>'
"""
def run(self, terms, variables=None, **kwargs):
variables = variables or {}
app_id, key = terms
app_domains = variables.get("domains", {}).get(app_id)
if not isinstance(app_domains, dict) or key not in app_domains:
raise AnsibleError(
f"dummy canonical_url: '{app_id}' has no canonical '{key}'"
)
scheme = (
"https" if _ansible_bool(variables.get("TLS_ENABLED", True)) else "http"
)
return [f"{scheme}://{app_domains[key]}"]
class TestDockerCardsLookup(unittest.TestCase):
def setUp(self):
self.test_roles_dir = tempfile.mkdtemp(prefix="test_roles_")
self.role_name = "web-app-dashboard"
self.role_dir = str(Path(self.test_roles_dir) / self.role_name)
Path(str(Path(self.role_dir) / "meta")).mkdir(parents=True)
Path(str(Path(self.role_dir) / "vars")).mkdir(parents=True)
vars_main = str(Path(self.role_dir) / ROLE_FILE_VARS_MAIN)
with Path(vars_main).open("w", encoding="utf-8") as f:
f.write("application_id: portfolio\n")
readme_path = str(Path(self.role_dir) / "README.md")
with Path(readme_path).open("w", encoding="utf-8") as f:
f.write("# Portfolio Application\nThis is a sample portfolio role.")
meta_main_path = str(Path(self.role_dir) / ROLE_FILE_META_MAIN)
meta_yaml = """
galaxy_info:
description: "A role for deploying a portfolio application."
"""
with Path(meta_main_path).open("w", encoding="utf-8") as f:
f.write(meta_yaml)
meta_info_path = str(Path(self.role_dir) / ROLE_FILE_META_INFO)
info_yaml = """
logo:
class: fa-solid fa-briefcase
"""
with Path(meta_info_path).open("w", encoding="utf-8") as f:
f.write(info_yaml)
self._orig_lookup_get = docker_cards_module.lookup_loader.get
class _VarsLookup:
def __init__(self, key):
self._key = key
def run(self, terms, variables=None, **kwargs):
return [(variables or {}).get(self._key, {})]
def _patched_get(name, loader=None, templar=None):
if name == "applications":
return _VarsLookup("applications")
if name == "domains":
return _VarsLookup("domains")
if name == "tls":
return DummyTlsResolveLookup(templar)
if name == "canonical_url":
return DummyCanonicalUrlLookup()
raise AnsibleError(f"Unexpected lookup requested: {name}")
docker_cards_module.lookup_loader.get = _patched_get
def tearDown(self):
docker_cards_module.lookup_loader.get = self._orig_lookup_get
shutil.rmtree(self.test_roles_dir)
def _base_fake_variables(self):
return {
"domains": {"portfolio": "myportfolio.com"},
"applications": {
"portfolio": {"services": {"dashboard": {"enabled": True}}}
},
"group_names": ["portfolio"],
"TLS_ENABLED": True,
"TLS_MODE": "letsencrypt",
"LETSENCRYPT_BASE_PATH": "/etc/letsencrypt",
"LETSENCRYPT_LIVE_PATH": "/etc/letsencrypt/live",
}
def _run_lookup(self, lookup_module, fake_variables):
lookup_module._templar = DummyTemplar(fake_variables)
lookup_module._loader = mock.MagicMock()
return lookup_module.run([self.test_roles_dir], variables=fake_variables)
def test_lookup_when_group_includes_application_id(self):
lookup_module = LookupModule()
fake_variables = self._base_fake_variables()
fake_variables["TLS_ENABLED"] = True
result = self._run_lookup(lookup_module, fake_variables)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 1)
cards = result[0]
self.assertIsInstance(cards, list)
self.assertEqual(len(cards), 1)
card = cards[0]
self.assertEqual(card["title"], "Portfolio Application")
self.assertEqual(card["text"], "A role for deploying a portfolio application.")
self.assertEqual(card["icon"]["class"], "fa-solid fa-briefcase")
self.assertEqual(card["url"], "https://myportfolio.com")
self.assertTrue(card["iframe"])
def test_iframe_flag_defaults_to_enabled(self):
lookup_module = LookupModule()
fake_variables = self._base_fake_variables()
result = self._run_lookup(lookup_module, fake_variables)
self.assertTrue(result[0][0]["iframe"])
def test_iframe_flag_false_decouples_from_enabled(self):
lookup_module = LookupModule()
fake_variables = self._base_fake_variables()
fake_variables["applications"]["portfolio"]["services"]["dashboard"][
"iframe"
] = False
result = self._run_lookup(lookup_module, fake_variables)
cards = result[0]
self.assertEqual(len(cards), 1)
self.assertFalse(cards[0]["iframe"])
def test_lookup_url_uses_https_when_tls_enabled_true(self):
lookup_module = LookupModule()
fake_variables = self._base_fake_variables()
fake_variables["TLS_ENABLED"] = True
result = self._run_lookup(lookup_module, fake_variables)
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
def test_lookup_url_uses_http_when_tls_enabled_false(self):
lookup_module = LookupModule()
fake_variables = self._base_fake_variables()
fake_variables["TLS_ENABLED"] = False
result = self._run_lookup(lookup_module, fake_variables)
self.assertEqual(result[0][0]["url"], "http://myportfolio.com")
def test_lookup_when_group_excludes_application_id(self):
lookup_module = LookupModule()
fake_variables = self._base_fake_variables()
fake_variables["group_names"] = []
result = self._run_lookup(lookup_module, fake_variables)
self.assertEqual(len(result), 1)
self.assertEqual(len(result[0]), 0)
def test_lookup_url_renders_domain_url_jinja(self):
lookup_module = LookupModule()
fake_variables = self._base_fake_variables()
fake_variables["domains"] = {"portfolio": "{{ DOMAIN_PRIMARY }}"}
fake_variables["DOMAIN_PRIMARY"] = "myportfolio.com"
fake_variables["TLS_ENABLED"] = True
result = self._run_lookup(lookup_module, fake_variables)
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
def test_lookup_url_https_when_tls_enabled_is_string(self):
lookup_module = LookupModule()
fake_variables = self._base_fake_variables()
fake_variables["TLS_ENABLED"] = "true"
result = self._run_lookup(lookup_module, fake_variables)
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
def test_lookup_url_http_when_tls_enabled_is_string(self):
lookup_module = LookupModule()
fake_variables = self._base_fake_variables()
fake_variables["TLS_ENABLED"] = "false"
result = self._run_lookup(lookup_module, fake_variables)
self.assertEqual(result[0][0]["url"], "http://myportfolio.com")
def _card_target_variables(self, dashboard):
fake_variables = self._base_fake_variables()
fake_variables["domains"] = {
"portfolio": {
"www": "myportfolio.com",
"client": "client.myportfolio.com",
}
}
fake_variables["applications"]["portfolio"]["services"]["dashboard"] = dashboard
return fake_variables
def test_card_url_targets_the_card_key(self):
fake_variables = self._card_target_variables(
{"enabled": True, "card": "client"}
)
result = self._run_lookup(LookupModule(), fake_variables)
self.assertEqual(result[0][0]["url"], "https://client.myportfolio.com")
def test_card_url_without_card_key_targets_the_primary_domain(self):
fake_variables = self._card_target_variables({"enabled": True})
result = self._run_lookup(LookupModule(), fake_variables)
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
def test_unknown_card_key_fails_loudly(self):
fake_variables = self._card_target_variables({"enabled": True, "card": "nope"})
with self.assertRaisesRegex(AnsibleError, "nope"):
self._run_lookup(LookupModule(), fake_variables)
if __name__ == "__main__":
unittest.main()