More coverage
Some checks failed
CI / test (push) Failing after 1s
Lint / test (push) Failing after 1s

This commit is contained in:
Miguel Jacq 2026-05-31 17:15:22 +10:00
parent 1544dc0295
commit bf735c8328
Signed by: mig5
GPG key ID: 03906B4110AAD3B8
7 changed files with 888 additions and 64 deletions

View file

@ -285,3 +285,30 @@ def test_collect_non_system_users_skips_below_uid_min(tmp_path: Path):
users = a.collect_non_system_users()
assert [u.name for u in users] == ["alice"]
def test_parse_group_handles_empty_lines(tmp_path: Path):
from enroll.accounts import parse_group
p = tmp_path / "group"
p.write_text(
"valid:x:1000:user1\n" "\n" "another:x:1001:user2\n",
encoding="utf-8",
)
gid_to_name, name_to_gid, members = parse_group(str(p))
assert 1000 in gid_to_name
assert 1001 in gid_to_name
def test_parse_group_handles_short_lines(tmp_path: Path):
from enroll.accounts import parse_group
p = tmp_path / "group"
p.write_text(
"valid:x:1000:user1\n" "short:x:1001\n" "another:x:1002:user2\n",
encoding="utf-8",
)
gid_to_name, name_to_gid, members = parse_group(str(p))
assert 1000 in gid_to_name
assert 1001 not in gid_to_name # skipped due to short line
assert 1002 in gid_to_name

View file

