233 lines
7.3 KiB
Python
233 lines
7.3 KiB
Python
"""
|
|
Tests for InvoiceValidator - TDD RED phase.
|
|
|
|
Test invoice field validation logic.
|
|
"""
|
|
import pytest
|
|
|
|
from backend.domain.invoice_validator import (
|
|
InvoiceValidator,
|
|
ValidationResult,
|
|
ValidationIssue,
|
|
)
|
|
|
|
|
|
class TestInvoiceValidator:
|
|
"""Test invoice validation logic."""
|
|
|
|
@pytest.fixture
|
|
def validator(self) -> InvoiceValidator:
|
|
"""Create validator instance with default settings."""
|
|
return InvoiceValidator()
|
|
|
|
@pytest.fixture
|
|
def validator_strict(self) -> InvoiceValidator:
|
|
"""Create validator with strict confidence threshold."""
|
|
return InvoiceValidator(min_confidence=0.8)
|
|
|
|
# ==================== Valid Invoice Tests ====================
|
|
|
|
def test_validate_complete_invoice_is_valid(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""Complete invoice with all required fields is valid."""
|
|
fields = {
|
|
"Amount": "1200.00",
|
|
"OCR": "123456789012",
|
|
"Bankgiro": "123-4567",
|
|
}
|
|
confidence = {
|
|
"Amount": 0.95,
|
|
"OCR": 0.90,
|
|
"Bankgiro": 0.85,
|
|
}
|
|
|
|
result = validator.validate(fields, confidence)
|
|
|
|
assert result.is_valid is True
|
|
assert len([i for i in result.issues if i.severity == "error"]) == 0
|
|
|
|
def test_validate_invoice_with_payment_line_is_valid(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""Invoice with payment_line as payment reference is valid."""
|
|
fields = {
|
|
"Amount": "500.00",
|
|
"payment_line": "# 123 # 500 00 5 > 308#",
|
|
}
|
|
confidence = {"Amount": 0.9, "payment_line": 0.85}
|
|
|
|
result = validator.validate(fields, confidence)
|
|
|
|
assert result.is_valid is True
|
|
|
|
# ==================== Invalid Invoice Tests ====================
|
|
|
|
def test_validate_missing_amount_is_invalid(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""Missing Amount field should produce error."""
|
|
fields = {
|
|
"OCR": "123456789012",
|
|
"Bankgiro": "123-4567",
|
|
}
|
|
confidence = {"OCR": 0.9, "Bankgiro": 0.85}
|
|
|
|
result = validator.validate(fields, confidence)
|
|
|
|
assert result.is_valid is False
|
|
error_fields = [i.field for i in result.issues if i.severity == "error"]
|
|
assert "Amount" in error_fields
|
|
|
|
def test_validate_missing_payment_reference_produces_warning(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""Missing all payment references should produce warning."""
|
|
fields = {"Amount": "1200.00"}
|
|
confidence = {"Amount": 0.9}
|
|
|
|
result = validator.validate(fields, confidence)
|
|
|
|
# Missing payment ref is warning, not error
|
|
warning_fields = [i.field for i in result.issues if i.severity == "warning"]
|
|
assert "payment_reference" in warning_fields
|
|
|
|
# ==================== Confidence Threshold Tests ====================
|
|
|
|
def test_validate_low_confidence_produces_warning(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""Fields below confidence threshold should produce warning."""
|
|
fields = {
|
|
"Amount": "1200.00",
|
|
"OCR": "123456789012",
|
|
}
|
|
confidence = {
|
|
"Amount": 0.9,
|
|
"OCR": 0.3, # Below default threshold of 0.5
|
|
}
|
|
|
|
result = validator.validate(fields, confidence)
|
|
|
|
low_conf_warnings = [
|
|
i for i in result.issues
|
|
if i.severity == "warning" and "confidence" in i.message.lower()
|
|
]
|
|
assert len(low_conf_warnings) > 0
|
|
|
|
def test_validate_strict_threshold_more_warnings(
|
|
self, validator_strict: InvoiceValidator
|
|
) -> None:
|
|
"""Strict validator should produce more warnings."""
|
|
fields = {
|
|
"Amount": "1200.00",
|
|
"OCR": "123456789012",
|
|
}
|
|
confidence = {
|
|
"Amount": 0.7, # Below 0.8 threshold
|
|
"OCR": 0.6, # Below 0.8 threshold
|
|
}
|
|
|
|
result = validator_strict.validate(fields, confidence)
|
|
|
|
low_conf_warnings = [
|
|
i for i in result.issues
|
|
if i.severity == "warning" and "confidence" in i.message.lower()
|
|
]
|
|
assert len(low_conf_warnings) >= 2
|
|
|
|
# ==================== Edge Cases ====================
|
|
|
|
def test_validate_empty_fields_is_invalid(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""Empty fields dict should be invalid."""
|
|
fields: dict[str, str | None] = {}
|
|
confidence: dict[str, float] = {}
|
|
|
|
result = validator.validate(fields, confidence)
|
|
|
|
assert result.is_valid is False
|
|
|
|
def test_validate_none_field_values_treated_as_missing(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""None values should be treated as missing."""
|
|
fields = {
|
|
"Amount": None,
|
|
"OCR": "123456789012",
|
|
}
|
|
confidence = {"OCR": 0.9}
|
|
|
|
result = validator.validate(fields, confidence)
|
|
|
|
assert result.is_valid is False
|
|
error_fields = [i.field for i in result.issues if i.severity == "error"]
|
|
assert "Amount" in error_fields
|
|
|
|
def test_validate_empty_string_treated_as_missing(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""Empty string should be treated as missing."""
|
|
fields = {
|
|
"Amount": "",
|
|
"OCR": "123456789012",
|
|
}
|
|
confidence = {"OCR": 0.9}
|
|
|
|
result = validator.validate(fields, confidence)
|
|
|
|
assert result.is_valid is False
|
|
|
|
# ==================== ValidationResult Properties ====================
|
|
|
|
def test_validation_result_is_immutable(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""ValidationResult should be a frozen dataclass."""
|
|
fields = {"Amount": "100.00", "OCR": "123"}
|
|
confidence = {"Amount": 0.9, "OCR": 0.9}
|
|
result = validator.validate(fields, confidence)
|
|
|
|
with pytest.raises((AttributeError, TypeError)):
|
|
result.is_valid = False # type: ignore
|
|
|
|
def test_validation_result_issues_is_tuple(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""Issues should be a tuple (immutable)."""
|
|
fields = {"Amount": "100.00"}
|
|
confidence = {"Amount": 0.9}
|
|
result = validator.validate(fields, confidence)
|
|
|
|
assert isinstance(result.issues, tuple)
|
|
|
|
def test_validation_result_has_confidence(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""ValidationResult should have confidence score."""
|
|
fields = {"Amount": "100.00", "OCR": "123"}
|
|
confidence = {"Amount": 0.9, "OCR": 0.8}
|
|
result = validator.validate(fields, confidence)
|
|
|
|
assert hasattr(result, "confidence")
|
|
assert 0.0 <= result.confidence <= 1.0
|
|
|
|
# ==================== ValidationIssue Tests ====================
|
|
|
|
def test_validation_issue_has_required_fields(
|
|
self, validator: InvoiceValidator
|
|
) -> None:
|
|
"""ValidationIssue must have field, severity, message."""
|
|
fields: dict[str, str | None] = {}
|
|
confidence: dict[str, float] = {}
|
|
result = validator.validate(fields, confidence)
|
|
|
|
assert len(result.issues) > 0
|
|
issue = result.issues[0]
|
|
|
|
assert hasattr(issue, "field")
|
|
assert hasattr(issue, "severity")
|
|
assert hasattr(issue, "message")
|
|
assert issue.severity in ("error", "warning", "info")
|