Research-Stack/4-Infrastructure/NoDupeLabs/tests/security_audit/test_validator_logic.py

814 lines
34 KiB
Python

# 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()