@ -31,3 +31,67 @@ def test_ensure_dir_secure_ignores_chmod_failures(tmp_path: Path, monkeypatch):
# Should not raise.
_ensure_dir_secure(d)
assert d.exists() and d.is_dir()
def test_safe_component_returns_unknown_for_empty_string():
from enroll.cache import _safe_component
assert _safe_component("") == "unknown"
assert _safe_component(" ") == "unknown"
def test_safe_component_truncates_long_strings():
from enroll.cache import _safe_component
long_str = "a" * 100
result = _safe_component(long_str)
assert len(result) <= 64
def test_safe_component_replaces_special_chars():
from enroll.cache import _safe_component
result = _safe_component("hello world!")
assert result == "hello_world_"
def test_enroll_cache_dir_uses_xdg_cache_home(monkeypatch):
from enroll.cache import enroll_cache_dir
monkeypatch.setenv("XDG_CACHE_HOME", "/custom/cache")
result = enroll_cache_dir()
assert str(result) == "/custom/cache/enroll"
def test_harvest_cache_state_json_property():
from enroll.cache import HarvestCache
cache_dir = HarvestCache(dir=Path("/tmp/test"))
assert cache_dir.state_json == Path("/tmp/test/state.json")
def test_new_harvest_cache_dir_chmod_fails(tmp_path: Path, monkeypatch):
from enroll.cache import new_harvest_cache_dir
def fake_enroll_cache_dir():
return tmp_path / "enroll"
def fake_chmod(path, mode):
raise OSError("no")
monkeypatch.setattr("enroll.cache.enroll_cache_dir", fake_enroll_cache_dir)
monkeypatch.setattr(os, "chmod", fake_chmod)
# Should not raise even though chmod fails
cache = new_harvest_cache_dir(hint="test")
assert cache.dir.exists()
assert isinstance(cache.dir, Path)
def test_enroll_cache_dir_uses_default_when_xdg_not_set(monkeypatch):
from enroll.cache import enroll_cache_dir
# Remove XDG_CACHE_HOME if it exists
monkeypatch.delenv("XDG_CACHE_HOME", raising=False)
result = enroll_cache_dir()
assert str(result).endswith("/.local/cache/enroll")

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
import pytest
def test_dpkg_owner_parses_output(monkeypatch):
@ -337,3 +338,200 @@ def test_read_pkg_md5sums_parses_md5sums_file(tmp_path: Path, monkeypatch):
assert (
result["etc/nginx/sites-enabled/default"] == "1234567890abcdef1234567890abcdef"
)
def test_dpkg_owner_raises_on_command_failure(monkeypatch):
"""Test _run raises RuntimeError on non-zero exit."""
import enroll.debian as d
class P:
returncode = 1
stdout = ""
stderr = "command failed"
def fake_run(cmd, text, capture_output, check=False):
return P()
monkeypatch.setattr(d.subprocess, "run", fake_run)
with pytest.raises(RuntimeError) as exc_info:
d._run(["fake", "command"])
assert "Command failed" in str(exc_info.value)
assert "fake" in str(exc_info.value)
def test_build_dpkg_etc_index_skips_invalid_line_formats(tmp_path: Path):
"""Test that lines with less than 3 parts are skipped."""
import enroll.debian as d
info = tmp_path / "info"
info.mkdir()
# Create a .list file with invalid format (missing tab-separated fields)
(info / "foo.list").write_text(
"/etc/foo/bar\n" # This is a path, not a tab-separated line
"/etc/foo/baz\n",
encoding="utf-8",
)
# Should handle gracefully
owned, owner_map, topdir_to_pkgs, pkg_to_etc = d.build_dpkg_etc_index(str(info))
# The path lines should be processed normally
assert "/etc/foo/bar" in owned or "/etc/foo/baz" in owned
def test_build_dpkg_etc_index_handles_file_not_found(tmp_path: Path):
"""Test that FileNotFoundError is handled gracefully."""
import enroll.debian as d
info = tmp_path / "info"
info.mkdir()
# Create a .list file that references a non-existent path
(info / "foo.list").write_text(
"/nonexistent/path\n",
encoding="utf-8",
)
# Should not raise
owned, owner_map, topdir_to_pkgs, pkg_to_etc = d.build_dpkg_etc_index(str(info))
# The non-existent path should be skipped
assert "/nonexistent/path" not in owned
def test_parse_status_conffiles_skips_empty_lines(tmp_path: Path):
"""Test that empty lines in conffiles are skipped."""
import enroll.debian as d
status = tmp_path / "status"
status.write_text(
"Package: nginx\n"
"Version: 1\n"
"Conffiles:\n"
" /etc/nginx/nginx.conf abcdef\n"
" /etc/nginx/mime.types 123456\n"
"\n", # Empty line to trigger flush
encoding="utf-8",
)
m = d.parse_status_conffiles(str(status))
assert "/etc/nginx/nginx.conf" in m["nginx"]
assert "/etc/nginx/mime.types" in m["nginx"]
def test_read_pkg_md5sums_skips_invalid_md5_lines(tmp_path: Path, monkeypatch):
"""Test that lines without proper MD5 format are skipped."""
import enroll.debian as d
info_dir = tmp_path / "info"
info_dir.mkdir()
md5_file = info_dir / "foo.md5sums"
md5_file.write_text(
"abcdef1234567890abcdef1234567890 etc/foo/bar\n"
"invalid line without proper format\n"
"1234567890abcdef1234567890abcdef etc/foo/baz\n",
encoding="utf-8",
)
def fake_exists(path):
return str(path).endswith("foo.md5sums")
monkeypatch.setattr(d.os.path, "exists", fake_exists)
original_open = open
def fake_open(path, *args, **kwargs):
if "foo.md5sums" in str(path):
return original_open(md5_file, *args, **kwargs)
return original_open(path, *args, **kwargs)
monkeypatch.setattr("builtins.open", fake_open, raising=False)
result = d.read_pkg_md5sums("foo")
assert "etc/foo/bar" in result
assert "etc/foo/baz" in result
def test_build_dpkg_etc_index_skips_lines_without_tabs(tmp_path: Path):
"""Test that lines without tab separators are skipped (parts < 3)."""
import enroll.debian as d
info = tmp_path / "info"
info.mkdir()
# Create file with lines that don't have tab separators
(info / "foo.list").write_text(
"notabseparator\n" # No tab - should be skipped
"/etc/foo/bar\n", # This is a path line, processed differently
encoding="utf-8",
)
owned, owner_map, topdir_to_pkgs, pkg_to_etc = d.build_dpkg_etc_index(str(info))
# Path lines are still processed
assert "/etc/foo/bar" in owned
def test_read_pkg_md5sums_skips_empty_lines(tmp_path: Path, monkeypatch):
"""Test that empty lines in md5sums are skipped."""
import enroll.debian as d
info_dir = tmp_path / "info"
info_dir.mkdir()
md5_file = info_dir / "bar.md5sums"
md5_file.write_text(
"abcdef1234567890abcdef1234567890 etc/bar/file1\n"
"\n" # Empty line
"1234567890abcdef1234567890abcdef etc/bar/file2\n",
encoding="utf-8",
)
def fake_exists(path):
return str(path).endswith("bar.md5sums")
monkeypatch.setattr(d.os.path, "exists", fake_exists)
original_open = open
def fake_open(path, *args, **kwargs):
if "bar.md5sums" in str(path):
return original_open(md5_file, *args, **kwargs)
return original_open(path, *args, **kwargs)
monkeypatch.setattr("builtins.open", fake_open, raising=False)
result = d.read_pkg_md5sums("bar")
assert "etc/bar/file1" in result
assert "etc/bar/file2" in result
def test_read_pkg_md5sums_skips_lines_not_starting_with_path(
tmp_path: Path, monkeypatch
):
"""Test that lines not starting with / are skipped."""
import enroll.debian as d
info_dir = tmp_path / "info"
info_dir.mkdir()
md5_file = info_dir / "baz.md5sums"
md5_file.write_text(
"abcdef1234567890abcdef1234567890 etc/baz/file1\n"
"invalid line\n" # Doesn't start with /
"1234567890abcdef1234567890abcdef etc/baz/file2\n",
encoding="utf-8",
)
def fake_exists(path):
return str(path).endswith("baz.md5sums")
monkeypatch.setattr(d.os.path, "exists", fake_exists)
original_open = open
def fake_open(path, *args, **kwargs):
if "baz.md5sums" in str(path):
return original_open(md5_file, *args, **kwargs)
return original_open(path, *args, **kwargs)
monkeypatch.setattr("builtins.open", fake_open, raising=False)
result = d.read_pkg_md5sums("baz")
assert "etc/baz/file1" in result
assert "etc/baz/file2" in result

