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