179 lines
7.3 KiB
Python
179 lines
7.3 KiB
Python
"""Structural regression tests for modules/core/local-domain-loopback.nix.
|
|
|
|
Verifies that the sovran-hosts-update helper:
|
|
- is built as a pkgs.writeShellApplication with explicit runtimeInputs
|
|
(gawk, gnugrep, coreutils);
|
|
- uses ``lib.getExe hostsUpdateScript`` for both the systemd ExecStart and
|
|
the activation script so that both contexts share the same Nix-store
|
|
executable;
|
|
- does NOT point ExecStart at the raw /etc path;
|
|
- includes element-calling in the supported domain list;
|
|
- uses ``awk -v`` for safe marker-variable passing rather than interpolating
|
|
marker text directly into the awk program;
|
|
- retains the domain validation regex (idempotency / injection prevention);
|
|
- exposes /etc/sovran-hosts-update.sh as a symlink via ``source =`` (not a
|
|
second raw ``text =`` body);
|
|
- does NOT rely on environment.systemPackages for the helper's dependencies.
|
|
"""
|
|
|
|
import shutil
|
|
import subprocess
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
NIX_FILE = (
|
|
REPO_ROOT
|
|
/ "modules"
|
|
/ "core"
|
|
/ "local-domain-loopback.nix"
|
|
)
|
|
|
|
|
|
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[6:] if line.startswith(" ") else line
|
|
for line in self.source[start:end].splitlines()
|
|
).lstrip("\n")
|
|
|
|
# ── writeShellApplication and explicit runtimeInputs ────────────────────
|
|
|
|
def test_uses_write_shell_application(self):
|
|
self.assertIn("pkgs.writeShellApplication", self.source)
|
|
|
|
def test_runtime_inputs_includes_coreutils(self):
|
|
self.assertIn("pkgs.coreutils", self.source)
|
|
|
|
def test_runtime_inputs_includes_gawk(self):
|
|
self.assertIn("pkgs.gawk", self.source)
|
|
|
|
def test_runtime_inputs_includes_gnugrep(self):
|
|
self.assertIn("pkgs.gnugrep", self.source)
|
|
|
|
def test_runtime_inputs_block_present(self):
|
|
self.assertIn("runtimeInputs", self.source)
|
|
|
|
# ── Both execution paths use lib.getExe ─────────────────────────────────
|
|
|
|
def test_exec_start_uses_lib_get_exe(self):
|
|
"""systemd ExecStart must reference the Nix-store executable."""
|
|
self.assertIn("ExecStart = lib.getExe hostsUpdateScript", self.source)
|
|
|
|
def test_activation_script_uses_lib_get_exe(self):
|
|
"""Activation text must call the same Nix-store executable."""
|
|
self.assertIn("${lib.getExe hostsUpdateScript}", self.source)
|
|
|
|
def test_exec_start_does_not_point_to_etc_path(self):
|
|
"""ExecStart must NOT use the raw /etc path (which lacks a deterministic PATH)."""
|
|
self.assertNotIn('ExecStart = "/etc/sovran-hosts-update.sh"', self.source)
|
|
|
|
# ── /etc symlink uses source =, not a second text = body ────────────────
|
|
|
|
def test_etc_entry_uses_source_not_text(self):
|
|
"""The /etc/sovran-hosts-update.sh entry must be a symlink (source =),
|
|
not a second raw script body (text =)."""
|
|
self.assertIn(
|
|
'environment.etc."sovran-hosts-update.sh".source', self.source
|
|
)
|
|
|
|
def test_etc_source_points_to_get_exe(self):
|
|
self.assertIn(
|
|
'environment.etc."sovran-hosts-update.sh".source = lib.getExe hostsUpdateScript',
|
|
self.source,
|
|
)
|
|
|
|
# ── element-calling domain is supported ─────────────────────────────────
|
|
|
|
def test_element_calling_domain_key_present(self):
|
|
self.assertIn("element-calling", self.source)
|
|
|
|
# ── Robust awk -v variable passing ──────────────────────────────────────
|
|
|
|
def test_awk_uses_dash_v_for_begin_marker(self):
|
|
"""awk must receive the begin marker via -v, not by shell interpolation."""
|
|
self.assertIn('awk -v begin=', self.source)
|
|
|
|
def test_awk_uses_dash_v_for_end_marker(self):
|
|
self.assertIn('-v end=', self.source)
|
|
|
|
def test_awk_does_not_interpolate_marker_into_program(self):
|
|
"""The old pattern interpolated $BEGIN_MARKER directly into the awk source."""
|
|
self.assertNotIn('/$BEGIN_MARKER', self.source)
|
|
self.assertNotIn('/$END_MARKER', self.source)
|
|
|
|
# ── Domain validation ────────────────────────────────────────────────────
|
|
|
|
def test_domain_validation_regex_present(self):
|
|
"""The hostname validation regex must still be present for injection prevention."""
|
|
self.assertIn("grep -qE", self.source)
|
|
self.assertIn("[a-zA-Z0-9]", self.source)
|
|
|
|
def test_invalid_domain_warning_present(self):
|
|
self.assertIn("skipping invalid domain value", self.source)
|
|
|
|
# ── No environment.systemPackages reliance ───────────────────────────────
|
|
|
|
def test_no_environment_system_packages_for_helper(self):
|
|
"""The helper's tools are declared via runtimeInputs; the module must
|
|
not add them to environment.systemPackages."""
|
|
self.assertNotIn("environment.systemPackages", self.source)
|
|
|
|
# ── Idempotency: existing Sovran block is removed before rewriting ───────
|
|
|
|
def test_existing_block_removal_logic_present(self):
|
|
"""awk strip of the managed block must be present for idempotency."""
|
|
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)
|
|
self.assertNotIn("SC2129", 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",
|
|
'.#nixosConfigurations.nixos.config.environment.etc."sovran-hosts-update.sh".source',
|
|
"--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):
|
|
self.assertIn("warning: sovran-hosts-update", self.source)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|