"""The bundled template library. 83 templates ship in the image, and a broken one only shows up when somebody pulls it and the deploy fails. These checks are what was run by hand when the library was written, made permanent: every template must be discoverable by the service, parse as YAML, name an image for every service, and keep its ``.env.example`` in sync with the variables its compose file actually uses. The last one is the rule that keeps the library trustworthy: a ``${VAR}`` without a default and without an ``.env.example`` entry deploys as an empty string, which is how you get a container listening on ``:`` or a database with a blank password. """ from __future__ import annotations import json import re from pathlib import Path import pytest import yaml TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates" #: ``${NAME}``, ``${NAME:-default}``, ``${NAME:?required}`` … VAR = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(:?[-?][^}]*)?\}") COMPOSE_NAMES = ("compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml") def _template_dirs(): return sorted(p for p in TEMPLATES_DIR.iterdir() if p.is_dir()) def _compose_file(folder: Path) -> Path | None: for name in COMPOSE_NAMES: if (folder / name).is_file(): return folder / name return None def _env_example(folder: Path) -> dict[str, str]: path = folder / ".env.example" if not path.is_file(): return {} out = {} for line in path.read_text().splitlines(): line = line.strip() if line and not line.startswith("#") and "=" in line: key, value = line.split("=", 1) out[key.strip()] = value.strip() return out def _compose_vars(text: str) -> dict[str, str]: """Variable name -> its modifier (``:-``, ``:?`` or empty).""" found: dict[str, str] = {} for match in VAR.finditer(text): name, modifier = match.group(1), match.group(2) or "" # A required marker anywhere wins over a defaulted use elsewhere. if name not in found or modifier.startswith((":?", "?")): found[name] = modifier return found SLUGS = [p.name for p in _template_dirs()] def test_the_library_is_not_empty(): assert len(SLUGS) > 50, f"only {len(SLUGS)} templates found — is the directory intact?" @pytest.mark.parametrize("slug", SLUGS) def test_template_is_discoverable_by_the_service(slug): """What the service lists is what ships — a folder without a compose file is silently skipped by ``_is_template`` and would never appear in the UI.""" from services import template_service detail = template_service.get_template(slug) assert detail is not None, f"{slug} is on disk but the service does not see it" assert detail["name"] assert detail["compose"].strip() @pytest.mark.parametrize("slug", SLUGS) def test_metadata_is_complete(slug): meta = json.loads((TEMPLATES_DIR / slug / "template.json").read_text()) for key in ("name", "description", "tags", "gpu"): assert key in meta, f"{slug}: template.json is missing '{key}'" assert meta["name"].strip() assert len(meta["description"]) > 20, f"{slug}: description is too thin to be useful" assert isinstance(meta["tags"], list) and meta["tags"], f"{slug}: needs at least one tag" @pytest.mark.parametrize("slug", SLUGS) def test_compose_parses_and_every_service_names_an_image(slug): folder = TEMPLATES_DIR / slug compose_file = _compose_file(folder) assert compose_file is not None, f"{slug}: no compose file" parsed = yaml.safe_load(compose_file.read_text()) assert isinstance(parsed, dict) and parsed.get("services"), f"{slug}: no services block" for name, service in parsed["services"].items(): assert "image" in service, f"{slug}: service '{name}' has no image" @pytest.mark.parametrize("slug", SLUGS) def test_every_variable_has_a_default_or_an_env_entry(slug): """No ``${VAR}`` may silently interpolate to an empty string. Either compose carries a default (``${PORT:-8080}``) or ``.env.example`` lists the variable so the user is prompted for it. """ folder = TEMPLATES_DIR / slug env = _env_example(folder) used = _compose_vars(_compose_file(folder).read_text()) missing = [ name for name, modifier in used.items() if name not in env and not modifier.startswith((":-", "-")) ] assert not missing, f"{slug}: {missing} have no default and no .env.example entry" @pytest.mark.parametrize("slug", SLUGS) def test_env_example_has_no_dead_entries(slug): """A variable in .env.example that compose never reads is a trap — it looks like a knob and does nothing.""" folder = TEMPLATES_DIR / slug used = _compose_vars(_compose_file(folder).read_text()) dead = [name for name in _env_example(folder) if name not in used] assert not dead, f"{slug}: .env.example defines {dead}, which compose never uses" @pytest.mark.parametrize("slug", SLUGS) def test_required_secrets_ship_empty(slug): """A template must never come with a working default password. Anything marked required (``${VAR:?…}``) has to be blank in .env.example so the deploy fails loudly instead of starting with a known credential. """ folder = TEMPLATES_DIR / slug env = _env_example(folder) required = [n for n, m in _compose_vars(_compose_file(folder).read_text()).items() if m.startswith((":?", "?"))] prefilled = [n for n in required if env.get(n)] assert not prefilled, f"{slug}: required secrets {prefilled} ship with a value" @pytest.mark.parametrize("slug", SLUGS) def test_extra_files_are_shipped_not_just_referenced(slug): """A ``./file`` bind mount must point at a file the template actually ships, or the deploy creates a *directory* there and the app misreads its config.""" folder = TEMPLATES_DIR / slug compose = _compose_file(folder).read_text() for match in re.finditer(r"^\s*-\s*\./([^:\s]+):", compose, re.M): referenced = folder / match.group(1) assert referenced.is_file(), ( f"{slug}: compose mounts ./{match.group(1)} but the template does not ship it" ) def test_pulling_a_template_promotes_env_example_to_env(tmp_path): """The pull path itself: the folder is copied, template.json is left behind and .env.example becomes a real .env.""" from services import template_service target = tmp_path / "pulled" template_service.copy_into_stack("uptime-kuma", "pulled", override=str(tmp_path)) assert (target / "compose.yaml").is_file() assert (target / ".env").is_file() assert not (target / ".env.example").exists() assert not (target / "template.json").exists()