Skip to content

Commit 336d1c1

Browse files
author
Nils Bars
committed
Add unit tests for ref-utils
Test coverage for: - assertion.py: file/dir existence checks - checks.py: pylint, mypy, flag checking - config.py: configuration and override_config context manager - decorator.py: test registration and run_tests() - error.py: custom exception types - process.py: privilege dropping and subprocess execution - serialization.py: JSON-based IPC serialization - utils.py: colored printing and environment handling Includes security tests verifying JSON serializer is safe against pickle-based exploits (DoS, attribute injection, etc).
1 parent 561d858 commit 336d1c1

11 files changed

Lines changed: 2730 additions & 0 deletions

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for ref-utils library."""

tests/conftest.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Pytest configuration and fixtures for ref-utils tests."""
2+
import sys
3+
from pathlib import Path
4+
from typing import Generator
5+
from unittest.mock import patch
6+
7+
import pytest
8+
9+
from ref_utils.config import Config, override_config
10+
11+
12+
@pytest.fixture
13+
def test_config(tmp_path: Path) -> Config:
14+
"""Provide a Config with all paths pointing to tmp_path."""
15+
return Config(
16+
user_environ_path=tmp_path / ".user_environ",
17+
pylint_config_path=tmp_path / "pylintrc",
18+
mypy_config_path=tmp_path / "mypyrc",
19+
user_home_path=tmp_path / "home",
20+
drop_uid=9999,
21+
drop_gid=9999,
22+
)
23+
24+
25+
@pytest.fixture
26+
def use_test_config(test_config: Config) -> Generator[Config, None, None]:
27+
"""Context fixture that overrides config for the test."""
28+
with override_config(test_config):
29+
yield test_config
30+
31+
32+
@pytest.fixture
33+
def user_environ_file(test_config: Config) -> Path:
34+
"""Create a mock user environment file with sample data."""
35+
content = "HOME=/home/user\x00USER=testuser\x00PATH=/usr/bin\x00"
36+
test_config.user_environ_path.write_text(content)
37+
return test_config.user_environ_path
38+
39+
40+
@pytest.fixture
41+
def disable_exception_hook() -> Generator[None, None, None]:
42+
"""Disable the global exception hook installed by ref_utils during tests."""
43+
original_hook = sys.excepthook
44+
yield
45+
sys.excepthook = original_hook
46+
47+
48+
@pytest.fixture
49+
def reset_registered_tasks() -> Generator[None, None, None]:
50+
"""Reset the global __registered_tasks dict between tests."""
51+
# Import the module to access its internal state
52+
from ref_utils import decorator
53+
54+
original_tasks = decorator.__registered_tasks.copy()
55+
decorator.__registered_tasks.clear()
56+
yield
57+
decorator.__registered_tasks.clear()
58+
decorator.__registered_tasks.update(original_tasks)
59+
60+
61+
@pytest.fixture
62+
def mock_subprocess_run():
63+
"""Mock subprocess.run for testing process functions."""
64+
with patch("subprocess.run") as mock_run:
65+
yield mock_run
66+
67+
68+
@pytest.fixture
69+
def mock_multiprocessing():
70+
"""Mock multiprocessing.Process and Pipe for testing privilege dropping."""
71+
with patch("ref_utils.process.Process") as mock_process, patch(
72+
"ref_utils.process.Pipe"
73+
) as mock_pipe:
74+
yield mock_process, mock_pipe

