432 lines
13 KiB
Elixir
432 lines
13 KiB
Elixir
defmodule IbanEx.ParserTest do
|
|
@moduledoc """
|
|
Comprehensive test coverage for IbanEx.Parser module.
|
|
Based on Test Coverage Improvement Plan - Phase 1: Critical Coverage.
|
|
"""
|
|
|
|
use ExUnit.Case, async: true
|
|
|
|
alias IbanEx.{Parser, TestData, Iban}
|
|
|
|
describe "parse/1 - comprehensive parsing" do
|
|
test "parses all 105 registry valid IBANs successfully" do
|
|
TestData.valid_ibans()
|
|
|> Enum.each(fn iban ->
|
|
assert {:ok, %Iban{}} = Parser.parse(iban), "Failed to parse: #{iban}"
|
|
end)
|
|
end
|
|
|
|
test "parses shortest IBAN (Norway, 15 chars)" do
|
|
{:ok, iban} = Parser.parse("NO9386011117947")
|
|
|
|
assert iban.country_code == "NO"
|
|
assert iban.check_digits == "93"
|
|
assert iban.bank_code == "8601"
|
|
assert iban.account_number == "111794"
|
|
assert iban.branch_code == nil
|
|
assert String.length(iban.iban) == 15
|
|
end
|
|
|
|
test "parses longest IBAN (Russia, 33 chars)" do
|
|
longest = TestData.edge_cases().longest
|
|
{:ok, iban} = Parser.parse(longest)
|
|
|
|
assert iban.country_code == "RU"
|
|
assert String.length(iban.iban) == 33
|
|
assert String.length(iban.bank_code) == 9
|
|
assert String.length(iban.branch_code) == 5
|
|
assert String.length(iban.account_number) == 15
|
|
end
|
|
|
|
test "parses IBAN with branch code (France)" do
|
|
{:ok, iban} = Parser.parse("FR1420041010050500013M02606")
|
|
|
|
assert iban.country_code == "FR"
|
|
assert iban.check_digits == "14"
|
|
assert iban.bank_code == "20041"
|
|
assert iban.branch_code == "01005"
|
|
assert iban.account_number == "0500013M026"
|
|
assert iban.national_check == "06"
|
|
end
|
|
|
|
test "parses IBAN with national check (Italy)" do
|
|
{:ok, iban} = Parser.parse("IT60X0542811101000000123456")
|
|
|
|
assert iban.country_code == "IT"
|
|
assert iban.check_digits == "60"
|
|
assert iban.national_check == "X"
|
|
assert iban.bank_code == "05428"
|
|
assert iban.branch_code == "11101"
|
|
assert iban.account_number == "000000123456"
|
|
end
|
|
|
|
test "parses IBAN with alphanumeric BBAN (Qatar)" do
|
|
{:ok, iban} = Parser.parse("QA58DOHB00001234567890ABCDEFG")
|
|
|
|
assert iban.country_code == "QA"
|
|
assert iban.check_digits == "58"
|
|
assert iban.bank_code == "DOHB"
|
|
assert iban.account_number == "00001234567890ABCDEFG"
|
|
end
|
|
|
|
test "parses electronic format" do
|
|
{:ok, iban} = Parser.parse("DE89370400440532013000")
|
|
|
|
assert iban.iban == "DE89370400440532013000"
|
|
assert iban.country_code == "DE"
|
|
end
|
|
|
|
test "parses print format with spaces" do
|
|
{:ok, iban} = Parser.parse("DE89 3704 0044 0532 0130 00")
|
|
|
|
# Parser should normalize to electronic format
|
|
assert iban.iban == "DE89370400440532013000"
|
|
assert iban.country_code == "DE"
|
|
end
|
|
|
|
test "returns error for invalid checksum" do
|
|
assert {:error, _} = Parser.parse("DE00370400440532013000")
|
|
end
|
|
|
|
test "returns error for unsupported country code" do
|
|
assert {:error, _} = Parser.parse("XX89370400440532013000")
|
|
end
|
|
|
|
test "returns error for invalid length (too short)" do
|
|
assert {:error, _} = Parser.parse("DE8937040044053201300")
|
|
end
|
|
|
|
test "returns error for invalid length (too long)" do
|
|
assert {:error, _} = Parser.parse("DE89370400440532013000XXX")
|
|
end
|
|
|
|
test "returns error for invalid characters" do
|
|
assert {:error, _} = Parser.parse("DE89370400440532013Ї00")
|
|
end
|
|
|
|
test "returns error for empty string" do
|
|
assert {:error, _} = Parser.parse("")
|
|
end
|
|
|
|
test "returns error for nil" do
|
|
assert {:error, _} = Parser.parse(nil)
|
|
end
|
|
end
|
|
|
|
describe "parse/1 - BBAN component extraction" do
|
|
test "extracts bank code for all countries" 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
|
|
end)
|
|
end
|
|
|
|
test "extracts branch code for countries that have it" do
|
|
# Countries with branch codes (55 countries)
|
|
ibans_with_branch = TestData.ibans_with(has_branch_code: true)
|
|
|
|
Enum.each(ibans_with_branch, fn iban ->
|
|
{:ok, parsed} = Parser.parse(iban)
|
|
assert is_binary(parsed.branch_code), "Missing branch code for: #{iban}"
|
|
assert String.length(parsed.branch_code) > 0
|
|
end)
|
|
end
|
|
|
|
test "sets branch code to nil for countries without it" do
|
|
ibans_without_branch = TestData.ibans_with(has_branch_code: false)
|
|
|
|
Enum.each(ibans_without_branch, fn iban ->
|
|
{:ok, parsed} = Parser.parse(iban)
|
|
assert is_nil(parsed.branch_code), "Unexpected branch code for: #{iban}"
|
|
end)
|
|
end
|
|
|
|
test "extracts national check for countries that have it" do
|
|
ibans_with_check = TestData.ibans_with(has_national_check: true)
|
|
|
|
Enum.each(ibans_with_check, fn iban ->
|
|
{:ok, parsed} = Parser.parse(iban)
|
|
assert is_binary(parsed.national_check), "Missing national check for: #{iban}"
|
|
assert String.length(parsed.national_check) > 0
|
|
end)
|
|
end
|
|
|
|
test "sets national check to nil for countries without it" do
|
|
ibans_without_check = TestData.ibans_with(has_national_check: false)
|
|
|
|
Enum.each(ibans_without_check, fn iban ->
|
|
{:ok, parsed} = Parser.parse(iban)
|
|
assert is_nil(parsed.national_check), "Unexpected national check for: #{iban}"
|
|
end)
|
|
end
|
|
|
|
test "extracts account number for all countries" 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
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "parse/1 - position calculations" do
|
|
test "correctly calculates positions for Germany (simple structure)" do
|
|
{:ok, iban} = Parser.parse("DE89370400440532013000")
|
|
|
|
# BBAN: 370400440532013000
|
|
# Bank code (8n): 37040044
|
|
# Account (10n): 0532013000
|
|
assert iban.bank_code == "37040044"
|
|
assert iban.account_number == "0532013000"
|
|
assert iban.branch_code == nil
|
|
end
|
|
|
|
test "correctly calculates positions for France (complex structure)" do
|
|
{:ok, iban} = Parser.parse("FR1420041010050500013M02606")
|
|
|
|
# BBAN: 20041010050500013M02606
|
|
# Bank (5n): 20041
|
|
# Branch (5n): 01005
|
|
# Account (11c): 0500013M026
|
|
# Check (2n): 06
|
|
assert iban.bank_code == "20041"
|
|
assert iban.branch_code == "01005"
|
|
assert iban.account_number == "0500013M026"
|
|
assert iban.national_check == "06"
|
|
end
|
|
|
|
test "correctly calculates positions for GB (6-digit sort code)" do
|
|
{:ok, iban} = Parser.parse("GB29NWBK60161331926819")
|
|
|
|
# Bank (4a): NWBK
|
|
# Branch/Sort (6n): 601613
|
|
# Account (8n): 31926819
|
|
assert iban.bank_code == "NWBK"
|
|
assert iban.branch_code == "601613"
|
|
assert iban.account_number == "31926819"
|
|
end
|
|
|
|
test "correctly calculates positions for Brazil (complex alphanumeric)" do
|
|
{:ok, iban} = Parser.parse("BR1800360305000010009795493C1")
|
|
|
|
# Brazil has: bank (8n) + branch (5n) + account (10n) + type (1a) + owner (1c)
|
|
assert String.length(iban.bank_code) == 8
|
|
assert String.length(iban.branch_code) == 5
|
|
# Account number includes type and owner
|
|
assert String.length(iban.account_number) > 10
|
|
end
|
|
end
|
|
|
|
describe "parse/1 - edge cases" do
|
|
test "handles all-numeric BBANs" do
|
|
# Germany has all-numeric BBAN
|
|
{:ok, iban} = Parser.parse("DE89370400440532013000")
|
|
|
|
assert iban.bank_code =~ ~r/^\d+$/
|
|
assert iban.account_number =~ ~r/^\d+$/
|
|
end
|
|
|
|
test "handles alphanumeric BBANs" do
|
|
# Qatar has alphanumeric BBAN
|
|
{:ok, iban} = Parser.parse("QA58DOHB00001234567890ABCDEFG")
|
|
|
|
assert iban.bank_code == "DOHB"
|
|
assert iban.account_number =~ ~r/[A-Z]/
|
|
end
|
|
|
|
test "handles IBANs with letters at the end" do
|
|
# Brazil ends with letter
|
|
{:ok, iban} = Parser.parse("BR1800360305000010009795493C1")
|
|
|
|
assert String.ends_with?(iban.account_number, "1") or
|
|
String.ends_with?(iban.iban, "1")
|
|
end
|
|
|
|
test "handles minimum length IBAN (15 chars)" do
|
|
{:ok, iban} = Parser.parse("NO9386011117947")
|
|
|
|
assert String.length(iban.iban) == 15
|
|
assert iban.country_code == "NO"
|
|
assert String.length(iban.bank_code) == 4
|
|
assert String.length(iban.account_number) == 6
|
|
end
|
|
|
|
test "handles maximum length IBAN (33 chars)" do
|
|
longest = TestData.edge_cases().longest
|
|
{:ok, iban} = Parser.parse(longest)
|
|
|
|
assert String.length(iban.iban) == 33
|
|
assert iban.country_code == "RU"
|
|
end
|
|
|
|
test "handles IBANs from all length categories" do
|
|
length_samples = [
|
|
# Shortest
|
|
15,
|
|
# Short
|
|
18,
|
|
# Medium-short (most common)
|
|
22,
|
|
# Medium-long (most common)
|
|
27,
|
|
# Long
|
|
29,
|
|
# Longest
|
|
33
|
|
]
|
|
|
|
Enum.each(length_samples, fn target_length ->
|
|
ibans = TestData.ibans_with(length: target_length)
|
|
|
|
if length(ibans) > 0 do
|
|
iban = List.first(ibans)
|
|
assert {:ok, parsed} = Parser.parse(iban)
|
|
assert String.length(parsed.iban) == target_length
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "parse/1 - normalization" do
|
|
test "normalizes print format to electronic format" do
|
|
print_format = "DE89 3704 0044 0532 0130 00"
|
|
electronic_format = "DE89370400440532013000"
|
|
|
|
{:ok, from_print} = Parser.parse(print_format)
|
|
{:ok, from_electronic} = Parser.parse(electronic_format)
|
|
|
|
assert from_print.iban == from_electronic.iban
|
|
assert from_print == from_electronic
|
|
end
|
|
|
|
test "removes whitespace from input" do
|
|
with_spaces = "FR14 2004 1010 0505 0001 3M02 606"
|
|
{:ok, iban} = Parser.parse(with_spaces)
|
|
|
|
refute String.contains?(iban.iban, " ")
|
|
end
|
|
|
|
test "preserves original electronic format when no spaces" do
|
|
original = "DE89370400440532013000"
|
|
{:ok, iban} = Parser.parse(original)
|
|
|
|
assert iban.iban == original
|
|
end
|
|
end
|
|
|
|
describe "parse/1 - error handling" do
|
|
test "returns descriptive error for unsupported country" do
|
|
assert {:error, reason} = Parser.parse("XX89370400440532013000")
|
|
assert reason != nil
|
|
end
|
|
|
|
test "returns descriptive error for invalid length" do
|
|
assert {:error, reason} = Parser.parse("DE8937040044053201")
|
|
assert reason != nil
|
|
end
|
|
|
|
test "returns descriptive error for invalid checksum" do
|
|
assert {:error, reason} = Parser.parse("DE00370400440532013000")
|
|
assert reason != nil
|
|
end
|
|
|
|
test "returns descriptive error for invalid characters" do
|
|
assert {:error, reason} = Parser.parse("DE89370400440532013Ї00")
|
|
assert reason != nil
|
|
end
|
|
|
|
test "returns descriptive error for empty input" do
|
|
assert {:error, reason} = Parser.parse("")
|
|
assert reason != nil
|
|
end
|
|
|
|
test "returns descriptive error for nil input" do
|
|
assert {:error, reason} = Parser.parse(nil)
|
|
assert reason != nil
|
|
end
|
|
end
|
|
|
|
describe "parse/1 - SEPA countries" do
|
|
test "parses all 53 SEPA country IBANs" do
|
|
sepa_ibans = TestData.valid_ibans(sepa_only: true)
|
|
|
|
assert length(sepa_ibans) == 53
|
|
|
|
Enum.each(sepa_ibans, fn iban ->
|
|
assert {:ok, %Iban{}} = Parser.parse(iban), "SEPA parsing failed: #{iban}"
|
|
end)
|
|
end
|
|
|
|
test "parses French territories using FR rules" do
|
|
# French territories: GF, GP, MQ, RE, etc.
|
|
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)
|
|
assert {:ok, parsed} = Parser.parse(iban)
|
|
assert parsed.country_code == territory
|
|
# Should follow FR structure
|
|
assert String.length(parsed.iban) == 27
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "parse/1 - registry compliance" do
|
|
test "parsed IBANs match registry specifications" do
|
|
TestData.all_country_codes()
|
|
|> Enum.each(fn country_code ->
|
|
spec = TestData.country_spec(country_code)
|
|
[iban] = TestData.valid_ibans(country: country_code)
|
|
|
|
{:ok, parsed} = Parser.parse(iban)
|
|
|
|
assert String.length(parsed.iban) == spec["iban_length"],
|
|
"Length mismatch for #{country_code}"
|
|
|
|
assert parsed.country_code == country_code
|
|
end)
|
|
end
|
|
|
|
test "BBAN components match registry positions" do
|
|
# Test a sample of countries with different structures
|
|
test_countries = ["DE", "FR", "GB", "IT", "ES", "BR", "NO", "RU"]
|
|
|
|
Enum.each(test_countries, fn country_code ->
|
|
spec = TestData.country_spec(country_code)
|
|
[iban] = TestData.valid_ibans(country: country_code)
|
|
|
|
{:ok, parsed} = Parser.parse(iban)
|
|
|
|
# Verify bank code length matches spec
|
|
bank_positions = spec["positions"]["bank_code"]
|
|
expected_bank_length = bank_positions["end"] - bank_positions["start"]
|
|
|
|
if expected_bank_length > 0 do
|
|
assert String.length(parsed.bank_code) == expected_bank_length,
|
|
"Bank code length mismatch for #{country_code}"
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
end
|