"""Deployment-shape checks. A variable set in Portainer's UI but missing from the compose file's `environment:` block silently does nothing. That cost real debugging time on the Mythica stack; this test makes it impossible to reintroduce here. """ import re from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent yaml = pytest.importorskip("yaml") def declared_in_compose() -> set[str]: spec = yaml.safe_load((ROOT / "docker-compose.yml").read_text()) env = spec["services"]["mediashelf"]["environment"] return {e.split("=", 1)[0] for e in env} def read_by_config() -> set[str]: src = (ROOT / "mediashelf" / "config.py").read_text() return (set(re.findall(r'_[bisf]\("([A-Z_][A-Z0-9_]*)"', src)) | set(re.findall(r'_csv\("([A-Z_][A-Z0-9_]*)"\)', src))) def test_every_config_var_is_declared_in_the_stack(): missing = read_by_config() - declared_in_compose() assert not missing, ( "these are read by config.py but absent from docker-compose.yml's " "environment block, so setting them in Portainer would silently do " f"nothing: {sorted(missing)}") def test_compose_declares_nothing_the_app_ignores(): extra = declared_in_compose() - read_by_config() assert not extra, f"compose declares unused variables: {sorted(extra)}" def test_keep_all_libraries_defaults_to_empty(): """Nothing ships kept (§6.6).""" spec = yaml.safe_load((ROOT / "docker-compose.yml").read_text()) env = spec["services"]["mediashelf"]["environment"] line = next(e for e in env if e.startswith("KEEP_ALL_LIBRARIES=")) assert line.endswith(":-}") or line.endswith("="), \ f"a library is pre-kept in the shipped stack: {line}" example = (ROOT / ".env.example").read_text() assert re.search(r"^KEEP_ALL_LIBRARIES=\s*$", example, re.M), \ ".env.example must not name a library" def test_image_is_not_pinned_to_bare_latest(): spec = yaml.safe_load((ROOT / "docker-compose.yml").read_text()) image = spec["services"]["mediashelf"]["image"] assert "latest" not in image, ( "a bare :latest tag is what forced the Mythica stack to be recreated " "when a PUT update kept serving old code (§11.2)") def test_dockerfile_runs_as_non_root(): df = (ROOT / "Dockerfile").read_text() user_lines = [l for l in df.splitlines() if l.startswith("USER ")] assert user_lines and not user_lines[-1].strip().endswith("root") def test_no_credentials_committed(): """Cheap guard against the SOAP-password-in-git-history problem.""" suspicious = re.compile( r"(PLEX_TOKEN|TAUTULLI_API_KEY|MEDIASHELF_SECRET_KEY)\s*=\s*['\"]?[A-Za-z0-9]{16,}") for path in list(ROOT.glob("*.yml")) + list(ROOT.glob("*.py")) + \ list((ROOT / "mediashelf").rglob("*.py")) + [ROOT / ".env.example"]: text = path.read_text() assert not suspicious.search(text), f"possible credential in {path.name}"