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