from __future__ import annotations import subprocess import tempfile import unittest from pathlib import Path from vpn_egressctl.errors import CommandError from vpn_egressctl.guard import apply_guard from tests.helpers import make_policy class GuardRunner: def __init__(self, *, missing_interface: bool = False, batch_result: int = 0) -> None: self.missing_interface = missing_interface self.batch_result = batch_result self.calls: list[tuple[list[str], str | None]] = [] def __call__(self, args: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: input_text = kwargs.get("input") self.calls.append((list(args), input_text if isinstance(input_text, str) else None)) if "link" in args and self.missing_interface: return subprocess.CompletedProcess(args, 1, "", "") if args[-2:] == ["-f", "-"]: return subprocess.CompletedProcess(args, self.batch_result, "", "") return subprocess.CompletedProcess(args, 0, "", "") class GuardTests(unittest.TestCase): def test_atomic_ruleset_uses_policy_interfaces(self) -> None: with tempfile.TemporaryDirectory() as directory: policy = make_policy(Path(directory)) runner = GuardRunner() apply_guard(policy, runner) batch = [item for args, item in runner.calls if args[-2:] == ["-f", "-"]][0] self.assertIsNotNone(batch) self.assertIn("delete table inet vpn_egress_guard", batch) self.assertIn('iifname "eth1" oifname "eth0"', batch) self.assertIn("counter reject", batch) def test_missing_interface_is_non_mutating(self) -> None: with tempfile.TemporaryDirectory() as directory: runner = GuardRunner(missing_interface=True) with self.assertRaises(CommandError): apply_guard(make_policy(Path(directory)), runner) self.assertFalse(any(args[-2:] == ["-f", "-"] for args, _ in runner.calls)) def test_batch_failure_is_reported(self) -> None: with tempfile.TemporaryDirectory() as directory: with self.assertRaises(CommandError): apply_guard(make_policy(Path(directory)), GuardRunner(batch_result=1)) if __name__ == "__main__": unittest.main()