Files
computer-playbook/tests/unit/utils/inventory/test_bundle_apps.py
T
kevinveenbirkenbachandClaude Opus 4.7 fd978a2897 feat(bundles): add deploy-bundles / redeploy-bundles flow
- new utils.inventory.bundle_apps resolver aggregates and dedupes role
  groups from inventories/bundles/{servers,workstations}/<name>/inventory.yml
- new make targets deploy-bundles (fresh-purged) and redeploy-bundles
  (reuse-kept), driven by BUNDLES=... env, wrapped by
  scripts/tests/deploy/local/deploy/{bundles,redeploy-bundles}.sh
- unit tests for the resolver under tests/unit/utils/inventory/
- document the new targets in docs/contributing/tools/make.md,
  docs/contributing/actions/testing.md, inventories/bundles/README.md
- collapse multi-line Makefile comments to one-liners for consistency
- disable nextcloud bbb plugin auto-enable (commented out)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 19:45:18 +02:00

177 lines
6.0 KiB
Python

"""Unit tests for `utils.inventory.bundle_apps`."""
from __future__ import annotations
import io
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
from utils.cache.yaml import _reset_cache_for_tests
from utils.inventory import bundle_apps
class TestSplitCsv(unittest.TestCase):
def test_splits_and_trims(self):
self.assertEqual(
bundle_apps._split_csv(" a , b,c "),
["a", "b", "c"],
)
def test_drops_empty_segments(self):
self.assertEqual(bundle_apps._split_csv("a,,b,"), ["a", "b"])
def test_empty_string(self):
self.assertEqual(bundle_apps._split_csv(""), [])
class _BundleFsMixin:
"""Build a fake inventories/bundles tree and patch SEARCH_DIRS."""
def setUp(self) -> None: # type: ignore[override]
_reset_cache_for_tests()
self._tmp = TemporaryDirectory()
self.addCleanup(self._tmp.cleanup) # type: ignore[attr-defined]
root = Path(self._tmp.name)
self.servers = root / "servers"
self.workstations = root / "workstations"
self.servers.mkdir()
self.workstations.mkdir()
patcher = patch.object(
bundle_apps,
"SEARCH_DIRS",
(self.servers, self.workstations),
)
patcher.start()
self.addCleanup(patcher.stop) # type: ignore[attr-defined]
def write_bundle(self, base: Path, name: str, apps: list[str]) -> Path:
bundle_dir = base / name
bundle_dir.mkdir()
inv = bundle_dir / "inventory.yml"
children = "".join(f" {a}:\n" for a in apps) or " {}\n"
inv.write_text(f"all:\n children:\n{children}", encoding="utf-8")
return inv
class TestLocate(_BundleFsMixin, unittest.TestCase):
def test_finds_in_servers(self):
self.write_bundle(self.servers, "alpha", ["a"])
self.assertEqual(
bundle_apps._locate("alpha"),
self.servers / "alpha" / "inventory.yml",
)
def test_finds_in_workstations(self):
self.write_bundle(self.workstations, "beta", ["b"])
self.assertEqual(
bundle_apps._locate("beta"),
self.workstations / "beta" / "inventory.yml",
)
def test_servers_wins_over_workstations(self):
self.write_bundle(self.servers, "dup", ["s"])
self.write_bundle(self.workstations, "dup", ["w"])
self.assertEqual(
bundle_apps._locate("dup"),
self.servers / "dup" / "inventory.yml",
)
def test_missing_returns_none(self):
self.assertIsNone(bundle_apps._locate("ghost"))
class TestResolve(_BundleFsMixin, unittest.TestCase):
def test_returns_apps_in_declaration_order(self):
self.write_bundle(self.servers, "edu", ["app_b", "app_a", "app_c"])
self.assertEqual(
bundle_apps.resolve(["edu"]),
["app_b", "app_a", "app_c"],
)
def test_deduplicates_across_bundles(self):
self.write_bundle(self.servers, "one", ["shared", "uniq1"])
self.write_bundle(self.workstations, "two", ["uniq2", "shared"])
self.assertEqual(
bundle_apps.resolve(["one", "two"]),
["shared", "uniq1", "uniq2"],
)
def test_missing_bundle_raises(self):
self.write_bundle(self.servers, "present", ["a"])
with self.assertRaises(FileNotFoundError) as ctx:
bundle_apps.resolve(["present", "absent"])
self.assertIn("absent", str(ctx.exception))
self.assertNotIn("present", str(ctx.exception))
def test_multiple_missing_listed(self):
with self.assertRaises(FileNotFoundError) as ctx:
bundle_apps.resolve(["x", "y"])
msg = str(ctx.exception)
self.assertIn("x", msg)
self.assertIn("y", msg)
def test_empty_children_yields_empty_list(self):
path = self.servers / "empty" / "inventory.yml"
path.parent.mkdir()
path.write_text("all:\n children: {}\n", encoding="utf-8")
self.assertEqual(bundle_apps.resolve(["empty"]), [])
def test_missing_all_key_tolerated(self):
path = self.servers / "weird" / "inventory.yml"
path.parent.mkdir()
path.write_text("other: 1\n", encoding="utf-8")
self.assertEqual(bundle_apps.resolve(["weird"]), [])
def test_null_children_tolerated(self):
path = self.servers / "nullkids" / "inventory.yml"
path.parent.mkdir()
path.write_text("all:\n children: ~\n", encoding="utf-8")
self.assertEqual(bundle_apps.resolve(["nullkids"]), [])
class TestMain(_BundleFsMixin, unittest.TestCase):
def _run(self, argv):
out = io.StringIO()
err = io.StringIO()
with patch("sys.stdout", out), patch("sys.stderr", err):
code = bundle_apps.main(argv)
return code, out.getvalue(), err.getvalue()
def test_success_prints_csv(self):
self.write_bundle(self.servers, "edu", ["nextcloud", "openwebui"])
code, out, err = self._run(["edu"])
self.assertEqual(code, 0)
self.assertEqual(out, "nextcloud,openwebui\n")
self.assertEqual(err, "")
def test_multiple_bundles_csv_arg(self):
self.write_bundle(self.servers, "a", ["x"])
self.write_bundle(self.workstations, "b", ["y"])
code, out, _ = self._run(["a,b"])
self.assertEqual(code, 0)
self.assertEqual(out, "x,y\n")
def test_blank_arg_returns_2(self):
code, _, err = self._run([" , "])
self.assertEqual(code, 2)
self.assertIn("no bundle names", err)
def test_missing_bundle_returns_2(self):
code, _, err = self._run(["ghost"])
self.assertEqual(code, 2)
self.assertIn("ghost", err)
def test_no_apps_resolved_returns_2(self):
path = self.servers / "empty" / "inventory.yml"
path.parent.mkdir()
path.write_text("all:\n children: {}\n", encoding="utf-8")
code, _, err = self._run(["empty"])
self.assertEqual(code, 2)
self.assertIn("no role groups", err)
if __name__ == "__main__":
unittest.main()