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.checkdigit;
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.assertTrue;
23  import static org.junit.jupiter.api.Assertions.fail;
24  
25  import java.io.ByteArrayInputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.ObjectInputStream;
28  import java.io.ObjectOutputStream;
29  import java.util.ArrayList;
30  import java.util.List;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.junit.jupiter.api.AfterEach;
35  import org.junit.jupiter.api.Test;
36  
37  /**
38   * Luhn Check Digit Test.
39   *
40   * @since 1.4
41   */
42  public abstract class AbstractCheckDigitTest {
43  
44      private static final String POSSIBLE_CHECK_DIGITS = "0123456789 ABCDEFHIJKLMNOPQRSTUVWXYZ\tabcdefghijklmnopqrstuvwxyz!@£$%^&*()_+";
45  
46      /** logging instance */
47      protected Log log = LogFactory.getLog(getClass());
48  
49      /** Check digit routine being tested */
50      protected int checkDigitLth = 1;
51  
52      /** Check digit routine being tested */
53      protected CheckDigit routine;
54  
55      /**
56       * Array of valid code values These must contain valid strings *including* the check digit.
57       *
58       * They are passed to: CheckDigit.isValid(expects string including checkdigit) which is expected to return true and
59       * AbstractCheckDigitTest.createInvalidCodes() which mangles the last character to check that the result is now invalid. and the truncated string is passed
60       * to CheckDigit.calculate(expects string without checkdigit) the result is compared with the last character
61       */
62      protected String[] valid;
63  
64      /**
65       * Array of invalid code values
66       *
67       * These are currently passed to both CheckDigit.calculate(expects a string without checkdigit) which is expected to throw an exception However that only
68       * applies if the string is syntactically incorrect; and CheckDigit.isValid(expects a string including checkdigit) which is expected to return false
69       *
70       * See https://issues.apache.org/jira/browse/VALIDATOR-344 for some dicussion on this
71       */
72      protected String[] invalid = { "12345678A" };
73  
74      /** code value which sums to zero */
75      protected String zeroSum = "0000000000";
76  
77      /** Prefix for error messages */
78      protected String missingMessage = "Code is missing";
79  
80      /**
81       * Returns the check digit (i.e. last character) for a code.
82       *
83       * @param code The code
84       * @return The check digit
85       */
86      protected String checkDigit(final String code) {
87          if (code == null || code.length() <= checkDigitLth) {
88              return "";
89          }
90          final int start = code.length() - checkDigitLth;
91          return code.substring(start);
92      }
93  
94      // private static final String POSSIBLE_CHECK_DIGITS = "0123456789";
95      /**
96       * Returns an array of codes with invalid check digits.
97       *
98       * @param codes Codes with valid check digits
99       * @return Codes with invalid check digits
100      */
101     protected String[] createInvalidCodes(final String[] codes) {
102         final List<String> list = new ArrayList<>();
103 
104         // create invalid check digit values
105         for (final String fullCode : codes) {
106             final String code = removeCheckDigit(fullCode);
107             final String check = checkDigit(fullCode);
108             for (int j = 0; j < POSSIBLE_CHECK_DIGITS.length(); j++) {
109                 final String curr = POSSIBLE_CHECK_DIGITS.substring(j, j + 1);// "" + Character.forDigit(j, 10);
110                 if (!curr.equals(check)) {
111                     list.add(code + curr);
112                 }
113             }
114         }
115 
116         return list.toArray(new String[0]);
117     }
118 
119     /**
120      * Returns a code with the Check Digit (i.e. last character) removed.
121      *
122      * @param code The code
123      * @return The code without the check digit
124      */
125     protected String removeCheckDigit(final String code) {
126         if (code == null || code.length() <= checkDigitLth) {
127             return null;
128         }
129         return code.substring(0, code.length() - checkDigitLth);
130     }
131 
132     /**
133      * Tear Down - clears routine and valid codes.
134      */
135     @AfterEach
136     protected void tearDown() {
137         valid = null;
138         routine = null;
139     }
140 
141     /**
142      * Test calculate() for invalid values.
143      */
144     @Test
145     public void testCalculateInvalid() {
146 
147         if (log.isDebugEnabled()) {
148             log.debug("testCalculateInvalid() for " + routine.getClass().getName());
149         }
150 
151         // test invalid code values
152         for (int i = 0; i < invalid.length; i++) {
153             try {
154                 final String code = invalid[i];
155                 if (log.isDebugEnabled()) {
156                     log.debug("   " + i + " Testing Invalid Check Digit, Code=[" + code + "]");
157                 }
158                 final String expected = checkDigit(code);
159                 final String actual = routine.calculate(removeCheckDigit(code));
160                 // If exception not thrown, check that the digit is incorrect instead
161                 if (expected.equals(actual)) {
162                     fail("Expected mismatch for " + code + " expected " + expected + " actual " + actual);
163                 }
164             } catch (final CheckDigitException e) {
165                 // possible failure messages:
166                 // Invalid ISBN Length ...
167                 // Invalid Character[ ...
168                 // Are there any others?
169                 assertTrue(e.getMessage().startsWith("Invalid "), "Invalid Character[" + i + "]=" + e.getMessage());
170 // WAS                assertTrue("Invalid Character[" +i +"]=" +  e.getMessage(), e.getMessage().startsWith("Invalid Character["));
171             }
172         }
173     }
174 
175     /**
176      * Test calculate() for valid values.
177      */
178     @Test
179     public void testCalculateValid() {
180         if (log.isDebugEnabled()) {
181             log.debug("testCalculateValid() for " + routine.getClass().getName());
182         }
183 
184         // test valid values
185         for (int i = 0; i < valid.length; i++) {
186             final String code = removeCheckDigit(valid[i]);
187             final String expected = checkDigit(valid[i]);
188             try {
189                 if (log.isDebugEnabled()) {
190                     log.debug("   " + i + " Testing Valid Check Digit, Code=[" + code + "] expected=[" + expected + "]");
191                 }
192                 assertEquals(expected, routine.calculate(code), "valid[" + i + "]: " + valid[i]);
193             } catch (final Exception e) {
194                 fail("valid[" + i + "]=" + valid[i] + " threw " + e);
195             }
196         }
197 
198     }
199 
200     /**
201      * Test isValid() for invalid values.
202      */
203     @Test
204     public void testIsValidFalse() {
205         if (log.isDebugEnabled()) {
206             log.debug("testIsValidFalse() for " + routine.getClass().getName());
207         }
208 
209         // test invalid code values
210         for (int i = 0; i < invalid.length; i++) {
211             if (log.isDebugEnabled()) {
212                 log.debug("   " + i + " Testing Invalid Code=[" + invalid[i] + "]");
213             }
214             assertFalse(routine.isValid(invalid[i]), "invalid[" + i + "]: " + invalid[i]);
215         }
216 
217         // test invalid check digit values
218         final String[] invalidCheckDigits = createInvalidCodes(valid);
219         for (int i = 0; i < invalidCheckDigits.length; i++) {
220             if (log.isDebugEnabled()) {
221                 log.debug("   " + i + " Testing Invalid Check Digit, Code=[" + invalidCheckDigits[i] + "]");
222             }
223             assertFalse(routine.isValid(invalidCheckDigits[i]), "invalid check digit[" + i + "]: " + invalidCheckDigits[i]);
224         }
225     }
226 
227     /**
228      * Test isValid() for valid values.
229      */
230     @Test
231     public void testIsValidTrue() {
232         if (log.isDebugEnabled()) {
233             log.debug("testIsValidTrue() for " + routine.getClass().getName());
234         }
235 
236         // test valid values
237         for (int i = 0; i < valid.length; i++) {
238             if (log.isDebugEnabled()) {
239                 log.debug("   " + i + " Testing Valid Code=[" + valid[i] + "]");
240             }
241             assertTrue(routine.isValid(valid[i]), "valid[" + i + "]: " + valid[i]);
242         }
243     }
244 
245     /**
246      * Test missing code
247      */
248     @Test
249     public void testMissingCode() {
250 
251         // isValid() null
252         assertFalse(routine.isValid(null), "isValid() Null");
253 
254         // isValid() zero length
255         assertFalse(routine.isValid(""), "isValid() Zero Length");
256 
257         // isValid() length 1
258         // Don't use 0, because that passes for Verhoef (not sure why yet)
259         assertFalse(routine.isValid("9"), "isValid() Length 1");
260 
261         // calculate() null
262         try {
263             routine.calculate(null);
264             fail("calculate() Null - expected exception");
265         } catch (final Exception e) {
266             assertEquals(missingMessage, e.getMessage(), "calculate() Null");
267         }
268 
269         // calculate() zero length
270         try {
271             routine.calculate("");
272             fail("calculate() Zero Length - expected exception");
273         } catch (final Exception e) {
274             assertEquals(missingMessage, e.getMessage(), "calculate() Zero Length");
275         }
276     }
277 
278     /**
279      * Test check digit serialization.
280      */
281     @Test
282     public void testSerialization() {
283         // Serialize the check digit routine
284         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
285         try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
286             oos.writeObject(routine);
287             oos.flush();
288         } catch (final Exception e) {
289             fail(routine.getClass().getName() + " error during serialization: " + e);
290         }
291 
292         // Deserialize the test object
293         Object result = null;
294         try (ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray())) {
295             ObjectInputStream ois = new ObjectInputStream(bais);
296             result = ois.readObject();
297         } catch (final Exception e) {
298             fail(routine.getClass().getName() + " error during deserialization: " + e);
299         }
300         assertNotNull(result);
301     }
302 
303     /**
304      * Test zero sum
305      */
306     @Test
307     public void testZeroSum() {
308         assertFalse(routine.isValid(zeroSum), "isValid() Zero Sum");
309         try {
310             routine.calculate(zeroSum);
311             fail("Zero Sum - expected exception");
312         } catch (final Exception e) {
313             assertEquals("Invalid code, sum is zero", e.getMessage(), "isValid() Zero Sum");
314         }
315     }
316 
317 }