Files
computer-playbook/tests/unit/python/plugins/lookup/test_docker_cards.py
T
kevinveenbirkenbachandClaude Opus 5.5 c855efc30a refactor(web-app-dashboard): hide a tile through services.dashboard.enabled instead of display
services.dashboard.enabled already says whether a role takes part in the dashboard: roles_with_service lists only roles with enabled and shared as tile-reachability targets, the CSP filter adds the dashboard to frame-ancestors only when it is set, and docker_cards uses it as the card's iframe flag. A separate display key duplicated that switch and left a role with enabled false still rendering a card. docker_cards now skips the card when services.dashboard.enabled is false (a role without a dashboard binding keeps its card, and a value that is not a boolean fails loudly), so display, its validator exception and its roles_with_service filter go again; base.md documents enabled as the per-host switch.

An inventory hides a tile with applications.<role>.services.dashboard.enabled: false, which also drops the dashboard framing of that role. Unit tests cover false, true, a missing binding, a rendered string and a non-boolean; forcing the helper to always show the card fails four of them.

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

453 lines
15 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)
def _add_role(self, role_name, application_id, info_yaml):
role_dir = Path(self.test_roles_dir) / role_name
(role_dir / "meta").mkdir(parents=True)
(role_dir / "vars").mkdir(parents=True)
(role_dir / ROLE_FILE_VARS_MAIN).write_text(
f"application_id: {application_id}\n", encoding="utf-8"
)
(role_dir / "README.md").write_text(f"# {role_name}\n", encoding="utf-8")
(role_dir / ROLE_FILE_META_MAIN).write_text(
'galaxy_info:\n description: "Second role."\n', encoding="utf-8"
)
(role_dir / ROLE_FILE_META_INFO).write_text(info_yaml, encoding="utf-8")
def _two_role_variables(self):
self._add_role("web-app-other", "other", "logo:\n class: fa-solid fa-cube\n")
fake_variables = self._base_fake_variables()
fake_variables["domains"]["other"] = "other.example.com"
fake_variables["applications"]["other"] = {
"services": {"dashboard": {"enabled": True}}
}
fake_variables["group_names"] = ["portfolio", "other"]
return fake_variables
def test_enabled_false_skips_the_card(self):
fake_variables = self._base_fake_variables()
fake_variables["applications"]["portfolio"]["services"]["dashboard"][
"enabled"
] = False
result = self._run_lookup(LookupModule(), fake_variables)
self.assertEqual(result, [[]])
def test_a_role_without_a_dashboard_binding_shows_the_card(self):
fake_variables = self._base_fake_variables()
fake_variables["applications"]["portfolio"]["services"] = {}
result = self._run_lookup(LookupModule(), fake_variables)
self.assertEqual([c["title"] for c in result[0]], ["Portfolio Application"])
def test_enabled_true_shows_the_card(self):
fake_variables = self._base_fake_variables()
result = self._run_lookup(LookupModule(), fake_variables)
self.assertEqual([c["title"] for c in result[0]], ["Portfolio Application"])
def test_enabled_false_hides_only_its_own_card(self):
fake_variables = self._two_role_variables()
fake_variables["applications"]["other"]["services"]["dashboard"]["enabled"] = (
False
)
result = self._run_lookup(LookupModule(), fake_variables)
self.assertEqual([c["title"] for c in result[0]], ["Portfolio Application"])
def test_both_cards_render_when_both_are_enabled(self):
fake_variables = self._two_role_variables()
result = self._run_lookup(LookupModule(), fake_variables)
self.assertEqual(
[c["title"] for c in result[0]],
["Portfolio Application", "web-app-other"],
)
def test_info_display_false_no_longer_hides_the_card(self):
meta_info_path = Path(self.role_dir) / ROLE_FILE_META_INFO
meta_info_path.write_text(
"logo:\n class: fa-solid fa-briefcase\ndisplay: false\n",
encoding="utf-8",
)
result = self._run_lookup(LookupModule(), self._base_fake_variables())
self.assertEqual([c["title"] for c in result[0]], ["Portfolio Application"])
def test_non_boolean_enabled_fails_loudly(self):
fake_variables = self._base_fake_variables()
fake_variables["applications"]["portfolio"]["services"]["dashboard"][
"enabled"
] = "maybe"
with self.assertRaisesRegex(AnsibleError, "portfolio"):
self._run_lookup(LookupModule(), fake_variables)
if __name__ == "__main__":
unittest.main()