from __future__ import annotations import subprocess import unittest from unittest import mock import sys from pathlib import Path SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" if str(SCRIPTS) not in sys.path: sys.path.insert(0, str(SCRIPTS)) import remote_exec as module class RemoteExecTests(unittest.TestCase): def test_shell_hostile_argv_is_one_quoted_remote_command(self) -> None: template = 'go-template={{range $k, $_ := .data}}{{$k}}{{"\\n"}}{{end}}' completed = subprocess.CompletedProcess([], 0, stdout="token-a\ntoken-b\n", stderr="") runner = mock.Mock(return_value=completed) module.run_remote( "railiance01", ["kubectl", "get", "secret", "example", "-o", template, "literal;$(false)", "a'b"], label="hostile argv", runner=runner, ) command = runner.call_args.args[0] self.assertEqual(["ssh", "-o", "BatchMode=yes", "railiance01"], command[:4]) self.assertIn("'go-template={{range $k, $_ := .data}}", command[4]) self.assertIn("'literal;$(false)'", command[4]) self.assertIn("'a'\"'\"'b'", command[4]) def test_invalid_host_and_nul_fail_closed(self) -> None: with self.assertRaises(module.RemoteExecutionError): module.run_remote("bad host", ["true"], label="invalid") with self.assertRaises(module.RemoteExecutionError): module.remote_command(["bad\0value"]) def test_error_does_not_include_remote_output(self) -> None: runner = mock.Mock( return_value=subprocess.CompletedProcess([], 9, stdout="sensitive", stderr="also-sensitive") ) with self.assertRaisesRegex(module.RemoteExecutionError, "exit 9") as caught: module.run_remote("railiance01", ["false"], label="safe failure", runner=runner) self.assertNotIn("sensitive", str(caught.exception)) if __name__ == "__main__": unittest.main()