mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 12:17:13 +00:00
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`, `requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from non-Markdown source files. Comments anchored to requirement numbers rot when requirements are renumbered or rewritten and conflate present-state behaviour (which the comment should describe) with the audit trail of how the code got there (which belongs in commit messages, PR descriptions, and the requirement file itself). * Document the rule in `docs/contributing/documentation.md` under "Comments". Code MUST describe the *why* in present-state language; contract enforcement lives in tests, not folklore. * Clean 254 source files across `roles/`, `tests/`, `plugins/`, `cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root `eslint.config.js`. Pure comment / docstring / log-message edits; no behavioural change. * Add `tests/lint/repository/test_no_req_references_in_code.py` to enforce the rule going forward across `.js`, `.py`, `.yml`, `.yaml`, `.j2`, and `.sh` files. The legacy `test_no_stale_requirement_refs.py` is exempt as the only file whose purpose is to detect these very tokens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
306 lines
10 KiB
Python
306 lines
10 KiB
Python
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from ansible.errors import AnsibleError
|
|
from jinja2 import Environment, StrictUndefined, select_autoescape
|
|
|
|
# Adjust the PYTHONPATH to include the lookup_plugins folder from the web-app-dashboard role.
|
|
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
|
|
|
|
# Keep non-strings untouched
|
|
if not isinstance(value, str):
|
|
return value
|
|
|
|
# Only render if it looks like a Jinja template
|
|
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 {}
|
|
|
|
# NEW API: want-path is positional (2nd term)
|
|
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")
|
|
|
|
# normalize domain mapping value (string / dict / list)
|
|
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 TestDockerCardsLookup(unittest.TestCase):
|
|
def setUp(self):
|
|
# Create a temporary directory to simulate the roles directory.
|
|
self.test_roles_dir = tempfile.mkdtemp(prefix="test_roles_")
|
|
|
|
# Create a sample role "web-app-dashboard" under that directory.
|
|
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")
|
|
|
|
# Create a sample README.md with a H1 line for the title.
|
|
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.")
|
|
|
|
# Create a sample meta/main.yml in the meta folder. Per
|
|
# the icon class lives in meta/info.yml (file-root convention),
|
|
# not under galaxy_info.
|
|
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)
|
|
|
|
# Patch tls lookup loader with a deterministic stub
|
|
self._orig_lookup_get = docker_cards_module.lookup_loader.get
|
|
|
|
def _patched_get(name, loader=None, templar=None):
|
|
if name != "tls":
|
|
raise AnsibleError(f"Unexpected lookup requested: {name}")
|
|
return DummyTlsResolveLookup(templar)
|
|
|
|
docker_cards_module.lookup_loader.get = _patched_get
|
|
|
|
# Route get_merged_domains through variables['domains'] so tests stay hermetic.
|
|
def _domains_from_vars(*, variables=None, **_kwargs):
|
|
return (variables or {}).get("domains", {})
|
|
|
|
self._domains_patcher = patch.object(
|
|
docker_cards_module,
|
|
"get_merged_domains",
|
|
side_effect=_domains_from_vars,
|
|
)
|
|
self._domains_patcher.start()
|
|
|
|
def tearDown(self):
|
|
# Restore patched lookup_loader
|
|
docker_cards_module.lookup_loader.get = self._orig_lookup_get
|
|
self._domains_patcher.stop()
|
|
|
|
# Remove the temporary roles directory after the test.
|
|
shutil.rmtree(self.test_roles_dir)
|
|
|
|
def _base_fake_variables(self):
|
|
# include TLS vars (even if our stub doesn't need all of them)
|
|
return {
|
|
"domains": {"portfolio": "myportfolio.com"},
|
|
"applications": {
|
|
# Per the materialised payload moved to services.<X>.
|
|
"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):
|
|
# Provide deterministic templating behavior for unit tests.
|
|
lookup_module._templar = DummyTemplar(fake_variables)
|
|
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_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"] = [] # Not including "portfolio"
|
|
|
|
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")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|