"""Tests for constraint validators.""" import pytest from veritext.core.exceptions import InvalidThresholdError from veritext.core.types import ValidationContext from veritext.validators import contains, excludes, length, readability from veritext.validators.constraint import ( ContainsValidator, ExcludesValidator, LengthValidator, ReadabilityValidator, ) class TestLengthValidator: """Tests for LengthValidator.""" def test_length_validator_min_chars_passes(self) -> None: """Test that validator passes when char count meets minimum.""" validator = LengthValidator(min_chars=10) context = ValidationContext() result = validator.check("hello world!", context) assert result.passed is True assert result.name == "length" assert result.actual["chars"] == 12 def test_length_validator_min_chars_fails(self) -> None: """Test that validator fails when char count below minimum.""" validator = LengthValidator(min_chars=20) context = ValidationContext() result = validator.check("hello", context) assert result.passed is False assert "< min" in result.message def test_length_validator_max_chars_passes(self) -> None: """Test that validator passes when char count within maximum.""" validator = LengthValidator(max_chars=20) context = ValidationContext() result = validator.check("hello world", context) assert result.passed is True assert result.actual["chars"] == 11 def test_length_validator_max_chars_fails(self) -> None: """Test that validator fails when char count exceeds maximum.""" validator = LengthValidator(max_chars=5) context = ValidationContext() result = validator.check("hello world", context) assert result.passed is False assert "> max" in result.message def test_length_validator_min_words_passes(self) -> None: """Test that validator passes when word count meets minimum.""" validator = LengthValidator(min_words=3) context = ValidationContext() result = validator.check("the quick brown fox", context) assert result.passed is True assert result.actual["words"] == 4 def test_length_validator_min_words_fails(self) -> None: """Test that validator fails when word count below minimum.""" validator = LengthValidator(min_words=10) context = ValidationContext() result = validator.check("hello world", context) assert result.passed is False assert "words < min" in result.message def test_length_validator_max_words_passes(self) -> None: """Test that validator passes when word count within maximum.""" validator = LengthValidator(max_words=5) context = ValidationContext() result = validator.check("hello world", context) assert result.passed is True def test_length_validator_max_words_fails(self) -> None: """Test that validator fails when word count exceeds maximum.""" validator = LengthValidator(max_words=2) context = ValidationContext() result = validator.check("the quick brown fox", context) assert result.passed is False assert "words > max" in result.message def test_length_validator_combined_constraints(self) -> None: """Test validator with multiple constraints.""" validator = LengthValidator( min_chars=5, max_chars=50, min_words=2, max_words=10 ) context = ValidationContext() result = validator.check("the quick brown fox", context) assert result.passed is True assert "min_chars" in result.threshold assert "max_chars" in result.threshold assert "min_words" in result.threshold assert "max_words" in result.threshold def test_length_validator_raises_when_no_constraints(self) -> None: """Test that validator raises when no constraints provided.""" with pytest.raises(InvalidThresholdError, match="At least one"): LengthValidator() def test_length_validator_raises_on_negative_values(self) -> None: """Test that negative constraint values raise error.""" with pytest.raises(InvalidThresholdError, match="min_chars must be >= 0"): LengthValidator(min_chars=-1) with pytest.raises(InvalidThresholdError, match="max_chars must be >= 0"): LengthValidator(max_chars=-1) with pytest.raises(InvalidThresholdError, match="min_words must be >= 0"): LengthValidator(min_words=-1) with pytest.raises(InvalidThresholdError, match="max_words must be >= 0"): LengthValidator(max_words=-1) def test_length_validator_raises_on_invalid_range(self) -> None: """Test that min > max raises error.""" with pytest.raises(InvalidThresholdError, match="cannot exceed max_chars"): LengthValidator(min_chars=100, max_chars=50) with pytest.raises(InvalidThresholdError, match="cannot exceed max_words"): LengthValidator(min_words=20, max_words=5) def test_length_factory_function(self) -> None: """Test the length() factory function.""" validator = length(min_chars=10, max_words=100) assert isinstance(validator, LengthValidator) assert validator.name == "length" class TestReadabilityValidator: """Tests for ReadabilityValidator.""" def test_readability_validator_max_grade_passes(self) -> None: """Test that validator passes when grade level within maximum.""" validator = ReadabilityValidator(max_grade=12.0) context = ValidationContext() # Simple text should have low grade level result = validator.check("The cat sat on the mat. It was a nice day.", context) assert result.passed is True assert result.name == "readability" assert "grade" in result.actual def test_readability_validator_max_grade_fails(self) -> None: """Test that validator fails when grade level exceeds maximum.""" validator = ReadabilityValidator(max_grade=1.0) context = ValidationContext() # Complex text result = validator.check( "The implementation of sophisticated methodologies necessitates " "comprehensive analytical frameworks for systematic evaluation.", context, ) assert result.passed is False assert "grade level" in result.message assert "> max" in result.message def test_readability_validator_min_ease_passes(self) -> None: """Test that validator passes when reading ease meets minimum.""" validator = ReadabilityValidator(min_ease=30.0) context = ValidationContext() # Simple text should have high reading ease result = validator.check("The cat sat. The dog ran. It was fun.", context) assert result.passed is True assert "ease" in result.actual def test_readability_validator_min_ease_fails(self) -> None: """Test that validator fails when reading ease below minimum.""" validator = ReadabilityValidator(min_ease=100.0) context = ValidationContext() result = validator.check( "The implementation of sophisticated methodologies necessitates " "comprehensive analytical frameworks.", context, ) assert result.passed is False assert "reading ease" in result.message assert "< min" in result.message def test_readability_validator_combined_constraints(self) -> None: """Test validator with both grade and ease constraints.""" validator = ReadabilityValidator(max_grade=12.0, min_ease=30.0) context = ValidationContext() result = validator.check("The cat sat on the mat.", context) assert "max_grade" in result.threshold assert "min_ease" in result.threshold def test_readability_validator_raises_when_no_constraints(self) -> None: """Test that validator raises when no constraints provided.""" with pytest.raises(InvalidThresholdError, match="At least one"): ReadabilityValidator() def test_readability_factory_function(self) -> None: """Test the readability() factory function.""" validator = readability(max_grade=8.0, min_ease=60.0) assert isinstance(validator, ReadabilityValidator) assert validator.name == "readability" class TestContainsValidator: """Tests for ContainsValidator.""" def test_contains_validator_passes_when_pattern_found(self) -> None: """Test that validator passes when all patterns are found.""" validator = ContainsValidator(patterns=["hello", "world"]) context = ValidationContext() result = validator.check("Hello World!", context) assert result.passed is True assert result.name == "contains" assert result.actual["found"] == 2 assert result.actual["missing"] == [] def test_contains_validator_fails_when_pattern_missing(self) -> None: """Test that validator fails when a pattern is missing.""" validator = ContainsValidator(patterns=["hello", "goodbye"]) context = ValidationContext() result = validator.check("Hello World!", context) assert result.passed is False assert "goodbye" in result.actual["missing"] assert "missing" in result.message def test_contains_validator_case_insensitive_by_default(self) -> None: """Test that matching is case-insensitive by default.""" validator = ContainsValidator(patterns=["HELLO"]) context = ValidationContext() result = validator.check("hello world", context) assert result.passed is True def test_contains_validator_case_sensitive(self) -> None: """Test case-sensitive matching.""" validator = ContainsValidator(patterns=["HELLO"], case_sensitive=True) context = ValidationContext() result = validator.check("hello world", context) assert result.passed is False def test_contains_validator_regex_patterns(self) -> None: """Test regex pattern matching.""" validator = ContainsValidator(patterns=[r"\d{3}-\d{4}"]) context = ValidationContext() result = validator.check("Call 555-1234 for info", context) assert result.passed is True def test_contains_validator_raises_on_empty_patterns(self) -> None: """Test that empty patterns list raises error.""" with pytest.raises(InvalidThresholdError, match="cannot be empty"): ContainsValidator(patterns=[]) def test_contains_factory_function(self) -> None: """Test the contains() factory function.""" validator = contains(patterns=["test"], case_sensitive=True) assert isinstance(validator, ContainsValidator) assert validator.name == "contains" class TestExcludesValidator: """Tests for ExcludesValidator.""" def test_excludes_validator_passes_when_pattern_absent(self) -> None: """Test that validator passes when all patterns are absent.""" validator = ExcludesValidator(patterns=["bad", "forbidden"]) context = ValidationContext() result = validator.check("This is good text.", context) assert result.passed is True assert result.name == "excludes" assert result.actual["found"] == [] def test_excludes_validator_fails_when_pattern_found(self) -> None: """Test that validator fails when a forbidden pattern is found.""" validator = ExcludesValidator(patterns=["bad", "forbidden"]) context = ValidationContext() result = validator.check("This is bad text.", context) assert result.passed is False assert "bad" in result.actual["found"] assert "forbidden" in result.message def test_excludes_validator_case_insensitive_by_default(self) -> None: """Test that matching is case-insensitive by default.""" validator = ExcludesValidator(patterns=["BAD"]) context = ValidationContext() result = validator.check("This is bad text.", context) assert result.passed is False def test_excludes_validator_case_sensitive(self) -> None: """Test case-sensitive matching.""" validator = ExcludesValidator(patterns=["BAD"], case_sensitive=True) context = ValidationContext() result = validator.check("This is bad text.", context) assert result.passed is True def test_excludes_validator_regex_patterns(self) -> None: """Test regex pattern matching.""" validator = ExcludesValidator(patterns=[r"\b\d{4}\b"]) # 4-digit numbers context = ValidationContext() # Should fail when pattern found result = validator.check("PIN is 1234", context) assert result.passed is False # Should pass when pattern absent result = validator.check("No numbers here", context) assert result.passed is True def test_excludes_validator_raises_on_empty_patterns(self) -> None: """Test that empty patterns list raises error.""" with pytest.raises(InvalidThresholdError, match="cannot be empty"): ExcludesValidator(patterns=[]) def test_excludes_factory_function(self) -> None: """Test the excludes() factory function.""" validator = excludes(patterns=["test"], case_sensitive=True) assert isinstance(validator, ExcludesValidator) assert validator.name == "excludes"