tests added
This commit is contained in:
210
test/support/iban_factory.exs
Normal file
210
test/support/iban_factory.exs
Normal file
@@ -0,0 +1,210 @@
|
||||
defmodule IbanEx.IbanFactory do
|
||||
@moduledoc """
|
||||
Factory for creating IBAN test fixtures with various attributes.
|
||||
Supports creating valid and invalid IBANs for comprehensive testing.
|
||||
"""
|
||||
|
||||
alias IbanEx.{Iban, Country}
|
||||
|
||||
@doc """
|
||||
Build an IBAN struct with custom attributes.
|
||||
|
||||
## Options
|
||||
- `:country_code` - Two-letter country code (default: "DE")
|
||||
- `:check_digits` - Two-digit check code (default: auto-calculated)
|
||||
- `:bank_code` - Bank identifier code
|
||||
- `:branch_code` - Branch identifier code
|
||||
- `:account_number` - Account number
|
||||
- `:national_check` - National check digit(s)
|
||||
- `:iban` - Full IBAN string (overrides other options)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IbanEx.IbanFactory.build(country_code: "DE")
|
||||
%IbanEx.Iban{country_code: "DE", ...}
|
||||
|
||||
iex> IbanEx.IbanFactory.build(iban: "DE89370400440532013000")
|
||||
%IbanEx.Iban{country_code: "DE", check_code: "89", ...}
|
||||
"""
|
||||
def build(attrs \\ []) do
|
||||
if iban_string = Keyword.get(attrs, :iban) do
|
||||
build_from_string(iban_string)
|
||||
else
|
||||
build_from_attrs(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Build an IBAN with an invalid checksum.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> iban = IbanEx.IbanFactory.build_with_invalid_checksum(country_code: "DE")
|
||||
iex> IbanEx.Validator.valid?(iban.iban)
|
||||
false
|
||||
"""
|
||||
def build_with_invalid_checksum(attrs \\ []) do
|
||||
iban = build(attrs)
|
||||
|
||||
# Flip the last digit of check code to make it invalid
|
||||
current_check = iban.check_code
|
||||
invalid_check = flip_last_digit(current_check)
|
||||
|
||||
invalid_iban =
|
||||
String.replace(iban.iban, ~r/^[A-Z]{2}\d{2}/, "#{iban.country_code}#{invalid_check}")
|
||||
|
||||
%{iban | iban: invalid_iban, check_code: invalid_check}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Build an IBAN with invalid length (too short).
|
||||
"""
|
||||
def build_with_invalid_length_short(attrs \\ []) do
|
||||
iban = build(attrs)
|
||||
invalid_iban = String.slice(iban.iban, 0..-2//1)
|
||||
|
||||
%{iban | iban: invalid_iban}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Build an IBAN with invalid length (too long).
|
||||
"""
|
||||
def build_with_invalid_length_long(attrs \\ []) do
|
||||
iban = build(attrs)
|
||||
invalid_iban = iban.iban <> "0"
|
||||
|
||||
%{iban | iban: invalid_iban}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Build an IBAN with invalid characters in BBAN.
|
||||
"""
|
||||
def build_with_invalid_characters(attrs \\ []) do
|
||||
iban = build(attrs)
|
||||
|
||||
# Replace a digit in the BBAN with an invalid character
|
||||
bban_start = 4
|
||||
iban_chars = String.graphemes(iban.iban)
|
||||
|
||||
invalid_chars =
|
||||
List.replace_at(iban_chars, bban_start + 2, "Ї")
|
||||
|> Enum.join()
|
||||
|
||||
%{iban | iban: invalid_chars}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Build an IBAN with unsupported country code.
|
||||
"""
|
||||
def build_with_unsupported_country do
|
||||
# Return the IBAN string directly since XX is not supported
|
||||
"XX89370400440532013000"
|
||||
end
|
||||
|
||||
# Private functions
|
||||
|
||||
defp build_from_string(iban_string) do
|
||||
case IbanEx.parse(iban_string) do
|
||||
{:ok, iban} -> iban
|
||||
{:error, _} -> raise "Invalid IBAN string: #{iban_string}"
|
||||
end
|
||||
end
|
||||
|
||||
defp build_from_attrs(attrs) do
|
||||
country_code = Keyword.get(attrs, :country_code, "DE")
|
||||
|
||||
# Get a valid example IBAN for this country
|
||||
example_iban = get_example_iban(country_code)
|
||||
|
||||
# Parse it to get the structure
|
||||
{:ok, base_iban} = IbanEx.parse(example_iban)
|
||||
|
||||
# Override with provided attributes
|
||||
%{
|
||||
base_iban
|
||||
| bank_code: Keyword.get(attrs, :bank_code, base_iban.bank_code),
|
||||
branch_code: Keyword.get(attrs, :branch_code, base_iban.branch_code),
|
||||
account_number: Keyword.get(attrs, :account_number, base_iban.account_number),
|
||||
national_check: Keyword.get(attrs, :national_check, base_iban.national_check)
|
||||
}
|
||||
|> rebuild_iban()
|
||||
end
|
||||
|
||||
defp get_example_iban(country_code) do
|
||||
# Use the test fixtures to get a valid example
|
||||
fixtures_path =
|
||||
Path.join([
|
||||
__DIR__,
|
||||
"..",
|
||||
"..",
|
||||
"docs",
|
||||
"international_wide_ibans",
|
||||
"iban_test_fixtures.json"
|
||||
])
|
||||
|
||||
fixtures =
|
||||
fixtures_path
|
||||
|> File.read!()
|
||||
|> JSON.decode!()
|
||||
|
||||
fixtures["valid_ibans"][country_code]["electronic"]
|
||||
end
|
||||
|
||||
defp rebuild_iban(iban) do
|
||||
# Reconstruct the BBAN from components
|
||||
bban_parts =
|
||||
[
|
||||
iban.bank_code,
|
||||
iban.branch_code,
|
||||
iban.account_number,
|
||||
iban.national_check
|
||||
]
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.join()
|
||||
|
||||
# Calculate the check digits
|
||||
check_digits = calculate_check_digits(iban.country_code, bban_parts)
|
||||
|
||||
iban_string = "#{iban.country_code}#{check_digits}#{bban_parts}"
|
||||
|
||||
%{iban | iban: iban_string, check_code: check_digits}
|
||||
end
|
||||
|
||||
defp calculate_check_digits(country_code, bban) do
|
||||
# Move country code and "00" to end, then mod 97
|
||||
rearranged = bban <> country_code <> "00"
|
||||
|
||||
# Replace letters with numbers (A=10, B=11, ..., Z=35)
|
||||
numeric =
|
||||
rearranged
|
||||
|> String.graphemes()
|
||||
|> Enum.map(fn char ->
|
||||
if char =~ ~r/[A-Z]/ do
|
||||
[char_code] = String.to_charlist(char)
|
||||
Integer.to_string(char_code - 55)
|
||||
else
|
||||
char
|
||||
end
|
||||
end)
|
||||
|> Enum.join()
|
||||
|
||||
# Calculate mod 97
|
||||
remainder =
|
||||
numeric
|
||||
|> String.to_integer()
|
||||
|> rem(97)
|
||||
|
||||
# Check digit is 98 - remainder
|
||||
check = 98 - remainder
|
||||
|
||||
check
|
||||
|> Integer.to_string()
|
||||
|> String.pad_leading(2, "0")
|
||||
end
|
||||
|
||||
defp flip_last_digit(check_code) do
|
||||
last_digit = String.last(check_code)
|
||||
flipped = if last_digit == "0", do: "1", else: "0"
|
||||
String.slice(check_code, 0..-2//1) <> flipped
|
||||
end
|
||||
end
|
||||
3477
test/support/iban_test_fixtures.json
Normal file
3477
test/support/iban_test_fixtures.json
Normal file
File diff suppressed because it is too large
Load Diff
231
test/support/test_data.exs
Normal file
231
test/support/test_data.exs
Normal file
@@ -0,0 +1,231 @@
|
||||
defmodule IbanEx.TestData do
|
||||
@moduledoc """
|
||||
Centralized test data management for IbanEx test suite.
|
||||
Provides access to IBAN registry fixtures and test case generators.
|
||||
"""
|
||||
|
||||
@fixtures_path Path.join([__DIR__, "iban_test_fixtures.json"])
|
||||
|
||||
@doc """
|
||||
Helper function to check if an IBAN is valid.
|
||||
Wraps IbanEx.Validator.validate/1 to provide a boolean result.
|
||||
"""
|
||||
def valid?(iban) do
|
||||
case IbanEx.Validator.validate(iban) do
|
||||
{:ok, _} -> true
|
||||
{:error, _} -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Load and decode the IBAN registry test fixtures.
|
||||
Returns the complete fixtures map with valid IBANs and country specs.
|
||||
"""
|
||||
def load_fixtures do
|
||||
@fixtures_path
|
||||
|> File.read!()
|
||||
|> JSON.decode!()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get valid IBANs for testing.
|
||||
|
||||
## Options
|
||||
- `:country` - Filter by country code (e.g., "DE", "FR")
|
||||
- `:sepa_only` - Only return SEPA country IBANs (default: false)
|
||||
- `:format` - `:electronic` or `:print` (default: :electronic)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IbanEx.TestData.valid_ibans(country: "DE")
|
||||
["DE89370400440532013000"]
|
||||
|
||||
iex> IbanEx.TestData.valid_ibans(sepa_only: true) |> length()
|
||||
53
|
||||
"""
|
||||
def valid_ibans(opts \\ []) do
|
||||
fixtures = load_fixtures()
|
||||
country = Keyword.get(opts, :country)
|
||||
sepa_only = Keyword.get(opts, :sepa_only, false)
|
||||
format = Keyword.get(opts, :format, :electronic)
|
||||
|
||||
valid_ibans = fixtures["valid_ibans"]
|
||||
country_specs = fixtures["country_specs"]
|
||||
|
||||
valid_ibans
|
||||
|> filter_by_country(country)
|
||||
|> filter_by_sepa(country_specs, sepa_only)
|
||||
|> extract_format(format)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get country specifications from the registry.
|
||||
|
||||
## Options
|
||||
- `:country` - Get spec for specific country code
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IbanEx.TestData.country_spec("DE")
|
||||
%{"country_name" => "Germany", "iban_length" => 22, ...}
|
||||
"""
|
||||
def country_spec(country_code) do
|
||||
load_fixtures()
|
||||
|> Map.get("country_specs")
|
||||
|> Map.get(country_code)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get all country codes from the registry.
|
||||
"""
|
||||
def all_country_codes do
|
||||
load_fixtures()
|
||||
|> Map.get("valid_ibans")
|
||||
|> Map.keys()
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get SEPA country codes from the registry.
|
||||
"""
|
||||
def sepa_country_codes do
|
||||
fixtures = load_fixtures()
|
||||
|
||||
fixtures["country_specs"]
|
||||
|> Enum.filter(fn {_code, spec} -> spec["sepa"] end)
|
||||
|> Enum.map(fn {code, _spec} -> code end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get edge case IBANs for testing boundary conditions.
|
||||
|
||||
Returns a map with:
|
||||
- `:shortest` - Shortest valid IBAN (Norway, 15 chars)
|
||||
- `:longest` - Longest valid IBAN (Russia, 33 chars)
|
||||
- `:complex` - Complex IBANs with branch codes and national checks
|
||||
"""
|
||||
def edge_cases do
|
||||
fixtures = load_fixtures()
|
||||
|
||||
%{
|
||||
shortest: fixtures["valid_ibans"]["NO"]["electronic"],
|
||||
longest: fixtures["valid_ibans"]["RU"]["electronic"],
|
||||
complex: [
|
||||
fixtures["valid_ibans"]["FR"]["electronic"],
|
||||
fixtures["valid_ibans"]["IT"]["electronic"],
|
||||
fixtures["valid_ibans"]["ES"]["electronic"]
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate a random valid IBAN from the registry.
|
||||
"""
|
||||
def random_valid_iban do
|
||||
fixtures = load_fixtures()
|
||||
country_code = fixtures["valid_ibans"] |> Map.keys() |> Enum.random()
|
||||
fixtures["valid_ibans"][country_code]["electronic"]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get all IBANs with specific characteristics.
|
||||
|
||||
## Options
|
||||
- `:length` - Filter by exact IBAN length
|
||||
- `:has_branch_code` - Filter by presence of branch code
|
||||
- `:has_national_check` - Filter by presence of national check digit
|
||||
- `:numeric_only` - Filter by numeric-only BBAN structure
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IbanEx.TestData.ibans_with(length: 22)
|
||||
["DE89370400440532013000", ...]
|
||||
"""
|
||||
def ibans_with(opts) do
|
||||
fixtures = load_fixtures()
|
||||
specs = fixtures["country_specs"]
|
||||
valid_ibans = fixtures["valid_ibans"]
|
||||
|
||||
specs
|
||||
|> filter_specs_by_options(opts)
|
||||
|> Enum.map(fn {code, _spec} -> valid_ibans[code]["electronic"] end)
|
||||
end
|
||||
|
||||
# Private functions
|
||||
|
||||
defp filter_by_country(valid_ibans, nil), do: valid_ibans
|
||||
|
||||
defp filter_by_country(valid_ibans, country) do
|
||||
Map.take(valid_ibans, [country])
|
||||
end
|
||||
|
||||
defp filter_by_sepa(valid_ibans, _country_specs, false), do: valid_ibans
|
||||
|
||||
defp filter_by_sepa(valid_ibans, country_specs, true) do
|
||||
sepa_codes =
|
||||
country_specs
|
||||
|> Enum.filter(fn {_code, spec} -> spec["sepa"] end)
|
||||
|> Enum.map(fn {code, _spec} -> code end)
|
||||
|
||||
Map.take(valid_ibans, sepa_codes)
|
||||
end
|
||||
|
||||
defp extract_format(valid_ibans, format) do
|
||||
format_key = Atom.to_string(format)
|
||||
|
||||
valid_ibans
|
||||
|> Enum.map(fn {_code, data} -> data[format_key] end)
|
||||
end
|
||||
|
||||
defp filter_specs_by_options(specs, opts) do
|
||||
specs
|
||||
|> filter_by_length(Keyword.get(opts, :length))
|
||||
|> filter_by_branch_code(Keyword.get(opts, :has_branch_code))
|
||||
|> filter_by_national_check(Keyword.get(opts, :has_national_check))
|
||||
|> filter_by_numeric_only(Keyword.get(opts, :numeric_only))
|
||||
end
|
||||
|
||||
defp filter_by_length(specs, nil), do: specs
|
||||
|
||||
defp filter_by_length(specs, length) do
|
||||
Enum.filter(specs, fn {_code, spec} -> spec["iban_length"] == length end)
|
||||
end
|
||||
|
||||
defp filter_by_branch_code(specs, nil), do: specs
|
||||
|
||||
defp filter_by_branch_code(specs, has_branch) do
|
||||
Enum.filter(specs, fn {_code, spec} ->
|
||||
has_branch_code?(spec) == has_branch
|
||||
end)
|
||||
end
|
||||
|
||||
defp filter_by_national_check(specs, nil), do: specs
|
||||
|
||||
defp filter_by_national_check(specs, has_check) do
|
||||
Enum.filter(specs, fn {_code, spec} ->
|
||||
has_national_check?(spec) == has_check
|
||||
end)
|
||||
end
|
||||
|
||||
defp filter_by_numeric_only(specs, nil), do: specs
|
||||
|
||||
defp filter_by_numeric_only(specs, numeric_only) do
|
||||
Enum.filter(specs, fn {_code, spec} ->
|
||||
is_numeric_only?(spec["bban_spec"]) == numeric_only
|
||||
end)
|
||||
end
|
||||
|
||||
defp has_branch_code?(spec) do
|
||||
positions = spec["positions"]["branch_code"]
|
||||
positions["start"] != positions["end"]
|
||||
end
|
||||
|
||||
defp has_national_check?(spec) do
|
||||
Map.has_key?(spec["positions"], "national_check")
|
||||
end
|
||||
|
||||
defp is_numeric_only?(bban_spec) do
|
||||
!String.contains?(bban_spec, ["!a", "!c"])
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user