Files
harness-tools/tests/nono/harness/test_nono.py
T

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()