""" 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