guide / tests /privacy /test_recognizers.py
saravanakum1
add privacy layer tests with openspec and LangSmith integration
1f54b5c
Raw
History Blame Contribute Delete
5.93 kB
"""
Tests for the three custom Presidio PatternRecognizers in src/privacy/redactor.py.
No spaCy model required — these tests exercise pure regex pattern matching only.
Run fast subset: pytest tests/privacy/test_recognizers.py
"""
import pytest
from presidio_analyzer import AnalyzerEngine
from presidio_analyzer.nlp_engine import NlpEngineProvider
from src.privacy.redactor import (
_aadhaar_recognizer,
_pan_recognizer,
_vehicle_registration_recognizer,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _matches(recognizer, text):
"""Return analyzer results from a single recognizer against text."""
return recognizer.analyze(text=text, entities=[recognizer.supported_entities[0]])
def _engine_with(*recognizers):
"""Build a minimal AnalyzerEngine with only the given recognizers (no spaCy)."""
nlp_config = {
"nlp_engine_name": "spacy",
"models": [{"lang_code": "en", "model_name": "en_core_web_sm"}],
}
try:
nlp_engine = NlpEngineProvider(nlp_configuration=nlp_config).create_engine()
except Exception:
pytest.skip("spaCy en_core_web_sm not available for collision test")
engine = AnalyzerEngine(nlp_engine=nlp_engine, supported_languages=["en"])
# Remove all default recognizers, add only ours
engine.registry.recognizers.clear()
for r in recognizers:
engine.registry.add_recognizer(r)
return engine
# ---------------------------------------------------------------------------
# IN_AADHAAR — valid formats
# ---------------------------------------------------------------------------
def test_aadhaar_valid_formats():
"""Space-separated, hyphen-separated, and continuous formats all match."""
recognizer = _aadhaar_recognizer()
space_result = _matches(recognizer, "My aadhaar is 2345 6789 0123")
assert len(space_result) == 1
assert space_result[0].entity_type == "IN_AADHAAR"
hyphen_result = _matches(recognizer, "UID: 2345-6789-0123")
assert len(hyphen_result) == 1
assert hyphen_result[0].entity_type == "IN_AADHAAR"
continuous_result = _matches(recognizer, "234567890123")
assert len(continuous_result) == 1
assert continuous_result[0].entity_type == "IN_AADHAAR"
# ---------------------------------------------------------------------------
# IN_AADHAAR — invalid formats
# ---------------------------------------------------------------------------
def test_aadhaar_invalid_formats():
"""First digit 1, 11-digit, and 13-digit inputs must not match."""
recognizer = _aadhaar_recognizer()
assert len(_matches(recognizer, "1345 6789 0123")) == 0, "first digit 1 should not match"
assert len(_matches(recognizer, "2345 6789 012")) == 0, "11 digits should not match"
assert len(_matches(recognizer, "2345 6789 01234")) == 0, "13 digits should not match"
# ---------------------------------------------------------------------------
# IN_PAN — valid format
# ---------------------------------------------------------------------------
def test_pan_valid_format():
"""Canonical PAN format (5 uppercase + 4 digits + 1 uppercase) matches."""
recognizer = _pan_recognizer()
result = _matches(recognizer, "My PAN is ABCDE1234F")
assert len(result) == 1
assert result[0].entity_type == "IN_PAN"
# ---------------------------------------------------------------------------
# IN_PAN — invalid formats
# ---------------------------------------------------------------------------
def test_pan_invalid_formats():
"""Lowercase, digit-ending, and 4-letter-prefix inputs must not match."""
recognizer = _pan_recognizer()
assert len(_matches(recognizer, "abcde1234f")) == 0, "lowercase PAN should not match"
assert len(_matches(recognizer, "ABCDE12345")) == 0, "PAN ending in digit should not match"
assert len(_matches(recognizer, "ABCD1234F")) == 0, "only 4 leading letters should not match"
# ---------------------------------------------------------------------------
# IN_VEHICLE_REGISTRATION — valid formats
# ---------------------------------------------------------------------------
def test_vehicle_registration_valid_formats():
"""2-digit district, 1-digit district, and 1-letter series all match."""
recognizer = _vehicle_registration_recognizer()
result_std = _matches(recognizer, "vehicle number MH01AB1234")
assert len(result_std) == 1
assert result_std[0].entity_type == "IN_VEHICLE_REGISTRATION"
result_1d = _matches(recognizer, "reg no DL3CAF0001")
assert len(result_1d) == 1
assert result_1d[0].entity_type == "IN_VEHICLE_REGISTRATION"
result_1s = _matches(recognizer, "number plate KA01A1234")
assert len(result_1s) == 1
assert result_1s[0].entity_type == "IN_VEHICLE_REGISTRATION"
# ---------------------------------------------------------------------------
# PAN vs vehicle registration collision — PAN score (0.85) wins
# ---------------------------------------------------------------------------
def test_pan_wins_over_vehicle_on_collision():
"""When a span matches both IN_PAN and IN_VEHICLE_REGISTRATION, IN_PAN wins."""
# ABCDE1234F — 5 letters + 4 digits + 1 letter — matches PAN regex exactly.
# It also satisfies the vehicle reg pattern (2+1+3+4 split: AB·C·DE·1234F is
# borderline, but Presidio score resolves it). We assert IN_PAN dominates.
pan_r = _pan_recognizer()
veh_r = _vehicle_registration_recognizer()
engine = _engine_with(pan_r, veh_r)
results = engine.analyze(
text="My PAN is ABCDE1234F",
entities=["IN_PAN", "IN_VEHICLE_REGISTRATION"],
language="en",
)
entity_types = {r.entity_type for r in results}
assert "IN_PAN" in entity_types
assert "IN_VEHICLE_REGISTRATION" not in entity_types