diff --git a/app/tests/test_local_domain_loopback_nix.py b/app/tests/test_local_domain_loopback_nix.py index 365930a..72d2a12 100644 --- a/app/tests/test_local_domain_loopback_nix.py +++ b/app/tests/test_local_domain_loopback_nix.py @@ -16,11 +16,28 @@ Verifies that the sovran-hosts-update helper: - does NOT rely on environment.systemPackages for the helper's dependencies. """ +import re +import shutil +import subprocess import unittest from pathlib import Path +NIX_STRING_INDENT = 6 +REPO_ROOT = Path(__file__).resolve().parents[2] +FLAKE_SOURCE = (REPO_ROOT / "flake.nix").read_text() +PRIMARY_NIXOS_CONFIGURATION_MATCH = re.search( + r"nixosConfigurations\.([A-Za-z0-9_-]+)\s*=", + FLAKE_SOURCE, +) +if PRIMARY_NIXOS_CONFIGURATION_MATCH is None: + raise RuntimeError("Could not determine the primary nixosConfigurations entry from flake.nix") +PRIMARY_NIXOS_CONFIGURATION = PRIMARY_NIXOS_CONFIGURATION_MATCH.group(1) +HELPER_BUILD_ATTR = ( + f'.#nixosConfigurations.{PRIMARY_NIXOS_CONFIGURATION}.config.environment.etc.' + '"sovran-hosts-update.sh".source' +) NIX_FILE = ( - Path(__file__).resolve().parents[2] + REPO_ROOT / "modules" / "core" / "local-domain-loopback.nix" @@ -31,6 +48,16 @@ class LocalDomainLoopbackNixStructureTests(unittest.TestCase): def setUp(self): self.source = NIX_FILE.read_text() + def _helper_script(self) -> str: + start = self.source.index("text = ''") + len("text = ''") + end = self.source.index(" '';", start) + return "\n".join( + line[NIX_STRING_INDENT:] + if line.startswith(" " * NIX_STRING_INDENT) + else line + for line in self.source[start:end].splitlines() + ).lstrip("\n") + # ── writeShellApplication and explicit runtimeInputs ──────────────────── def test_uses_write_shell_application(self): @@ -120,6 +147,42 @@ class LocalDomainLoopbackNixStructureTests(unittest.TestCase): self.assertIn("skip=1", self.source) self.assertIn("skip=0", self.source) + def test_managed_block_uses_grouped_append_redirect(self): + self.assertIn('} >> "$TMP"', self.source) + self.assertEqual(self.source.count('>> "$TMP"'), 1) + + def test_helper_script_passes_shellcheck(self): + shellcheck = shutil.which("shellcheck") + if shellcheck is None: + self.skipTest("shellcheck is not installed") + proc = subprocess.run( + [shellcheck, "-s", "bash", "-"], + input=self._helper_script(), + text=True, + capture_output=True, + check=False, + ) + output = proc.stdout + proc.stderr + self.assertEqual(proc.returncode, 0, output) + + def test_helper_derivation_builds_when_nix_available(self): + nix = shutil.which("nix") + if nix is None: + self.skipTest("nix is not installed") + proc = subprocess.run( + [ + nix, + "build", + HELPER_BUILD_ATTR, + "--no-link", + ], + cwd=REPO_ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + # ── Activation script warns on failure rather than silently swallowing ─── def test_activation_script_emits_warning_on_failure(self): diff --git a/modules/core/local-domain-loopback.nix b/modules/core/local-domain-loopback.nix index a49f684..89adfed 100644 --- a/modules/core/local-domain-loopback.nix +++ b/modules/core/local-domain-loopback.nix @@ -107,11 +107,13 @@ let # ── Step 4: append the Sovran block if there are any entries ────────── if [ -n "$ENTRIES" ]; then - printf '\n%s\n' "$BEGIN_MARKER" >> "$TMP" - printf '%s\n' "# These entries route configured service domains to local Caddy." >> "$TMP" - printf '%s\n' "# They are managed automatically — do not edit this block." >> "$TMP" - printf '%s\n' "$ENTRIES" >> "$TMP" - printf '%s\n' "$END_MARKER" >> "$TMP" + { + printf '\n%s\n' "$BEGIN_MARKER" + printf '%s\n' "# These entries route configured service domains to local Caddy." + printf '%s\n' "# They are managed automatically — do not edit this block." + printf '%s\n' "$ENTRIES" + printf '%s\n' "$END_MARKER" + } >> "$TMP" fi # ── Step 5: atomically replace /etc/hosts ─────────────────────────────