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