"""The route-switch root helper, tested unprivileged against a fake tree. `deploy/dashboards-routectl.py` is the phase 33 writer: the dashboard reaches a gateway config file only through it (through the agent's `POST /route`, through the systemd-activated socket). Its safety property is the write protocol's order — locate, state, edit, validate, reload, prove, record — and the refusals that end it before a byte moves. So the tests run the **real helper** (`--root` pointed at a fake tree), against the **captured** gateway configs (`tests/fixtures/gateway_*.cfg|.conf`, the fleet's real ingress bytes, never a toy file), with `nginx` / `haproxy` / `systemctl` faked on `PATH` as scripts that record their argv and fail on order. The assertions are on the bytes: one line changed and nothing else, a byte-identical restore, a refusal that wrote nothing, the state sidecar's mode at birth, the sink's header and its removal rule, and the socket's peer gate. The helper's own `--serve` is driven the way the unit does (the connected socket on stdin), and the structural pins (no `shell=True`, no path from the wire, the one `systemctl` verb, the Python 3.10 floor) are greps over the file, the `auth.py`/`certs.py` precedent. """ from __future__ import annotations import argparse import ast import hashlib import importlib.util import json import os import pwd import re import socket import stat import subprocess import sys import types from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent DEPLOY = ROOT / "deploy" FIXTURES = ROOT / "tests" / "fixtures" HELPER = DEPLOY / "dashboards-routectl.py" AGENT = ROOT / "agent" / "host_agent.py" #: The captured gateway tree, the same mapping `tests/conftest.py` lays out — the #: helper's `--root` reads the file the real ingress runs. GATEWAY_TREE = { "etc/haproxy/haproxy.cfg": "gateway_haproxy.cfg", "etc/nginx/nginx.conf": "gateway_nginx_main.conf", "etc/nginx/conf.d/http.conf": "gateway_nginx_conf_d_http.conf", "etc/nginx/stream.d/stream.conf": "gateway_nginx_stream_d_stream.conf", } #: The ids the dashboard publishes for these lines (phase 30's vocabulary), and the #: two refusals that are not ids at all. HAPROXY_ROUTE = "haproxy:5201->web.reeselink.com:5201" # the iperf3 server line VHOST_ROUTE = "nginx:4443->nextcloud.reeselink.com:11000" # the 4443 vhost's proxy_pass MAP_ROUTE = "nginx:443->reeseapps-brain-of-reese.reeselink.com:443" # brain.reeseapps.com MAP_SINK_ID = "nginx:443->127.0.0.1:44450" # while off, the map entry is addressed by its sink value IMMICH_ROUTE = "nginx:443->reeseapps-immich.reeselink.com:443" MAP_DEFAULT = "nginx:443->127.0.0.1:4443" # the map's default: a fallback, not a service SINK_HEADER = "# managed by dashboards-routectl — do not edit" IMMICH_ORIGINAL = " immich.reeseapps.com reeseapps-immich.reeselink.com:443;" def _tree(tmp_path: Path) -> Path: """The captured configs, laid into a fake `--root` tree byte for byte.""" root = tmp_path / "root" for relative, fixture in GATEWAY_TREE.items(): target = root / relative target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes((FIXTURES / fixture).read_bytes()) return root def _stubs(tmp_path: Path) -> tuple[Path, Path]: """Fake `nginx` / `haproxy` / `systemctl` on `PATH`: they record their argv in `$FAKE_CALLS_LOG` and fail on order — `FAKE_CHECK_FAIL` makes the checker exit 1 with its own first line, `FAKE_RELOAD_FAIL` makes `reload` fail and `status` print the unit's line.""" stubs = tmp_path / "stubs" stubs.mkdir() log = tmp_path / "calls.log" for name in ("nginx", "haproxy", "systemctl"): script = stubs / name script.write_text( "#!/bin/sh\n" f'printf "%s %s\\n" "{name}" "$*" >> "$FAKE_CALLS_LOG"\n' "case \"$1\" in\n" " -t|-c)\n" " if [ -n \"$FAKE_CHECK_FAIL\" ]; then\n" f' echo "fake {name}: configuration file test failed"\n' " exit 1\n" " fi\n" " ;;\n" " reload)\n" " if [ -n \"$FAKE_RELOAD_FAIL\" ]; then exit 1; fi\n" " ;;\n" " status)\n" ' if [ -n "$FAKE_RELOAD_FAIL" ]; then echo "fake unit: reload failed"; fi\n' " ;;\n" "esac\n" "exit 0\n", encoding="utf-8") script.chmod(0o755) return stubs, log def _run(tmp_path: Path, stubs: Path, log: Path, args: list[str], *, check_fail: bool = False, reload_fail: bool = False, agent_module: str | None = None ) -> tuple[subprocess.CompletedProcess[str], dict, list[str]]: """One helper invocation the way the operator runs it by hand: the real binary, the fake tree, the fake proxy commands. Returns the process, the one JSON answer line, and the recorded calls so far (the log is appended across invocations).""" argv = [sys.executable, str(HELPER), "--root", str(tmp_path / "root"), "--agent-module", str(AGENT) if agent_module is None else agent_module, "--state", str(tmp_path / "state.json"), "--sink-conf", "/etc/nginx/stream.d/00-dashboards-sink.conf", "--sink-address", "127.0.0.1:44450", "--nginx-unit", "nginx", "--haproxy-unit", "haproxy", *args] env = { "PATH": f"{stubs}{os.pathsep}{os.environ.get('PATH', '')}", "FAKE_CALLS_LOG": str(log), "FAKE_CHECK_FAIL": "1" if check_fail else "", "FAKE_RELOAD_FAIL": "1" if reload_fail else "", "HOME": str(tmp_path), } proc = subprocess.run(argv, capture_output=True, text=True, env=env, timeout=120) lines = [line for line in proc.stdout.splitlines() if line.strip()] answer: dict = json.loads(lines[-1]) if lines else {} calls = log.read_text().splitlines() if log.exists() else [] return proc, answer, calls def _serve(tmp_path: Path, tree: Path, allow_uid: str, request: bytes) -> tuple[int, str]: """`--serve` the way the unit does: the connected socket on fd 0/fd 1, one request, one answer line. (The `tests/test_deploy_scripts.py` transport pin, re-used for the wire-body gates the overview demands in this file.)""" child, parent = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM) proc = subprocess.Popen( [sys.executable, str(HELPER), "--serve", "--allow-uid", allow_uid, "--agent-module", str(AGENT), "--root", str(tree), "--state", str(tmp_path / "serve-state.json"), "--sink-conf", "/etc/nginx/stream.d/00-dashboards-sink.conf", "--sink-address", "127.0.0.1:44450", "--nginx-unit", "nginx", "--haproxy-unit", "haproxy"], stdin=child.fileno(), stdout=child.fileno(), stderr=subprocess.PIPE, close_fds=True) parent.sendall(request) answer = parent.recv(65536).decode("utf-8").strip() parent.close() err = proc.stderr.read().decode() if proc.stderr is not None else "" proc.wait(timeout=60) child.close() if proc.returncode not in (0, 1): raise AssertionError( f"the helper exited {proc.returncode} instead of answering: {answer!r} {err}") return proc.returncode, answer def _load(name: str, path: Path) -> types.ModuleType: """Import a stdlib module by file path, the way the helper imports the agent.""" spec = importlib.util.spec_from_file_location(name, path) assert spec is not None and spec.loader is not None, f"no loader for {path}" module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def _snapshot(tree: Path) -> dict[Path, bytes]: return {path: path.read_bytes() for path in sorted(tree.rglob("*")) if path.is_file()} def _sink(tree: Path) -> Path: return tree / "etc/nginx/stream.d/00-dashboards-sink.conf" @pytest.fixture def tree(tmp_path: Path) -> Path: return _tree(tmp_path) @pytest.fixture def stubs(tmp_path: Path) -> tuple[Path, Path]: return _stubs(tmp_path) def test_disabling_one_route_changes_exactly_one_line(tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """The whole write, against the fakes: one hunk, one line, every other byte identical — asserted on the bytes, not on a count. The reload is the one verb the file executes, and the state sidecar names the write.""" stubs_dir, log = stubs hap = tree / "etc/haproxy/haproxy.cfg" before_files = _snapshot(tree) before = hap.read_bytes() proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", HAPROXY_ROUTE, "--apply"]) assert proc.returncode == 0, proc.stderr assert answer == {"ok": True, "applied": True, "validated": True, "reloaded": True, "status": "", "state": "off"}, answer after = hap.read_bytes() b, a = before.decode("utf-8").splitlines(), after.decode("utf-8").splitlines() assert len(b) == len(a), "the edit is in place: one line, never an add or a remove" changed = [i for i in range(len(b)) if b[i] != a[i]] assert len(changed) == 1, f"exactly one line changed byte for byte, nothing else: {changed}" index = changed[0] assert b[index] == " server iperf3 web.reeselink.com:5201 check inter 5s" assert a[index] == b[index] + " #off iperf3", ( "the marker is the whole trailing comment; the original stands verbatim before it") for path, blob in before_files.items(): if path != hap: assert path.read_bytes() == blob, f"{path} moved when one haproxy line was marked" assert "systemctl reload haproxy" in calls, "the one verb, for the one unit" assert "systemctl reload nginx" not in calls assert not [c for c in calls if "status" in c], ( f"a successful reload never asks the unit for its status: {calls}") state = json.loads((tmp_path / "state.json").read_text(encoding="utf-8")) assert set(state) == {HAPROXY_ROUTE} row = state[HAPROXY_ROUTE] assert row["action"] == "disable" assert row["file"] == "/etc/haproxy/haproxy.cfg" assert row["written"] == b[index] + " #off iperf3" assert row["original"] == b[index] assert row["sha256"] == hashlib.sha256(after).hexdigest() def test_the_vhost_arm_comments_the_proxy_pass_line_and_restores( tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """The second arm: the vhost's `proxy_pass` is commented the same way, the sibling vhost on another port is untouched, and the re-enable is byte-identical.""" stubs_dir, log = stubs http = tree / "etc/nginx/conf.d/http.conf" before = http.read_bytes() proc, answer, _calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", VHOST_ROUTE, "--apply"]) assert proc.returncode == 0, proc.stderr assert answer["state"] == "off" lines = http.read_text().splitlines() assert " proxy_pass http://nextcloud.reeselink.com:11000; #off nextcloud-11000" in lines assert " proxy_pass http://nextcloud.reeselink.com:11001;" in lines, ( "the 8443 vhost's own line is a different fact; it does not move") proc, answer, calls = _run(tmp_path, stubs_dir, log, ["enable", "--id", VHOST_ROUTE, "--apply"]) assert proc.returncode == 0, proc.stderr assert answer["state"] == "on" assert http.read_bytes() == before, "the re-enable restores the file byte for byte" assert "systemctl reload nginx" in calls assert "systemctl reload haproxy" not in calls def test_a_disabled_map_entry_points_at_the_sink_and_keeps_its_original( tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """The third arm: the map entry is **rewritten to the sink** (commenting it would fall through to the map's `default` and mis-route the domain), the sink file is created on first need with its header, and the re-enable — addressed by the sink value the entry carries while off — restores the file byte-identical and removes the sink the helper wrote. A `dry_run` writes nothing at all, not even the sink.""" stubs_dir, log = stubs stream = tree / "etc/nginx/stream.d/stream.conf" sink = _sink(tree) original_line = " brain.reeseapps.com reeseapps-brain-of-reese.reeselink.com:443;" before = stream.read_bytes() # the dry run: the unified diff and nothing else — no file, no checker, no reload proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", MAP_ROUTE]) assert proc.returncode == 0, proc.stderr assert answer["ok"] is True and answer["applied"] is False assert answer["changed"] is True and answer["dry_run"] is True assert f"-{original_line}" in answer["diff"] assert "+ brain.reeseapps.com 127.0.0.1:44450; #off brain.reeseapps.com" in answer["diff"] assert stream.read_bytes() == before, "a dry run writes nothing" assert not sink.exists(), "a dry run writes no file at all (not even the sink's)" assert calls == [], f"a dry run neither validates nor reloads: {calls}" assert not (tmp_path / "state.json").exists() proc, answer, _calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", MAP_ROUTE, "--apply"]) assert proc.returncode == 0, proc.stderr assert answer["state"] == "off" assert sink.is_file(), "the sink is created on first need, before the checker runs" sink_text = sink.read_text(encoding="utf-8") assert sink_text.splitlines()[0] == SINK_HEADER assert "listen 127.0.0.1:44450;" in sink_text assert "proxy_pass" not in sink_text, "the block, not a vhost, is what the page's `sink_endpoint` recognises" lines = stream.read_text().splitlines() assert " brain.reeseapps.com 127.0.0.1:44450; #off brain.reeseapps.com" in lines assert original_line not in lines # the enable is addressed by the sink value the entry carries while off proc, answer, calls = _run(tmp_path, stubs_dir, log, ["enable", "--id", MAP_SINK_ID, "--apply"]) assert proc.returncode == 0, proc.stderr assert answer["state"] == "on" assert stream.read_bytes() == before, "the re-enable restores the file byte-identical to the pre-drill capture" assert not sink.exists(), "the helper removes the sink it wrote once nothing points at it" assert json.loads((tmp_path / "state.json").read_text(encoding="utf-8")) == {} assert "systemctl reload nginx" in calls def test_the_map_default_is_refused_and_nothing_is_written( tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """The map's `default` is the fallback for every unmapped SNI, not a service: the id that addresses it is refused at the wire, before a byte moves or a checker runs.""" stubs_dir, log = stubs before = _snapshot(tree) proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", MAP_DEFAULT, "--apply"]) assert proc.returncode == 1 assert answer == {"ok": False, "applied": False, "refused": "map-default"} assert _snapshot(tree) == before, "a refused write writes nothing" assert calls == [], f"a wire-level refusal reaches neither the checker nor the unit: {calls}" def test_an_unknown_id_is_refused(tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: stubs_dir, log = stubs before = _snapshot(tree) proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", "haproxy:9999->10.0.0.9:9999", "--apply"]) assert proc.returncode == 1 assert answer == {"ok": False, "detail": "not-found"} assert _snapshot(tree) == before assert calls == [] def test_disabling_a_disabled_route_changes_nothing_and_reloads_nothing( tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """Step 2: a route that is already off is an answer, not a write — no file, no checker, no reload. The fake `systemctl` log stays empty on the second call.""" stubs_dir, log = stubs hap = tree / "etc/haproxy/haproxy.cfg" _proc, _answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", HAPROXY_ROUTE, "--apply"]) after_first = hap.read_bytes() seen = len(calls) proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", HAPROXY_ROUTE, "--apply"]) assert proc.returncode == 0, proc.stderr assert answer == {"ok": True, "applied": False, "changed": False, "id": HAPROXY_ROUTE, "state": "off"} assert hap.read_bytes() == after_first assert len(calls) == seen, f"an already-off route changes nothing and reloads nothing: {calls[seen:]}" def test_a_failing_validator_restores_the_previous_bytes(tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """Step 5: the checker says no, the previous bytes go back byte-identical, nothing reloads, and no state is recorded — the answer carries the checker's own first line and the non-zero exit says it out.""" stubs_dir, log = stubs hap = tree / "etc/haproxy/haproxy.cfg" before = hap.read_bytes() proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", HAPROXY_ROUTE, "--apply"], check_fail=True) assert proc.returncode == 1 assert answer["ok"] is False and answer["applied"] is False assert answer["validated"] is False and answer["restored"] is True assert answer["detail"] == "fake haproxy: configuration file test failed" assert hap.read_bytes() == before, "the restore is byte-identical" assert [c for c in calls if c.startswith("haproxy -c")] == [ f"haproxy -c -f {tmp_path / 'root' / 'etc/haproxy/haproxy.cfg'}"], calls assert not [c for c in calls if c.startswith("systemctl")], ( f"a failed validator reloads nothing: {calls}") assert not (tmp_path / "state.json").exists(), "a failed write records no state" def test_a_failing_reload_says_so_and_leaves_the_file_applied( tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """Step 6: the config is correct and the reload is not — the file stays applied, the answer says so with the unit's own status line, and the state is still recorded (the write happened; the reload is the operator's next action).""" stubs_dir, log = stubs hap = tree / "etc/haproxy/haproxy.cfg" proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", HAPROXY_ROUTE, "--apply"], reload_fail=True) assert proc.returncode == 0, "a reload failure is an answered call, not a crash" assert answer["ok"] is True and answer["applied"] is True assert answer["validated"] is True and answer["reloaded"] is False assert answer["status"] == "fake unit: reload failed" assert " #off iperf3" in hap.read_text(), "the applied file stays applied" assert "systemctl reload haproxy" in calls assert "systemctl status haproxy" in calls, "the answer carries the unit's own line" state = json.loads((tmp_path / "state.json").read_text(encoding="utf-8")) assert HAPROXY_ROUTE in state, "the write happened; the state says so" def test_reload_is_never_a_restart() -> None: """The only verb this file executes is `systemctl reload `: a restart severs every live connection through the fleet's only ingress. Pinned at the call position (the docstring's negation is backticked, never an argv).""" src = HELPER.read_text(encoding="utf-8") verbs = re.findall(r'\["systemctl", "(\w+)"', src) assert verbs == ["reload", "status"], f"the only systemctl verbs: {verbs}" assert not re.search(r'["\']restart["\']', src), ( "no quoted `restart` anywhere: the docstring refuses it in words, the argv never carries it") def test_the_helper_imports_the_agents_parser_and_refuses_without_it( tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """The parser is the agent's, imported by file path: a helper that cannot import it refuses to run at all, names the path, and writes nothing — two parsers reading one config are two opinions about the same file.""" stubs_dir, log = stubs before = _snapshot(tree) missing = tmp_path / "no-such-agent.py" proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", HAPROXY_ROUTE, "--apply"], agent_module=str(missing)) assert proc.returncode == 1 assert answer == {"ok": False, "detail": f"cannot import the agent module {missing}: not a file"} assert _snapshot(tree) == before assert calls == [] def test_state_is_0600_and_drift_is_refused(tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """The sidecar's mode is its birth mode, and `enable` restores only what this helper wrote: a hand-edited line is `drift` — a refusal with no byte change, never a guess. The other arm is a line this helper never wrote (no state row).""" stubs_dir, log = stubs hap = tree / "etc/haproxy/haproxy.cfg" _proc, _answer, _calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", HAPROXY_ROUTE, "--apply"]) state_path = tmp_path / "state.json" assert stat.S_IMODE(state_path.stat().st_mode) == 0o600, "the mode is the birth mode" off_line = " server iperf3 web.reeselink.com:5201 check inter 5s #off iperf3" assert off_line in hap.read_text(encoding="utf-8") edited = hap.read_text(encoding="utf-8").replace( off_line, " server iperf3 web.reeselink.com:5201 check inter 5s backup") hap.write_text(edited, encoding="utf-8") seen = len(_calls) proc, answer, calls = _run(tmp_path, stubs_dir, log, ["enable", "--id", HAPROXY_ROUTE, "--apply"]) assert proc.returncode == 1 assert answer["ok"] is False and answer["applied"] is False and answer["drift"] is True assert answer["detail"] == "the line was edited after the helper wrote it" assert hap.read_text(encoding="utf-8") == edited, "drift is refused, not guessed at: no byte moves" assert len(calls) == seen, f"a drift refusal touches neither the checker nor the unit: {calls[seen:]}" # the other arm: a line this helper never wrote (no state row at all) (tmp_path / "state.json").unlink() vhost = tree / "etc/nginx/conf.d/http.conf" before_vhost = vhost.read_bytes() proc, answer, _calls = _run(tmp_path, stubs_dir, log, ["enable", "--id", VHOST_ROUTE, "--apply"]) assert proc.returncode == 1 assert answer["ok"] is False and answer["drift"] is True assert answer["detail"] == "no state row: this helper did not write the line" assert vhost.read_bytes() == before_vhost def test_serve_refuses_a_peer_that_is_not_the_agent_uid(tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """The second look, in the fake-peer test: a connected caller whose uid is not `--allow-uid` is refused before the body is read — the gate never opens for a caller it was not given.""" if hasattr(os, "geteuid") and os.geteuid() == 0: pytest.skip("running as root: the foreign-uid arm needs a foreign uid") code, answer = _serve(tmp_path, tree, "root", (json.dumps({"id": HAPROXY_ROUTE, "action": "show"}) + "\n").encode()) assert code == 1 body = json.loads(answer) assert body["ok"] is False # the refusal names the caller's own uid and the user it was not: the gate # opened for no one, and the dialer gets the reason, not a timeout assert body["detail"] == f"peer: uid {os.geteuid()} is not root", ( f"the refusal names the uid and the user: {body}") def test_serve_accepts_exactly_id_action_dry_run(tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """The wire's gate, from the accepted side: a named peer's `show` is answered, a wider body is refused with the wire's own sentence, and a `dry_run` write answers the diff without writing anything (not even the sink's file).""" if hasattr(os, "geteuid") and os.geteuid() == 0: pytest.skip("running as root: the accept arm needs the caller to be the allowed uid") me = pwd.getpwuid(os.geteuid()).pw_name code, answer = _serve(tmp_path, tree, me, (json.dumps({"id": HAPROXY_ROUTE, "action": "show"}) + "\n").encode()) assert code == 0 body = json.loads(answer) assert body == {"ok": True, "id": HAPROXY_ROUTE, "state": "on", "file": "/etc/haproxy/haproxy.cfg"}, body code, answer = _serve(tmp_path, tree, me, (json.dumps({"id": HAPROXY_ROUTE, "action": "show", "command": "reboot"}) + "\n").encode()) assert code == 1 body = json.loads(answer) assert body == {"ok": False, "detail": "body: exactly {id, action, dry_run}"} hap = tree / "etc/haproxy/haproxy.cfg" before = hap.read_bytes() code, answer = _serve(tmp_path, tree, me, (json.dumps({"id": HAPROXY_ROUTE, "action": "disable", "dry_run": True}) + "\n").encode()) assert code == 0 body = json.loads(answer) assert body["ok"] is True and body["applied"] is False and body["dry_run"] is True assert "diff" in body assert hap.read_bytes() == before, "a dry run over the wire writes nothing" assert not _sink(tree).exists() def test_no_shell_and_no_path_from_the_wire() -> None: """The structural pins (the `auth.py`/`certs.py` precedent): every subprocess call is a fixed argv list — no `shell=True` — and the wire takes an id, never a path, a line number or a pattern; `--serve` accepts exactly `{id, action, dry_run}` and every other key is a refusal.""" src = HELPER.read_text(encoding="utf-8") assert not re.search(r"subprocess\.\w+\([^)]*shell\s*=", src, re.S), ( "no `shell=` at a call position (the docstring's negation is backticked; " "every call is a fixed argv list — `shell=True` included)") flags = re.findall(r'add_argument\(\s*"([^"]+)"', src) assert flags, "the flags must be there for the pin to mean anything" assert not {flag for flag in flags if flag in ("--file", "--line", "--pattern")}, ( f"the wire takes an id, never a path/line/pattern; the flags are {flags}") assert 'set(body) - {"id", "action", "dry_run"}' in src, ( "the wire's gate: anything wider than {id, action, dry_run} is a refusal") assert '"body: exactly {id, action, dry_run}"' in src def test_the_sink_file_is_left_alone_when_someone_elses_wrote_it( tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """An operator's file at the sink's path is not the helper's to overwrite: the `map` disable refuses before the config write, and the file is untouched.""" stubs_dir, log = stubs sink = _sink(tree) operator_file = "# an operator's file\nserver {\n listen 127.0.0.1:44451;\n}\n" sink.write_text(operator_file, encoding="utf-8") before = _snapshot(tree) proc, answer, calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", MAP_ROUTE, "--apply"]) assert proc.returncode == 1 assert answer == {"ok": False, "detail": "sink-file-not-ours: /etc/nginx/stream.d/00-dashboards-sink.conf"} assert _snapshot(tree) == before, "the refusal writes nothing (the operator's file is byte-identical)" assert calls == [], f"the refusal happens before the checker and the unit: {calls}" def test_the_sink_is_removed_only_when_it_is_empty_of_our_routes( tree: Path, stubs: tuple[Path, Path], tmp_path: Path) -> None: """Two routes off: two lines carry the sink value, and enabling by that id is `ambiguous` — two facts, never a guess, because no line number crosses the wire. After the operator restores one line by hand, the enable succeeds and the sink goes with it. The remains-arm is pinned on the helper's own function: a map entry still pointing at the sink keeps the file (the wire cannot reach that arm — the ambiguity above is why).""" stubs_dir, log = stubs stream = tree / "etc/nginx/stream.d/stream.conf" sink = _sink(tree) _proc, _answer, _calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", MAP_ROUTE, "--apply"]) _proc, _answer, _calls = _run(tmp_path, stubs_dir, log, ["disable", "--id", IMMICH_ROUTE, "--apply"]) assert sink.is_file() proc, answer, _calls = _run(tmp_path, stubs_dir, log, ["enable", "--id", MAP_SINK_ID, "--apply"]) assert proc.returncode == 1 assert answer["ok"] is False assert "ambiguous" in answer["detail"], f"two lines at the sink are two facts: {answer}" text = stream.read_text(encoding="utf-8") text = text.replace(" immich.reeseapps.com 127.0.0.1:44450; #off immich.reeseapps.com", IMMICH_ORIGINAL) stream.write_text(text, encoding="utf-8") proc, answer, _calls = _run(tmp_path, stubs_dir, log, ["enable", "--id", MAP_SINK_ID, "--apply"]) assert proc.returncode == 0, proc.stderr assert answer["state"] == "on" assert not sink.exists(), "the helper removes the sink it wrote once no route still points at it" # the remains-arm, on the helper's own function: a route still points at the sink helper = _load("routectl_under_test", HELPER) agent = _load("agent_under_test", AGENT) ctx = argparse.Namespace(root=str(tree), haproxy_config="/etc/haproxy/haproxy.cfg", nginx_config="/etc/nginx/nginx.conf", state_file=str(tmp_path / "sink-state.json"), sink_conf="/etc/nginx/stream.d/00-dashboards-sink.conf", sink_address="127.0.0.1:44450", nginx_unit="nginx", haproxy_unit="haproxy", actor="manual", allow_uid="nobody") write = helper.Write(ctx, agent, "nginx", 443, "127.0.0.1", 44450, MAP_SINK_ID) sink.write_text(f"{SINK_HEADER}\nserver {{\n listen 127.0.0.1:44450;\n}}\n", encoding="utf-8") documents = {"/etc/nginx/stream.d/stream.conf": "map $ssl_preread_server_name $targetBackend {\n" " default 127.0.0.1:4443;\n" " a.reeseapps.com 127.0.0.1:44450;\n" "}\n"} write.remove_sink_if_empty(documents) assert sink.is_file(), "a route still points at the sink: it stays" def test_the_helper_parses_on_python_3_10() -> None: """The helper is installed beside the agent and must run on a host's Python 3.10: the floor is in the header, and the file carries no 3.11+ syntax.""" src = HELPER.read_text(encoding="utf-8") assert "# requires: python >=3.10" in src ast.parse(src, feature_version=(3, 10))