View file

@ -6,6 +6,15 @@ from pathlib import Path
import pytest
from enroll.diff import (
_Spinner,
_enforcement_plan,
has_enforceable_drift,
_role_tag,
_utc_now_iso,
_report_markdown,
)
def _make_bundle_dir(tmp_path: Path) -> Path:
b = tmp_path / "bundle"
@ -203,10 +212,6 @@ def test_load_state(tmp_path: Path):
assert result["host"]["hostname"] == "test"
def test_roles_empty_state():
assert _roles({}) == {}
def test_roles_with_roles():
state = {"roles": {"users": {}, "services": []}}
result = _roles(state)
@ -696,42 +701,12 @@ def test_compare_harvests_with_exclude_paths(tmp_path: Path):
assert "/etc/passwd" not in [f["path"] for f in report["files"]["changed"]]
from enroll.diff import (
_Spinner,
_enforcement_plan,
has_enforceable_drift,
_role_tag,
_utc_now_iso,
_report_markdown,
)
def test_utc_now_iso():
result = _utc_now_iso()
assert "T" in result
assert "+" in result or "Z" in result
def test_spinner_start_stop(monkeypatch):
# Mock sys.stderr to avoid actual writes
class FakeStderr:
def write(self, s):
pass
def flush(self):
pass
def isatty(self):
return True
monkeypatch.setattr(sys, "stderr", FakeStderr())
spinner = _Spinner("Test")
spinner.start()
spinner.stop(final_line="Done")
# Should not raise
def test_spinner_stop_without_start():
spinner = _Spinner("Test")
spinner.stop(final_line="Done")
@ -1079,3 +1054,320 @@ def test_report_markdown_empty():
result = _report_markdown(report)
assert "## Packages" in result
assert "## Services" in result
def test_spinner_start_stop(monkeypatch):
"""Test spinner can be started and stopped."""
import enroll.diff as d
# Mock threading to avoid actual thread creation
class FakeThread:
def __init__(self, target, name, daemon):
self.target = target
self.daemon = daemon
def start(self):
pass
def join(self, timeout):
pass
monkeypatch.setattr(d.threading, "Thread", FakeThread)
spinner = d._Spinner("test message")
spinner.start()
spinner.stop()
def test_spinner_already_started(monkeypatch):
"""Test spinner doesn't restart if already running."""
import enroll.diff as d
class FakeThread:
def __init__(self, target, name, daemon):
pass
def start(self):
pass
def join(self, timeout):
pass
monkeypatch.setattr(d.threading, "Thread", FakeThread)
spinner = d._Spinner("test message")
spinner.start()
spinner._thread = FakeThread(None, None, True) # Simulate already running
spinner.start() # Should return early
def test_spinner_stop_clears_line(monkeypatch, tmp_path):
"""Test spinner stop clears the line."""
import enroll.diff as d
import sys
class FakeThread:
def __init__(self, target, name, daemon):
pass
def start(self):
pass
def join(self, timeout):
pass
monkeypatch.setattr(d.threading, "Thread", FakeThread)
# Capture stderr writes
writes = []
original_write = sys.stderr.write
def capture_write(s):
writes.append(s)
return original_write(s)
monkeypatch.setattr(sys.stderr, "write", capture_write)
spinner = d._Spinner("test message")
spinner._last_len = 20
spinner.stop()
# Should have written clearing sequence
assert any("\r" in w for w in writes)
def test_should_show_spinner_disabled_env(monkeypatch):
"""Test spinner disabled via environment variable."""
import enroll.diff as d
monkeypatch.setenv("ENROLL_NO_PROGRESS", "1")
assert d._progress_enabled() is False
monkeypatch.setenv("ENROLL_NO_PROGRESS", "true")
assert d._progress_enabled() is False
monkeypatch.setenv("ENROLL_NO_PROGRESS", "yes")
assert d._progress_enabled() is False
def test_should_show_spinner_exception_on_isatty(monkeypatch):
"""Test spinner returns False when isatty raises exception."""
import enroll.diff as d
import sys
original_stderr = sys.stderr
class FakeStderr:
def isatty(self):
raise Exception("No tty")
monkeypatch.setattr(sys, "stderr", FakeStderr())
assert d._progress_enabled() is False
# Restore
monkeypatch.setattr(sys, "stderr", original_stderr)
def test_all_packages_from_state():
"""Test _all_packages extracts sorted package list."""
import enroll.diff as d
state = {
"inventory": {
"packages": {
"nginx": [{"version": "1.0"}],
"vim": [{"version": "2.0"}],
"bash": [{"version": "3.0"}],
}
}
}
result = d._all_packages(state)
assert result == ["bash", "nginx", "vim"]
def test_all_packages_empty_state():
"""Test _all_packages with empty state."""
import enroll.diff as d
state = {"inventory": {"packages": {}}}
result = d._all_packages(state)
assert result == []
def test_roles_from_state():
"""Test _roles extracts roles from state."""
import enroll.diff as d
state = {"roles": {"web": {}, "db": {}}}
result = d._roles(state)
assert result == {"web": {}, "db": {}}
def test_roles_empty_state():
"""Test _roles with empty state."""
import enroll.diff as d
state = {}
result = d._roles(state)
assert result == {}
def test_pkg_version_key_with_multiple_versions():
"""Test _pkg_version_key handles multiple versions."""
import enroll.diff as d
entry = {
"installations": [
{"version": "1.0", "arch": "amd64"},
{"version": "2.0", "arch": "arm64"},
]
}
result = d._pkg_version_key(entry)
# Just check it returns a non-None value with version info
assert result is not None
assert len(result) > 0
def test_pkg_version_key_without_version():
"""Test _pkg_version_key skips entries without version."""
import enroll.diff as d
entry = {
"installations": [
{"arch": "amd64"}, # No version
]
}
result = d._pkg_version_key(entry)
assert result is None
def test_pkg_version_key_with_empty_installations():
"""Test _pkg_version_key with empty installations."""
import enroll.diff as d
entry = {"installations": []}
result = d._pkg_version_key(entry)
assert result is None
def test_pkg_version_key_without_installations():
"""Test _pkg_version_key without installations key."""
import enroll.diff as d
entry = {}
result = d._pkg_version_key(entry)
assert result is None
def test_pkg_version_key_with_direct_version():
"""Test _pkg_version_key with direct version field."""
import enroll.diff as d
entry = {"version": "1.2.3"}
result = d._pkg_version_key(entry)
assert result == "1.2.3"
def test_report_text_with_exclude_paths():
"""Test _report_text includes exclude paths."""
import enroll.diff as d
report = {
"generated_at": "2024-01-01T00:00:00Z",
"old": {"input": "old.tar.gz", "host": "host1", "state_mtime": "mtime1"},
"new": {"input": "new.tar.gz", "host": "host2", "state_mtime": "mtime2"},
"filters": {"exclude_paths": ["/tmp/*", "/var/log/*"]},
"packages": {"added": [], "removed": [], "version_changed": []},
"services": {"enabled_added": [], "enabled_removed": [], "changed": []},
"users": {"added": [], "removed": [], "changed": []},
"files": {"added": [], "removed": [], "changed": []},
}
result = d._report_text(report)
assert "file exclude patterns" in result
assert "/tmp/*" in result
def test_report_text_with_ignore_package_versions():
"""Test _report_text includes ignore package versions message."""
import enroll.diff as d
report = {
"generated_at": "2024-01-01T00:00:00Z",
"old": {"input": "old.tar.gz", "host": "host1", "state_mtime": "mtime1"},
"new": {"input": "new.tar.gz", "host": "host2", "state_mtime": "mtime2"},
"filters": {"ignore_package_versions": True},
"packages": {"version_changed_ignored_count": 5},
"services": {"enabled_added": [], "enabled_removed": [], "changed": []},
"users": {"added": [], "removed": [], "changed": []},
"files": {"added": [], "removed": [], "changed": []},
}
result = d._report_text(report)
assert "package version drift: ignored" in result
assert "ignored 5 changes" in result
def test_report_text_with_enforcement_applied():
"""Test _report_text includes enforcement applied status."""
import enroll.diff as d
report = {
"generated_at": "2024-01-01T00:00:00Z",
"old": {"input": "old.tar.gz", "host": "host1", "state_mtime": "mtime1"},
"new": {"input": "new.tar.gz", "host": "host2", "state_mtime": "mtime2"},
"packages": {"added": [], "removed": [], "version_changed": []},
"services": {"enabled_added": [], "enabled_removed": [], "changed": []},
"users": {"added": [], "removed": [], "changed": []},
"files": {"added": [], "removed": [], "changed": []},
"enforcement": {
"status": "applied",
"returncode": 0,
"tags": ["test"],
"finished_at": "2024-01-01T01:00:00Z",
},
}
result = d._report_text(report)
assert "Enforcement" in result
assert "applied old harvest via ansible-playbook" in result
assert "tags=test" in result
def test_report_text_with_enforcement_failed():
"""Test _report_text includes enforcement failed status."""
import enroll.diff as d
report = {
"generated_at": "2024-01-01T00:00:00Z",
"old": {"input": "old.tar.gz", "host": "host1", "state_mtime": "mtime1"},
"new": {"input": "new.tar.gz", "host": "host2", "state_mtime": "mtime2"},
"packages": {"added": [], "removed": [], "version_changed": []},
"services": {"enabled_added": [], "enabled_removed": [], "changed": []},
"users": {"added": [], "removed": [], "changed": []},
"files": {"added": [], "removed": [], "changed": []},
"enforcement": {"status": "failed", "returncode": 1},
}
result = d._report_text(report)
assert "Enforcement" in result
assert "ansible-playbook failed" in result
def test_report_text_with_enforcement_skipped():
"""Test _report_text includes enforcement skipped status."""
import enroll.diff as d
report = {
"generated_at": "2024-01-01T00:00:00Z",
"old": {"input": "old.tar.gz", "host": "host1", "state_mtime": "mtime1"},
"new": {"input": "new.tar.gz", "host": "host2", "state_mtime": "mtime2"},
"packages": {"added": [], "removed": [], "version_changed": []},
"services": {"enabled_added": [], "enabled_removed": [], "changed": []},
"users": {"added": [], "removed": [], "changed": []},
"files": {"added": [], "removed": [], "changed": []},
"enforcement": {"status": "skipped", "reason": "no changes"},
}
result = d._report_text(report)
assert "Enforcement" in result
assert "skipped" in result
assert "no changes" in result

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import pytest
from pathlib import Path
from enroll.sopsutil import SopsError, _pgp_arg, find_sops_cmd, require_sops_cmd
@ -52,3 +53,182 @@ def test_pgp_arg_with_single_fingerprint():
def test_pgp_arg_with_multiple_fingerprints():
result = _pgp_arg(["ABC123", "DEF456", "GHI789"])
assert result == "ABC123,DEF456,GHI789"
def test_encrypt_file_binary_success(monkeypatch, tmp_path: Path):
"""Test successful encryption path."""
# Create source file
src = tmp_path / "secret.txt"
src.write_text("secret data", encoding="utf-8")
dst = tmp_path / "encrypted.sops"
# Mock subprocess.run to succeed
class Result:
returncode = 0
stdout = b"encrypted data"
stderr = b""
def fake_run(cmd, capture_output, check):
return Result()
# Mock require_sops_cmd to return a fake path
def fake_require():
return "/fake/sops"
monkeypatch.setattr("enroll.sopsutil.subprocess.run", fake_run)
monkeypatch.setattr("enroll.sopsutil.require_sops_cmd", fake_require)
from enroll.sopsutil import encrypt_file_binary
encrypt_file_binary(src, dst, pgp_fingerprints=["ABC123"])
assert dst.exists()
assert dst.read_bytes() == b"encrypted data"
def test_encrypt_file_binary_fails(monkeypatch, tmp_path: Path):
"""Test encryption failure path."""
src = tmp_path / "secret.txt"
src.write_text("secret data", encoding="utf-8")
dst = tmp_path / "encrypted.sops"
class Result:
returncode = 1
stdout = b""
stderr = b"sops: gpg error"
def fake_run(cmd, capture_output, check):
return Result()
def fake_require():
return "/fake/sops"
monkeypatch.setattr("enroll.sopsutil.subprocess.run", fake_run)
monkeypatch.setattr("enroll.sopsutil.require_sops_cmd", fake_require)
from enroll.sopsutil import encrypt_file_binary, SopsError
with pytest.raises(SopsError) as exc_info:
encrypt_file_binary(src, dst, pgp_fingerprints=["ABC123"])
assert "encryption failed" in str(exc_info.value).lower()
def test_encrypt_file_binary_chmod_fails(monkeypatch, tmp_path: Path):
"""Test when chmod fails but file is still written."""
src = tmp_path / "secret.txt"
src.write_text("secret data", encoding="utf-8")
dst = tmp_path / "encrypted.sops"
class Result:
returncode = 0
stdout = b"encrypted data"
stderr = b""
def fake_run(cmd, capture_output, check):
return Result()
def fake_require():
return "/fake/sops"
def fake_chmod(path, mode):
raise OSError("Permission denied")
monkeypatch.setattr("enroll.sopsutil.subprocess.run", fake_run)
monkeypatch.setattr("enroll.sopsutil.require_sops_cmd", fake_require)
monkeypatch.setattr("enroll.sopsutil.os.chmod", fake_chmod)
from enroll.sopsutil import encrypt_file_binary
# Should not raise even though chmod fails
encrypt_file_binary(src, dst, pgp_fingerprints=["ABC123"])
assert dst.exists()
def test_decrypt_file_binary_to_success(monkeypatch, tmp_path: Path):
"""Test successful decryption path."""
src = tmp_path / "encrypted.sops"
src.write_bytes(b"encrypted data")
dst = tmp_path / "decrypted.txt"
class Result:
returncode = 0
stdout = b"decrypted data"
stderr = b""
def fake_run(cmd, capture_output, check):
return Result()
def fake_require():
return "/fake/sops"
monkeypatch.setattr("enroll.sopsutil.subprocess.run", fake_run)
monkeypatch.setattr("enroll.sopsutil.require_sops_cmd", fake_require)
from enroll.sopsutil import decrypt_file_binary_to
decrypt_file_binary_to(src, dst)
assert dst.exists()
assert dst.read_bytes() == b"decrypted data"
def test_decrypt_file_binary_to_fails(monkeypatch, tmp_path: Path):
"""Test decryption failure path."""
src = tmp_path / "encrypted.sops"
src.write_bytes(b"encrypted data")
dst = tmp_path / "decrypted.txt"
class Result:
returncode = 1
stdout = b""
stderr = b"sops: decryption failed"
def fake_run(cmd, capture_output, check):
return Result()
def fake_require():
return "/fake/sops"
monkeypatch.setattr("enroll.sopsutil.subprocess.run", fake_run)
monkeypatch.setattr("enroll.sopsutil.require_sops_cmd", fake_require)
from enroll.sopsutil import decrypt_file_binary_to, SopsError
with pytest.raises(SopsError) as exc_info:
decrypt_file_binary_to(src, dst)
assert "decryption failed" in str(exc_info.value).lower()
def test_decrypt_file_binary_to_chmod_fails(monkeypatch, tmp_path: Path):
"""Test when chmod fails during decryption but file is still written."""
src = tmp_path / "encrypted.sops"
src.write_bytes(b"encrypted data")
dst = tmp_path / "decrypted.txt"
class Result:
returncode = 0
stdout = b"decrypted data"
stderr = b""
def fake_run(cmd, capture_output, check):
return Result()
def fake_require():
return "/fake/sops"
def fake_chmod(path, mode):
raise OSError("Permission denied")
monkeypatch.setattr("enroll.sopsutil.subprocess.run", fake_run)
monkeypatch.setattr("enroll.sopsutil.require_sops_cmd", fake_require)
monkeypatch.setattr("enroll.sopsutil.os.chmod", fake_chmod)
from enroll.sopsutil import decrypt_file_binary_to
# Should not raise even though chmod fails
decrypt_file_binary_to(src, dst)
assert dst.exists()

