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