Files
windyboy f7e54692a9 Initial project setup: Obsidian intelligent journal organizer
- Add core agent architecture with Command + Skill pattern
- Implement Claude API integration for content analysis
- Add Obsidian REST API integration for vault operations
- Create conversational interface (v2.0) with natural language processing
- Add comprehensive configuration management and validation
- Include project documentation and developer guides
- Set up testing framework with unit, integration, and property tests
- Add Kiro specs for Claude API configuration and code quality improvements
- Configure project steering files for development guidelines
2025-12-31 17:55:10 +08:00

526 lines
20 KiB
Python

"""
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