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