Skip to main content
Glama
test_exceptions.py13.8 kB
""" Property-based tests for exception classes. **Feature: service-layer-refactoring** **Validates: Requirements 5.2, 5.3, 5.4, 5.5** This module tests: - Property 11: Exception Serialization Round-Trip - Property 12: ValidationError Context Population """ from hypothesis import given, settings from hypothesis import strategies as st from banana_image_mcp.core.exceptions import ( ConfigurationError, ErrorCode, FileOperationError, GeminiAPIError, ImageProcessingError, NanoBananaError, ValidationError, ) # ============================================================================= # Hypothesis Strategies # ============================================================================= # Strategy for generating error codes error_code_strategy = st.sampled_from(list(ErrorCode)) # Strategy for generating context dictionaries context_strategy = st.dictionaries( keys=st.text(min_size=1, max_size=20, alphabet=st.characters(whitelist_categories=("L", "N"))), values=st.one_of( st.text(max_size=50), st.integers(), st.floats(allow_nan=False, allow_infinity=False), st.booleans(), st.none(), ), max_size=5, ) # Strategy for generating error messages message_strategy = st.text(min_size=1, max_size=200) # Strategy for generating field names field_strategy = st.text( min_size=1, max_size=50, alphabet=st.characters(whitelist_categories=("L", "N", "Pc")) ) # Strategy for generating field values (various types, excluding None) value_strategy = st.one_of( st.text(min_size=1, max_size=150), st.integers(), st.floats(allow_nan=False, allow_infinity=False), st.booleans(), st.lists(st.integers(), min_size=1, max_size=5), ) # Strategy for generating field values including None value_strategy_with_none = st.one_of( st.text(max_size=150), st.integers(), st.floats(allow_nan=False, allow_infinity=False), st.booleans(), st.none(), st.lists(st.integers(), max_size=5), ) # ============================================================================= # Property 11: Exception Serialization Round-Trip # ============================================================================= class TestExceptionSerializationRoundTrip: """ **Feature: service-layer-refactoring, Property 11: Exception Serialization Round-Trip** *For any* NanoBananaError with message, error_code, context, and cause, calling `to_dict()` SHALL produce a dictionary that contains all provided information, and `str()` SHALL format as "[ERROR_CODE] message" when error_code is provided. """ @given( message=message_strategy, error_code=st.one_of(st.none(), error_code_strategy), context=st.one_of(st.none(), context_strategy), ) @settings(max_examples=100) def test_to_dict_contains_all_information( self, message: str, error_code: ErrorCode | None, context: dict | None, ): """ **Feature: service-layer-refactoring, Property 11: Exception Serialization Round-Trip** to_dict() should contain all provided information. """ error = NanoBananaError( message=message, error_code=error_code, context=context, ) result = error.to_dict() # Type should always be present assert result["type"] == "NanoBananaError" # Message should always be present assert result["message"] == message # Code should be present only when error_code is provided if error_code is not None: assert "code" in result assert result["code"] == error_code.value else: assert "code" not in result # Context should be present only when non-empty if context: assert "context" in result assert result["context"] == context else: assert "context" not in result @given( message=message_strategy, error_code=error_code_strategy, ) @settings(max_examples=100) def test_str_format_with_error_code(self, message: str, error_code: ErrorCode): """ **Feature: service-layer-refactoring, Property 11: Exception Serialization Round-Trip** str() should format as "[ERROR_CODE] message" when error_code is provided. """ error = NanoBananaError(message=message, error_code=error_code) result = str(error) assert result == f"[{error_code.value}] {message}" @given(message=message_strategy) @settings(max_examples=100) def test_str_format_without_error_code(self, message: str): """ **Feature: service-layer-refactoring, Property 11: Exception Serialization Round-Trip** str() should return just the message when error_code is None. """ error = NanoBananaError(message=message, error_code=None) result = str(error) assert result == message @given( message=message_strategy, error_code=st.one_of(st.none(), error_code_strategy), context=st.one_of(st.none(), context_strategy), ) @settings(max_examples=100) def test_to_dict_with_cause_exception( self, message: str, error_code: ErrorCode | None, context: dict | None, ): """ **Feature: service-layer-refactoring, Property 11: Exception Serialization Round-Trip** to_dict() should include cause when provided. """ cause = ValueError("original error") error = NanoBananaError( message=message, error_code=error_code, context=context, cause=cause, ) result = error.to_dict() assert "cause" in result assert result["cause"] == str(cause) class TestSubclassSerializationRoundTrip: """Test that all exception subclasses properly serialize.""" @given( message=message_strategy, error_code=st.one_of(st.none(), error_code_strategy), ) @settings(max_examples=50) def test_configuration_error_serialization(self, message: str, error_code: ErrorCode | None): """ConfigurationError should serialize correctly.""" error = ConfigurationError(message=message, error_code=error_code) result = error.to_dict() assert result["type"] == "ConfigurationError" assert result["message"] == message @given( message=message_strategy, error_code=st.one_of(st.none(), error_code_strategy), ) @settings(max_examples=50) def test_gemini_api_error_serialization(self, message: str, error_code: ErrorCode | None): """GeminiAPIError should serialize correctly.""" error = GeminiAPIError(message=message, error_code=error_code) result = error.to_dict() assert result["type"] == "GeminiAPIError" assert result["message"] == message @given( message=message_strategy, error_code=st.one_of(st.none(), error_code_strategy), ) @settings(max_examples=50) def test_image_processing_error_serialization(self, message: str, error_code: ErrorCode | None): """ImageProcessingError should serialize correctly.""" error = ImageProcessingError(message=message, error_code=error_code) result = error.to_dict() assert result["type"] == "ImageProcessingError" assert result["message"] == message @given( message=message_strategy, error_code=st.one_of(st.none(), error_code_strategy), ) @settings(max_examples=50) def test_file_operation_error_serialization(self, message: str, error_code: ErrorCode | None): """FileOperationError should serialize correctly.""" error = FileOperationError(message=message, error_code=error_code) result = error.to_dict() assert result["type"] == "FileOperationError" assert result["message"] == message # ============================================================================= # Property 12: ValidationError Context Population # ============================================================================= class TestValidationErrorContextPopulation: """ **Feature: service-layer-refactoring, Property 12: ValidationError Context Population** *For any* ValidationError created with field and value parameters, the exception's context dictionary SHALL contain "field" and "value" keys with the provided values. """ @given( message=message_strategy, field=field_strategy, value=value_strategy, ) @settings(max_examples=100) def test_field_and_value_in_context( self, message: str, field: str, value, ): """ **Feature: service-layer-refactoring, Property 12: ValidationError Context Population** Field and value should be automatically added to context. """ error = ValidationError( message=message, field=field, value=value, ) # Field should be in context assert "field" in error.context assert error.context["field"] == field # Value should be in context (possibly truncated) assert "value" in error.context str_value = str(value) expected_value = str_value[:100] if len(str_value) > 100 else str_value assert error.context["value"] == expected_value # Field and value should also be accessible as attributes assert error.field == field assert error.value == value @given( message=message_strategy, field=field_strategy, value=st.one_of( st.text(min_size=1, max_size=150), # Non-empty text st.integers(), st.floats(allow_nan=False, allow_infinity=False), st.booleans(), st.lists(st.integers(), min_size=1, max_size=5), # Non-empty lists ), extra_context=context_strategy, ) @settings(max_examples=100) def test_field_and_value_merged_with_existing_context( self, message: str, field: str, value, extra_context: dict, ): """ **Feature: service-layer-refactoring, Property 12: ValidationError Context Population** Field and value should be merged with any existing context. """ error = ValidationError( message=message, context=extra_context, field=field, value=value, ) # Field and value should be present (value is not None in this test) assert "field" in error.context assert "value" in error.context # Extra context should also be preserved (unless overwritten by field/value) for key, val in extra_context.items(): if key not in ("field", "value"): assert error.context[key] == val @given(message=message_strategy) @settings(max_examples=50) def test_none_field_not_in_context(self, message: str): """ **Feature: service-layer-refactoring, Property 12: ValidationError Context Population** When field is None, it should not be added to context. """ error = ValidationError(message=message, field=None, value=None) assert "field" not in error.context assert "value" not in error.context @given( message=message_strategy, field=field_strategy, ) @settings(max_examples=50) def test_field_only_in_context(self, message: str, field: str): """ **Feature: service-layer-refactoring, Property 12: ValidationError Context Population** When only field is provided (value is None), only field should be in context. """ error = ValidationError(message=message, field=field, value=None) assert "field" in error.context assert error.context["field"] == field assert "value" not in error.context @given(message=message_strategy) @settings(max_examples=50) def test_long_value_truncation(self, message: str): """ **Feature: service-layer-refactoring, Property 12: ValidationError Context Population** Long values should be truncated to 100 characters. """ long_value = "x" * 200 # 200 character string error = ValidationError(message=message, field="test_field", value=long_value) # Value in context should be truncated assert len(error.context["value"]) == 100 assert error.context["value"] == "x" * 100 # Original value should be preserved in attribute assert error.value == long_value @given( message=message_strategy, error_code=error_code_strategy, field=field_strategy, value=value_strategy, ) @settings(max_examples=100) def test_validation_error_to_dict_includes_context( self, message: str, error_code: ErrorCode, field: str, value, ): """ **Feature: service-layer-refactoring, Property 12: ValidationError Context Population** to_dict() should include the populated context with field and value. """ error = ValidationError( message=message, error_code=error_code, field=field, value=value, ) result = error.to_dict() assert result["type"] == "ValidationError" assert result["message"] == message assert result["code"] == error_code.value assert "context" in result assert result["context"]["field"] == field

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zengwenliang416/banana-image-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server