""" Comprehensive tests for FileIntegrityManager to increase coverage. Tests all public and internal methods with mocked DB sessions and verifiers. """ import pytest from unittest.mock import patch, MagicMock from pathlib import Path from datetime import datetime, UTC from contextlib import contextmanager # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_mock_session(): """Create a mock DB session with chainable query interface.""" session = MagicMock() return session @contextmanager def _mock_session_cm(session): """Context manager wrapper for mock session.""" yield session def _make_verifier( should=True, file_type="faiss_index", algorithm="sha256", checksum="abc123", allows_mod=False, ): """Create a mock verifier with configurable behavior.""" v = MagicMock() v.should_verify.return_value = should v.get_file_type.return_value = file_type v.get_algorithm.return_value = algorithm v.calculate_checksum.return_value = checksum v.allows_modifications.return_value = allows_mod return v def _make_record(**overrides): """Create a mock FileIntegrityRecord with sensible defaults.""" defaults = dict( id=1, file_path="/resolved/path", file_type="faiss_index", checksum="abc123", algorithm="sha256", file_size=1024, file_mtime=1000.0, verify_on_load=True, allow_modifications=False, total_verifications=5, last_verified_at=datetime(2025, 1, 1, tzinfo=UTC), last_verification_passed=True, consecutive_successes=3, consecutive_failures=0, created_at=datetime(2025, 1, 1, tzinfo=UTC), updated_at=None, related_entity_type=None, related_entity_id=None, ) defaults.update(overrides) record = MagicMock() for k, v in defaults.items(): setattr(record, k, v) return record # Patch path for the session context used inside integrity_manager module _SESSION_PATCH = ( "local_deep_research.security.file_integrity.integrity_manager" ".get_user_db_session" ) _HAS_CTX_PATCH = ( "local_deep_research.security.file_integrity.integrity_manager" "._has_session_context" ) @pytest.fixture def integrity_manager(): """ Build a FileIntegrityManager with mocked session context. Yields (manager, session). Patcher is automatically stopped on teardown. """ session = _make_mock_session() with patch(_HAS_CTX_PATCH, True), patch(_SESSION_PATCH) as mock_get_session: mock_get_session.return_value = _mock_session_cm(session) # cleanup_all_old_failures queries count then returns session.query.return_value.count.return_value = 0 from local_deep_research.security.file_integrity.integrity_manager import ( FileIntegrityManager, ) mgr = FileIntegrityManager(username="testuser", password="testpass") # Re-patch for subsequent calls patcher = patch( _SESSION_PATCH, side_effect=lambda u, p: _mock_session_cm(session) ) patcher.start() mgr._test_session = session yield mgr, session patcher.stop() def _build_manager(session=None): """ Build a FileIntegrityManager with mocked session context. Returns (manager, session). Caller MUST call mgr._test_patcher.stop() or use the integrity_manager fixture instead. NOTE: Prefer the integrity_manager fixture for new tests. This helper is kept for TestInit tests that need custom session setup. """ if session is None: session = _make_mock_session() with patch(_HAS_CTX_PATCH, True), patch(_SESSION_PATCH) as mock_get_session: mock_get_session.return_value = _mock_session_cm(session) # cleanup_all_old_failures queries count then returns session.query.return_value.count.return_value = 0 from local_deep_research.security.file_integrity.integrity_manager import ( FileIntegrityManager, ) mgr = FileIntegrityManager(username="testuser", password="testpass") # Re-patch for subsequent calls patcher = patch( _SESSION_PATCH, side_effect=lambda u, p: _mock_session_cm(session) ) patcher.start() mgr._test_patcher = patcher mgr._test_session = session return mgr, session # --------------------------------------------------------------------------- # Tests: __init__ # --------------------------------------------------------------------------- class TestInit: def test_raises_import_error_when_no_session_context(self): with patch(_HAS_CTX_PATCH, False): from local_deep_research.security.file_integrity.integrity_manager import ( FileIntegrityManager, ) with pytest.raises(ImportError, match="requires Flask"): FileIntegrityManager(username="user") def test_successful_init_sets_attributes(self): mgr, _session = _build_manager() assert mgr.username == "testuser" assert mgr.password == "testpass" assert mgr.verifiers == [] mgr._test_patcher.stop() def test_startup_cleanup_logs_when_deleted(self): session = _make_mock_session() # First call to cleanup_all_old_failures: count returns > MAX with ( patch(_HAS_CTX_PATCH, True), patch(_SESSION_PATCH) as mock_get_session, ): mock_get_session.return_value = _mock_session_cm(session) # total_failures > MAX_TOTAL_FAILURES to trigger deletion session.query.return_value.count.return_value = 10005 session.query.return_value.order_by.return_value.limit.return_value.all.return_value = [ MagicMock() for _ in range(5) ] from local_deep_research.security.file_integrity.integrity_manager import ( FileIntegrityManager, ) mgr = FileIntegrityManager(username="user") mgr._test_patcher = patch(_SESSION_PATCH) # dummy # No assertion needed beyond not raising def test_startup_cleanup_handles_exception(self): """If startup cleanup raises, __init__ should still succeed.""" with ( patch(_HAS_CTX_PATCH, True), patch(_SESSION_PATCH) as mock_get_session, ): mock_get_session.side_effect = RuntimeError("db down") from local_deep_research.security.file_integrity.integrity_manager import ( FileIntegrityManager, ) # Should not raise thanks to try/except in __init__ mgr = FileIntegrityManager(username="user") assert mgr.username == "user" # --------------------------------------------------------------------------- # Tests: _normalize_path # --------------------------------------------------------------------------- class TestNormalizePath: def test_returns_resolved_string(self, integrity_manager): mgr, _ = integrity_manager p = Path("/some/../some/file.txt") result = mgr._normalize_path(p) assert result == str(p.resolve()) assert isinstance(result, str) def test_relative_path_becomes_absolute(self, integrity_manager): mgr, _ = integrity_manager p = Path("relative/file.txt") result = mgr._normalize_path(p) assert Path(result).is_absolute() # --------------------------------------------------------------------------- # Tests: register_verifier # --------------------------------------------------------------------------- class TestRegisterVerifier: def test_appends_verifier(self, integrity_manager): mgr, _ = integrity_manager v1 = _make_verifier() v2 = _make_verifier(file_type="pdf") mgr.register_verifier(v1) mgr.register_verifier(v2) assert mgr.verifiers == [v1, v2] # --------------------------------------------------------------------------- # Tests: _get_verifier_for_file # --------------------------------------------------------------------------- class TestGetVerifierForFile: def test_returns_matching_verifier(self, integrity_manager): mgr, _ = integrity_manager v1 = _make_verifier(should=False) v2 = _make_verifier(should=True, file_type="pdf") mgr.register_verifier(v1) mgr.register_verifier(v2) result = mgr._get_verifier_for_file(Path("/some/file.pdf")) assert result is v2 def test_returns_first_matching(self, integrity_manager): mgr, _ = integrity_manager v1 = _make_verifier(should=True, file_type="faiss") v2 = _make_verifier(should=True, file_type="pdf") mgr.register_verifier(v1) mgr.register_verifier(v2) result = mgr._get_verifier_for_file(Path("/file")) assert result is v1 def test_returns_none_when_no_match(self, integrity_manager): mgr, _ = integrity_manager v1 = _make_verifier(should=False) mgr.register_verifier(v1) result = mgr._get_verifier_for_file(Path("/file")) assert result is None def test_returns_none_with_no_verifiers(self, integrity_manager): mgr, _ = integrity_manager result = mgr._get_verifier_for_file(Path("/file")) assert result is None # --------------------------------------------------------------------------- # Tests: _needs_verification # --------------------------------------------------------------------------- class TestNeedsVerification: def test_file_missing_returns_true(self, integrity_manager): mgr, _ = integrity_manager record = _make_record() path = MagicMock(spec=Path) path.exists.return_value = False assert mgr._needs_verification(record, path) is True def test_never_verified_returns_true(self, integrity_manager): mgr, _ = integrity_manager record = _make_record(last_verified_at=None) path = MagicMock(spec=Path) path.exists.return_value = True assert mgr._needs_verification(record, path) is True def test_no_stored_mtime_returns_true(self, integrity_manager): mgr, _ = integrity_manager record = _make_record(file_mtime=None) path = MagicMock(spec=Path) path.exists.return_value = True path.stat.return_value.st_mtime = 1000.0 assert mgr._needs_verification(record, path) is True def test_mtime_changed_returns_true(self, integrity_manager): mgr, _ = integrity_manager record = _make_record(file_mtime=1000.0) path = MagicMock(spec=Path) path.exists.return_value = True path.stat.return_value.st_mtime = 2000.0 # clearly different assert mgr._needs_verification(record, path) is True def test_mtime_unchanged_returns_false(self, integrity_manager): mgr, _ = integrity_manager record = _make_record(file_mtime=1000.0) path = MagicMock(spec=Path) path.exists.return_value = True path.stat.return_value.st_mtime = 1000.0 assert mgr._needs_verification(record, path) is False def test_mtime_tiny_float_difference_returns_false(self, integrity_manager): """Differences within 0.001 threshold should not trigger verification.""" mgr, _ = integrity_manager record = _make_record(file_mtime=1000.0) path = MagicMock(spec=Path) path.exists.return_value = True path.stat.return_value.st_mtime = 1000.0005 # within threshold assert mgr._needs_verification(record, path) is False # --------------------------------------------------------------------------- # Tests: _do_verification # --------------------------------------------------------------------------- class TestDoVerification: def test_file_missing(self, integrity_manager): mgr, session = integrity_manager record = _make_record() path = MagicMock(spec=Path) path.exists.return_value = False passed, reason = mgr._do_verification(record, path, session) assert passed is False assert reason == "file_missing" def test_no_verifier(self, integrity_manager): mgr, session = integrity_manager record = _make_record() path = MagicMock(spec=Path) path.exists.return_value = True # No verifiers registered passed, reason = mgr._do_verification(record, path, session) assert passed is False assert reason == "no_verifier" def test_checksum_match(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier(checksum="abc123") mgr.register_verifier(v) record = _make_record(checksum="abc123") path = MagicMock(spec=Path) path.exists.return_value = True path.stat.return_value.st_mtime = 1234.0 passed, reason = mgr._do_verification(record, path, session) assert passed is True assert reason is None # mtime should be updated on record assert record.file_mtime == 1234.0 def test_checksum_mismatch(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier(checksum="different_checksum") mgr.register_verifier(v) record = _make_record(checksum="abc123") path = MagicMock(spec=Path) path.exists.return_value = True passed, reason = mgr._do_verification(record, path, session) assert passed is False assert reason == "checksum_mismatch" def test_checksum_calculation_exception(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier() v.calculate_checksum.side_effect = IOError("read error") mgr.register_verifier(v) record = _make_record() path = MagicMock(spec=Path) path.exists.return_value = True passed, reason = mgr._do_verification(record, path, session) assert passed is False assert "checksum_calculation_failed" in reason assert "read error" in reason # --------------------------------------------------------------------------- # Tests: _update_stats # --------------------------------------------------------------------------- class TestUpdateStats: def test_passed(self, integrity_manager): mgr, session = integrity_manager record = _make_record( total_verifications=5, consecutive_successes=2, consecutive_failures=1, ) mgr._update_stats(record, True, session) assert record.total_verifications == 6 assert record.consecutive_successes == 3 assert record.consecutive_failures == 0 assert record.last_verification_passed is True assert record.last_verified_at is not None def test_failed(self, integrity_manager): mgr, session = integrity_manager record = _make_record( total_verifications=10, consecutive_successes=5, consecutive_failures=0, ) mgr._update_stats(record, False, session) assert record.total_verifications == 11 assert record.consecutive_successes == 0 assert record.consecutive_failures == 1 assert record.last_verification_passed is False # --------------------------------------------------------------------------- # Tests: _log_failure # --------------------------------------------------------------------------- class TestLogFailure: def test_logs_with_existing_file(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier(checksum="actual_cs") mgr.register_verifier(v) record = _make_record(id=1, checksum="expected_cs") path = MagicMock(spec=Path) path.exists.return_value = True path.stat.return_value.st_size = 2048 # Setup session for cleanup queries session.query.return_value.filter_by.return_value.count.return_value = 0 mgr._log_failure(record, path, "checksum_mismatch", session) session.add.assert_called_once() failure_obj = session.add.call_args[0][0] assert failure_obj.expected_checksum == "expected_cs" assert failure_obj.actual_checksum == "actual_cs" assert failure_obj.file_size == 2048 assert failure_obj.failure_reason == "checksum_mismatch" def test_logs_with_missing_file(self, integrity_manager): mgr, session = integrity_manager record = _make_record(id=1, checksum="expected_cs") path = MagicMock(spec=Path) path.exists.return_value = False session.query.return_value.filter_by.return_value.count.return_value = 0 mgr._log_failure(record, path, "file_missing", session) session.add.assert_called_once() failure_obj = session.add.call_args[0][0] assert failure_obj.actual_checksum is None assert failure_obj.file_size is None assert failure_obj.failure_reason == "file_missing" def test_logs_with_verifier_exception(self, integrity_manager): """If verifier.calculate_checksum raises, actual_checksum stays None.""" mgr, session = integrity_manager v = _make_verifier() v.calculate_checksum.side_effect = IOError("cannot read") mgr.register_verifier(v) record = _make_record(id=1) path = MagicMock(spec=Path) path.exists.return_value = True session.query.return_value.filter_by.return_value.count.return_value = 0 mgr._log_failure(record, path, "error", session) failure_obj = session.add.call_args[0][0] assert failure_obj.actual_checksum is None def test_triggers_global_cleanup_on_id_mod_100(self, integrity_manager): mgr, session = integrity_manager record = _make_record(id=200) # 200 % 100 == 0 path = MagicMock(spec=Path) path.exists.return_value = False # Per-file cleanup: count under limit session.query.return_value.filter_by.return_value.count.return_value = 0 # Global cleanup: count under threshold session.query.return_value.count.return_value = 5000 mgr._log_failure(record, path, "test", session) # count() was called for both per-file and global checks assert ( session.query.return_value.count.called or session.query.return_value.filter_by.return_value.count.called ) # --------------------------------------------------------------------------- # Tests: _cleanup_old_failures # --------------------------------------------------------------------------- class TestCleanupOldFailures: def test_no_cleanup_when_under_limit(self, integrity_manager): mgr, session = integrity_manager record = _make_record(id=1) session.query.return_value.filter_by.return_value.count.return_value = ( 50 ) mgr._cleanup_old_failures(record, session) # Should not attempt to delete session.delete.assert_not_called() def test_cleanup_when_over_limit(self, integrity_manager): mgr, session = integrity_manager record = _make_record(id=1) session.query.return_value.filter_by.return_value.count.return_value = ( 110 ) old_failures = [MagicMock() for _ in range(10)] session.query.return_value.filter_by.return_value.order_by.return_value.limit.return_value.all.return_value = old_failures mgr._cleanup_old_failures(record, session) assert session.delete.call_count == 10 def test_cleanup_at_exactly_limit(self, integrity_manager): """Exactly at MAX_FAILURES_PER_FILE should not trigger cleanup.""" mgr, session = integrity_manager record = _make_record(id=1) session.query.return_value.filter_by.return_value.count.return_value = ( 100 ) mgr._cleanup_old_failures(record, session) session.delete.assert_not_called() # --------------------------------------------------------------------------- # Tests: _check_global_cleanup_needed # --------------------------------------------------------------------------- class TestCheckGlobalCleanupNeeded: def test_no_cleanup_under_threshold(self, integrity_manager): mgr, session = integrity_manager # threshold = 10000 * 1.2 = 12000 session.query.return_value.count.return_value = 11000 mgr._check_global_cleanup_needed(session) session.delete.assert_not_called() def test_cleanup_over_threshold(self, integrity_manager): mgr, session = integrity_manager session.query.return_value.count.return_value = 13000 # > 12000 old_failures = [MagicMock() for _ in range(3000)] # 13000 - 10000 session.query.return_value.order_by.return_value.limit.return_value.all.return_value = old_failures mgr._check_global_cleanup_needed(session) assert session.delete.call_count == 3000 def test_exactly_at_threshold_no_cleanup(self, integrity_manager): mgr, session = integrity_manager session.query.return_value.count.return_value = 12000 # == threshold mgr._check_global_cleanup_needed(session) session.delete.assert_not_called() # --------------------------------------------------------------------------- # Tests: record_file # --------------------------------------------------------------------------- class TestRecordFile: def test_new_file(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier( checksum="newcs", file_type="faiss_index", algorithm="sha256" ) mgr.register_verifier(v) path = MagicMock(spec=Path) path.exists.return_value = True path.resolve.return_value = Path("/resolved/new_file") path.stat.return_value.st_size = 4096 path.stat.return_value.st_mtime = 2000.0 # No existing record session.query.return_value.filter_by.return_value.first.return_value = ( None ) mgr.record_file( path, related_entity_type="rag_index", related_entity_id=42 ) session.add.assert_called_once() session.commit.assert_called_once() session.refresh.assert_called_once() def test_existing_file_updates(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier(checksum="updatedcs") mgr.register_verifier(v) path = MagicMock(spec=Path) path.exists.return_value = True path.resolve.return_value = Path("/resolved/existing") path.stat.return_value.st_size = 8192 path.stat.return_value.st_mtime = 3000.0 existing_record = _make_record() session.query.return_value.filter_by.return_value.first.return_value = ( existing_record ) mgr.record_file(path) assert existing_record.checksum == "updatedcs" assert existing_record.file_size == 8192 assert existing_record.file_mtime == 3000.0 session.commit.assert_called_once() # session.add should NOT be called for updates session.add.assert_not_called() def test_file_not_found(self, integrity_manager): mgr, _ = integrity_manager path = MagicMock(spec=Path) path.exists.return_value = False with pytest.raises(FileNotFoundError, match="File not found"): mgr.record_file(path) def test_no_verifier(self, integrity_manager): mgr, _ = integrity_manager path = MagicMock(spec=Path) path.exists.return_value = True # No verifiers registered with pytest.raises(ValueError, match="No verifier registered"): mgr.record_file(path) # --------------------------------------------------------------------------- # Tests: verify_file # --------------------------------------------------------------------------- class TestVerifyFile: def test_no_record_rejects_file(self, integrity_manager): # Reject-unknown: a file with no integrity record must fail closed # rather than being auto-registered (trust-on-first-use gap). The # security property is that record_file is NOT called on this path. mgr, session = integrity_manager v = _make_verifier(checksum="cs") mgr.register_verifier(v) path = MagicMock(spec=Path) path.exists.return_value = True path.resolve.return_value = Path("/resolved/path") path.stat.return_value.st_size = 100 path.stat.return_value.st_mtime = 500.0 # No record found -> must be rejected, not auto-created session.query.return_value.filter_by.return_value.first.return_value = ( None ) with patch.object(mgr, "record_file") as mock_record: passed, reason = mgr.verify_file(path) assert passed is False assert reason == "no_integrity_record" mock_record.assert_not_called() def test_no_record_rejects_even_without_verifier(self, integrity_manager): # Rejection happens before any verifier lookup, so a missing record # is rejected identically whether or not a verifier is registered. mgr, session = integrity_manager path = MagicMock(spec=Path) path.exists.return_value = True path.resolve.return_value = Path("/resolved/path") # No record found, no verifiers registered session.query.return_value.filter_by.return_value.first.return_value = ( None ) with patch.object(mgr, "record_file") as mock_record: passed, reason = mgr.verify_file(path) assert passed is False assert reason == "no_integrity_record" mock_record.assert_not_called() def test_skips_if_unchanged(self, integrity_manager): mgr, session = integrity_manager record = _make_record(file_mtime=1000.0) session.query.return_value.filter_by.return_value.first.return_value = ( record ) path = MagicMock(spec=Path) path.exists.return_value = True path.resolve.return_value = Path("/resolved/path") path.stat.return_value.st_mtime = 1000.0 # unchanged passed, reason = mgr.verify_file(path) assert passed is True assert reason is None def test_verify_passes(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier(checksum="abc123") mgr.register_verifier(v) record = _make_record( checksum="abc123", file_mtime=1000.0, total_verifications=5, consecutive_successes=2, consecutive_failures=0, ) session.query.return_value.filter_by.return_value.first.return_value = ( record ) path = MagicMock(spec=Path) path.exists.return_value = True path.resolve.return_value = Path("/resolved/path") path.stat.return_value.st_mtime = ( 2000.0 # changed, triggers verification ) passed, reason = mgr.verify_file(path, force=True) assert passed is True assert reason is None session.commit.assert_called() def test_verify_fails(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier(checksum="wrong") mgr.register_verifier(v) record = _make_record( id=1, checksum="abc123", file_mtime=1000.0, total_verifications=5, consecutive_successes=2, consecutive_failures=0, ) session.query.return_value.filter_by.return_value.first.return_value = ( record ) # Per-file cleanup count session.query.return_value.filter_by.return_value.count.return_value = 0 path = MagicMock(spec=Path) path.exists.return_value = True path.resolve.return_value = Path("/resolved/path") path.stat.return_value.st_mtime = 2000.0 path.stat.return_value.st_size = 1024 passed, reason = mgr.verify_file(path, force=True) assert passed is False assert reason == "checksum_mismatch" session.commit.assert_called() def test_force_overrides_skip(self, integrity_manager): """force=True should verify even when mtime unchanged.""" mgr, session = integrity_manager v = _make_verifier(checksum="abc123") mgr.register_verifier(v) record = _make_record(checksum="abc123", file_mtime=1000.0) session.query.return_value.filter_by.return_value.first.return_value = ( record ) path = MagicMock(spec=Path) path.exists.return_value = True path.resolve.return_value = Path("/resolved/path") path.stat.return_value.st_mtime = 1000.0 # unchanged passed, reason = mgr.verify_file(path, force=True) assert passed is True # Should have called _do_verification (verifier was used) v.calculate_checksum.assert_called_once() # --------------------------------------------------------------------------- # Tests: update_checksum # --------------------------------------------------------------------------- class TestUpdateChecksum: def test_success(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier(checksum="newcs") mgr.register_verifier(v) record = _make_record() session.query.return_value.filter_by.return_value.first.return_value = ( record ) path = MagicMock(spec=Path) path.exists.return_value = True path.stat.return_value.st_size = 5000 path.stat.return_value.st_mtime = 9000.0 mgr.update_checksum(path) assert record.checksum == "newcs" assert record.file_size == 5000 assert record.file_mtime == 9000.0 session.commit.assert_called_once() def test_file_not_found(self, integrity_manager): mgr, _ = integrity_manager path = MagicMock(spec=Path) path.exists.return_value = False with pytest.raises(FileNotFoundError): mgr.update_checksum(path) def test_no_verifier(self, integrity_manager): mgr, _ = integrity_manager path = MagicMock(spec=Path) path.exists.return_value = True with pytest.raises(ValueError, match="No verifier registered"): mgr.update_checksum(path) def test_no_record(self, integrity_manager): mgr, session = integrity_manager v = _make_verifier(checksum="cs") mgr.register_verifier(v) session.query.return_value.filter_by.return_value.first.return_value = ( None ) path = MagicMock(spec=Path) path.exists.return_value = True path.stat.return_value.st_size = 100 path.stat.return_value.st_mtime = 100.0 with pytest.raises(ValueError, match="No integrity record exists"): mgr.update_checksum(path) # --------------------------------------------------------------------------- # Tests: get_file_stats # --------------------------------------------------------------------------- class TestGetFileStats: def test_with_record(self, integrity_manager): mgr, session = integrity_manager record = _make_record( total_verifications=10, last_verified_at=datetime(2025, 6, 1, tzinfo=UTC), last_verification_passed=True, consecutive_successes=5, consecutive_failures=0, file_type="faiss_index", created_at=datetime(2025, 1, 1, tzinfo=UTC), ) session.query.return_value.filter_by.return_value.first.return_value = ( record ) result = mgr.get_file_stats(Path("/some/file")) assert result is not None assert result["total_verifications"] == 10 assert result["consecutive_successes"] == 5 assert result["consecutive_failures"] == 0 assert result["file_type"] == "faiss_index" assert result["last_verification_passed"] is True def test_without_record(self, integrity_manager): mgr, session = integrity_manager session.query.return_value.filter_by.return_value.first.return_value = ( None ) result = mgr.get_file_stats(Path("/nonexistent")) assert result is None # --------------------------------------------------------------------------- # Tests: get_failure_history # --------------------------------------------------------------------------- class TestGetFailureHistory: def test_with_records(self, integrity_manager): mgr, session = integrity_manager record = _make_record(id=1) session.query.return_value.filter_by.return_value.first.return_value = ( record ) failures = [MagicMock(), MagicMock()] session.query.return_value.filter_by.return_value.order_by.return_value.limit.return_value.all.return_value = failures result = mgr.get_failure_history(Path("/some/file"), limit=50) assert len(result) == 2 # Each failure should be expunged from session assert session.expunge.call_count == 2 def test_without_record(self, integrity_manager): mgr, session = integrity_manager session.query.return_value.filter_by.return_value.first.return_value = ( None ) result = mgr.get_failure_history(Path("/no/record")) assert result == [] def test_default_limit(self, integrity_manager): """Default limit should be 100.""" mgr, session = integrity_manager record = _make_record(id=1) session.query.return_value.filter_by.return_value.first.return_value = ( record ) session.query.return_value.filter_by.return_value.order_by.return_value.limit.return_value.all.return_value = [] mgr.get_failure_history(Path("/file")) # Verify limit was called via the chain session.query.return_value.filter_by.return_value.order_by.return_value.limit.assert_called() # --------------------------------------------------------------------------- # Tests: cleanup_all_old_failures # --------------------------------------------------------------------------- class TestCleanupAllOldFailures: def test_under_limit(self, integrity_manager): mgr, session = integrity_manager session.query.return_value.count.return_value = 5000 # under 10000 result = mgr.cleanup_all_old_failures() assert result == 0 session.delete.assert_not_called() def test_over_limit(self, integrity_manager): mgr, session = integrity_manager session.query.return_value.count.return_value = 10500 old_failures = [MagicMock() for _ in range(500)] session.query.return_value.order_by.return_value.limit.return_value.all.return_value = old_failures result = mgr.cleanup_all_old_failures() assert result == 500 assert session.delete.call_count == 500 session.commit.assert_called() def test_exactly_at_limit(self, integrity_manager): mgr, session = integrity_manager session.query.return_value.count.return_value = 10000 result = mgr.cleanup_all_old_failures() assert result == 0 # --------------------------------------------------------------------------- # Tests: get_total_failure_count # --------------------------------------------------------------------------- class TestGetTotalFailureCount: def test_returns_count(self, integrity_manager): mgr, session = integrity_manager session.query.return_value.count.return_value = 42 result = mgr.get_total_failure_count() assert result == 42 def test_returns_zero(self, integrity_manager): mgr, session = integrity_manager session.query.return_value.count.return_value = 0 result = mgr.get_total_failure_count() assert result == 0