ContainsValidator and ExcludesValidator now pre-compile regex patterns during initialisation and raise InvalidThresholdError if invalid.
345 lines
14 KiB
Python
345 lines
14 KiB
Python
"""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_validator_raises_on_invalid_regex(self) -> None:
|
|
"""Test that invalid regex pattern raises error at init time."""
|
|
with pytest.raises(InvalidThresholdError, match="Invalid regex"):
|
|
ContainsValidator(patterns=[r"[invalid"])
|
|
|
|
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_validator_raises_on_invalid_regex(self) -> None:
|
|
"""Test that invalid regex pattern raises error at init time."""
|
|
with pytest.raises(InvalidThresholdError, match="Invalid regex"):
|
|
ExcludesValidator(patterns=[r"[invalid"])
|
|
|
|
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"
|