""" Unit tests for error_handling module. Tests custom exceptions, ErrorHandler, and error sanitization. """ import pytest import logging from unittest.mock import Mock, patch from datetime import datetime from error_handling import ( JournalOrganizerError, ConfigurationError, APIError, ValidationError, FileSystemError, SecurityError, ErrorContext, ErrorHandler, get_error_handler, set_error_handler, audit_error_message_security ) class TestJournalOrganizerError: """Test base JournalOrganizerError class""" def test_basic_error_creation(self): """Test creating basic error""" error = JournalOrganizerError("Test error message") assert str(error) == "Test error message" assert error.message == "Test error message" assert error.context == {} assert error.cause is None assert error.timestamp is not None def test_error_with_context(self): """Test creating error with context""" context = {"key": "value", "number": 42} error = JournalOrganizerError("Test error", context=context) assert error.context == context def test_error_with_cause(self): """Test creating error with cause""" original_error = ValueError("Original error") error = JournalOrganizerError("Wrapped error", cause=original_error) assert error.cause == original_error def test_to_dict(self): """Test converting error to dictionary""" context = {"test_key": "test_value"} original_error = RuntimeError("Original") error = JournalOrganizerError("Test error", context=context, cause=original_error) error_dict = error.to_dict() assert error_dict["error_type"] == "JournalOrganizerError" assert error_dict["message"] == "Test error" assert error_dict["context"] == context assert error_dict["cause"] == "Original" assert "timestamp" in error_dict class TestConfigurationError: """Test ConfigurationError class""" def test_basic_configuration_error(self): """Test basic configuration error""" error = ConfigurationError("Config error", config_key="api_key") assert error.message == "Config error" assert error.context["config_key"] == "api_key" def test_configuration_error_with_sensitive_value(self): """Test configuration error with sensitive value redaction""" error = ConfigurationError( "Invalid API key", config_key="api_key", config_value="sk-secret-key-123" ) assert error.context["config_key"] == "api_key" assert error.context["config_value"] == "[REDACTED]" def test_configuration_error_with_non_sensitive_value(self): """Test configuration error with non-sensitive value""" error = ConfigurationError( "Invalid timeout", config_key="timeout", config_value="30" ) assert error.context["config_key"] == "timeout" assert error.context["config_value"] == "30" class TestAPIError: """Test APIError class""" def test_basic_api_error(self): """Test basic API error""" error = APIError("API call failed", api_name="claude", status_code=500) assert error.message == "API call failed" assert error.context["api_name"] == "claude" assert error.context["status_code"] == 500 def test_api_error_with_long_response(self): """Test API error with long response data truncation""" long_response = "x" * 1000 error = APIError("API error", response_data=long_response) assert len(error.context["response_data"]) <= 503 # 500 + "..." assert error.context["response_data"].endswith("...") def test_api_error_with_short_response(self): """Test API error with short response data""" short_response = "Short error" error = APIError("API error", response_data=short_response) assert error.context["response_data"] == short_response class TestValidationError: """Test ValidationError class""" def test_basic_validation_error(self): """Test basic validation error""" error = ValidationError( "Invalid email", field_name="email", field_value="invalid-email", validation_rule="email_format" ) assert error.message == "Invalid email" assert error.context["field_name"] == "email" assert error.context["field_value"] == "invalid-email" assert error.context["validation_rule"] == "email_format" def test_validation_error_with_sensitive_field(self): """Test validation error with sensitive field value redaction""" error = ValidationError( "Invalid password", field_name="password", field_value="secret123" ) assert error.context["field_name"] == "password" assert error.context["field_value"] == "[REDACTED]" def test_validation_error_with_long_value(self): """Test validation error with long field value truncation""" long_value = "x" * 200 error = ValidationError( "Invalid input", field_name="description", field_value=long_value ) assert len(error.context["field_value"]) == 100 # Truncated to 100 chars class TestFileSystemError: """Test FileSystemError class""" def test_basic_filesystem_error(self): """Test basic filesystem error""" error = FileSystemError( "File not found", file_path="/path/to/file.txt", operation="read" ) assert error.message == "File not found" assert error.context["file_path"] == "/path/to/file.txt" assert error.context["operation"] == "read" class TestSecurityError: """Test SecurityError class""" def test_basic_security_error(self): """Test basic security error""" error = SecurityError( "Path traversal detected", security_issue="path_traversal", attempted_path="../../../etc/passwd", risk_level="critical" ) assert error.message == "Path traversal detected" assert error.context["security_issue"] == "path_traversal" assert error.context["attempted_path"] == "../../../etc/passwd" assert error.context["risk_level"] == "critical" def test_security_error_with_long_path(self): """Test security error with long path truncation""" long_path = "/" + "x" * 600 error = SecurityError("Security violation", attempted_path=long_path) assert len(error.context["attempted_path"]) == 500 # Truncated class TestErrorContext: """Test ErrorContext class""" def test_valid_error_context(self): """Test creating valid error context""" context = ErrorContext( component="test_component", operation="test_operation", user_message="User friendly message", technical_details={"key": "value"}, severity="warning" ) assert context.component == "test_component" assert context.operation == "test_operation" assert context.user_message == "User friendly message" assert context.technical_details == {"key": "value"} assert context.severity == "warning" def test_error_context_defaults(self): """Test error context with default values""" context = ErrorContext( component="test_component", operation="test_operation" ) assert context.user_message == "" assert context.technical_details == {} assert context.severity == "error" def test_error_context_validation_empty_component(self): """Test error context validation with empty component""" with pytest.raises(ValueError, match="component cannot be empty"): ErrorContext(component="", operation="test_operation") def test_error_context_validation_empty_operation(self): """Test error context validation with empty operation""" with pytest.raises(ValueError, match="operation cannot be empty"): ErrorContext(component="test_component", operation="") def test_error_context_validation_invalid_severity(self): """Test error context validation with invalid severity""" with pytest.raises(ValueError, match="severity must be one of"): ErrorContext( component="test_component", operation="test_operation", severity="invalid" ) def test_error_context_validation_invalid_technical_details(self): """Test error context validation with invalid technical_details""" with pytest.raises(ValueError, match="technical_details must be a dictionary"): ErrorContext( component="test_component", operation="test_operation", technical_details="not a dict" ) def test_to_dict(self): """Test converting error context to dictionary""" context = ErrorContext( component="test_component", operation="test_operation", user_message="Test message", technical_details={"key": "value"}, severity="info" ) context_dict = context.to_dict() assert context_dict["component"] == "test_component" assert context_dict["operation"] == "test_operation" assert context_dict["user_message"] == "Test message" assert context_dict["technical_details"] == {"key": "value"} assert context_dict["severity"] == "info" class TestErrorHandler: """Test ErrorHandler class""" def setup_method(self): """Set up test fixtures""" self.mock_logger = Mock(spec=logging.Logger) self.error_handler = ErrorHandler(self.mock_logger) def test_error_handler_creation(self): """Test creating error handler""" handler = ErrorHandler() assert handler.logger is not None handler_with_logger = ErrorHandler(self.mock_logger) assert handler_with_logger.logger == self.mock_logger def test_handle_error_with_custom_error(self): """Test handling custom JournalOrganizerError""" error = ConfigurationError("Config error", config_key="api_key") context = ErrorContext( component="config", operation="load", user_message="Please check your configuration" ) result = self.error_handler.handle_error(error, context) assert result["success"] is False assert result["error"] == "Config error" assert result["message"] == "Please check your configuration" assert result["component"] == "config" assert result["operation"] == "load" assert result["error_type"] == "ConfigurationError" assert "timestamp" in result # Check that logger was called self.mock_logger.log.assert_called_once() def test_handle_error_with_generic_error(self): """Test handling generic Python exception""" error = ValueError("Generic error") context = ErrorContext( component="test", operation="test_op", severity="warning" ) result = self.error_handler.handle_error(error, context) assert result["success"] is False assert result["error"] == "Generic error" assert result["error_type"] == "ValueError" # Check that logger was called with warning level self.mock_logger.log.assert_called_once() call_args = self.mock_logger.log.call_args assert call_args[0][0] == logging.WARNING # Log level def test_handle_api_error(self): """Test handling API errors""" error = RuntimeError("Connection failed") result = self.error_handler.handle_api_error(error, "claude", "analyze_text") assert result["success"] is False assert "claude API error" in result["error"] assert result["component"] == "claude_api" assert result["operation"] == "analyze_text" assert "Failed to communicate with claude" in result["message"] def test_handle_api_error_with_api_error_instance(self): """Test handling APIError instance""" api_error = APIError("API failed", api_name="obsidian", status_code=404) result = self.error_handler.handle_api_error(api_error, "obsidian", "read_note") assert result["success"] is False assert result["error"] == "API failed" assert result["component"] == "obsidian_api" def test_handle_validation_error(self): """Test handling validation errors""" error = ValueError("Invalid format") result = self.error_handler.handle_validation_error(error, "email", "validate_input") assert result["success"] is False assert "Invalid email" in result["error"] assert result["component"] == "validation" assert result["operation"] == "validate_input" assert "Please check your email" in result["message"] def test_handle_configuration_error(self): """Test handling configuration errors""" error = ValueError("Missing key") result = self.error_handler.handle_configuration_error(error, "api_key") assert result["success"] is False assert "Configuration error for api_key" in result["error"] assert result["component"] == "configuration" assert result["operation"] == "load_config" assert "Please check your API key configuration" in result["message"] def test_sanitize_error_message_api_key(self): """Test sanitizing error messages with API keys""" message = "Error: api_key=sk-secret-key-123 is invalid" sanitized = self.error_handler._sanitize_error_message(message) assert "sk-secret-key-123" not in sanitized assert "api_key=[REDACTED]" in sanitized def test_sanitize_error_message_bearer_token(self): """Test sanitizing error messages with Bearer tokens""" message = "Authorization failed: Bearer abc123xyz789" sanitized = self.error_handler._sanitize_error_message(message) assert "abc123xyz789" not in sanitized assert "[REDACTED]" in sanitized def test_sanitize_error_message_email(self): """Test sanitizing error messages with email addresses""" message = "Failed to send email to user@example.com" sanitized = self.error_handler._sanitize_error_message(message) assert "user@example.com" not in sanitized assert "[EMAIL_REDACTED]" in sanitized def test_sanitize_error_message_file_paths(self): """Test sanitizing error messages with user file paths""" message = "Cannot access /Users/john/Documents/secret.txt" sanitized = self.error_handler._sanitize_error_message(message) assert "john" not in sanitized assert "[USER_REDACTED]" in sanitized def test_sanitize_context_data(self): """Test sanitizing context data""" data = { "api_key": "secret-key-123", "username": "john_doe", "password": "secret123", "timeout": 30, "nested": { "token": "bearer-token-xyz", "safe_value": "public_info" } } sanitized = self.error_handler._sanitize_context_data(data) assert sanitized["api_key"] == "[REDACTED]" assert sanitized["username"] == "john_doe" # Not sensitive assert sanitized["password"] == "[REDACTED]" assert sanitized["timeout"] == 30 assert sanitized["nested"]["token"] == "[REDACTED]" assert sanitized["nested"]["safe_value"] == "public_info" def test_sanitize_context_data_non_dict(self): """Test sanitizing non-dictionary context data""" result = self.error_handler._sanitize_context_data("not a dict") assert result == "not a dict" class TestGlobalErrorHandler: """Test global error handler functions""" def test_get_error_handler_singleton(self): """Test that get_error_handler returns singleton""" handler1 = get_error_handler() handler2 = get_error_handler() assert handler1 is handler2 def test_set_error_handler(self): """Test setting custom error handler""" custom_handler = ErrorHandler() set_error_handler(custom_handler) retrieved_handler = get_error_handler() assert retrieved_handler is custom_handler class TestAuditErrorMessageSecurity: """Test error message security auditing""" def test_audit_clean_message(self): """Test auditing clean message with no issues""" message = "Simple error message with no sensitive data" result = audit_error_message_security(message) assert result["has_issues"] is False assert result["issues"] == [] assert result["risk_level"] == "low" def test_audit_message_with_email(self): """Test auditing message with email address""" message = "Failed to send notification to user@example.com" result = audit_error_message_security(message) assert result["has_issues"] is True assert len(result["issues"]) == 1 assert result["issues"][0]["type"] == "email_address" assert result["risk_level"] == "medium" def test_audit_message_with_api_key(self): """Test auditing message with API key""" message = "Authentication failed: api_key=sk-secret-123" result = audit_error_message_security(message) assert result["has_issues"] is True assert any(issue["type"] == "credential_pattern" for issue in result["issues"]) assert result["risk_level"] == "high" def test_audit_message_with_bearer_token(self): """Test auditing message with Bearer token""" message = "Authorization header: Bearer abc123xyz789" result = audit_error_message_security(message) assert result["has_issues"] is True assert any(issue["type"] == "bearer_token" for issue in result["issues"]) assert result["risk_level"] == "high" def test_audit_message_with_ip_address(self): """Test auditing message with IP address""" message = "Connection failed to 192.168.1.100" result = audit_error_message_security(message) assert result["has_issues"] is True assert any(issue["type"] == "ip_address" for issue in result["issues"]) assert result["risk_level"] == "medium" def test_audit_message_with_multiple_issues(self): """Test auditing message with multiple security issues""" message = "Failed to connect to 192.168.1.100 with api_key=secret123 for user@example.com" result = audit_error_message_security(message) assert result["has_issues"] is True assert len(result["issues"]) >= 2 # Should find multiple issues assert result["risk_level"] == "high" # High due to credential pattern