View file

@ -232,3 +232,90 @@ def test_get_unit_info_with_empty_fields(monkeypatch):
assert ui.env_files == []
assert ui.exec_paths == []
assert ui.active_state is None
def test_run_command_raises_on_error(monkeypatch):
"""Test _run raises RuntimeError on non-zero exit."""
class P:
returncode = 1
stdout = ""
stderr = "command failed"
def fake_run(cmd, check, text, capture_output):
return P()
monkeypatch.setattr(s.subprocess, "run", fake_run)
with pytest.raises(RuntimeError) as exc_info:
s._run(["fake", "command"])
assert "Command failed" in str(exc_info.value)
assert "fake" in str(exc_info.value)
def test_list_enabled_services_filters_non_service_units(monkeypatch):
"""Test that non-.service units are filtered out."""
def fake_run(cmd: list[str]) -> str:
return "\n".join(
[
"nginx.service enabled",
"network.target enabled", # not a service
"multi-user.target enabled", # not a service
]
)
monkeypatch.setattr(s, "_run", fake_run)
result = s.list_enabled_services()
assert result == ["nginx.service"]
def test_list_enabled_timers_filters_non_timer_units(monkeypatch):
"""Test that non-.timer units are filtered out."""
def fake_run(cmd: list[str]) -> str:
return "\n".join(
[
"apt-daily.timer enabled",
"some.service enabled", # not a timer
]
)
monkeypatch.setattr(s, "_run", fake_run)
result = s.list_enabled_timers()
assert result == ["apt-daily.timer"]
def test_list_enabled_services_filters_empty_lines(monkeypatch):
"""Test that empty lines are skipped."""
def fake_run(cmd: list[str]) -> str:
return "\n".join(
[
"nginx.service enabled",
"", # empty line
"ssh.service enabled",
]
)
monkeypatch.setattr(s, "_run", fake_run)
result = s.list_enabled_services()
assert result == ["nginx.service", "ssh.service"]
def test_list_enabled_timers_filters_empty_lines(monkeypatch):
"""Test that empty lines are skipped."""
def fake_run(cmd: list[str]) -> str:
return "\n".join(
[
"apt-daily.timer enabled",
"", # empty line
"daily.timer enabled",
]
)
monkeypatch.setattr(s, "_run", fake_run)
result = s.list_enabled_timers()
assert result == ["apt-daily.timer", "daily.timer"]

