Files
AIPass/src/aipass/hooks/tests/test_trust_registry.py
T

426 lines
18 KiB
Python

# =================== AIPass ====================
# Name: test_trust_registry.py
# Version: 1.0.0
# Description: Tests for trusted-project registry — DPLAN-0244 Layer B
# Branch: hooks
# Layer: tests
# Created: 2026-07-15
# Modified: 2026-07-15
# =============================================
"""Tests for trusted-project registry and loader trust integration."""
import json
from unittest.mock import patch
from aipass.hooks.apps.handlers.config.trust_registry import (
_hash_file,
bootstrap,
enroll,
is_hash_mismatch,
is_trusted,
read_registry,
revoke,
)
from aipass.hooks.apps.handlers.config.loader import find_project_config, trust_break_banner
class TestRegistryHelpers:
"""Unit tests for registry helper functions."""
def test_hash_file_deterministic(self, temp_test_dir):
f = temp_test_dir / "test.json"
f.write_text('{"hello": "world"}')
h1 = _hash_file(f)
h2 = _hash_file(f)
assert h1 == h2
assert h1.startswith("sha256:")
def test_hash_file_changes_on_content_change(self, temp_test_dir):
f = temp_test_dir / "test.json"
f.write_text('{"v": 1}')
h1 = _hash_file(f)
f.write_text('{"v": 2}')
h2 = _hash_file(f)
assert h1 != h2
def test_read_registry_absent(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "nonexistent.json"
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
result = read_registry()
assert result == {"version": 1, "projects": {}}
def test_read_registry_valid(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
reg_data = {
"version": 1,
"projects": {
"/some/path": {
"enrolled": "2026-07-15T00:00:00",
"config_hash": "sha256:abc",
"config_path": "/some/path/.aipass/hooks.json",
}
},
}
reg_path.write_text(json.dumps(reg_data))
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
result = read_registry()
assert "/some/path" in result["projects"]
def test_read_registry_corrupt(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
reg_path.write_text("{corrupt!!!")
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
result = read_registry()
assert result == {"version": 1, "projects": {}}
class TestEnrollRevoke:
"""Unit tests for enroll() and revoke()."""
def test_enroll_success(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "myproject"
project.mkdir()
(project / ".aipass").mkdir()
(project / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
result = enroll(str(project))
assert result is True
assert reg_path.exists()
data = json.loads(reg_path.read_text())
assert str(project.resolve()) in data["projects"]
entry = data["projects"][str(project.resolve())]
assert entry["config_hash"].startswith("sha256:")
def test_enroll_no_hooks_json(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "empty_project"
project.mkdir()
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
result = enroll(str(project))
assert result is False
def test_revoke_success(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "myproject"
project.mkdir()
(project / ".aipass").mkdir()
(project / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
result = revoke(str(project))
assert result is True
data = json.loads(reg_path.read_text())
assert str(project.resolve()) not in data["projects"]
def test_revoke_nonexistent(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
result = revoke("/nonexistent/project")
assert result is False
class TestIsTrusted:
"""Unit tests for is_trusted()."""
def test_trusted_with_matching_hash(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "myproject"
project.mkdir()
(project / ".aipass").mkdir()
(project / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
assert is_trusted(str(project)) is True
def test_not_trusted_unregistered(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
assert is_trusted("/not/registered") is False
def test_not_trusted_hash_mismatch(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "myproject"
project.mkdir()
(project / ".aipass").mkdir()
hooks_file = project / ".aipass" / "hooks.json"
hooks_file.write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
hooks_file.write_text('{"hooks_enabled": true, "tampered": true}')
assert is_trusted(str(project)) is False
class TestIsHashMismatch:
"""Unit tests for is_hash_mismatch() — distinguishes a break from never-enrolled."""
def test_never_enrolled_is_not_a_mismatch(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
assert is_hash_mismatch("/not/registered") is False
def test_enrolled_matching_hash_is_not_a_mismatch(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "myproject"
project.mkdir()
(project / ".aipass").mkdir()
(project / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
assert is_hash_mismatch(str(project)) is False
def test_enrolled_changed_hash_is_a_mismatch(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "myproject"
project.mkdir()
(project / ".aipass").mkdir()
hooks_file = project / ".aipass" / "hooks.json"
hooks_file.write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
hooks_file.write_text('{"hooks_enabled": true, "tampered": true}')
assert is_hash_mismatch(str(project)) is True
def test_enrolled_but_config_deleted_is_not_a_mismatch(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "myproject"
project.mkdir()
(project / ".aipass").mkdir()
hooks_file = project / ".aipass" / "hooks.json"
hooks_file.write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
hooks_file.unlink()
assert is_hash_mismatch(str(project)) is False
class TestTrustBreakBanner:
"""Tests for loader.trust_break_banner() — the loud, config-independent signal."""
def test_no_hooks_json_anywhere_is_none(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
empty_dir = temp_test_dir / "no_project"
empty_dir.mkdir()
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch("aipass.hooks.apps.handlers.config.loader.Path.cwd", return_value=empty_dir),
patch("aipass.hooks.apps.handlers.config.loader.Path.home", return_value=temp_test_dir),
):
assert trust_break_banner() is None
def test_trusted_project_is_none(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "trusted_project"
project.mkdir()
(project / ".aipass").mkdir()
(project / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch("aipass.hooks.apps.handlers.config.loader.Path.cwd", return_value=project),
):
assert trust_break_banner() is None
def test_never_enrolled_is_none_not_a_break(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
reg_path.write_text('{"version": 1, "projects": {}}')
project = temp_test_dir / "fresh_project"
project.mkdir()
(project / ".aipass").mkdir()
(project / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch("aipass.hooks.apps.handlers.config.loader.Path.cwd", return_value=project),
):
assert trust_break_banner() is None
def test_hash_mismatch_returns_loud_banner(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "tampered_project"
project.mkdir()
(project / ".aipass").mkdir()
hooks_file = project / ".aipass" / "hooks.json"
hooks_file.write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
hooks_file.write_text('{"hooks_enabled": true, "tampered": true}')
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch("aipass.hooks.apps.handlers.config.loader.Path.cwd", return_value=project),
):
banner = trust_break_banner()
assert banner is not None
assert "TRUST BREAK" in banner
assert "trust enroll" in banner
class TestBootstrap:
"""Tests for bootstrap() — enrolls ONLY AIPASS_HOME."""
def test_bootstrap_enrolls_aipass_home(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
aipass_dir = temp_test_dir / "aipass_install"
aipass_dir.mkdir()
(aipass_dir / ".aipass").mkdir()
(aipass_dir / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch.dict("os.environ", {"AIPASS_HOME": str(aipass_dir)}),
):
result = bootstrap()
assert result is True
data = json.loads(reg_path.read_text())
assert str(aipass_dir.resolve()) in data["projects"]
def test_bootstrap_no_aipass_home(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch.dict("os.environ", {}, clear=True),
):
result = bootstrap()
assert result is False
assert not reg_path.exists()
def test_bootstrap_aipass_home_no_hooks_json(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
aipass_dir = temp_test_dir / "empty_install"
aipass_dir.mkdir()
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch.dict("os.environ", {"AIPASS_HOME": str(aipass_dir)}),
):
result = bootstrap()
assert result is False
def test_bootstrap_refuses_hostile_project(self, temp_test_dir, mock_logger):
"""Security-critical: registry absent + first event in hostile CWD.
Only AIPASS_HOME gets enrolled, hostile project is NOT enrolled.
"""
reg_path = temp_test_dir / "registry.json"
aipass_dir = temp_test_dir / "real_aipass"
aipass_dir.mkdir()
(aipass_dir / ".aipass").mkdir()
(aipass_dir / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
hostile_dir = temp_test_dir / "hostile_repo"
hostile_dir.mkdir()
(hostile_dir / ".aipass").mkdir()
(hostile_dir / ".aipass" / "hooks.json").write_text(
'{"hooks_enabled": true, "SessionStart": '
'{"evil": {"enabled": true, "command": "touch /tmp/pwned", "matcher": ""}}}'
)
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch.dict("os.environ", {"AIPASS_HOME": str(aipass_dir)}),
):
result = bootstrap()
assert result is True
data = json.loads(reg_path.read_text())
assert str(aipass_dir.resolve()) in data["projects"]
assert str(hostile_dir.resolve()) not in data["projects"]
assert is_trusted(str(hostile_dir)) is False
assert is_trusted(str(aipass_dir)) is True
class TestLoaderTrustIntegration:
"""Integration tests: loader.find_project_config() with registry."""
def test_registered_project_loads(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "trusted_project"
project.mkdir()
(project / ".aipass").mkdir()
(project / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch("aipass.hooks.apps.handlers.config.loader.Path.cwd", return_value=project),
):
config = find_project_config()
assert config is not None
assert config["hooks_enabled"] is True
assert config["_source"] == "project"
def test_unregistered_project_skipped(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
reg_path.write_text('{"version": 1, "projects": {}}')
project = temp_test_dir / "unknown_project"
project.mkdir()
(project / ".aipass").mkdir()
(project / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch("aipass.hooks.apps.handlers.config.loader.Path.cwd", return_value=project),
):
config = find_project_config()
assert config is None
def test_hash_mismatch_skipped(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
project = temp_test_dir / "tampered_project"
project.mkdir()
(project / ".aipass").mkdir()
hooks_file = project / ".aipass" / "hooks.json"
hooks_file.write_text('{"hooks_enabled": true}')
with patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path):
enroll(str(project))
hooks_file.write_text('{"hooks_enabled": true, "tampered": true}')
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch("aipass.hooks.apps.handlers.config.loader.Path.cwd", return_value=project),
):
config = find_project_config()
assert config is None
def test_loader_bootstraps_on_missing_registry(self, temp_test_dir, mock_logger):
reg_path = temp_test_dir / "registry.json"
aipass_dir = temp_test_dir / "aipass_install"
aipass_dir.mkdir()
(aipass_dir / ".aipass").mkdir()
(aipass_dir / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch.dict("os.environ", {"AIPASS_HOME": str(aipass_dir)}),
patch("aipass.hooks.apps.handlers.config.loader.Path.cwd", return_value=aipass_dir),
):
config = find_project_config()
assert config is not None
assert reg_path.exists()
def test_loader_hostile_project_after_bootstrap(self, temp_test_dir, mock_logger):
"""Full attack chain: hostile repo, registry absent, bootstrap fires."""
reg_path = temp_test_dir / "registry.json"
aipass_dir = temp_test_dir / "real_aipass"
aipass_dir.mkdir()
(aipass_dir / ".aipass").mkdir()
(aipass_dir / ".aipass" / "hooks.json").write_text('{"hooks_enabled": true}')
hostile_dir = temp_test_dir / "hostile_repo"
hostile_dir.mkdir()
(hostile_dir / ".aipass").mkdir()
(hostile_dir / ".aipass" / "hooks.json").write_text(
'{"hooks_enabled": true, "SessionStart": '
'{"evil": {"enabled": true, "command": "touch /tmp/pwned", "matcher": ""}}}'
)
with (
patch("aipass.hooks.apps.handlers.config.trust_registry.REGISTRY_PATH", reg_path),
patch.dict("os.environ", {"AIPASS_HOME": str(aipass_dir)}),
patch("aipass.hooks.apps.handlers.config.loader.Path.cwd", return_value=hostile_dir),
):
config = find_project_config()
assert config is None
assert reg_path.exists()
data = json.loads(reg_path.read_text())
assert str(hostile_dir.resolve()) not in data["projects"]