Files
iban-ex/test/iban_ex/registry_validation_test.exs
2025-11-29 22:47:56 -05:00

481 lines
15 KiB
Elixir

defmodule IbanEx.RegistryValidationTest do
@moduledoc """
Comprehensive registry validation tests for IbanEx.
Validates IbanEx implementation against the official SWIFT IBAN Registry Release 100.
Ensures all 105 countries/territories are correctly supported with proper:
- IBAN lengths
- BBAN structures
- Component positions
- SEPA classifications
- Check digit validation
Based on Test Coverage Improvement Plan and IBAN Registry Summary.
"""
use ExUnit.Case, async: true
alias IbanEx.{Validator, Parser, Country, TestData}
describe "Registry coverage - all 105 countries" do
test "validates all 105 registry IBANs successfully" do
valid_ibans = TestData.valid_ibans()
assert length(valid_ibans) == 105,
"Expected 105 IBANs from registry, got #{length(valid_ibans)}"
Enum.each(valid_ibans, fn iban ->
assert TestData.valid?(iban),
"Registry IBAN validation failed: #{iban}"
end)
end
test "parses all 105 registry IBANs successfully" do
valid_ibans = TestData.valid_ibans()
Enum.each(valid_ibans, fn iban ->
assert {:ok, _parsed} = Parser.parse(iban),
"Registry IBAN parsing failed: #{iban}"
end)
end
test "all registry country codes are supported" do
registry_codes = TestData.all_country_codes()
supported_codes = Country.supported_country_codes() |> Enum.sort()
registry_set = MapSet.new(registry_codes)
supported_set = MapSet.new(supported_codes)
missing = MapSet.difference(registry_set, supported_set) |> MapSet.to_list()
extra = MapSet.difference(supported_set, registry_set) |> MapSet.to_list()
assert missing == [], "Missing country codes: #{inspect(missing)}"
assert extra == [], "Extra unsupported codes: #{inspect(extra)}"
assert length(registry_codes) == length(supported_codes)
end
end
describe "IBAN length validation - 18 unique lengths (15-33)" do
test "validates shortest IBAN (Norway, 15 chars)" do
shortest = TestData.edge_cases().shortest
assert String.length(shortest) == 15
assert TestData.valid?(shortest)
assert shortest =~ ~r/^NO\d{13}$/
end
test "validates longest IBAN (Russia, 33 chars)" do
longest = TestData.edge_cases().longest
assert String.length(longest) == 33
assert TestData.valid?(longest)
assert longest =~ ~r/^RU\d{31}$/
end
test "validates all unique IBAN lengths from registry" do
# Registry has 18 unique lengths: 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33
fixtures = TestData.load_fixtures()
lengths_in_registry =
fixtures["country_specs"]
|> Map.values()
|> Enum.map(fn spec -> spec["iban_length"] end)
|> Enum.uniq()
|> Enum.sort()
assert 15 in lengths_in_registry
assert 33 in lengths_in_registry
assert length(lengths_in_registry) >= 18
# Validate at least one IBAN for each length
Enum.each(lengths_in_registry, fn target_length ->
ibans = TestData.ibans_with(length: target_length)
assert length(ibans) > 0, "No IBANs found for length #{target_length}"
iban = List.first(ibans)
assert String.length(iban) == target_length
assert TestData.valid?(iban)
end)
end
test "country module sizes match registry lengths" do
TestData.all_country_codes()
|> Enum.each(fn country_code ->
spec = TestData.country_spec(country_code)
expected_length = spec["iban_length"]
country_module = Country.country_module(country_code)
actual_length = country_module.size()
assert actual_length == expected_length,
"Length mismatch for #{country_code}: expected #{expected_length}, got #{actual_length}"
end)
end
end
describe "BBAN structure validation" do
test "all countries have bank codes (100% coverage)" do
TestData.valid_ibans()
|> Enum.each(fn iban ->
{:ok, parsed} = Parser.parse(iban)
assert is_binary(parsed.bank_code),
"Missing bank code for: #{iban}"
assert String.length(parsed.bank_code) > 0,
"Empty bank code for: #{iban}"
end)
end
test "55 countries have branch codes" do
ibans_with_branch = TestData.ibans_with(has_branch_code: true)
# Registry indicates 52% (55 countries) have branch codes
assert length(ibans_with_branch) >= 50,
"Expected ~55 countries with branch codes, got #{length(ibans_with_branch)}"
Enum.each(ibans_with_branch, fn iban ->
{:ok, parsed} = Parser.parse(iban)
assert is_binary(parsed.branch_code)
assert String.length(parsed.branch_code) > 0
end)
end
test "13 countries have national check digits" do
ibans_with_check = TestData.ibans_with(has_national_check: true)
# Registry indicates 12% (13 countries) have national check digits
assert length(ibans_with_check) >= 10,
"Expected ~13 countries with national check, got #{length(ibans_with_check)}"
Enum.each(ibans_with_check, fn iban ->
{:ok, parsed} = Parser.parse(iban)
assert is_binary(parsed.national_check)
assert String.length(parsed.national_check) > 0
end)
end
test "all countries have account numbers (100% coverage)" do
TestData.valid_ibans()
|> Enum.each(fn iban ->
{:ok, parsed} = Parser.parse(iban)
assert is_binary(parsed.account_number),
"Missing account number for: #{iban}"
assert String.length(parsed.account_number) > 0,
"Empty account number for: #{iban}"
end)
end
end
describe "Character type distribution" do
test "validates numeric-only BBANs (68 countries, 64.8%)" do
numeric_ibans = TestData.ibans_with(numeric_only: true)
assert length(numeric_ibans) >= 65,
"Expected ~68 numeric-only countries, got #{length(numeric_ibans)}"
# Verify they are actually numeric
Enum.each(numeric_ibans, fn iban ->
{:ok, parsed} = Parser.parse(iban)
bban = String.slice(iban, 4..-1//1
assert bban =~ ~r/^\d+$/,
"Expected numeric BBAN for #{parsed.country_code}, got: #{bban}"
end)
end
test "validates alphanumeric BBANs (31+ countries, 29.5%)" do
alphanumeric_ibans = TestData.ibans_with(numeric_only: false)
assert length(alphanumeric_ibans) >= 30,
"Expected ~31 alphanumeric countries, got #{length(alphanumeric_ibans)}"
# Verify they contain letters
Enum.each(alphanumeric_ibans, fn iban ->
bban = String.slice(iban, 4..-1//1
assert bban =~ ~r/[A-Z]/,
"Expected alphanumeric BBAN for #{iban}, got: #{bban}"
end)
end
test "validates specific alphanumeric examples from registry" do
# Bahrain: 4!a14!c pattern
{:ok, bh} = Parser.parse("BH67BMAG00001299123456")
assert bh.bank_code == "BMAG"
assert bh.bank_code =~ ~r/^[A-Z]{4}$/
# Qatar: 4!a21!c pattern
{:ok, qa} = Parser.parse("QA58DOHB00001234567890ABCDEFG")
assert qa.bank_code == "DOHB"
assert qa.account_number =~ ~r/[A-Z]/
end
end
describe "SEPA country validation (53 countries)" do
test "validates all 53 SEPA country IBANs" do
sepa_ibans = TestData.valid_ibans(sepa_only: true)
assert length(sepa_ibans) == 53,
"Expected 53 SEPA IBANs, got #{length(sepa_ibans)}"
Enum.each(sepa_ibans, fn iban ->
assert TestData.valid?(iban),
"SEPA IBAN validation failed: #{iban}"
end)
end
test "validates major SEPA countries" do
major_sepa = %{
"DE" => "Germany",
"FR" => "France",
"GB" => "United Kingdom",
"IT" => "Italy",
"ES" => "Spain",
"NL" => "Netherlands",
"CH" => "Switzerland",
"AT" => "Austria",
"BE" => "Belgium",
"SE" => "Sweden"
}
Enum.each(major_sepa, fn {code, name} ->
[iban] = TestData.valid_ibans(country: code)
assert TestData.valid?(iban),
"Major SEPA country #{name} (#{code}) validation failed"
end)
end
test "validates SEPA territory mappings" do
# French territories use FR rules
french_territories = [
"GF",
"GP",
"MQ",
"RE",
"PF",
"TF",
"YT",
"NC",
"BL",
"MF",
"PM",
"WF"
]
Enum.each(french_territories, fn territory ->
ibans = TestData.valid_ibans(country: territory)
if length(ibans) > 0 do
iban = List.first(ibans)
{:ok, parsed} = Parser.parse(iban)
# Should have same length as France (27 chars)
assert String.length(iban) == 27,
"French territory #{territory} should have 27 chars like France"
end
end)
# British territories
british_territories = ["IM", "JE", "GG"]
Enum.each(british_territories, fn territory ->
ibans = TestData.valid_ibans(country: territory)
if length(ibans) > 0 do
iban = List.first(ibans)
{:ok, parsed} = Parser.parse(iban)
# Should have same length as GB (22 chars)
assert String.length(iban) == 22,
"British territory #{territory} should have 22 chars like GB"
end
end)
end
test "SEPA country specs match registry" do
sepa_codes = TestData.sepa_country_codes()
assert length(sepa_codes) == 53
assert "DE" in sepa_codes
assert "FR" in sepa_codes
assert "GB" in sepa_codes
end
end
describe "Checksum validation across all countries" do
test "all 105 registry IBANs have valid checksums" do
TestData.valid_ibans()
|> Enum.each(fn iban ->
violations = Validator.violations(iban)
refute :invalid_checksum in violations,
"Invalid checksum for registry IBAN: #{iban}"
end)
end
test "checksum validation works for shortest IBAN" do
shortest = TestData.edge_cases().shortest
assert TestData.valid?(shortest)
# Test with invalid checksum
invalid = String.replace(shortest, ~r/^NO\d{2}/, "NO00")
refute TestData.valid?(invalid)
end
test "checksum validation works for longest IBAN" do
longest = TestData.edge_cases().longest
assert TestData.valid?(longest)
# Test with invalid checksum
invalid = String.replace(longest, ~r/^RU\d{2}/, "RU00")
refute TestData.valid?(invalid)
end
test "checksum validation works for alphanumeric BBANs" do
# Bahrain
assert TestData.valid?("BH67BMAG00001299123456")
refute TestData.valid?("BH00BMAG00001299123456")
# Qatar
assert TestData.valid?("QA58DOHB00001234567890ABCDEFG")
refute TestData.valid?("QA00DOHB00001234567890ABCDEFG")
end
end
describe "Component position accuracy" do
test "Germany - simple numeric structure (8!n10!n)" do
{:ok, de} = Parser.parse("DE89370400440532013000")
assert de.bank_code == "37040044"
assert String.length(de.bank_code) == 8
assert de.account_number == "0532013000"
assert String.length(de.account_number) == 10
assert de.branch_code == nil
assert de.national_check == nil
end
test "France - complex structure with all components (5!n5!n11!c2!n)" do
{:ok, fr} = Parser.parse("FR1420041010050500013M02606")
assert fr.bank_code == "20041"
assert String.length(fr.bank_code) == 5
assert fr.branch_code == "01005"
assert String.length(fr.branch_code) == 5
assert fr.account_number == "0500013M026"
assert String.length(fr.account_number) == 11
assert fr.national_check == "06"
assert String.length(fr.national_check) == 2
end
test "Russia - longest with 9-digit bank, 5-digit branch (9!n5!n15!n)" do
longest = TestData.edge_cases().longest
{:ok, ru} = Parser.parse(longest)
assert String.length(ru.bank_code) == 9
assert String.length(ru.branch_code) == 5
assert String.length(ru.account_number) == 15
end
test "Norway - shortest with minimal structure (4!n6!n1!n)" do
{:ok, no} = Parser.parse("NO9386011117947")
assert String.length(no.bank_code) == 4
# 6 + 1 check digit
assert String.length(no.account_number) == 7
assert no.branch_code == nil
end
test "GB - 6-digit sort code as branch (4!a6!n8!n)" do
{:ok, gb} = Parser.parse("GB29NWBK60161331926819")
assert gb.bank_code == "NWBK"
assert String.length(gb.bank_code) == 4
assert gb.branch_code == "601613"
assert String.length(gb.branch_code) == 6
assert gb.account_number == "31926819"
assert String.length(gb.account_number) == 8
end
end
describe "Registry metadata validation" do
test "registry contains 105 total countries/territories" do
fixtures = TestData.load_fixtures()
metadata = fixtures["metadata"]
assert metadata["total_countries"] == 105
end
test "registry contains 53 SEPA countries" do
fixtures = TestData.load_fixtures()
metadata = fixtures["metadata"]
assert metadata["sepa_countries"] == 53
end
test "registry source is SWIFT IBAN Registry" do
fixtures = TestData.load_fixtures()
metadata = fixtures["metadata"]
assert metadata["source"] == "SWIFT IBAN Registry"
end
test "all countries have complete specifications" do
TestData.all_country_codes()
|> Enum.each(fn country_code ->
spec = TestData.country_spec(country_code)
assert spec["country_name"] != nil
assert spec["iban_length"] != nil
assert spec["bban_length"] != nil
assert spec["iban_spec"] != nil
assert spec["bban_spec"] != nil
assert Map.has_key?(spec, "sepa")
assert spec["positions"] != nil
end)
end
end
describe "Print format vs Electronic format" do
test "validates both formats for all countries" do
fixtures = TestData.load_fixtures()
Enum.each(fixtures["valid_ibans"], fn {_code, data} ->
electronic = data["electronic"]
print = data["print"]
assert TestData.valid?(electronic),
"Electronic format validation failed: #{electronic}"
assert TestData.valid?(print),
"Print format validation failed: #{print}"
end)
end
test "electronic and print formats normalize to same value" do
fixtures = TestData.load_fixtures()
Enum.each(fixtures["valid_ibans"], fn {_code, data} ->
electronic = data["electronic"]
print = data["print"]
{:ok, from_electronic} = Parser.parse(electronic)
{:ok, from_print} = Parser.parse(print)
assert from_electronic.iban == from_print.iban,
"Normalization mismatch: #{electronic} vs #{print}"
end)
end
end
end