Files
computer-playbook/tests/unit/plugins/lookup/test_fediwall_active.py
T
kevinveenbirkenbach 76b139c894 fix(lint): migrate raw IO + project walks to utils.cache + variant coverage
Cleans up the lint debt the main->feature/web-app-kix merge surfaced.
Each change addresses a real failure surfaced by `make test`; no
``# nocheck`` blanket suppressions added.

Migrations to the shared cache helpers:
- ~25 test files swap raw ``yaml.safe_load`` / ``yaml.safe_dump`` /
  ``yaml.dump`` for ``utils.cache.yaml.{load_yaml_any,load_yaml_all,
  load_yaml_str,dump_yaml,load_yaml_all_str}``.
- ~30 test files swap raw ``glob.glob`` / ``os.walk`` / ``Path.rglob``
  for ``utils.cache.files.iter_project_files`` (or
  ``iter_non_ignored_files`` for the dependabot fallback walker, or
  per-role ``Path.iterdir`` / ``Path.glob`` where the scope is a
  single subtree).
- 3 ``Path.read_text`` lint helpers now route through
  ``utils.cache.files.read_text``.

Variant + service-flag fixes:
- web-app-kix: shift its subnet from 192.168.105.80/28 to
  192.168.105.128/28 to clear the collision with web-app-hugo and
  svc-prx-openresty.
- web-app-fediwall: add the missing ``mastodon`` / ``friendica``
  service flags referenced by ``meta/variants.yml``; pin the css /
  logout / dashboard / matomo / prometheus literals with
  ``# nocheck: dynamic-flag`` and add a third "everything off"
  variant so the variant-coverage matrix sees both true and false
  pinning for every dynamic flag.