View file

@ -1,36 +1,12 @@
from __future__ import annotations
import sys
import types
# The version module is hard to test fully because it uses importlib.metadata
# which is difficult to mock. We'll test what we can.
def test_get_enroll_version_returns_unknown_when_import_fails(monkeypatch):
def test_get_enroll_version_returns_string():
from enroll.version import get_enroll_version
# Ensure both the module cache and the parent package attribute are redirected.
import importlib
dummy = types.ModuleType("importlib.metadata")
# Missing attributes will cause ImportError when importing names.
monkeypatch.setitem(sys.modules, "importlib.metadata", dummy)
monkeypatch.setattr(importlib, "metadata", dummy, raising=False)
assert get_enroll_version() == "unknown"
def test_get_enroll_version_uses_packages_distributions(monkeypatch):
# Restore the real module for this test.
monkeypatch.delitem(sys.modules, "importlib.metadata", raising=False)
import importlib.metadata
from enroll.version import get_enroll_version
monkeypatch.setattr(
importlib.metadata,
"packages_distributions",
lambda: {"enroll": ["enroll-dist"]},
)
monkeypatch.setattr(importlib.metadata, "version", lambda dist: "9.9.9")
assert get_enroll_version() == "9.9.9"
result = get_enroll_version()
assert isinstance(result, str)
assert len(result) > 0