1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.validator.routines;
18
19 import static org.junit.jupiter.api.Assertions.assertEquals;
20 import static org.junit.jupiter.api.Assertions.assertFalse;
21 import static org.junit.jupiter.api.Assertions.assertNotNull;
22 import static org.junit.jupiter.api.Assertions.assertNull;
23 import static org.junit.jupiter.api.Assertions.assertThrows;
24 import static org.junit.jupiter.api.Assertions.assertTrue;
25 import static org.junit.jupiter.api.Assertions.fail;
26
27 import java.io.Reader;
28 import java.nio.charset.Charset;
29 import java.nio.file.Files;
30 import java.nio.file.Path;
31 import java.nio.file.Paths;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collection;
35 import java.util.Date;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
40 import java.util.stream.Collectors;
41 import java.util.stream.Stream;
42
43 import org.apache.commons.csv.CSVFormat;
44 import org.apache.commons.csv.CSVParser;
45 import org.apache.commons.csv.CSVRecord;
46 import org.apache.commons.validator.routines.IBANValidator.Validator;
47 import org.apache.commons.validator.routines.checkdigit.IBANCheckDigit;
48 import org.junit.jupiter.api.Assumptions;
49 import org.junit.jupiter.api.Test;
50 import org.junit.jupiter.params.ParameterizedTest;
51 import org.junit.jupiter.params.provider.Arguments;
52 import org.junit.jupiter.params.provider.FieldSource;
53 import org.junit.jupiter.params.provider.MethodSource;
54
55
56
57
58 class IBANValidatorTest {
59
60 private static final IBANValidator VALIDATOR = IBANValidator.getInstance();
61
62
63
64 private static final String IBAN_PART = "(?:(\\d+)!([acn]))";
65
66 private static final Pattern IBAN_PAT = Pattern
67 .compile(IBAN_PART + IBAN_PART + IBAN_PART + IBAN_PART + "?" + IBAN_PART + "?" + IBAN_PART + "?" + IBAN_PART + "?");
68
69
70
71
72
73
74
75
76
77
78
79 private static final String IBAN_REGISTRY = "iban_registry_v99.txt";
80 private static final Charset IBAN_REGISTRY_CHARSET = Charset.forName("windows-1252");
81 private static final int MS_PER_DAY = 1000 * 60 * 60 * 24;
82 private static final long MAX_AGE_DAYS = 180;
83
84
85
86
87
88
89
90 private static final List<String> VALID_IBAN_FIXTURES = Arrays.asList(
91 "AD1200012030200359100100",
92 "AE070331234567890123456",
93 "AL47212110090000000235698741",
94 "AT611904300234573201",
95 "AZ21NABZ00000000137010001944",
96 "BA391290079401028494",
97 "BE68539007547034",
98 "BG80BNBG96611020345678",
99 "BH67BMAG00001299123456",
100 "BI4210000100010000332045181",
101 "BR1800000000141455123924100C2",
102 "BR1800360305000010009795493C1",
103 "BR9700360305000010009795493P1",
104 "BY13NBRB3600900000002Z00AB00",
105 "CH9300762011623852957",
106 "CR05015202001026284066",
107 "CY17002001280000001200527600",
108 "CZ6508000000192000145399",
109 "CZ9455000000001011038930",
110 "DE89370400440532013000",
111 "DJ2110002010010409943020008",
112 "DK5000400440116243",
113 "DO28BAGR00000001212453611324",
114 "EE382200221020145685",
115 "EG380019000500000000263180002",
116 "ES9121000418450200051332",
117 "FI2112345600000785",
118 "FI5542345670000081",
119
120 "AX2112345600000785",
121 "AX5542345670000081",
122 "FK88SC123456789012",
123 "FO6264600001631634",
124 "FR1420041010050500013M02606",
125
126 "BL6820041010050500013M02606",
127 "GF4120041010050500013M02606",
128 "GP1120041010050500013M02606",
129 "MF8420041010050500013M02606",
130 "MQ5120041010050500013M02606",
131 "NC8420041010050500013M02606",
132 "PF5720041010050500013M02606",
133 "PM3620041010050500013M02606",
134 "RE4220041010050500013M02606",
135 "TF2120041010050500013M02606",
136 "WF9120041010050500013M02606",
137 "YT3120041010050500013M02606",
138 "GB29NWBK60161331926819",
139
140
141
142
143 "GE29NB0000000101904917",
144 "GI75NWBK000000007099453",
145 "GL8964710001000206",
146 "GR1601101250000000012300695",
147 "GT82TRAJ01020000001210029690",
148 "HN88CABF00000000000250005469",
149 "HR1210010051863000160",
150 "HU42117730161111101800000000",
151 "IE29AIBK93115212345678",
152 "IL620108000000099999999",
153 "IQ98NBIQ850123456789012",
154 "IS140159260076545510730339",
155 "IT60X0542811101000000123456",
156 "JO94CBJO0010000000000131000302",
157 "KW81CBKU0000000000001234560101",
158 "KZ86125KZT5004100100",
159 "LB62099900000001001901229114",
160 "LC55HEMM000100010012001200023015",
161 "LI21088100002324013AA",
162 "LT121000011101001000",
163 "LU280019400644750000",
164 "LY83002048000020100120361",
165 "LV80BANK0000435195001",
166 "LY83002048000020100120361",
167 "MC5811222000010123456789030",
168 "MD24AG000225100013104168",
169 "ME25505000012345678951",
170 "MK07250120000058984",
171 "MN121234123456789123",
172 "MR1300020001010000123456753",
173 "MT84MALT011000012345MTLCAST001S",
174 "MU17BOMM0101101030300200000MUR",
175 "NI45BAPR00000013000003558124",
176 "NL91ABNA0417164300",
177 "NO9386011117947",
178 "OM810180000001299123456",
179 "PK36SCBL0000001123456702",
180 "PL61109010140000071219812874",
181 "PS92PALS000000000400123456702",
182 "PT50000201231234567890154",
183 "QA58DOHB00001234567890ABCDEFG",
184 "RO49AAAA1B31007593840000",
185 "RS35260005601001611379",
186 "RU0204452560040702810412345678901",
187 "SA0380000000608010167519",
188 "SC18SSCB11010000000000001497USD",
189 "SD8811123456789012",
190 "SE4550000000058398257466",
191 "SI56191000000123438",
192 "SI56263300012039086",
193 "SK3112000000198742637541",
194 "SM86U0322509800000000270100",
195 "SO211000001001000100141",
196 "ST68000100010051845310112",
197 "SV62CENR00000000000000700025",
198 "SV43ACAT00000000000000123123",
199 "TL380080012345678910157",
200 "TN5910006035183598478831",
201 "TR330006100519786457841326",
202 "UA213223130000026007233566001",
203 "UA213996220000026007233566001",
204 "VA59001123000012345678",
205 "VG96VPVG0000012345678901",
206 "XK051212012345678906",
207 "YE15CBYE0001018861234567891234"
208 );
209
210
211
212 private static final List<String> INVALID_IBAN_FIXTURES = Arrays.asList(
213 "",
214 " ",
215 "A",
216 "AB",
217 "FR1420041010050500013m02606",
218 "MT84MALT011000012345mtlcast001s",
219 "LI21088100002324013aa",
220 "QA58DOHB00001234567890abcdefg",
221 "RO49AAAA1b31007593840000",
222 "LC62HEMM000100010012001200023015",
223 "BY00NBRB3600000000000Z00AB00",
224 "ST68000200010192194210112",
225 "SV62CENR0000000000000700025",
226 "NI04BAPR00000013000003558124",
227 "RU1704452522540817810538091310419"
228 );
229
230
231 private static String fmtRE(final String ibanPat, final int ibanLength) {
232 final Matcher m = IBAN_PAT.matcher(ibanPat);
233 if (!m.matches()) {
234 throw new IllegalArgumentException("Unexpected IBAN pattern " + ibanPat);
235 }
236 final StringBuilder sb = new StringBuilder();
237 int len = Integer.parseInt(m.group(1));
238 int totLen = len;
239 String curType = m.group(2);
240 for (int i = 3; i <= m.groupCount(); i += 2) {
241 if (m.group(i) == null) {
242 break;
243 }
244 final int count = Integer.parseInt(m.group(i));
245 totLen += count;
246 final String type = m.group(i + 1);
247 if (type.equals(curType)) {
248 len += count;
249 } else {
250 sb.append(formatToRE(curType, len));
251 curType = type;
252 len = count;
253 }
254 }
255 sb.append(formatToRE(curType, len));
256 assertEquals(ibanLength, totLen, "Wrong length for " + ibanPat);
257 return sb.toString();
258 }
259
260
261 private static String formatToRE(final String type, final int len) {
262 final char ctype = type.charAt(0);
263 switch (ctype) {
264 case 'n':
265 return String.format("\\d{%d}", len);
266 case 'a':
267 return String.format("[A-Z]{%d}", len);
268 case 'c':
269 return String.format("[A-Z0-9]{%d}", len);
270 default:
271 throw new IllegalArgumentException("Unexpected type " + type);
272 }
273 }
274
275 static Collection<Arguments> ibanRegistrySource() throws Exception {
276 final Path ibanRegistry = Paths.get(IBANValidator.class.getResource(IBAN_REGISTRY).toURI());
277
278 final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter('\t').build();
279 final Reader rdr = Files.newBufferedReader(ibanRegistry, IBAN_REGISTRY_CHARSET);
280
281 CSVRecord country = null;
282 CSVRecord cc = null;
283 CSVRecord additionalCc = null;
284 CSVRecord structure = null;
285 CSVRecord length = null;
286
287 try (CSVParser p = new CSVParser(rdr, format)) {
288 for (final CSVRecord o : p) {
289 final String item = o.get(0);
290 switch (item) {
291 case "Name of country":
292 country = o;
293 break;
294 case "IBAN prefix country code (ISO 3166)":
295 cc = o;
296 break;
297 case "Country code includes other countries/territories":
298 additionalCc = o;
299 break;
300 case "IBAN structure":
301 structure = o;
302 break;
303 case "IBAN length":
304 length = o;
305 break;
306 default:
307 break;
308 }
309 }
310 }
311
312 assertNotNull(country);
313 assertNotNull(cc);
314 assertNotNull(additionalCc);
315 assertNotNull(structure);
316 assertNotNull(length);
317
318 final Collection<Arguments> result = new ArrayList<>();
319 for (int i = 1; i < country.size(); i++) {
320 final String ac = additionalCc.get(i);
321 final List<String> aCountry = Arrays.stream(ac.split(","))
322 .filter(s -> !"N/A".equals(s))
323 .map(s -> s.replace("(French part)", ""))
324 .map(String::trim)
325 .filter(s -> !s.isEmpty())
326 .collect(Collectors.toList());
327 result.add(Arguments.of(country.get(i), cc.get(i), aCountry, Integer.parseInt(length.get(i)), structure.get(i)));
328 }
329
330 return result;
331 }
332
333 static Collection<Arguments> ibanRegistrySourceExamples() throws Exception {
334 final Path ibanRegistry = Paths.get(IBANValidator.class.getResource(IBAN_REGISTRY).toURI());
335
336 final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter('\t').build();
337 final Reader rdr = Files.newBufferedReader(ibanRegistry, IBAN_REGISTRY_CHARSET);
338
339 CSVRecord country = null;
340 CSVRecord electronicExample = null;
341 CSVRecord lastUpdateDate = null;
342
343 try (CSVParser p = new CSVParser(rdr, format)) {
344 for (final CSVRecord o : p) {
345 final String item = o.get(0);
346 switch (item) {
347 case "Name of country":
348 country = o;
349 break;
350 case "IBAN electronic format example":
351 electronicExample = o;
352 break;
353 case "Last update date":
354 lastUpdateDate = o;
355 break;
356 default:
357 break;
358 }
359 }
360 }
361
362 assertNotNull(country);
363 final int arraySize = country.size();
364 assertNotNull(electronicExample);
365 assertEquals(arraySize, electronicExample.size());
366 assertNotNull(lastUpdateDate);
367 assertEquals(arraySize, lastUpdateDate.size());
368
369 final Collection<Arguments> result = new ArrayList<>();
370 Date lastDate = new Date(0);
371 String lastUpdated = null;
372 for (int i = 1; i < country.size(); i++) {
373 result.add(Arguments.of(country.get(i), electronicExample.get(i)));
374 final String mmyy = lastUpdateDate.get(i);
375 final Date dt = DateValidator.getInstance().validate(mmyy, "MMM-yy", Locale.ROOT);
376 if (dt.after(lastDate)) {
377 lastDate = dt;
378 lastUpdated = mmyy;
379 }
380 }
381 final long age = (new Date().getTime() - lastDate.getTime()) / MS_PER_DAY;
382 if (age > MAX_AGE_DAYS) {
383 System.out.println("WARNING: expected recent last update date, but found: " + lastUpdated);
384 }
385 return result;
386 }
387
388 public static Stream<Arguments> validateIbanStatuses() {
389 return Stream.of(
390 Arguments.of("XX", IBANValidatorStatus.UNKNOWN_COUNTRY),
391 Arguments.of("AD0101", IBANValidatorStatus.INVALID_LENGTH),
392 Arguments.of("AD12XX012030200359100100", IBANValidatorStatus.INVALID_PATTERN),
393 Arguments.of("AD9900012030200359100100", IBANValidatorStatus.INVALID_CHECKSUM),
394 Arguments.of("AD1200012030200359100100", IBANValidatorStatus.VALID)
395 );
396 }
397
398 @ParameterizedTest
399 @MethodSource("ibanRegistrySourceExamples")
400 void testExampleAccountsShouldBeValid(final String countryName, final String example) {
401 Assumptions.assumeFalse(INVALID_IBAN_FIXTURES.contains(example), "Skip invalid example: " + example + " for " + countryName);
402 assertTrue(IBANValidator.getInstance().isValid(example), "IBAN validator returned false for " + example + " for " + countryName);
403 }
404
405 @Test
406 void testGetRegexValidatorPatterns() {
407 assertNotNull(VALIDATOR.getValidator("GB").getRegexValidator().getPatterns(), "GB");
408 }
409
410 @Test
411 void testGetValidator() {
412 assertNotNull(VALIDATOR.getValidator("GB"), "GB");
413 assertNull(VALIDATOR.getValidator("gb"), "gb");
414 }
415
416 @Test
417 void testHasValidator() {
418 assertTrue(VALIDATOR.hasValidator("GB"), "GB");
419 assertFalse(VALIDATOR.hasValidator("gb"), "gb");
420 }
421
422 @ParameterizedTest
423 @FieldSource("INVALID_IBAN_FIXTURES")
424 void testInValid(final String invalidIban) {
425 assertNotNull(INVALID_IBAN_FIXTURES);
426 assertFalse(VALIDATOR.isValid(invalidIban), invalidIban);
427 }
428
429 @ParameterizedTest
430 @FieldSource("VALID_IBAN_FIXTURES")
431 void testMoreValid(final String invalidIban) {
432 assertNotNull(VALID_IBAN_FIXTURES);
433 assertTrue(VALIDATOR.isValid(invalidIban), invalidIban);
434 }
435
436 @Test
437 void testNull() {
438 assertFalse(VALIDATOR.isValid(null), "isValid(null)");
439 }
440
441 @Test
442 void testSetDefaultValidator1() {
443 final IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> VALIDATOR.setValidator("GB", 15, "GB"));
444 assertEquals("The singleton validator cannot be modified", thrown.getMessage());
445
446 }
447
448 @Test
449 void testSetDefaultValidator2() {
450 final IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> VALIDATOR.setValidator("GB", -1, "GB"));
451 assertEquals("The singleton validator cannot be modified", thrown.getMessage());
452 }
453
454 @Test
455 void testSetValidatorLC() {
456 final IBANValidator validator = new IBANValidator();
457 final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> validator.setValidator("gb", 15, "GB"));
458 assertEquals("Invalid country Code; must be exactly 2 upper-case characters", thrown.getMessage());
459 }
460
461 @Test
462 void testSetValidatorLen1() {
463 final IBANValidator validator = new IBANValidator();
464 assertNotNull(validator.setValidator("GB", -1, ""), "should be present");
465 assertNull(validator.setValidator("GB", -1, ""), "no longer present");
466 }
467
468 @Test
469 void testSetValidatorLen35() {
470 final IBANValidator validator = new IBANValidator();
471 final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> validator.setValidator("GB", 35, "GB"));
472 assertEquals("Invalid length parameter, must be in range 8 to 34 inclusive: 35", thrown.getMessage());
473 }
474
475 @Test
476 void testSetValidatorLen7() {
477 final IBANValidator validator = new IBANValidator();
478 final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> validator.setValidator("GB", 7, "GB"));
479 assertEquals("Invalid length parameter, must be in range 8 to 34 inclusive: 7", thrown.getMessage());
480 }
481
482 @Test
483 void testSorted() {
484 final IBANValidator validator = new IBANValidator();
485 final Validator[] vals = validator.getDefaultValidators();
486 assertNotNull(vals);
487 for (int i = 1; i < vals.length; i++) {
488 if (vals[i].countryCode.compareTo(vals[i - 1].countryCode) <= 0) {
489 fail("Not sorted: " + vals[i].countryCode + " <= " + vals[i - 1].countryCode);
490 }
491 }
492 }
493
494 @ParameterizedTest
495 @FieldSource("VALID_IBAN_FIXTURES")
496 void testValid(final String iban) {
497 assertTrue(IBANCheckDigit.IBAN_CHECK_DIGIT.isValid(iban), "Checksum fail: " + iban);
498 assertTrue(VALIDATOR.hasValidator(iban), "Missing validator: " + iban);
499 assertTrue(VALIDATOR.isValid(iban), iban);
500 }
501
502 @ParameterizedTest
503 @MethodSource("validateIbanStatuses")
504 void testValidateIbanStatuses(final String iban, final IBANValidatorStatus expectedStatus) {
505 assertEquals(expectedStatus, IBANValidator.getInstance().validate(iban));
506 }
507
508 @ParameterizedTest
509 @MethodSource("ibanRegistrySource")
510 void testValidatorShouldExistWithProperConfiguration(final String countryName, final String countryCode, final List<String> acountyCode,
511 final int ibanLength, final String structure) throws Exception {
512 final String countryInfo = " countryCode: " + countryCode + ", countryName: " + countryName;
513 final Validator validator = IBANValidator.getInstance().getValidator(countryCode);
514
515 assertNotNull(validator, "IBAN validator returned null for" + countryInfo);
516 assertEquals(ibanLength, validator.getIbanLength(), "IBAN length should be " + ibanLength + " for" + countryInfo);
517
518 final List<String> allPatterns = Arrays.stream(validator.getRegexValidator().getPatterns()).map(Pattern::pattern).collect(Collectors.toList());
519
520 final String re = fmtRE(structure.substring(2), ibanLength - 2);
521 assertTrue(allPatterns.remove(countryCode + re), "No pattern " + countryCode + re + " found for " + countryInfo);
522 for (final String ac : acountyCode) {
523 assertTrue(allPatterns.remove(ac + re), "No additional country code " + ac + " found for " + countryInfo);
524 }
525 assertTrue(allPatterns.isEmpty(), "Unrecognized patterns: " + allPatterns + " for" + countryInfo);
526 }
527 }