"""Unit tests for the per-role `meta/variants.yml` matrix-deploy loader (`utils.cache.applications.get_variants`) and the variant-free base-config default that `get_merged_applications` falls back on when no inventory-level `applications.` override is set.""" import os import sys import textwrap import unittest from pathlib import Path from . import PROJECT_ROOT if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from utils.cache import _reset_cache_for_tests from utils.cache.applications import ( _build_variants, get_application_defaults, get_merged_applications, get_variants, ) from utils.cache.applications import ( _load_variants_overrides as _load_yaml_variant_list, ) from utils.roles.mapping import ( ROLE_FILE_META_SERVER, ROLE_FILE_META_SERVICES, ROLE_FILE_META_VARIANTS, ) def _write_role( roles_dir: Path, name: str, *, config: str | None = None, meta: str | None = None, server: str | None = None, ) -> None: """Write a synthetic role tree under //meta/. ``config`` -> meta/services.yml content (the file root IS the services map) ``server`` -> meta/server.yml content ``meta`` -> meta/variants.yml content (matrix-deploy variant list) """ role = roles_dir / name (role / "meta").mkdir(parents=True, exist_ok=True) if config is not None: (role / ROLE_FILE_META_SERVICES).write_text(textwrap.dedent(config)) if server is not None: (role / ROLE_FILE_META_SERVER).write_text(textwrap.dedent(server)) if meta is not None: (role / ROLE_FILE_META_VARIANTS).write_text(textwrap.dedent(meta)) class TestLoadYamlVariantList(unittest.TestCase): def test_missing_file_yields_single_empty_variant(self): self.assertEqual(_load_yaml_variant_list(Path("/nonexistent")), [{}]) def test_empty_file_yields_single_empty_variant(self): path = Path(self._write_tmp("")) self.assertEqual(_load_yaml_variant_list(path), [{}]) def test_empty_list_yields_single_empty_variant(self): path = Path(self._write_tmp("[]\n")) self.assertEqual(_load_yaml_variant_list(path), [{}]) def test_null_entry_normalised_to_empty_dict(self): path = Path(self._write_tmp("- null\n- {a: 1}\n")) self.assertEqual(_load_yaml_variant_list(path), [{}, {"a": 1}]) def test_non_list_root_rejected(self): path = Path(self._write_tmp("a: 1\n")) with self.assertRaises(TypeError): _load_yaml_variant_list(path) def test_non_mapping_entry_rejected(self): path = Path(self._write_tmp("- 42\n")) with self.assertRaises(ValueError): _load_yaml_variant_list(path) def _write_tmp(self, content: str) -> str: import tempfile with tempfile.NamedTemporaryFile("w", suffix=".yml", delete=False) as f: f.write(content) self.addCleanup(os.remove, f.name) return f.name class TestApplicationVariants(unittest.TestCase): def setUp(self): import tempfile self.tmp = tempfile.mkdtemp(prefix="variants-") self.roles_dir = Path(self.tmp) _reset_cache_for_tests() def tearDown(self): import shutil shutil.rmtree(self.tmp, ignore_errors=True) _reset_cache_for_tests() def test_role_without_meta_yields_single_variant(self): _write_role(self.roles_dir, "web-app-foo", config="foo:\n hello: world\n") variants = _build_variants(self.roles_dir) self.assertEqual(list(variants.keys()), ["web-app-foo"]) self.assertEqual(len(variants["web-app-foo"]), 1) self.assertEqual( variants["web-app-foo"][0]["services"]["foo"]["hello"], "world" ) def test_role_with_two_variants_deep_merges_each_entry(self): role = self.roles_dir / "web-app-bar" (role / "meta").mkdir(parents=True, exist_ok=True) (role / ROLE_FILE_META_SERVICES).write_text( textwrap.dedent( """ bar: enabled: false """ ) ) (role / ROLE_FILE_META_SERVER).write_text( textwrap.dedent( """ domains: canonical: ["bar.example"] """ ) ) (role / ROLE_FILE_META_VARIANTS).write_text( textwrap.dedent( """ - {} - services: bar: enabled: true server: domains: canonical: ["bar.example", "shop.bar.example"] """ ) ) variants = _build_variants(self.roles_dir)["web-app-bar"] self.assertEqual(len(variants), 2) self.assertEqual(variants[0]["services"]["bar"]["enabled"], False) self.assertEqual(variants[0]["server"]["domains"]["canonical"], ["bar.example"]) self.assertEqual(variants[1]["services"]["bar"]["enabled"], True) self.assertEqual( variants[1]["server"]["domains"]["canonical"], ["bar.example", "shop.bar.example"], ) def test_get_application_defaults_returns_variant_free_base_config(self): _write_role( self.roles_dir, "web-app-baz", config="baz:\n value: 1\n", meta=textwrap.dedent( """ - {} - services: baz: value: 2 """ ), ) defaults = get_application_defaults(roles_dir=self.roles_dir) self.assertEqual(defaults["web-app-baz"]["services"]["baz"]["value"], 1) def test_get_application_defaults_omits_variant_zero_only_keys(self): """Defaults MUST come from the variant-free base config, not variant 0. Otherwise a service key that only variant 0 enables (and that variant N intentionally omits) leaks into variant N's runtime view via deep-merge in `get_merged_applications`.""" _write_role( self.roles_dir, "web-app-leak", config="alpha:\n enabled: false\n", meta=textwrap.dedent( """ - services: alpha: enabled: true shared: true - services: beta: enabled: true shared: true """ ), ) defaults = get_application_defaults(roles_dir=self.roles_dir) alpha = defaults["web-app-leak"]["services"]["alpha"] self.assertEqual(alpha.get("enabled"), False) self.assertNotIn("beta", defaults["web-app-leak"]["services"]) def test_get_variants_caches_per_roles_dir(self): _write_role(self.roles_dir, "web-app-cache", config="cache:\n x: 1\n") first = get_variants(roles_dir=self.roles_dir) second = get_variants(roles_dir=self.roles_dir) self.assertEqual(first, second) second["web-app-cache"][0]["services"]["cache"]["x"] = 999 self.assertEqual( get_variants(roles_dir=self.roles_dir)["web-app-cache"][0]["services"][ "cache" ]["x"], 1, ) class TestMergedApplicationsUsesBaseConfig(unittest.TestCase): """The runtime loader reads no active-variant selector. Variant N data is baked into the inventory by `cli.administration.deploy.development.inventory.build_dev_inventory` at init time and reaches the merged payload as an `applications.` override. The loader's defaults are the role's variant-free base config so no variant's service flags leak into another variant's runtime view via deep-merge; inventory-level overrides remain authoritative.""" def setUp(self): import tempfile self.tmp = tempfile.mkdtemp(prefix="variants-merged-") self.roles_dir = Path(self.tmp) _reset_cache_for_tests() _write_role( self.roles_dir, "web-app-multi", server=textwrap.dedent( """ domains: canonical: ["multi.example"] """ ), meta=textwrap.dedent( """ - {} - server: domains: canonical: - "blog.multi.example" - "shop.multi.example" """ ), ) def tearDown(self): import shutil shutil.rmtree(self.tmp, ignore_errors=True) _reset_cache_for_tests() def test_no_inventory_overrides_yield_base_config(self): merged = get_merged_applications( variables={}, roles_dir=str(self.roles_dir), ) self.assertEqual( merged["web-app-multi"]["server"]["domains"]["canonical"], ["multi.example"], ) def test_inventory_override_swaps_in_variant_one_payload(self): variant_one = { "server": { "domains": { "canonical": ["blog.multi.example", "shop.multi.example"], }, }, } merged = get_merged_applications( variables={"applications": {"web-app-multi": variant_one}}, roles_dir=str(self.roles_dir), ) self.assertEqual( merged["web-app-multi"]["server"]["domains"]["canonical"], ["blog.multi.example", "shop.multi.example"], ) def test_active_variants_marker_is_ignored_by_loader(self): merged = get_merged_applications( variables={"_active_variants": {"web-app-multi": 1}}, roles_dir=str(self.roles_dir), ) self.assertEqual( merged["web-app-multi"]["server"]["domains"]["canonical"], ["multi.example"], ) if __name__ == "__main__": unittest.main()