tests/test_assertion.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""Tests for ref_utils.assertion module."""
2+
from io import StringIO
3+
from pathlib import Path
4+
from unittest.mock import patch
5+
6+
from ref_utils.assertion import _assert, assert_is_dir, assert_is_exec, assert_is_file
7+
8+
9+
class TestAssert:
10+
"""Tests for the _assert helper function."""
11+
12+
def test_assert_true_condition(self) -> None:
13+
"""Test that _assert returns True for true conditions."""
14+
assert _assert(True, "error message", silent=False) is True
15+
16+
def test_assert_false_condition_silent(self) -> None:
17+
"""Test that _assert returns False silently when silent=True."""
18+
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
19+
result = _assert(False, "error message", silent=True)
20+
assert result is False
21+
# Should not print anything in silent mode
22+
assert mock_stdout.getvalue() == ""
23+
24+
def test_assert_false_condition_prints_error(self) -> None:
25+
"""Test that _assert prints error message when silent=False."""
26+
with patch("ref_utils.assertion.print_err") as mock_print_err:
27+
result = _assert(False, "test error", silent=False)
28+
assert result is False
29+
mock_print_err.assert_called_once()
30+
call_args = mock_print_err.call_args[0][0]
31+
assert "test error" in call_args
32+
33+
34+
class TestAssertIsExec:
35+
"""Tests for assert_is_exec function."""
36+
37+
def test_existing_executable(self, tmp_path: Path) -> None:
38+
"""Test with an existing executable file."""
39+
exec_file = tmp_path / "test_exec"
40+
exec_file.write_text("#!/bin/bash\necho hello")
41+
exec_file.chmod(0o755)
42+
43+
assert assert_is_exec(exec_file) is True
44+
45+
def test_existing_executable_as_string(self, tmp_path: Path) -> None:
46+
"""Test with path provided as string."""
47+
exec_file = tmp_path / "test_exec"
48+
exec_file.write_text("#!/bin/bash\necho hello")
49+
exec_file.chmod(0o755)
50+
51+
assert assert_is_exec(str(exec_file)) is True
52+
53+
def test_non_existing_file(self, tmp_path: Path) -> None:
54+
"""Test with a non-existing file."""
55+
non_existing = tmp_path / "does_not_exist"
56+
57+
with patch("ref_utils.assertion.print_err"):
58+
assert assert_is_exec(non_existing) is False
59+
60+
def test_non_executable_file(self, tmp_path: Path) -> None:
61+
"""Test with an existing but non-executable file."""
62+
non_exec = tmp_path / "not_exec"
63+
non_exec.write_text("some content")
64+
non_exec.chmod(0o644)
65+
66+
with patch("ref_utils.assertion.print_err"):
67+
assert assert_is_exec(non_exec) is False
68+
69+
def test_directory_not_executable(self, tmp_path: Path) -> None:
70+
"""Test that a directory is not considered an executable."""
71+
test_dir = tmp_path / "test_dir"
72+
test_dir.mkdir()
73+
74+
with patch("ref_utils.assertion.print_err"):
75+
assert assert_is_exec(test_dir) is False
76+
77+
def test_silent_mode(self, tmp_path: Path) -> None:
78+
"""Test silent mode doesn't print errors."""
79+
non_existing = tmp_path / "does_not_exist"
80+
81+
with patch("ref_utils.assertion.print_err") as mock_print:
82+
assert assert_is_exec(non_existing, silent=True) is False
83+
mock_print.assert_not_called()
84+
85+
86+
class TestAssertIsFile:
87+
"""Tests for assert_is_file function."""
88+
89+
def test_existing_file(self, tmp_path: Path) -> None:
90+
"""Test with an existing file."""
91+
test_file = tmp_path / "test_file.txt"
92+
test_file.write_text("content")
93+
94+
assert assert_is_file(test_file) is True
95+
96+
def test_existing_file_as_string(self, tmp_path: Path) -> None:
97+
"""Test with path provided as string."""
98+
test_file = tmp_path / "test_file.txt"
99+
test_file.write_text("content")
100+
101+
assert assert_is_file(str(test_file)) is True
102+
103+
def test_non_existing_file(self, tmp_path: Path) -> None:
104+
"""Test with a non-existing file."""
105+
non_existing = tmp_path / "does_not_exist"
106+
107+
with patch("ref_utils.assertion.print_err"):
108+
assert assert_is_file(non_existing) is False
109+
110+
def test_directory_is_not_file(self, tmp_path: Path) -> None:
111+
"""Test that a directory is not considered a file."""
112+
test_dir = tmp_path / "test_dir"
113+
test_dir.mkdir()
114+
115+
with patch("ref_utils.assertion.print_err"):
116+
assert assert_is_file(test_dir) is False
117+
118+
def test_silent_mode(self, tmp_path: Path) -> None:
119+
"""Test silent mode doesn't print errors."""
120+
non_existing = tmp_path / "does_not_exist"
121+
122+
with patch("ref_utils.assertion.print_err") as mock_print:
123+
assert assert_is_file(non_existing, silent=True) is False
124+
mock_print.assert_not_called()
125+
126+
127+
class TestAssertIsDir:
128+
"""Tests for assert_is_dir function."""
129+
130+
def test_existing_directory(self, tmp_path: Path) -> None:
131+
"""Test with an existing directory."""
132+
test_dir = tmp_path / "test_dir"
133+
test_dir.mkdir()
134+
135+
assert assert_is_dir(test_dir) is True
136+
137+
def test_existing_directory_as_string(self, tmp_path: Path) -> None:
138+
"""Test with path provided as string."""
139+
test_dir = tmp_path / "test_dir"
140+
test_dir.mkdir()
141+
142+
assert assert_is_dir(str(test_dir)) is True
143+
144+
def test_non_existing_directory(self, tmp_path: Path) -> None:
145+
"""Test with a non-existing directory."""
146+
non_existing = tmp_path / "does_not_exist"
147+
148+
with patch("ref_utils.assertion.print_err"):
149+
assert assert_is_dir(non_existing) is False
150+
151+
def test_file_is_not_directory(self, tmp_path: Path) -> None:
152+
"""Test that a file is not considered a directory."""
153+
test_file = tmp_path / "test_file.txt"
154+
test_file.write_text("content")
155+
156+
with patch("ref_utils.assertion.print_err"):
157+
assert assert_is_dir(test_file) is False
158+
159+
def test_silent_mode(self, tmp_path: Path) -> None:
160+
"""Test silent mode doesn't print errors."""
161+
non_existing = tmp_path / "does_not_exist"
162+
163+
with patch("ref_utils.assertion.print_err") as mock_print:
164+
assert assert_is_dir(non_existing, silent=True) is False
165+
mock_print.assert_not_called()
166+
167+
def test_tmp_path_is_directory(self, tmp_path: Path) -> None:
168+
"""Test that tmp_path fixture itself is a directory."""
169+
assert assert_is_dir(tmp_path) is True

0 commit comments

Comments
 (0)