View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.validator.routines;
18  
19  import static org.hamcrest.MatcherAssert.assertThat;
20  import static org.hamcrest.core.Is.is;
21  import static org.hamcrest.core.IsEqual.equalTo;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertThrows;
26  import static org.junit.jupiter.api.Assertions.assertTrue;
27  import static org.junit.jupiter.api.Assertions.fail;
28  
29  import java.io.File;
30  import java.io.FileInputStream;
31  import java.io.InputStreamReader;
32  import java.io.Reader;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  
36  import org.apache.commons.csv.CSVFormat;
37  import org.apache.commons.csv.CSVParser;
38  import org.apache.commons.csv.CSVRecord;
39  import org.apache.commons.validator.routines.IBANValidator.Validator;
40  import org.apache.commons.validator.routines.checkdigit.IBANCheckDigit;
41  import org.junit.jupiter.api.Test;
42  
43  /**
44   * Tests {@link IBANValidator}.
45   */
46  public class IBANValidatorTest {
47  
48      private static final IBANValidator VALIDATOR = IBANValidator.getInstance();
49  
50      // Unfortunately Java only returns the last match of repeated patterns
51      // Use a manual repeat instead
52      private static final String IBAN_PART = "(?:(\\d+)!([acn]))"; // Assume all parts are fixed length
53  
54      private static final Pattern IBAN_PAT = Pattern
55              .compile("([A-Z]{2})" + IBAN_PART + IBAN_PART + IBAN_PART + IBAN_PART + "?" + IBAN_PART + "?" + IBAN_PART + "?" + IBAN_PART + "?");
56  
57      // It's not clear whether IBANs can contain lower case characters
58      // so we test for both where possible
59      // Note that the BIC near the start of the code is always upper case or digits
60      // @formatter:off
61      private static final String[] VALID_IBAN_FIXTURES = {
62              "AD1200012030200359100100",
63              "AE070331234567890123456",
64              "AL47212110090000000235698741",
65              "AT611904300234573201",
66              "AZ21NABZ00000000137010001944",
67              "BA391290079401028494",
68              "BE68539007547034",
69              "BG80BNBG96611020345678",
70              "BH67BMAG00001299123456",
71              "BI4210000100010000332045181",
72              "BR1800000000141455123924100C2",
73              "BR1800360305000010009795493C1",
74              "BR9700360305000010009795493P1",
75              "BY13NBRB3600900000002Z00AB00",
76              "CH9300762011623852957",
77              "CR05015202001026284066",
78              "CY17002001280000001200527600",
79              "CZ6508000000192000145399",
80              "CZ9455000000001011038930",
81              "DE89370400440532013000",
82              "DJ2110002010010409943020008",
83              "DK5000400440116243",
84              "DO28BAGR00000001212453611324",
85              "EE382200221020145685",
86              "EG380019000500000000263180002",
87              "ES9121000418450200051332",
88              "FI2112345600000785",
89              "FI5542345670000081",
90                // FI other
91                "AX2112345600000785", // FI other
92                "AX5542345670000081", // FI other
93              "FK88SC123456789012",
94              "FO6264600001631634",
95              "FR1420041010050500013M02606",
96                // FR 'other'
97                "BL6820041010050500013M02606", // FR other
98                "GF4120041010050500013M02606", // FR other
99                "GP1120041010050500013M02606", // FR other
100               "MF8420041010050500013M02606", // FR other
101               "MQ5120041010050500013M02606", // FR other
102               "NC8420041010050500013M02606", // FR other
103               "PF5720041010050500013M02606", // FR other
104               "PM3620041010050500013M02606", // FR other
105               "RE4220041010050500013M02606", // FR other
106               "TF2120041010050500013M02606", // FR other
107               "WF9120041010050500013M02606", // FR other
108               "YT3120041010050500013M02606", // FR other
109             "GB29NWBK60161331926819",
110               // GB 'other'
111 //              "IM...", // GB other
112 //              "JE...", // GB other
113 //              "GG...", // GB other
114             "GE29NB0000000101904917",
115             "GI75NWBK000000007099453",
116             "GL8964710001000206",
117             "GR1601101250000000012300695",
118             "GT82TRAJ01020000001210029690",
119             "HR1210010051863000160",
120             "HU42117730161111101800000000",
121             "IE29AIBK93115212345678",
122             "IL620108000000099999999",
123             "IQ98NBIQ850123456789012",
124             "IS140159260076545510730339",
125             "IT60X0542811101000000123456",
126             "JO94CBJO0010000000000131000302",
127             "KW81CBKU0000000000001234560101",
128             "KZ86125KZT5004100100",
129             "LB62099900000001001901229114",
130             "LC55HEMM000100010012001200023015",
131             "LI21088100002324013AA",
132             "LT121000011101001000",
133             "LU280019400644750000",
134             "LY83002048000020100120361",
135             "LV80BANK0000435195001",
136             "LY83002048000020100120361",
137             "MC5811222000010123456789030",
138             "MD24AG000225100013104168",
139             "ME25505000012345678951",
140             "MK07250120000058984",
141             "MN121234123456789123",
142             "MR1300020001010000123456753",
143             "MT84MALT011000012345MTLCAST001S",
144             "MU17BOMM0101101030300200000MUR",
145             "NI45BAPR00000013000003558124",
146             "NL91ABNA0417164300",
147             "NO9386011117947",
148             "OM810180000001299123456",
149             "PK36SCBL0000001123456702",
150             "PL61109010140000071219812874",
151             "PS92PALS000000000400123456702",
152             "PT50000201231234567890154",
153             "QA58DOHB00001234567890ABCDEFG",
154             "RO49AAAA1B31007593840000",
155             "RS35260005601001611379",
156             "RU0204452560040702810412345678901",
157             "SA0380000000608010167519",
158             "SC18SSCB11010000000000001497USD",
159             "SD8811123456789012",
160             "SE4550000000058398257466",
161             "SI56191000000123438",
162             "SI56263300012039086",
163             "SK3112000000198742637541",
164             "SM86U0322509800000000270100",
165             "SO211000001001000100141",
166             "ST68000100010051845310112",
167             "SV62CENR00000000000000700025",
168             "SV43ACAT00000000000000123123",
169             "TL380080012345678910157",
170             "TN5910006035183598478831",
171             "TR330006100519786457841326",
172             "UA213223130000026007233566001",
173             "UA213996220000026007233566001",
174             "VA59001123000012345678",
175             "VG96VPVG0000012345678901",
176             "XK051212012345678906",
177     };
178     // @formatter:on
179 
180     // @formatter:off
181     private static final String[] INVALID_IBAN_FIXTURES = {
182             "",                        // empty
183             "   ",                     // empty
184             "A",                       // too short
185             "AB",                      // too short
186             "FR1420041010050500013m02606", // lowercase version
187             "MT84MALT011000012345mtlcast001s", // lowercase version
188             "LI21088100002324013aa", // lowercase version
189             "QA58DOHB00001234567890abcdefg", // lowercase version
190             "RO49AAAA1b31007593840000", // lowercase version
191             "LC62HEMM000100010012001200023015", // wrong in SWIFT
192             "BY00NBRB3600000000000Z00AB00", // Wrong in SWIFT v73
193             "ST68000200010192194210112", // ditto
194             "SV62CENR0000000000000700025", // ditto
195     };
196     // @formatter:on
197 
198     private static int checkIBAN(final File file, final IBANValidator val) throws Exception {
199         // The IBAN Registry (TXT) file is a TAB-separated file
200         // Rows are the entry types, columns are the countries
201         final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter('\t').build();
202         final Reader rdr = new InputStreamReader(new FileInputStream(file), "ISO_8859_1");
203         try (final CSVParser p = new CSVParser(rdr, format)) {
204             CSVRecord country = null;
205             CSVRecord cc = null;
206             CSVRecord structure = null;
207             CSVRecord length = null;
208             for (final CSVRecord o : p) {
209                 final String item = o.get(0);
210                 if ("Name of country".equals(item)) {
211                     country = o;
212                 } else if ("IBAN prefix country code (ISO 3166)".equals(item)) {
213                     cc = o;
214                 } else if ("IBAN structure".equals(item)) {
215                     structure = o;
216                 } else if ("IBAN length".equals(item)) {
217                     length = o;
218                 }
219             }
220             assertNotNull(country);
221             assertNotNull(length);
222             assertNotNull(structure);
223             assertNotNull(cc);
224             for (int i = 1; i < country.size(); i++) {
225                 try {
226 
227                     final String newLength = length.get(i).split("!")[0]; // El Salvador currently has "28!n"
228                     final String newRE = fmtRE(structure.get(i), Integer.parseInt(newLength));
229                     final Validator valre = val.getValidator(cc.get(i));
230                     if (valre == null) {
231                         System.out.println("// Missing entry:");
232                         printEntry(cc.get(i), newLength, newRE, country.get(i));
233                     } else {
234                         final String currentLength = Integer.toString(valre.ibanLength);
235                         final String currentRE = valre.getRegexValidator().toString().replaceAll("^.+?\\{(.+)}", "$1") // Extract RE from RegexValidator{re}
236                                                                                                                        // string
237                                 .replace("\\d", "\\\\d"); // convert \d to \\d
238                         // The above assumes that the RegexValidator contains a single Regex
239                         if (currentRE.equals(newRE) && currentLength.equals(newLength)) {
240 
241                         } else {
242                             System.out.println("// Expected: " + newRE + ", " + newLength + " Actual: " + currentRE + ", " + currentLength);
243                             printEntry(cc.get(i), newLength, newRE, country.get(i));
244                         }
245 
246                     }
247 
248                 } catch (final IllegalArgumentException e) {
249                     e.printStackTrace();
250                 }
251             }
252             p.close();
253             return country.size();
254         }
255     }
256 
257     private static String fmtRE(final String ibanPat, final int ibanLen) {
258         final Matcher m = IBAN_PAT.matcher(ibanPat);
259         if (!m.matches()) {
260             throw new IllegalArgumentException("Unexpected IBAN pattern " + ibanPat);
261         }
262         final StringBuilder sb = new StringBuilder();
263         final String cc = m.group(1); // country code
264         int totalLen = cc.length();
265         sb.append(cc);
266         int len = Integer.parseInt(m.group(2)); // length of part
267         String curType = m.group(3); // part type
268         for (int i = 4; i <= m.groupCount(); i += 2) {
269             if (m.group(i) == null) { // reached an optional group
270                 break;
271             }
272             final int count = Integer.parseInt(m.group(i));
273             final String type = m.group(i + 1);
274             if (type.equals(curType)) { // more of the same type
275                 len += count;
276             } else {
277                 sb.append(formatToRE(curType, len));
278                 totalLen += len;
279                 curType = type;
280                 len = count;
281             }
282         }
283         sb.append(formatToRE(curType, len));
284         totalLen += len;
285         if (ibanLen != totalLen) {
286             throw new IllegalArgumentException("IBAN pattern " + ibanPat + " does not match length " + ibanLen);
287         }
288         return sb.toString();
289     }
290 
291     // convert IBAN type string and length to regex
292     private static String formatToRE(final String type, final int len) {
293         final char ctype = type.charAt(0); // assume type.length() == 1
294         switch (ctype) {
295         case 'n':
296             return String.format("\\\\d{%d}", len);
297         case 'a':
298             return String.format("[A-Z]{%d}", len);
299         case 'c':
300             return String.format("[A-Z0-9]{%d}", len);
301         default:
302             throw new IllegalArgumentException("Unexpected type " + type);
303         }
304     }
305 
306     public static void main(final String[] a) throws Exception {
307         final IBANValidator validator = new IBANValidator();
308         final File ibanTsv = new File("target", "iban-registry.tsv");
309         int countries = 0;
310         if (ibanTsv.canRead()) {
311             countries = checkIBAN(ibanTsv, validator);
312         } else {
313             System.out.println("Please load the file " + ibanTsv.getCanonicalPath() + " from https://www.swift.com/standards/data-standards/iban");
314         }
315         System.out.println("Processed " + countries + " countries.");
316     }
317 
318     private static void printEntry(final String ccode, final String length, final String ib, final String country) {
319         final String fmt = String.format("\"%s\"", ib);
320         System.out.printf("            new Validator(\"%s\", %s, %-40s), // %s\n", ccode, length, fmt, country);
321     }
322 
323     @Test
324     public void testGetRegexValidatortPatterns() {
325         assertNotNull(VALIDATOR.getValidator("GB").getRegexValidator().getPatterns(), "GB");
326     }
327 
328     @Test
329     public void testGetValidator() {
330         assertNotNull(VALIDATOR.getValidator("GB"), "GB");
331         assertNull(VALIDATOR.getValidator("gb"), "gb");
332     }
333 
334     @Test
335     public void testHasValidator() {
336         assertTrue(VALIDATOR.hasValidator("GB"), "GB");
337         assertFalse(VALIDATOR.hasValidator("gb"), "gb");
338     }
339 
340     @Test
341     public void testInValid() {
342         for (final String f : INVALID_IBAN_FIXTURES) {
343             assertFalse(VALIDATOR.isValid(f), f);
344         }
345     }
346 
347     @Test
348     public void testNull() {
349         assertFalse(VALIDATOR.isValid(null), "isValid(null)");
350     }
351 
352     @Test
353     public void testSetDefaultValidator1() {
354         final IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> VALIDATOR.setValidator("GB", 15, "GB"));
355         assertThat(thrown.getMessage(), is(equalTo("The singleton validator cannot be modified")));
356     }
357 
358     @Test
359     public void testSetDefaultValidator2() {
360         final IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> VALIDATOR.setValidator("GB", -1, "GB"));
361         assertThat(thrown.getMessage(), is(equalTo("The singleton validator cannot be modified")));
362     }
363 
364     @Test
365     public void testSetValidatorLC() {
366         final IBANValidator validator = new IBANValidator();
367         final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> validator.setValidator("gb", 15, "GB"));
368         assertThat(thrown.getMessage(), is(equalTo("Invalid country Code; must be exactly 2 upper-case characters")));
369     }
370 
371     @Test
372     public void testSetValidatorLen1() {
373         final IBANValidator validator = new IBANValidator();
374         assertNotNull(validator.setValidator("GB", -1, ""), "should be present");
375         assertNull(validator.setValidator("GB", -1, ""), "no longer present");
376     }
377 
378     @Test
379     public void testSetValidatorLen35() {
380         final IBANValidator validator = new IBANValidator();
381         final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> validator.setValidator("GB", 35, "GB"));
382         assertThat(thrown.getMessage(), is(equalTo("Invalid length parameter, must be in range 8 to 34 inclusive: 35")));
383     }
384 
385     @Test
386     public void testSetValidatorLen7() {
387         final IBANValidator validator = new IBANValidator();
388         final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> validator.setValidator("GB", 7, "GB"));
389         assertThat(thrown.getMessage(), is(equalTo("Invalid length parameter, must be in range 8 to 34 inclusive: 7")));
390     }
391 
392     @Test
393     public void testSorted() {
394         final IBANValidator validator = new IBANValidator();
395         final Validator[] vals = validator.getDefaultValidators();
396         assertNotNull(vals);
397         for (int i = 1; i < vals.length; i++) {
398             if (vals[i].countryCode.compareTo(vals[i - 1].countryCode) <= 0) {
399                 fail("Not sorted: " + vals[i].countryCode + " <= " + vals[i - 1].countryCode);
400             }
401         }
402     }
403 
404     @Test
405     public void testValid() {
406         for (final String f : VALID_IBAN_FIXTURES) {
407             assertTrue(IBANCheckDigit.IBAN_CHECK_DIGIT.isValid(f), "Checksum fail: " + f);
408             assertTrue(VALIDATOR.hasValidator(f), "Missing validator: " + f);
409             assertTrue(VALIDATOR.isValid(f), f);
410         }
411     }
412 }