Files
computer-playbook/tests/unit/plugins/lookup/test_addon_env_flags.py
T
kevinveenbirkenbachandClaude Opus 4.8 238bc285c7 refactor(lookups): reach merged config via the lookup-loader SPOT
Consumer lookup plugins now reach the merged config through the Ansible
lookup loader instead of importing get_merged_applications /
get_merged_domains from utils.cache. The merge stays one cached
implementation surfaced via the applications/config/domains/users lookups;
consumers call lookup_loader.get(...).run() (the compose_networks idiom).

- get_merged_applications walks the whole meta/ tree recursively
  (meta/**/*.yml to applications.<app>.<path>, addons and any depth) and
  folds the raw meta/volumes.yml subtree onto applications.<app>.volumes
  after the templar render so the Jinja source: strings stay unrendered.
- 31 consumer lookups rerouted; providers (applications/config/
  applications_current_play/users/domains) and the raw-volume accessors
  (volume/nginx) keep the cache import behind # nocheck: lookup-cache-import.
- New lint test_no_cache_imports_in_lookups enforces the contract.
- nfs_flavor is now the flavor SPOT (svc-storage-nfs-server + nfs_client_src
  resolve via lookup('nfs_flavor')); removed the unused STORAGE_NFS_FLAVOR
  group_var. Extracted extract_default_credentials to
  utils/cache/schema_credentials.py to keep applications.py under the cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:35:06 +02:00

209 lines
7.6 KiB
Python

