# SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2025 Allaun """Tests for nodupe/tools/security_audit/validator_logic.py - Input validation utilities. Comprehensive tests covering: - Type validation - Range validation - String validation (length, pattern) - Path validation - Collection validation - Error handling paths - Edge cases """ import os import tempfile from pathlib import Path import pytest from nodupe.tools.security_audit.validator_logic import ( ValidationError, Validators, ) # ============================================================================= # Test ValidationError Exception # ============================================================================= class TestValidationError: """Test ValidationError exception class.""" def test_validation_error_creation(self): """ValidationError can be created with message.""" error = ValidationError("Test validation error") assert str(error) == "Test validation error" def test_validation_error_inherits_from_exception(self): """ValidationError inherits from Exception.""" error = ValidationError("Test") assert isinstance(error, Exception) def test_validation_error_with_cause(self): """ValidationError can wrap another exception.""" try: try: raise ValueError("Original error") except ValueError as e: raise ValidationError("Validation failed") from e except ValidationError as ve: assert ve.__cause__ is not None assert isinstance(ve.__cause__, ValueError) # ============================================================================= # Test validate_type # ============================================================================= class TestValidateType: """Test validate_type method.""" def test_validate_type_int(self): """validate_type accepts correct int type.""" assert Validators.validate_type(42, int) is True def test_validate_type_str(self): """validate_type accepts correct str type.""" assert Validators.validate_type("hello", str) is True def test_validate_type_float(self): """validate_type accepts correct float type.""" assert Validators.validate_type(3.14, float) is True def test_validate_type_list(self): """validate_type accepts correct list type.""" assert Validators.validate_type([1, 2, 3], list) is True def test_validate_type_dict(self): """validate_type accepts correct dict type.""" assert Validators.validate_type({"key": "value"}, dict) is True def test_validate_type_none_allowed(self): """validate_type allows None when allow_none=True.""" assert Validators.validate_type(None, str, allow_none=True) is True def test_validate_type_none_not_allowed(self): """validate_type raises error for None when allow_none=False.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_type(None, str, allow_none=False) assert "Expected type str, got NoneType" in str(exc_info.value) def test_validate_type_wrong_type(self): """validate_type raises error for wrong type.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_type(42, str) assert "Expected type str, got int" in str(exc_info.value) def test_validate_type_subclass_allowed(self): """validate_type accepts subclass instances.""" # bool is a subclass of int assert Validators.validate_type(True, int) is True def test_validate_type_custom_class(self): """validate_type works with custom classes.""" class CustomClass: """Custom class for testing type validation.""" pass obj = CustomClass() assert Validators.validate_type(obj, CustomClass) is True with pytest.raises(ValidationError): Validators.validate_type(obj, str) # ============================================================================= # Test validate_range # ============================================================================= class TestValidateRange: """Test validate_range method.""" def test_validate_range_within_bounds(self): """validate_range accepts value within bounds.""" assert Validators.validate_range(5, min_val=0, max_val=10) is True def test_validate_range_no_bounds(self): """validate_range accepts value when no bounds specified.""" assert Validators.validate_range(100) is True def test_validate_range_min_only(self): """validate_range with only minimum bound.""" assert Validators.validate_range(10, min_val=5) is True def test_validate_range_max_only(self): """validate_range with only maximum bound.""" assert Validators.validate_range(5, max_val=10) is True def test_validate_range_at_min_boundary_inclusive(self): """validate_range accepts value at minimum boundary (inclusive).""" assert Validators.validate_range(0, min_val=0, inclusive=True) is True def test_validate_range_at_max_boundary_inclusive(self): """validate_range accepts value at maximum boundary (inclusive).""" assert Validators.validate_range(10, max_val=10, inclusive=True) is True def test_validate_range_at_min_boundary_exclusive(self): """validate_range rejects value at minimum boundary (exclusive).""" with pytest.raises(ValidationError) as exc_info: Validators.validate_range(0, min_val=0, inclusive=False) assert "<= minimum" in str(exc_info.value) def test_validate_range_at_max_boundary_exclusive(self): """validate_range rejects value at maximum boundary (exclusive).""" with pytest.raises(ValidationError) as exc_info: Validators.validate_range(10, max_val=10, inclusive=False) assert ">= maximum" in str(exc_info.value) def test_validate_range_below_min(self): """validate_range rejects value below minimum.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_range(-1, min_val=0) assert "< minimum" in str(exc_info.value) def test_validate_range_above_max(self): """validate_range rejects value above maximum.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_range(11, max_val=10) assert "> maximum" in str(exc_info.value) def test_validate_range_float(self): """validate_range works with float values.""" assert Validators.validate_range(3.14, min_val=0.0, max_val=10.0) is True def test_validate_range_non_numeric(self): """validate_range raises error for non-numeric types.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_range("not a number", min_val=0) assert "Expected numeric type" in str(exc_info.value) def test_validate_range_bool_rejected(self): """validate_range rejects boolean (even though bool is subclass of int).""" # Note: bool is subclass of int, so this actually passes # But the test verifies the behavior assert Validators.validate_range(True, min_val=0, max_val=1) is True # ============================================================================= # Test validate_string_length # ============================================================================= class TestValidateStringLength: """Test validate_string_length method.""" def test_validate_string_length_within_bounds(self): """validate_string_length accepts string within bounds.""" assert Validators.validate_string_length("hello", min_length=1, max_length=10) is True def test_validate_string_length_no_bounds(self): """validate_string_length accepts string when no bounds specified.""" assert Validators.validate_string_length("test") is True def test_validate_string_length_min_only(self): """validate_string_length with only minimum bound.""" assert Validators.validate_string_length("hello", min_length=3) is True def test_validate_string_length_max_only(self): """validate_string_length with only maximum bound.""" assert Validators.validate_string_length("hi", max_length=10) is True def test_validate_string_length_at_min_boundary(self): """validate_string_length accepts string at minimum length.""" assert Validators.validate_string_length("ab", min_length=2) is True def test_validate_string_length_at_max_boundary(self): """validate_string_length accepts string at maximum length.""" assert Validators.validate_string_length("abc", max_length=3) is True def test_validate_string_length_too_short(self): """validate_string_length rejects string below minimum.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_string_length("hi", min_length=5) assert "< minimum" in str(exc_info.value) def test_validate_string_length_too_long(self): """validate_string_length rejects string above maximum.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_string_length("hello world", max_length=5) assert "> maximum" in str(exc_info.value) def test_validate_string_length_empty_string(self): """validate_string_length handles empty string.""" assert Validators.validate_string_length("", min_length=0, max_length=10) is True with pytest.raises(ValidationError): Validators.validate_string_length("", min_length=1) def test_validate_string_length_non_string(self): """validate_string_length raises error for non-string types.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_string_length(123, min_length=1) assert "Expected string" in str(exc_info.value) def test_validate_string_length_unicode(self): """validate_string_length handles unicode strings.""" assert Validators.validate_string_length("hello", min_length=1) is True assert Validators.validate_string_length("", max_length=0) is True # ============================================================================= # Test validate_pattern # ============================================================================= class TestValidatePattern: """Test validate_pattern method.""" def test_validate_pattern_matches(self): """validate_pattern accepts string matching pattern.""" assert Validators.validate_pattern("test123", r"^[a-z]+[0-9]+$") is True def test_validate_pattern_full_match(self): """validate_pattern uses match (from start).""" assert Validators.validate_pattern("abc123", r"^[a-z]+") is True def test_validate_pattern_no_match(self): """validate_pattern rejects string not matching pattern.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_pattern("123abc", r"^[a-z]+$") assert "doesn't match pattern" in str(exc_info.value) def test_validate_pattern_non_string(self): """validate_pattern raises error for non-string value.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_pattern(123, r"^[0-9]+$") assert "Expected string" in str(exc_info.value) def test_validate_pattern_invalid_regex(self): """validate_pattern raises error for invalid regex pattern.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_pattern("test", r"[invalid(regex") assert "Invalid regex pattern" in str(exc_info.value) def test_validate_pattern_email_format(self): """validate_pattern can validate email-like format.""" pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" assert Validators.validate_pattern("test@example.com", pattern) is True def test_validate_pattern_phone_format(self): """validate_pattern can validate phone-like format.""" pattern = r"^\d{3}-\d{3}-\d{4}$" assert Validators.validate_pattern("123-456-7890", pattern) is True # ============================================================================= # Test validate_email # ============================================================================= class TestValidateEmail: """Test validate_email method.""" def test_validate_email_valid(self): """validate_email accepts valid email addresses.""" valid_emails = [ "test@example.com", "user.name@domain.org", "user+tag@example.co.uk", "a@b.co", ] for email in valid_emails: assert Validators.validate_email(email) is True def test_validate_email_invalid_no_at(self): """validate_email rejects email without @ symbol.""" with pytest.raises(ValidationError): Validators.validate_email("invalid.email") def test_validate_email_invalid_no_domain(self): """validate_email rejects email without domain.""" with pytest.raises(ValidationError): Validators.validate_email("test@") def test_validate_email_invalid_no_tld(self): """validate_email rejects email without TLD.""" with pytest.raises(ValidationError): Validators.validate_email("test@example") def test_validate_email_invalid_spaces(self): """validate_email rejects email with spaces.""" with pytest.raises(ValidationError): Validators.validate_email("test @example.com") def test_validate_email_non_string(self): """validate_email raises error for non-string.""" with pytest.raises(ValidationError): Validators.validate_email(123) # ============================================================================= # Test validate_path # ============================================================================= class TestValidatePath: """Test validate_path method.""" def test_validate_path_string_path(self): """validate_path accepts string path.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = os.path.join(tmpdir, "test.txt") Path(test_file).write_text("test") assert Validators.validate_path(test_file) is True def test_validate_path_path_object(self): """validate_path accepts Path object.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.txt" test_file.write_text("test") assert Validators.validate_path(test_file) is True def test_validate_path_must_exist_exists(self): """validate_path with must_exist=True accepts existing path.""" with tempfile.TemporaryDirectory() as tmpdir: assert Validators.validate_path(tmpdir, must_exist=True) is True def test_validate_path_must_exist_not_exists(self): """validate_path with must_exist=True rejects non-existing path.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_path("/nonexistent/path", must_exist=True) assert "does not exist" in str(exc_info.value) def test_validate_path_must_be_file(self): """validate_path with must_be_file=True accepts files.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.txt" test_file.write_text("test") assert Validators.validate_path(test_file, must_be_file=True) is True def test_validate_path_must_be_file_not_file(self): """validate_path with must_be_file=True rejects directories.""" with tempfile.TemporaryDirectory() as tmpdir: with pytest.raises(ValidationError) as exc_info: Validators.validate_path(tmpdir, must_be_file=True) assert "not a file" in str(exc_info.value) def test_validate_path_must_be_dir(self): """validate_path with must_be_dir=True accepts directories.""" with tempfile.TemporaryDirectory() as tmpdir: assert Validators.validate_path(tmpdir, must_be_dir=True) is True def test_validate_path_must_be_dir_not_dir(self): """validate_path with must_be_dir=True rejects files.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.txt" test_file.write_text("test") with pytest.raises(ValidationError) as exc_info: Validators.validate_path(test_file, must_be_dir=True) assert "not a directory" in str(exc_info.value) def test_validate_path_non_path_type(self): """validate_path raises error for non-Path/str types.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_path(123) assert "Expected Path or str" in str(exc_info.value) def test_validate_path_must_be_file_not_exists(self): """validate_path with must_be_file=True rejects non-existing file.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_path("/nonexistent/file.txt", must_be_file=True) assert "File does not exist" in str(exc_info.value) def test_validate_path_must_be_dir_not_exists(self): """validate_path with must_be_dir=True rejects non-existing directory.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_path("/nonexistent/dir", must_be_dir=True) assert "Directory does not exist" in str(exc_info.value) # ============================================================================= # Test validate_enum # ============================================================================= class TestValidateEnum: """Test validate_enum method.""" def test_validate_enum_allowed_value(self): """validate_enum accepts value in allowed list.""" assert Validators.validate_enum("red", ["red", "green", "blue"]) is True def test_validate_enum_not_allowed(self): """validate_enum rejects value not in allowed list.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_enum("yellow", ["red", "green", "blue"]) assert "not in allowed values" in str(exc_info.value) def test_validate_enum_numeric_values(self): """validate_enum works with numeric values.""" assert Validators.validate_enum(1, [1, 2, 3]) is True with pytest.raises(ValidationError): Validators.validate_enum(4, [1, 2, 3]) def test_validate_enum_empty_list(self): """validate_enum rejects any value when allowed list is empty.""" with pytest.raises(ValidationError): Validators.validate_enum("anything", []) def test_validate_enum_mixed_types(self): """validate_enum works with mixed type allowed values.""" allowed = [1, "one", 2.0, None] assert Validators.validate_enum(1, allowed) is True assert Validators.validate_enum("one", allowed) is True assert Validators.validate_enum(2.0, allowed) is True assert Validators.validate_enum(None, allowed) is True # ============================================================================= # Test validate_dict_keys # ============================================================================= class TestValidateDictKeys: """Test validate_dict_keys method.""" def test_validate_dict_keys_required_present(self): """validate_dict_keys accepts dict with required keys.""" assert Validators.validate_dict_keys( {"a": 1, "b": 2}, required_keys=["a"] ) is True def test_validate_dict_keys_required_missing(self): """validate_dict_keys rejects dict missing required keys.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_dict_keys( {"a": 1}, required_keys=["a", "b"] ) assert "Missing required keys" in str(exc_info.value) def test_validate_dict_keys_allowed_only(self): """validate_dict_keys accepts dict with only allowed keys.""" assert Validators.validate_dict_keys( {"a": 1, "b": 2}, allowed_keys=["a", "b", "c"] ) is True def test_validate_dict_keys_allowed_extra(self): """validate_dict_keys rejects dict with extra keys.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_dict_keys( {"a": 1, "b": 2, "c": 3}, allowed_keys=["a", "b"] ) assert "Unexpected keys" in str(exc_info.value) def test_validate_dict_keys_no_constraints(self): """validate_dict_keys accepts dict when no constraints specified.""" assert Validators.validate_dict_keys({"any": "key"}) is True def test_validate_dict_keys_non_dict(self): """validate_dict_keys raises error for non-dict types.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_dict_keys([1, 2, 3], required_keys=["a"]) assert "Expected dict" in str(exc_info.value) def test_validate_dict_keys_empty_dict(self): """validate_dict_keys handles empty dict.""" # No required keys - OK assert Validators.validate_dict_keys({}, required_keys=[]) is True # Required keys - fails with pytest.raises(ValidationError): Validators.validate_dict_keys({}, required_keys=["a"]) # ============================================================================= # Test validate_list_items # ============================================================================= class TestValidateListItems: """Test validate_list_items method.""" def test_validate_list_items_correct_type(self): """validate_list_items accepts list with correct item types.""" assert Validators.validate_list_items([1, 2, 3], int) is True def test_validate_list_items_wrong_type(self): """validate_list_items rejects list with wrong item types.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_list_items([1, "two", 3], int) assert "expected int" in str(exc_info.value).lower() def test_validate_list_items_min_items_met(self): """validate_list_items accepts list meeting minimum items.""" assert Validators.validate_list_items([1, 2, 3], int, min_items=2) is True def test_validate_list_items_min_items_not_met(self): """validate_list_items rejects list below minimum items.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_list_items([1], int, min_items=2) assert "minimum" in str(exc_info.value).lower() def test_validate_list_items_max_items_met(self): """validate_list_items accepts list meeting maximum items.""" assert Validators.validate_list_items([1, 2], int, max_items=3) is True def test_validate_list_items_max_items_exceeded(self): """validate_list_items rejects list exceeding maximum items.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_list_items([1, 2, 3, 4, 5], int, max_items=3) assert "maximum" in str(exc_info.value).lower() def test_validate_list_items_empty_list(self): """validate_list_items handles empty list.""" assert Validators.validate_list_items([], int, min_items=0) is True with pytest.raises(ValidationError): Validators.validate_list_items([], int, min_items=1) def test_validate_list_items_non_list(self): """validate_list_items raises error for non-list types.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_list_items("not a list", str) assert "Expected list" in str(exc_info.value) def test_validate_list_items_custom_class(self): """validate_list_items works with custom class types.""" class CustomClass: """Custom class for testing list item validation.""" pass items = [CustomClass(), CustomClass()] assert Validators.validate_list_items(items, CustomClass) is True # ============================================================================= # Test validate_boolean # ============================================================================= class TestValidateBoolean: """Test validate_boolean method.""" def test_validate_boolean_true(self): """validate_boolean accepts True.""" assert Validators.validate_boolean(True) is True def test_validate_boolean_false(self): """validate_boolean accepts False.""" assert Validators.validate_boolean(False) is True def test_validate_boolean_int_rejected(self): """validate_boolean rejects integer 1.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_boolean(1) assert "Expected bool" in str(exc_info.value) def test_validate_boolean_int_zero_rejected(self): """validate_boolean rejects integer 0.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_boolean(0) assert "Expected bool" in str(exc_info.value) def test_validate_boolean_string_rejected(self): """validate_boolean rejects string 'True'.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_boolean("True") assert "Expected bool" in str(exc_info.value) # ============================================================================= # Test validate_positive # ============================================================================= class TestValidatePositive: """Test validate_positive method.""" def test_validate_positive_positive_int(self): """validate_positive accepts positive integer.""" assert Validators.validate_positive(1) is True assert Validators.validate_positive(100) is True def test_validate_positive_positive_float(self): """validate_positive accepts positive float.""" assert Validators.validate_positive(0.001) is True assert Validators.validate_positive(3.14) is True def test_validate_positive_zero_rejected(self): """validate_positive rejects zero.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_positive(0) assert "not positive" in str(exc_info.value) def test_validate_positive_negative_rejected(self): """validate_positive rejects negative values.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_positive(-1) assert "not positive" in str(exc_info.value) def test_validate_positive_non_numeric_rejected(self): """validate_positive rejects non-numeric types.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_positive("not a number") assert "Expected numeric type" in str(exc_info.value) # ============================================================================= # Test validate_non_negative # ============================================================================= class TestValidateNonNegative: """Test validate_non_negative method.""" def test_validate_non_negative_positive(self): """validate_non_negative accepts positive values.""" assert Validators.validate_non_negative(1) is True assert Validators.validate_non_negative(100) is True def test_validate_non_negative_zero(self): """validate_non_negative accepts zero.""" assert Validators.validate_non_negative(0) is True assert Validators.validate_non_negative(0.0) is True def test_validate_non_negative_float(self): """validate_non_negative accepts positive floats.""" assert Validators.validate_non_negative(0.001) is True assert Validators.validate_non_negative(3.14) is True def test_validate_non_negative_negative_rejected(self): """validate_non_negative rejects negative values.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_non_negative(-1) assert "is negative" in str(exc_info.value) def test_validate_non_negative_non_numeric_rejected(self): """validate_non_negative rejects non-numeric types.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_non_negative("not a number") assert "Expected numeric type" in str(exc_info.value) # ============================================================================= # Test validate_non_empty # ============================================================================= class TestValidateNonEmpty: """Test validate_non_empty method.""" def test_validate_non_empty_string(self): """validate_non_empty accepts non-empty string.""" assert Validators.validate_non_empty("hello") is True def test_validate_non_empty_list(self): """validate_non_empty accepts non-empty list.""" assert Validators.validate_non_empty([1, 2, 3]) is True def test_validate_non_empty_dict(self): """validate_non_empty accepts non-empty dict.""" assert Validators.validate_non_empty({"key": "value"}) is True def test_validate_non_empty_set(self): """validate_non_empty accepts non-empty set.""" assert Validators.validate_non_empty({1, 2, 3}) is True def test_validate_non_empty_tuple(self): """validate_non_empty accepts non-empty tuple.""" assert Validators.validate_non_empty((1, 2, 3)) is True def test_validate_non_empty_string_empty(self): """validate_non_empty rejects empty string.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_non_empty("") assert "empty" in str(exc_info.value).lower() def test_validate_non_empty_list_empty(self): """validate_non_empty rejects empty list.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_non_empty([]) assert "empty" in str(exc_info.value).lower() def test_validate_non_empty_dict_empty(self): """validate_non_empty rejects empty dict.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_non_empty({}) assert "empty" in str(exc_info.value).lower() def test_validate_non_empty_set_empty(self): """validate_non_empty rejects empty set.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_non_empty(set()) assert "empty" in str(exc_info.value).lower() def test_validate_non_empty_tuple_empty(self): """validate_non_empty rejects empty tuple.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_non_empty(()) assert "empty" in str(exc_info.value).lower() def test_validate_non_empty_none_rejected(self): """validate_non_empty rejects None.""" with pytest.raises(ValidationError) as exc_info: Validators.validate_non_empty(None) assert "empty" in str(exc_info.value).lower() # ============================================================================= # Test Edge Cases and Integration # ============================================================================= class TestValidatorEdgeCases: """Test edge cases and integration scenarios.""" def test_validate_type_bool_is_int(self): """verify bool is subclass of int for type validation.""" # This is Python behavior - bool is subclass of int assert Validators.validate_type(True, int) is True assert Validators.validate_type(False, int) is True def test_validate_range_very_large_numbers(self): """validate_range handles very large numbers.""" assert Validators.validate_range(10**100, min_val=0) is True def test_validate_string_length_very_long(self): """validate_string_length handles very long strings.""" long_string = "a" * 10000 assert Validators.validate_string_length(long_string, max_length=20000) is True def test_validate_pattern_special_characters(self): """validate_pattern handles special regex characters.""" # Pattern with special characters assert Validators.validate_pattern("a.b", r"a\.b") is True def test_validate_path_symlink(self): """validate_path handles symlinks.""" with tempfile.TemporaryDirectory() as tmpdir: test_file = Path(tmpdir) / "test.txt" test_file.write_text("test") symlink = Path(tmpdir) / "link.txt" symlink.symlink_to(test_file) assert Validators.validate_path(symlink, must_exist=True) is True def test_validate_list_items_nested_lists(self): """validate_list_items with nested lists.""" nested = [[1, 2], [3, 4]] assert Validators.validate_list_items(nested, list) is True def test_validate_dict_keys_nested_dict(self): """validate_dict_keys with nested dictionaries.""" nested = {"outer": {"inner": 1}} assert Validators.validate_dict_keys(nested, required_keys=["outer"]) is True def test_multiple_validators_chain(self): """Multiple validators can be chained.""" value = "hello" # Chain multiple validations assert Validators.validate_type(value, str) is True assert Validators.validate_string_length(value, min_length=1, max_length=10) is True assert Validators.validate_non_empty(value) is True def test_validation_error_message_format(self): """ValidationError messages are descriptive.""" # Type error try: Validators.validate_type(42, str) except ValidationError as e: assert "Expected type str, got int" in str(e) # Range error try: Validators.validate_range(-1, min_val=0) except ValidationError as e: assert "minimum" in str(e).lower() # String length error try: Validators.validate_string_length("hi", min_length=5) except ValidationError as e: assert "minimum" in str(e).lower()