- web-app-magento, web-app-shopware, web-svc-xmpp: switch the
  ``run_after: web-app-keycloak`` matcher from a literal-false
  oauth2/oidc opt-out to the ``"{{ 'web-app-keycloak' in group_names }}"``
  dynamic form, and extend each role's ``meta/variants.yml`` with the
  matching true/false (and shopware's LDAP-only) coverage.
- web-app-fusiondirectory, web-app-matrix: declare the
  ``matomo`` flag so their ``run_after: web-app-matomo`` resolves
  through an explicit services entry; add matomo true/false to both
  variants files.
- web-app-fediwall, web-app-mini-qr: add the
  ``# nocheck: logout`` opt-out directly above ``logout:`` so the
  logout-dashboard guard accepts the legitimately-disabled state.
- ~30 ``services.yml`` files: insert
  ``# nocheck: dynamic-flag`` directly above the literal
  ``css`` / ``redis`` / ``view`` / ``worker`` / ``email`` / ``html``
  / ``mariadb`` / ``opensearch`` / ``talk`` / ``logout`` blocks that
  the dynamic-flag guard surfaced.

Code-quality follow-ups uncovered by ruff:
- utils/cache/yaml.py: collapse ``load_yaml_all_str`` to a one-line
  return + on-call ``# nocheck: direct-yaml`` so the lint accepts
  the cache module's own ``yaml.safe_load_all`` invocation.
- utils/templating.py: replace the ``Optional[Any]`` import that
  slipped in from main with the modern ``Any | None``; rewrite the
  cleanup ``try/except/pass`` blocks as
  ``contextlib.suppress(Exception)`` so the imported
  ``contextlib`` module is consumed consistently.
- 7 new-from-main lint test files (test_inline_script_block_size,
  test_no_inline_multiline_php_in_sh, test_no_lookup_config_jinja_default,
  test_no_inline_multiline_sql, test_web_role_no_web_dependency,
  test_shared_primary_entity_run_once, test_fediwall_active): drop
  their local ``Path(__file__).parents[N]`` PROJECT_ROOT
  computations and import the package-level constant instead.
- roles/web-app-moodle/templates/playwright.env.j2: drop the
  unreferenced ``KEYCLOAK_REALM_NAME`` env key that the
  playwright-env-keys-used guard flagged.
2026-05-08 00:03:14 +02:00

305 lines
11 KiB
Python

import importlib.util
import sys
import unittest
from types import ModuleType
from unittest.mock import MagicMock, patch
def _load_lookup_module() -> ModuleType:
"""Load the fediwall_active lookup plugin directly from the global
plugins/lookup/ path. Stubs out ansible imports so the unit test
does not require the ansible runtime."""
# Stub minimal ansible.errors / ansible.plugins.lookup /
# ansible.plugins.loader so the plugin module imports cleanly.
if "ansible" not in sys.modules:
ansible_pkg = ModuleType("ansible")
sys.modules["ansible"] = ansible_pkg
if "ansible.errors" not in sys.modules:
errors_mod = ModuleType("ansible.errors")
class _AnsibleError(Exception):
pass
errors_mod.AnsibleError = _AnsibleError
sys.modules["ansible.errors"] = errors_mod
if "ansible.plugins" not in sys.modules:
plugins_pkg = ModuleType("ansible.plugins")
sys.modules["ansible.plugins"] = plugins_pkg
if "ansible.plugins.lookup" not in sys.modules:
lookup_mod = ModuleType("ansible.plugins.lookup")
class _LookupBase:
pass
lookup_mod.LookupBase = _LookupBase
sys.modules["ansible.plugins.lookup"] = lookup_mod
if "ansible.plugins.loader" not in sys.modules:
loader_mod = ModuleType("ansible.plugins.loader")
loader_mod.lookup_loader = MagicMock()
sys.modules["ansible.plugins.loader"] = loader_mod
from . import PROJECT_ROOT
plugin_path = PROJECT_ROOT / "plugins" / "lookup" / "fediwall_active.py"
spec = importlib.util.spec_from_file_location("fediwall_active", plugin_path)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
class TestSelectActive(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._module = _load_lookup_module()
def test_returns_only_siblings_present_in_group_names(self):
siblings = ["web-app-mastodon", "web-app-pixelfed", "web-app-friendica"]
group_names = [
"all",
"web-app-mastodon",
"web-app-friendica",
"svc-db-postgres",
]
self.assertEqual(
type(self)._module.select_active(siblings, group_names),
["web-app-mastodon", "web-app-friendica"],
)
def test_preserves_input_order(self):
siblings = ["web-app-friendica", "web-app-mastodon", "web-app-pixelfed"]
group_names = ["web-app-mastodon", "web-app-pixelfed", "web-app-friendica"]
self.assertEqual(
type(self)._module.select_active(siblings, group_names),
["web-app-friendica", "web-app-mastodon", "web-app-pixelfed"],
)
def test_returns_empty_when_no_overlap(self):
siblings = ["web-app-mastodon", "web-app-pixelfed"]
group_names = ["all", "svc-db-postgres", "web-app-nextcloud"]
self.assertEqual(type(self)._module.select_active(siblings, group_names), [])
def test_returns_empty_when_siblings_is_empty(self):
self.assertEqual(type(self)._module.select_active([], ["web-app-mastodon"]), [])
def test_returns_empty_when_siblings_is_none(self):
self.assertEqual(
type(self)._module.select_active(None, ["web-app-mastodon"]), []
)
def test_returns_empty_when_group_names_is_empty(self):
self.assertEqual(type(self)._module.select_active(["web-app-mastodon"], []), [])
def test_returns_empty_when_group_names_is_none(self):
self.assertEqual(
type(self)._module.select_active(["web-app-mastodon"], None), []
)
def test_preserves_duplicates_in_siblings(self):
siblings = ["web-app-mastodon", "web-app-mastodon"]
group_names = ["web-app-mastodon"]
self.assertEqual(
type(self)._module.select_active(siblings, group_names),
["web-app-mastodon", "web-app-mastodon"],
)
class TestResolveForWant(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._module = _load_lookup_module()
cls._AnsibleError = sys.modules["ansible.errors"].AnsibleError
def test_siblings_want_does_not_call_resolver(self):
def boom(_):
raise AssertionError("resolver must not be invoked for want='siblings'")
out = type(self)._module.resolve_for_want(
["web-app-mastodon", "web-app-pixelfed"],
["web-app-mastodon"],
"siblings",
boom,
)
self.assertEqual(out, ["web-app-mastodon"])
def test_url_bases_want_uses_resolver_per_active_sibling(self):
siblings = ["web-app-mastodon", "web-app-pixelfed", "web-app-friendica"]
group_names = ["web-app-mastodon", "web-app-friendica"]
def resolver(s):
return f"https://{s}.example/"
out = type(self)._module.resolve_for_want(
siblings, group_names, "url_bases", resolver
)
self.assertEqual(
out,
[
"https://web-app-mastodon.example/",
"https://web-app-friendica.example/",
],
)
def test_domains_want_uses_resolver_per_active_sibling(self):
siblings = ["web-app-mastodon", "web-app-pixelfed"]
group_names = ["web-app-pixelfed"]
def resolver(s):
return s.removeprefix("web-app-") + ".example"
out = type(self)._module.resolve_for_want(
siblings, group_names, "domains", resolver
)
self.assertEqual(out, ["pixelfed.example"])
def test_returns_empty_when_no_active_sibling(self):
out = type(self)._module.resolve_for_want(
["web-app-mastodon"],
["svc-db-postgres"],
"url_bases",
lambda _: "https://nope/",
)
self.assertEqual(out, [])
def test_invalid_want_raises(self):
with self.assertRaises(type(self)._AnsibleError):
type(self)._module.resolve_for_want(
["web-app-mastodon"],
["web-app-mastodon"],
"bogus",
lambda s: s,
)
def test_resolver_required_for_non_siblings_want(self):
with self.assertRaises(type(self)._AnsibleError):
type(self)._module.resolve_for_want(
["web-app-mastodon"],
["web-app-mastodon"],
"url_bases",
None,
)
class TestLookupModuleRun(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._module = _load_lookup_module()
cls._AnsibleError = sys.modules["ansible.errors"].AnsibleError
def _make_lookup(self):
lookup = type(self)._module.LookupModule()
lookup._templar = MagicMock()
lookup._loader = MagicMock()
return lookup
def test_rejects_missing_term(self):
lookup = self._make_lookup()
with self.assertRaises(type(self)._AnsibleError):
lookup.run([], variables={"group_names": []})
def test_rejects_extra_terms(self):
lookup = self._make_lookup()
with self.assertRaises(type(self)._AnsibleError):
lookup.run(["url_bases", "extra"], variables={"group_names": []})
def test_rejects_unknown_want(self):
lookup = self._make_lookup()
with self.assertRaises(type(self)._AnsibleError):
lookup.run(["bogus"], variables={"group_names": []})
def test_siblings_want_returns_active_set_only(self):
config = MagicMock()
config.run.return_value = [
["web-app-mastodon", "web-app-pixelfed", "web-app-friendica"]
]
with patch.object(type(self)._module.lookup_loader, "get", return_value=config):
lookup = self._make_lookup()
result = lookup.run(
["siblings"],
variables={"group_names": ["web-app-mastodon", "web-app-friendica"]},
)
self.assertEqual(result, [["web-app-mastodon", "web-app-friendica"]])
config.run.assert_called_once()
def test_url_bases_want_calls_tls_lookup_per_active_sibling(self):
config = MagicMock()
config.run.return_value = [
["web-app-mastodon", "web-app-pixelfed", "web-app-friendica"]
]
tls = MagicMock()
tls.run.side_effect = [
["https://microblog.example/"],
["https://social.example/"],
]
def fake_get(name, **_):
if name == "config":
return config
if name == "tls":
return tls
raise AssertionError(f"unexpected lookup '{name}'")
with patch.object(
type(self)._module.lookup_loader, "get", side_effect=fake_get
):
lookup = self._make_lookup()
result = lookup.run(
["url_bases"],
variables={"group_names": ["web-app-mastodon", "web-app-friendica"]},
)
self.assertEqual(
result,
[["https://microblog.example/", "https://social.example/"]],
)
self.assertEqual(tls.run.call_count, 2)
called_with = [call.args[0] for call in tls.run.call_args_list]
self.assertEqual(
called_with,
[
["web-app-mastodon", "url.base"],
["web-app-friendica", "url.base"],
],
)
def test_domains_want_calls_domain_lookup_per_active_sibling(self):
config = MagicMock()
config.run.return_value = [["web-app-mastodon", "web-app-pixelfed"]]
domain = MagicMock()
domain.run.side_effect = [["microblog.example"], ["photos.example"]]
def fake_get(name, **_):
if name == "config":
return config
if name == "domain":
return domain
raise AssertionError(f"unexpected lookup '{name}'")
with patch.object(
type(self)._module.lookup_loader, "get", side_effect=fake_get
):
lookup = self._make_lookup()
result = lookup.run(
["domains"],
variables={"group_names": ["web-app-mastodon", "web-app-pixelfed"]},
)
self.assertEqual(result, [["microblog.example", "photos.example"]])
def test_falls_back_to_templar_available_variables_when_variables_missing(self):
config = MagicMock()
config.run.return_value = [["web-app-mastodon"]]
with patch.object(type(self)._module.lookup_loader, "get", return_value=config):
lookup = self._make_lookup()
lookup._templar.available_variables = {"group_names": ["web-app-mastodon"]}
result = lookup.run(["siblings"], variables=None)
self.assertEqual(result, [["web-app-mastodon"]])
if __name__ == "__main__":
unittest.main()