Spaces:
Running
Running
| import pytest | |
| from engine.id_parser import ( | |
| validate_luhn, | |
| parse_id_number, | |
| extract_id_number, | |
| cross_validate, | |
| validate_id_for_country, | |
| _normalize_date, | |
| _check_date_transposition, | |
| ) | |
| class TestValidateLuhn: | |
| def test_valid_id(self): | |
| # Known valid SA ID number | |
| assert validate_luhn("8801015800082") is True | |
| def test_invalid_id_bad_checksum(self): | |
| assert validate_luhn("8801015800083") is False | |
| def test_invalid_too_short(self): | |
| assert validate_luhn("880101580008") is False | |
| def test_invalid_too_long(self): | |
| assert validate_luhn("88010158000820") is False | |
| def test_invalid_non_digits(self): | |
| assert validate_luhn("880101580008A") is False | |
| def test_empty_string(self): | |
| assert validate_luhn("") is False | |
| def test_none(self): | |
| assert validate_luhn(None) is False | |
| def test_valid_female_citizen(self): | |
| assert validate_luhn("9202204720083") is True | |
| def test_valid_male_resident(self): | |
| assert validate_luhn("7801015800182") is True | |
| class TestParseIdNumber: | |
| def test_male_citizen(self): | |
| result = parse_id_number("8801015800082") | |
| assert result["date_of_birth"] == "1988-01-01" | |
| assert result["sex"] == "Male" | |
| assert result["citizenship"] == "SA Citizen" | |
| assert result["is_valid"] is True | |
| def test_female(self): | |
| result = parse_id_number("9202204720083") | |
| assert result["sex"] == "Female" | |
| assert result["is_valid"] is True | |
| def test_permanent_resident(self): | |
| result = parse_id_number("7801015800182") | |
| assert result["citizenship"] == "Permanent Resident" | |
| def test_invalid_id(self): | |
| result = parse_id_number("1234567890123") | |
| assert result["is_valid"] is False | |
| def test_empty(self): | |
| result = parse_id_number("") | |
| assert result["is_valid"] is False | |
| assert result["date_of_birth"] is None | |
| class TestExtractIdNumber: | |
| def test_find_in_clean_text(self): | |
| blocks = [ | |
| ([[0, 0], [100, 0], [100, 20], [0, 20]], "IDENTITY NUMBER", 0.95), | |
| ([[0, 25], [200, 25], [200, 45], [0, 45]], "8801015800082", 0.92), | |
| ] | |
| id_num, conf = extract_id_number(blocks) | |
| assert id_num == "8801015800082" | |
| assert conf > 0 | |
| def test_find_with_spaces(self): | |
| blocks = [ | |
| ([[0, 0], [200, 0], [200, 20], [0, 20]], "8801 0158 0008 2", 0.85), | |
| ] | |
| id_num, conf = extract_id_number(blocks) | |
| assert id_num == "8801015800082" | |
| def test_find_with_dashes(self): | |
| blocks = [ | |
| ([[0, 0], [200, 0], [200, 20], [0, 20]], "880101-5800-082", 0.85), | |
| ] | |
| id_num, conf = extract_id_number(blocks) | |
| assert id_num == "8801015800082" | |
| def test_no_valid_id(self): | |
| blocks = [ | |
| ([[0, 0], [100, 0], [100, 20], [0, 20]], "REPUBLIC OF SOUTH AFRICA", 0.95), | |
| ([[0, 25], [100, 25], [100, 45], [0, 45]], "IDENTITY CARD", 0.90), | |
| ] | |
| id_num, conf = extract_id_number(blocks) | |
| assert id_num is None | |
| class TestCrossValidate: | |
| def test_all_match(self): | |
| fields = { | |
| "date_of_birth": "01/01/1988", | |
| "sex": "Male", | |
| "citizenship_status": "SA Citizen", | |
| } | |
| parsed = { | |
| "date_of_birth": "1988-01-01", | |
| "sex": "Male", | |
| "citizenship": "SA Citizen", | |
| "is_valid": True, | |
| } | |
| result = cross_validate(fields, parsed) | |
| assert result["luhn_valid"] is True | |
| assert result["dob_cross_check"] is True | |
| assert result["gender_cross_check"] is True | |
| assert result["citizenship_cross_check"] is True | |
| def test_gender_mismatch(self): | |
| fields = {"sex": "Female"} | |
| parsed = {"sex": "Male", "is_valid": True} | |
| result = cross_validate(fields, parsed) | |
| assert result["gender_cross_check"] is False | |
| class TestNormalizeDate: | |
| def test_iso_format(self): | |
| assert _normalize_date("1988-01-01") == "1988-01-01" | |
| def test_slash_format(self): | |
| assert _normalize_date("01/01/1988") == "1988-01-01" | |
| def test_dash_dmy(self): | |
| assert _normalize_date("01-01-1988") == "1988-01-01" | |
| def test_invalid(self): | |
| assert _normalize_date("not a date") is None | |
| class TestDateTransposition: | |
| def test_exact_match(self): | |
| result = _check_date_transposition("01/01/1988", "1988-01-01") | |
| assert result["exact_match"] is True | |
| assert result["transposed_match"] is False | |
| def test_transposed_day_month(self): | |
| # OCR reads "1988-05-03" but ID actually encodes 1988-03-05 (day/month swapped) | |
| result = _check_date_transposition("1988-05-03", "1988-03-05") | |
| assert result["transposed_match"] is True | |
| assert result["corrected_date"] == "1988-03-05" | |
| def test_no_match(self): | |
| result = _check_date_transposition("15/06/1990", "1988-01-01") | |
| assert result["exact_match"] is False | |
| assert result["transposed_match"] is False | |
| def test_none_input(self): | |
| result = _check_date_transposition(None, "1988-01-01") | |
| assert result["exact_match"] is False | |
| def test_unambiguous_date_not_transposed(self): | |
| # Day=25 can't be a month, so transposition shouldn't match | |
| result = _check_date_transposition("25/06/1990", "1990-06-25") | |
| assert result["exact_match"] is True | |
| class TestValidateIdForCountry: | |
| def test_sa_valid(self): | |
| result = validate_id_for_country("8801015800082", "ZAF") | |
| assert result["valid"] is True | |
| assert result["method"] == "luhn" | |
| def test_sa_invalid_luhn(self): | |
| result = validate_id_for_country("8801015800083", "ZAF") | |
| assert result["valid"] is False | |
| def test_nigeria_valid(self): | |
| result = validate_id_for_country("12345678901", "NGA") | |
| assert result["valid"] is True | |
| assert result["method"] == "regex" | |
| def test_nigeria_invalid_too_short(self): | |
| result = validate_id_for_country("1234567890", "NGA") | |
| assert result["valid"] is False | |
| def test_kenya_valid(self): | |
| result = validate_id_for_country("12345678", "KEN") | |
| assert result["valid"] is True | |
| def test_zimbabwe_valid(self): | |
| result = validate_id_for_country("12345678A12", "ZWE") | |
| assert result["valid"] is True | |
| def test_zimbabwe_invalid(self): | |
| result = validate_id_for_country("12345678112", "ZWE") | |
| assert result["valid"] is False | |
| def test_uganda_valid(self): | |
| result = validate_id_for_country("AB123456789012", "UGA") | |
| assert result["valid"] is True | |
| def test_zambia_valid(self): | |
| result = validate_id_for_country("1234567890", "ZMB") | |
| assert result["valid"] is True | |
| def test_unknown_country_passes(self): | |
| result = validate_id_for_country("ANY123", "XXX") | |
| assert result["valid"] is True | |
| assert result["method"] == "no_rules" | |
| def test_empty_id_fails(self): | |
| result = validate_id_for_country("", "ZAF") | |
| assert result["valid"] is False | |
| class TestCrossValidateTransposition: | |
| def test_transposed_dob_auto_corrected(self): | |
| # OCR extracted date has day/month swapped compared to ID-encoded date | |
| fields = { | |
| "date_of_birth": "1988-05-03", | |
| "sex": "Male", | |
| } | |
| parsed = { | |
| "date_of_birth": "1988-03-05", | |
| "sex": "Male", | |
| "citizenship": "SA Citizen", | |
| "is_valid": True, | |
| } | |
| result = cross_validate(fields, parsed) | |
| assert result["dob_cross_check"] is True | |
| assert result["dob_transposed"] is True | |
| # The field should be corrected in-place to match the ID | |
| assert fields["date_of_birth"] == "1988-03-05" | |