import unittest
from unittest import mock
from unittest.mock import patch
from ansible.errors import AnsibleError
from plugins.lookup.addon_env_flags import LookupModule, env_key
def _applications_loader(apps):
def _get(name, *a, **k):
if name == "applications":
return mock.MagicMock(run=lambda *_a, **_k: [apps])
return mock.MagicMock(run=lambda *_a, **_k: [None])
loader_mock = mock.MagicMock()
loader_mock.get.side_effect = _get
return loader_mock
class TestEnvKey(unittest.TestCase):
"""env_key() must match addon-gating.js envKey() exactly."""
def test_simple(self):
self.assertEqual(env_key("collectives"), "COLLECTIVES_ADDON_ENABLED")
def test_underscores_preserved(self):
self.assertEqual(env_key("files_bpm"), "FILES_BPM_ADDON_ENABLED")
self.assertEqual(
env_key("integration_gitlab"), "INTEGRATION_GITLAB_ADDON_ENABLED"
)
def test_non_alnum_runs_collapse(self):
self.assertEqual(
env_key("ldap-authenticator"), "LDAP_AUTHENTICATOR_ADDON_ENABLED"
)
self.assertEqual(env_key("a.b-c"), "A_B_C_ADDON_ENABLED")
class TestAddonEnvFlagsLookup(unittest.TestCase):
def setUp(self):
self.lookup = LookupModule()
self.lookup._loader = mock.MagicMock()
self.addons = {
"collectives": {"enabled": True, "required": True},
"contacts": {"enabled": True, "required": True},
"optional_on": {"enabled": True, "required": False},
"required_off": {"enabled": False, "required": True},
"off_optional": {"enabled": False, "required": False},
"str_flags": {"enabled": "true", "required": "true"},
"no_required": {"enabled": True},
}
self._patchers = [
patch(
"plugins.lookup.addon_env_flags.lookup_loader",
_applications_loader({}),
),
patch("plugins.lookup.addon_env_flags.get", return_value=self.addons),
patch(
"plugins.lookup.addon_env_flags._render_with_templar",
side_effect=lambda v, **k: v,
),
]
for p in self._patchers:
p.start()
self.addCleanup(lambda: [p.stop() for p in self._patchers])
def _flags(self):
out = self.lookup.run(["web-app-x"], variables={})[0]
return dict(line.split("=", 1) for line in out.splitlines())
def test_enabled_and_required_true(self):
f = self._flags()
self.assertEqual(f["COLLECTIVES_ADDON_ENABLED"], "true")
self.assertEqual(f["CONTACTS_ADDON_ENABLED"], "true")
def test_required_false_skipped_even_if_enabled(self):
self.assertEqual(self._flags()["OPTIONAL_ON_ADDON_ENABLED"], "false")
def test_disabled_is_false(self):
f = self._flags()
self.assertEqual(f["REQUIRED_OFF_ADDON_ENABLED"], "false")
self.assertEqual(f["OFF_OPTIONAL_ADDON_ENABLED"], "false")
def test_missing_required_defaults_false(self):
self.assertEqual(self._flags()["NO_REQUIRED_ADDON_ENABLED"], "false")
def test_string_flags_coerced(self):
self.assertEqual(self._flags()["STR_FLAGS_ADDON_ENABLED"], "true")
def test_one_line_per_addon_sorted_by_key(self):
lines = self.lookup.run(["web-app-x"], variables={})[0].splitlines()
self.assertEqual(len(lines), len(self.addons))
keys = [ln.split("=", 1)[0] for ln in lines]
self.assertEqual(keys, sorted(keys))
def test_no_terms_raises(self):
with self.assertRaises(AnsibleError):
self.lookup.run([], variables={})
def test_too_many_terms_raises(self):
with self.assertRaises(AnsibleError):
self.lookup.run(["a", "b"], variables={})
class TestNoAddons(unittest.TestCase):
def test_empty_addons_yields_empty_string(self):
lookup = LookupModule()
lookup._loader = mock.MagicMock()
with (
patch(
"plugins.lookup.addon_env_flags.lookup_loader",
_applications_loader({}),
),
patch("plugins.lookup.addon_env_flags.get", return_value={}),
patch(
"plugins.lookup.addon_env_flags._render_with_templar",
side_effect=lambda v, **k: v,
),
):
self.assertEqual(lookup.run(["web-app-x"], variables={}), [""])
class TestBridgeDeploymentGating(unittest.TestCase):
"""A bridged addon's flag is true only when one of its partner roles is in the
deployed playwright set (TEST_E2E_PLAYWRIGHT_APPS). When the set cannot be
resolved the gating is skipped (flag falls back to enabled AND required)."""
def setUp(self):
self.lookup = LookupModule()
self.lookup._loader = mock.MagicMock()
self.addons = {
"gl_bridge": {"enabled": True, "required": True, "bridges": ["gitlab"]},
"masto_bridge": {
"enabled": True,
"required": True,
"bridges": ["mastodon"],
},
"multi_bridge": {
"enabled": True,
"required": True,
"bridges": ["openwebui", "flowise"],
},
"no_bridge": {"enabled": True, "required": True},
"disabled_bridge": {
"enabled": False,
"required": True,
"bridges": ["gitlab"],
},
}
self._patchers = [
patch(
"plugins.lookup.addon_env_flags.lookup_loader",
_applications_loader({}),
),
patch("plugins.lookup.addon_env_flags.get", return_value=self.addons),
patch(
"plugins.lookup.addon_env_flags._render_with_templar",
side_effect=lambda v, **k: v,
),
]
for p in self._patchers:
p.start()
self.addCleanup(lambda: [p.stop() for p in self._patchers])
def _flags(self, variables):
out = self.lookup.run(["web-app-x"], variables=variables)[0]
return dict(line.split("=", 1) for line in out.splitlines())
def test_partner_deployed_keeps_true(self):
f = self._flags(
{"TEST_E2E_PLAYWRIGHT_APPS": ["web-app-gitlab", "web-app-nextcloud"]}
)
self.assertEqual(f["GL_BRIDGE_ADDON_ENABLED"], "true")
def test_partner_not_deployed_gated_false(self):
f = self._flags(
{"TEST_E2E_PLAYWRIGHT_APPS": ["web-app-gitlab", "web-app-nextcloud"]}
)
self.assertEqual(f["MASTO_BRIDGE_ADDON_ENABLED"], "false")
def test_any_partner_of_multi_bridge_counts(self):
f = self._flags({"TEST_E2E_PLAYWRIGHT_APPS": ["svc-ai-flowise"]})
self.assertEqual(f["MULTI_BRIDGE_ADDON_ENABLED"], "true")
def test_no_bridge_addon_unaffected(self):
f = self._flags({"TEST_E2E_PLAYWRIGHT_APPS": ["web-app-nextcloud"]})
self.assertEqual(f["NO_BRIDGE_ADDON_ENABLED"], "true")
def test_disabled_addon_stays_false(self):
f = self._flags({"TEST_E2E_PLAYWRIGHT_APPS": ["web-app-gitlab"]})
self.assertEqual(f["DISABLED_BRIDGE_ADDON_ENABLED"], "false")
def test_no_deployed_set_falls_back_no_gating(self):
f = self._flags({})
self.assertEqual(f["MASTO_BRIDGE_ADDON_ENABLED"], "true")
self.assertEqual(f["GL_BRIDGE_ADDON_ENABLED"], "true")
def test_deployed_set_as_string_is_split(self):
f = self._flags(
{"TEST_E2E_PLAYWRIGHT_APPS": "web-app-gitlab web-app-nextcloud"}
)
self.assertEqual(f["GL_BRIDGE_ADDON_ENABLED"], "true")
self.assertEqual(f["MASTO_BRIDGE_ADDON_ENABLED"], "false")
if __name__ == "__main__":
unittest.main()