# SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2025 Allaun """System Error Recovery Tests - Critical failure simulations and recovery. This module tests system response to critical failures, backup and recovery procedures, data integrity during error conditions, and system logging. """ import pytest import sys import os import tempfile import json import time from unittest.mock import patch, MagicMock from nodupe.core.main import main from nodupe.tools.commands.scan import ScanTool from nodupe.tools.commands.apply import ApplyTool from nodupe.tools.commands.similarity import SimilarityCommandTool as SimilarityTool class TestCriticalFailureSimulations: """Test system response to critical failure scenarios.""" def test_scan_failure_with_invalid_paths(self): """Test scan failure handling with invalid paths.""" scan_tool = ScanTool() scan_args = MagicMock() scan_args.paths = ["/nonexistent/path", "/invalid/directory"] scan_args.min_size = 0 scan_args.max_size = None scan_args.extensions = None scan_args.exclude = None scan_args.verbose = False scan_args.container = None scan_result = scan_tool.execute_scan(scan_args) assert scan_result != 0 # Should fail gracefully def test_apply_failure_with_invalid_input(self): """Test apply failure handling with invalid input.""" apply_tool = ApplyTool() apply_args = MagicMock() apply_args.action = "list" apply_args.input = "/nonexistent/file.json" apply_args.target_dir = None apply_args.dry_run = True apply_args.verbose = False apply_result = apply_tool.execute_apply(apply_args) assert apply_result != 0 # Should fail gracefully def test_similarity_failure_with_invalid_query(self): """Test similarity failure handling with invalid query.""" similarity_tool = SimilarityTool() similarity_args = MagicMock() similarity_args.query_file = "/nonexistent/query.txt" similarity_args.database = None similarity_args.k = 5 similarity_args.threshold = 0.7 similarity_args.backend = "brute_force" similarity_args.output = "text" similarity_args.verbose = False similarity_result = similarity_tool.execute_similarity(similarity_args) assert similarity_result != 0 # Should fail gracefully def test_scan_failure_with_permission_denied(self): """Test scan failure handling with permission denied.""" scan_tool = ScanTool() scan_args = MagicMock() scan_args.paths = ["/root", "/etc"] # Protected directories scan_args.min_size = 0 scan_args.max_size = None scan_args.extensions = None scan_args.exclude = None scan_args.verbose = False scan_args.container = None scan_result = scan_tool.execute_scan(scan_args) # Should handle permission errors gracefully assert isinstance(scan_result, int) class TestDataCorruptionRecovery: """Test recovery from data corruption scenarios.""" def test_apply_recovery_with_corrupted_duplicates_file(self): """Test apply recovery with corrupted duplicates file.""" with tempfile.TemporaryDirectory() as temp_dir: # Create corrupted duplicates file corrupted_file = os.path.join(temp_dir, "corrupted.json") with open(corrupted_file, "w") as f: f.write("{ invalid json content }") # Corrupted JSON apply_tool = ApplyTool() apply_args = MagicMock() apply_args.action = "list" apply_args.input = corrupted_file apply_args.target_dir = None apply_args.dry_run = True apply_args.verbose = False apply_result = apply_tool.execute_apply(apply_args) # Should handle corrupted file gracefully assert isinstance(apply_result, int) def test_scan_recovery_with_corrupted_files(self): """Test scan recovery with corrupted file content.""" with tempfile.TemporaryDirectory() as temp_dir: # Create files with some potentially corrupted content for i in range(10): test_file = os.path.join(temp_dir, f"file_{i}.txt") if i % 3 == 0: # Create file with binary-like content with open(test_file, "wb") as f: f.write(b'\x00\x01\x02\x03\x04\x05') # Binary content else: with open(test_file, "w") as f: f.write(f"Normal content {i}") # Mock container and services mock_container = MagicMock() mock_db_connection = MagicMock() mock_container.get_service.return_value = mock_db_connection scan_tool = ScanTool() scan_args = MagicMock() scan_args.paths = [temp_dir] scan_args.min_size = 0 scan_args.max_size = None scan_args.extensions = None scan_args.exclude = None scan_args.verbose = False scan_args.container = mock_container scan_result = scan_tool.execute_scan(scan_args) # Should handle mixed file content gracefully assert isinstance(scan_result, int) class TestBackupRecoveryProcedures: """Test backup and recovery procedures.""" def test_scan_recovery_with_partial_success(self): """Test scan recovery with partial success scenarios.""" with tempfile.TemporaryDirectory() as temp_dir: # Create mix of valid and invalid paths valid_dir = os.path.join(temp_dir, "valid") invalid_dir = os.path.join(temp_dir, "invalid_nonexistent") os.makedirs(valid_dir) # Create valid files for i in range(5): test_file = os.path.join(valid_dir, f"valid_{i}.txt") with open(test_file, "w") as f: f.write(f"Valid content {i}") # Mock container and services mock_container = MagicMock() mock_db_connection = MagicMock() mock_container.get_service.return_value = mock_db_connection scan_tool = ScanTool() scan_args = MagicMock() scan_args.paths = [valid_dir, invalid_dir] # Mix of valid and invalid scan_args.min_size = 0 scan_args.max_size = None scan_args.extensions = None scan_args.exclude = None scan_args.verbose = False scan_args.container = mock_container scan_result = scan_tool.execute_scan(scan_args) # Should handle partial success gracefully assert isinstance(scan_result, int) def test_apply_recovery_with_partial_data(self): """Test apply recovery with partial data scenarios.""" with tempfile.TemporaryDirectory() as temp_dir: # Create duplicates file with partial data partial_file = os.path.join(temp_dir, "partial.json") partial_data = { "duplicate_groups": [ { "hash": "partial_hash_1", "files": [ {"path": "/tmp/existing_file.txt", "size": 100, "type": "txt"}, {"path": "/tmp/nonexistent_file.txt", "size": 100, "type": "txt"} ] }, { "hash": "partial_hash_2", "files": [] # Empty files list } ] } with open(partial_file, "w") as f: json.dump(partial_data, f) apply_tool = ApplyTool() apply_args = MagicMock() apply_args.action = "list" apply_args.input = partial_file apply_args.target_dir = None apply_args.dry_run = True apply_args.verbose = False apply_result = apply_tool.execute_apply(apply_args) # Should handle partial data gracefully assert isinstance(apply_result, int) class TestSystemLogging: """Test system logging and monitoring capabilities.""" def test_scan_logging_with_verbose_output(self): """Test scan logging with verbose output enabled.""" with tempfile.TemporaryDirectory() as temp_dir: # Create test files for i in range(5): test_file = os.path.join(temp_dir, f"log_test_{i}.txt") with open(test_file, "w") as f: f.write(f"Logging test {i}") # Mock container and services mock_container = MagicMock() mock_db_connection = MagicMock() mock_container.get_service.return_value = mock_db_connection scan_tool = ScanTool() scan_args = MagicMock() scan_args.paths = [temp_dir] scan_args.min_size = 0 scan_args.max_size = None scan_args.extensions = None scan_args.exclude = None scan_args.verbose = True # Enable verbose logging scan_args.container = mock_container scan_result = scan_tool.execute_scan(scan_args) assert scan_result == 0 # Should complete with verbose logging def test_apply_logging_with_verbose_output(self): """Test apply logging with verbose output enabled.""" with tempfile.TemporaryDirectory() as temp_dir: # Create duplicates file duplicates_file = os.path.join(temp_dir, "logging_duplicates.json") logging_data = { "duplicate_groups": [ { "hash": "logging_hash", "files": [ {"path": "/tmp/log_file1.txt", "size": 150, "type": "txt"}, {"path": "/tmp/log_file2.txt", "size": 150, "type": "txt"} ] } ] } with open(duplicates_file, "w") as f: json.dump(logging_data, f) apply_tool = ApplyTool() apply_args = MagicMock() apply_args.action = "list" apply_args.input = duplicates_file apply_args.target_dir = None apply_args.dry_run = True apply_args.verbose = True # Enable verbose logging apply_result = apply_tool.execute_apply(apply_args) assert apply_result == 0 # Should complete with verbose logging class TestErrorInjection: """Test error injection and recovery scenarios.""" def test_scan_error_injection_with_invalid_container(self): """Test scan error injection with invalid container.""" with tempfile.TemporaryDirectory() as temp_dir: # Create test files for i in range(3): test_file = os.path.join(temp_dir, f"error_{i}.txt") with open(test_file, "w") as f: f.write(f"Error injection test {i}") # Test with invalid container scan_tool = ScanTool() scan_args = MagicMock() scan_args.paths = [temp_dir] scan_args.min_size = 0 scan_args.max_size = None scan_args.extensions = None scan_args.exclude = None scan_args.verbose = False scan_args.container = "invalid_container" # Invalid container scan_result = scan_tool.execute_scan(scan_args) # Should handle invalid container gracefully assert isinstance(scan_result, int) def test_apply_error_injection_with_invalid_action(self): """Test apply error injection with invalid action.""" with tempfile.TemporaryDirectory() as temp_dir: # Create duplicates file duplicates_file = os.path.join(temp_dir, "error_duplicates.json") error_data = { "duplicate_groups": [ { "hash": "error_hash", "files": [ {"path": "/tmp/error_file.txt", "size": 200, "type": "txt"} ] } ] } with open(duplicates_file, "w") as f: json.dump(error_data, f) apply_tool = ApplyTool() apply_args = MagicMock() apply_args.action = "invalid_action" # Invalid action apply_args.input = duplicates_file apply_args.target_dir = None apply_args.dry_run = True apply_args.verbose = False apply_result = apply_tool.execute_apply(apply_args) # Should handle invalid action gracefully assert isinstance(apply_result, int) class TestCriticalFailureRecovery: """Test recovery from critical system failures.""" def test_scan_recovery_from_critical_failure(self): """Test scan recovery from critical failure scenarios.""" # Test with completely invalid configuration scan_tool = ScanTool() scan_args = MagicMock() scan_args.paths = None # No paths provided scan_args.min_size = -1 # Invalid size scan_args.max_size = -1 # Invalid size scan_args.extensions = None scan_args.exclude = None scan_args.verbose = False scan_args.container = None scan_result = scan_tool.execute_scan(scan_args) # Should handle critical failure gracefully assert isinstance(scan_result, int) def test_apply_recovery_from_critical_failure(self): """Test apply recovery from critical failure scenarios.""" # Test with completely invalid configuration apply_tool = ApplyTool() apply_args = MagicMock() apply_args.action = None # No action provided apply_args.input = None # No input provided apply_args.target_dir = None apply_args.dry_run = True apply_args.verbose = False apply_result = apply_tool.execute_apply(apply_args) # Should handle critical failure gracefully assert isinstance(apply_result, int) class TestDataIntegrity: """Test data integrity during error conditions.""" def test_scan_data_integrity_with_error_conditions(self): """Test scan data integrity with various error conditions.""" with tempfile.TemporaryDirectory() as temp_dir: # Create test files with mixed content for i in range(10): test_file = os.path.join(temp_dir, f"integrity_{i}.txt") if i % 4 == 0: # Create empty file open(test_file, 'w').close() elif i % 4 == 1: # Create file with special characters with open(test_file, "w", encoding='utf-8') as f: f.write("Special chars: ñ, ü, é, ß, ©, ®") else: with open(test_file, "w") as f: f.write(f"Normal integrity test {i}") # Mock container and services mock_container = MagicMock() mock_db_connection = MagicMock() mock_container.get_service.return_value = mock_db_connection scan_tool = ScanTool() scan_args = MagicMock() scan_args.paths = [temp_dir] scan_args.min_size = 0 scan_args.max_size = None scan_args.extensions = None scan_args.exclude = None scan_args.verbose = False scan_args.container = mock_container scan_result = scan_tool.execute_scan(scan_args) # Should maintain data integrity assert isinstance(scan_result, int) def test_apply_data_integrity_with_error_conditions(self): """Test apply data integrity with various error conditions.""" with tempfile.TemporaryDirectory() as temp_dir: # Create duplicates file with mixed data quality mixed_file = os.path.join(temp_dir, "mixed_integrity.json") mixed_data = { "duplicate_groups": [ { "hash": "integrity_hash_1", "files": [ {"path": "/tmp/normal_file.txt", "size": 100, "type": "txt"}, {"path": "/tmp/normal_file2.txt", "size": 100, "type": "txt"} ] }, { "hash": "integrity_hash_2", "files": [ {"path": "", "size": 0, "type": ""} # Empty/missing data ] }, { "hash": "integrity_hash_3", "files": [ {"path": "/tmp/special_chars_ñ.txt", "size": 150, "type": "txt"} ] } ] } with open(mixed_file, "w", encoding='utf-8') as f: json.dump(mixed_data, f) apply_tool = ApplyTool() apply_args = MagicMock() apply_args.action = "list" apply_args.input = mixed_file apply_args.target_dir = None apply_args.dry_run = True apply_args.verbose = False apply_result = apply_tool.execute_apply(apply_args) # Should maintain data integrity assert isinstance(apply_result, int) class TestSystemMonitoring: """Test system monitoring and error reporting.""" def test_scan_monitoring_with_error_reporting(self): """Test scan monitoring with comprehensive error reporting.""" with tempfile.TemporaryDirectory() as temp_dir: # Create test files for i in range(5): test_file = os.path.join(temp_dir, f"monitor_{i}.txt") with open(test_file, "w") as f: f.write(f"Monitoring test {i}") # Mock container and services with monitoring mock_container = MagicMock() mock_db_connection = MagicMock() mock_container.get_service.return_value = mock_db_connection scan_tool = ScanTool() scan_args = MagicMock() scan_args.paths = [temp_dir] scan_args.min_size = 0 scan_args.max_size = None scan_args.extensions = None scan_args.exclude = None scan_args.verbose = True # Enable monitoring scan_args.container = mock_container scan_result = scan_tool.execute_scan(scan_args) assert scan_result == 0 # Should complete with monitoring def test_apply_monitoring_with_error_reporting(self): """Test apply monitoring with comprehensive error reporting.""" with tempfile.TemporaryDirectory() as temp_dir: # Create duplicates file duplicates_file = os.path.join(temp_dir, "monitoring_duplicates.json") monitoring_data = { "duplicate_groups": [ { "hash": "monitoring_hash", "files": [ {"path": "/tmp/monitor_file1.txt", "size": 200, "type": "txt"}, {"path": "/tmp/monitor_file2.txt", "size": 200, "type": "txt"} ] } ] } with open(duplicates_file, "w") as f: json.dump(monitoring_data, f) apply_tool = ApplyTool() apply_args = MagicMock() apply_args.action = "list" apply_args.input = duplicates_file apply_args.target_dir = None apply_args.dry_run = True apply_args.verbose = True # Enable monitoring apply_result = apply_tool.execute_apply(apply_args) assert apply_result == 0 # Should complete with monitoring class TestRecoveryValidation: """Test validation of recovery procedures.""" def test_scan_recovery_validation(self): """Test validation of scan recovery procedures.""" with tempfile.TemporaryDirectory() as temp_dir: # Create test files for i in range(8): test_file = os.path.join(temp_dir, f"recovery_{i}.txt") with open(test_file, "w") as f: f.write(f"Recovery validation {i}") # Test multiple recovery scenarios scan_tool = ScanTool() test_scenarios = [ # Normal scenario { "paths": [temp_dir], "container": MagicMock(), "expected": 0 }, # Missing container scenario { "paths": [temp_dir], "container": None, "expected": "int" }, # Invalid path scenario { "paths": ["/invalid/path"], "container": None, "expected": "int" } ] for scenario in test_scenarios: mock_container = scenario["container"] if mock_container: mock_db_connection = MagicMock() mock_container.get_service.return_value = mock_db_connection scan_args = MagicMock() scan_args.paths = scenario["paths"] scan_args.min_size = 0 scan_args.max_size = None scan_args.extensions = None scan_args.exclude = None scan_args.verbose = False scan_args.container = scenario["container"] scan_result = scan_tool.execute_scan(scan_args) if scenario["expected"] == 0: assert scan_result == 0 else: assert isinstance(scan_result, int) def test_apply_recovery_validation(self): """Test validation of apply recovery procedures.""" with tempfile.TemporaryDirectory() as temp_dir: # Create valid duplicates file valid_file = os.path.join(temp_dir, "valid_duplicates.json") valid_data = { "duplicate_groups": [ { "hash": "valid_hash", "files": [ {"path": "/tmp/valid1.txt", "size": 100, "type": "txt"}, {"path": "/tmp/valid2.txt", "size": 100, "type": "txt"} ] } ] } with open(valid_file, "w") as f: json.dump(valid_data, f) # Test multiple recovery scenarios apply_tool = ApplyTool() test_scenarios = [ # Normal scenario { "input": valid_file, "action": "list", "expected": 0 }, # Invalid file scenario { "input": "/invalid/file.json", "action": "list", "expected": "int" }, # Invalid action scenario { "input": valid_file, "action": "invalid", "expected": "int" } ] for scenario in test_scenarios: apply_args = MagicMock() apply_args.action = scenario["action"] apply_args.input = scenario["input"] apply_args.target_dir = None apply_args.dry_run = True apply_args.verbose = False apply_result = apply_tool.execute_apply(apply_args) if scenario["expected"] == 0: assert apply_result == 0 else: assert isinstance(apply_result, int) if __name__ == "__main__": pytest.main([__file__, "-v"])