Files
computer-playbook/tests/unit/utils/roles/test_entity_apps.py
T
kevinveenbirkenbachandClaude Opus 4.7 820fb0a222 fix(purge): clear stale nginx vhosts between matrix-deploy rounds
Matrix-deploy variant transitions left per-domain vhost files behind:
when a service dropped out of a round's include set, sys-svc-compose's
docker prune removed its container but the vhost under
<NGINX_DIR>/conf.d/servers/{http,https}/<domain>.conf survived, making
the next round's HTTPS probe see 502 instead of TLS-error 000 (5 failed
test-development jobs on run 25705903504).

The inter-round purge in cli.administration.deploy.development.deploy
already invokes scripts/tests/deploy/local/purge/entity.sh with the
previous round's full include — the missing piece was an entity-keyed
nginx primitive next to db/compose/dir. Add it:

- utils.roles.entity_apps.apps_for_entity: reverse of get_entity_name,
  walks roles/*/vars/main.yml to map entity -> [application_id]
- utils.cleanup.nginx_vhosts: resolves entity -> apps -> canonical+alias
  domains (via build_applications_from_roles + iter_app_domains so it
  stays ansible-free) and removes the matching vhost files
- scripts/container/purge/entity/nginx.sh: thin bash wrapper,
  symmetric to db/compose/dir
- scripts/container/purge/apps.sh: drive nginx.sh inside the entity
  loop, alongside db/compose/dir

Tests: 14 new unit tests covering the reverse-lookup edge cases and
the purge iterator (existing-only / aliases / multi-entity / no-op CLI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:17:26 +02:00

102 lines
3.7 KiB
Python

"""Unit tests for `utils.roles.entity_apps.apps_for_entity`."""
from __future__ import annotations
import os
import shutil
import tempfile
import unittest
from pathlib import Path
from utils.cache.yaml import _reset_cache_for_tests, dump_yaml
from utils.roles.entity_apps import apps_for_entity
from utils.roles.mapping import ROLE_FILE_VARS_MAIN
class TestAppsForEntity(unittest.TestCase):
def setUp(self) -> None:
_reset_cache_for_tests()
self.tmp = Path(tempfile.mkdtemp(prefix="apps_for_entity_test_"))
self.roles_dir = self.tmp / "roles"
self.roles_dir.mkdir(parents=True, exist_ok=True)
# `get_entity_name` resolves categories.yml relative to cwd or
# PROJECT_ROOT, so a temp cwd with our roles tree is the simplest
# way to feed it a minimal categories layout for the test.
dump_yaml(
self.roles_dir / "categories.yml",
{
"roles": {
"web": {
"app": {"title": "Applications", "invokable": True},
"svc": {"title": "Services", "invokable": True},
}
}
},
)
self._cwd = Path.cwd()
os.chdir(self.tmp)
self.addCleanup(lambda: os.chdir(self._cwd))
self.addCleanup(lambda: shutil.rmtree(self.tmp, ignore_errors=True))
def _mk_role(self, name: str, application_id: str | None) -> None:
rd = self.roles_dir / name
(rd / "vars").mkdir(parents=True, exist_ok=True)
payload: dict = {}
if application_id is not None:
payload["application_id"] = application_id
dump_yaml(rd / ROLE_FILE_VARS_MAIN, payload)
def test_returns_apps_under_entity(self) -> None:
self._mk_role("web-app-matomo", "web-app-matomo")
self._mk_role("web-app-dashboard", "web-app-dashboard")
self._mk_role("web-svc-cdn", "web-svc-cdn")
self.assertEqual(
apps_for_entity("matomo", roles_dir=self.roles_dir),
["web-app-matomo"],
)
self.assertEqual(
apps_for_entity("cdn", roles_dir=self.roles_dir),
["web-svc-cdn"],
)
def test_falls_back_to_role_dir_name_when_application_id_missing(self) -> None:
# vars/main.yml present but no application_id → role dir name wins.
self._mk_role("web-app-foo", None)
self.assertEqual(
apps_for_entity("foo", roles_dir=self.roles_dir),
["web-app-foo"],
)
def test_unknown_entity_returns_empty(self) -> None:
self._mk_role("web-app-matomo", "web-app-matomo")
self.assertEqual(
apps_for_entity("does-not-exist", roles_dir=self.roles_dir),
[],
)
def test_blank_entity_returns_empty(self) -> None:
self._mk_role("web-app-matomo", "web-app-matomo")
self.assertEqual(apps_for_entity("", roles_dir=self.roles_dir), [])
self.assertEqual(apps_for_entity(" ", roles_dir=self.roles_dir), [])
def test_missing_roles_dir_returns_empty(self) -> None:
ghost = self.tmp / "ghost-roles"
self.assertEqual(apps_for_entity("matomo", roles_dir=ghost), [])
def test_result_is_sorted_and_unique(self) -> None:
# Two roles map to the same entity via the longest-prefix rule.
self._mk_role("web-app-matomo", "web-app-matomo")
self._mk_role("web-svc-matomo", "web-svc-matomo")
# Different entities — sanity check ordering across calls
got = apps_for_entity("matomo", roles_dir=self.roles_dir)
self.assertEqual(got, sorted(got))
self.assertEqual(len(got), len(set(got)))
if __name__ == "__main__": # pragma: no cover
unittest.main()