181 lines
7.7 KiB
Python
181 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import stat
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from .fixtures import isolated_environment
|
|
from .nono import NonoAdapter
|
|
|
|
|
|
class NonoAdapterTests(unittest.TestCase):
|
|
def _environment(self, root: Path, *, home: Path | None = None) -> dict[str, str]:
|
|
return {
|
|
"HOME": str(home or root),
|
|
"XDG_CONFIG_HOME": str(root),
|
|
"XDG_STATE_HOME": str(root),
|
|
"XDG_CACHE_HOME": str(root),
|
|
"XDG_RUNTIME_DIR": str(root),
|
|
"TMPDIR": str(root),
|
|
"PATH": "/usr/bin:/bin",
|
|
}
|
|
|
|
def _fake_nono(self, directory: Path, log: Path) -> Path:
|
|
executable = directory / "fake-nono"
|
|
executable.write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
#!/usr/bin/env python3
|
|
import pathlib, sys, time
|
|
pathlib.Path({str(log)!r}).write_text('\\n'.join(sys.argv[1:]), encoding='utf-8')
|
|
if '--version' in sys.argv:
|
|
print('nono 0.74.0')
|
|
if 'slow' in sys.argv:
|
|
print('x' * (2 * 1024 * 1024), flush=True)
|
|
print('y' * (2 * 1024 * 1024), file=sys.stderr, flush=True)
|
|
time.sleep(2)
|
|
print('stdout:' + ('x' * 500))
|
|
print('stderr:' + ('y' * 500), file=sys.stderr)
|
|
"""
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
executable.chmod(0o700)
|
|
return executable
|
|
|
|
def test_validate_show_and_run_are_exact_argv_and_no_active_namespace(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
log = root / "argv.log"
|
|
executable = self._fake_nono(root, log)
|
|
profile = root / "temporary.json"
|
|
profile.write_text("{}", encoding="utf-8")
|
|
adapter = NonoAdapter(
|
|
executable,
|
|
cwd=root,
|
|
environment=self._environment(root),
|
|
)
|
|
|
|
adapter.validate_temporary_profile(profile)
|
|
validation_argv = log.read_text(encoding="utf-8").splitlines()
|
|
self.assertEqual(validation_argv[:3], ["profile", "validate", "--strict"])
|
|
self.assertEqual(Path(validation_argv[3]).name, "profile.json")
|
|
self.assertNotEqual(validation_argv[3], str(profile))
|
|
adapter.show_temporary_profile(profile)
|
|
show_argv = log.read_text(encoding="utf-8").splitlines()
|
|
self.assertEqual(show_argv[:2], ["profile", "show"])
|
|
self.assertEqual(Path(show_argv[2]).name, "profile.json")
|
|
self.assertEqual(show_argv[3:], ["--json"])
|
|
adapter.run_temporary_profile(profile, ["/usr/bin/true", "--fixed"])
|
|
run_argv = log.read_text(encoding="utf-8").splitlines()
|
|
self.assertEqual(run_argv[:2], ["run", "--profile"])
|
|
self.assertEqual(Path(run_argv[2]).name, "profile.json")
|
|
self.assertEqual(run_argv[3:], ["--", "/usr/bin/true", "--fixed"])
|
|
adapter.run_temporary_profile(
|
|
profile,
|
|
["/usr/bin/true"],
|
|
allow_cwd=True,
|
|
)
|
|
cwd_argv = log.read_text(encoding="utf-8").splitlines()
|
|
self.assertEqual(cwd_argv[:2], ["run", "--profile"])
|
|
self.assertEqual(Path(cwd_argv[2]).name, "profile.json")
|
|
self.assertEqual(cwd_argv[3:], ["--allow-cwd", "--", "/usr/bin/true"])
|
|
|
|
active = root / "active" / ".config" / "nono" / "profiles" / "name.json"
|
|
# An explicitly injected active namespace is rejected by the same
|
|
# path guard used for HOME's conventional namespace.
|
|
active.parent.mkdir(parents=True)
|
|
os.chmod(root / "active", 0o700)
|
|
adapter_with_home = NonoAdapter(
|
|
executable,
|
|
cwd=root,
|
|
environment=self._environment(root, home=root / "active"),
|
|
)
|
|
active.write_text("{}", encoding="utf-8")
|
|
with self.assertRaises(ValueError):
|
|
adapter_with_home.validate(active)
|
|
hardlink = root / "active-hardlink.json"
|
|
os.link(active, hardlink)
|
|
with self.assertRaisesRegex(ValueError, "cannot alias"):
|
|
adapter_with_home.validate(hardlink)
|
|
|
|
def test_result_output_is_bounded_and_timeout_is_recorded(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
executable = self._fake_nono(root, root / "argv.log")
|
|
profile = root / "temporary.json"
|
|
profile.write_text("{}", encoding="utf-8")
|
|
adapter = NonoAdapter(
|
|
executable,
|
|
cwd=root,
|
|
timeout=0.1,
|
|
max_output=32,
|
|
environment=self._environment(root),
|
|
)
|
|
result = adapter.run(profile, ["slow"])
|
|
self.assertTrue(result.timed_out)
|
|
self.assertLessEqual(len(result.stdout), 32)
|
|
self.assertLessEqual(len(result.stderr), 32)
|
|
|
|
def test_commands_are_argv_only(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
executable = self._fake_nono(root, root / "argv.log")
|
|
profile = root / "temporary.json"
|
|
profile.write_text("{}", encoding="utf-8")
|
|
adapter = NonoAdapter(
|
|
executable,
|
|
cwd=root,
|
|
environment=self._environment(root),
|
|
)
|
|
with self.assertRaises(TypeError):
|
|
adapter.run(profile, "echo unsafe") # type: ignore[arg-type]
|
|
|
|
def test_environment_is_required_and_symlinked_profiles_are_rejected(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
executable = self._fake_nono(root, root / "argv.log")
|
|
with self.assertRaisesRegex(ValueError, "explicit isolated environment"):
|
|
NonoAdapter(executable, cwd=root)
|
|
for unsafe_name in ("GITHUB_TOKEN", "AWS_ACCESS_KEY_ID", "LD_PRELOAD"):
|
|
with self.subTest(environment=unsafe_name):
|
|
unsafe_environment = self._environment(root)
|
|
unsafe_environment[unsafe_name] = "must-not-pass"
|
|
with self.assertRaisesRegex(ValueError, "allowlist"):
|
|
NonoAdapter(
|
|
executable,
|
|
cwd=root,
|
|
environment=unsafe_environment,
|
|
)
|
|
profile = root / "profile.json"
|
|
profile.write_text("{}", encoding="utf-8")
|
|
linked = root / "linked.json"
|
|
linked.symlink_to(profile)
|
|
adapter = NonoAdapter(
|
|
executable,
|
|
cwd=root,
|
|
environment=self._environment(root),
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "symlinked"):
|
|
adapter.validate(linked)
|
|
fifo = root / "fifo-profile.json"
|
|
os.mkfifo(fifo, mode=0o600)
|
|
with self.assertRaisesRegex(ValueError, "regular file"):
|
|
adapter.validate(fifo)
|
|
|
|
|
|
class FixtureProfileValidationTests(unittest.TestCase):
|
|
def test_fixture_directories_are_not_install_targets(self) -> None:
|
|
with isolated_environment() as fixture:
|
|
profile = Path(__file__).parents[1] / "fixtures" / "core" / "minimal.json"
|
|
self.assertNotIn(str(profile), fixture.env.values())
|
|
self.assertTrue(profile.is_file())
|
|
self.assertNotEqual(stat.S_IMODE(profile.stat().st_mode), 0o600)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|