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;
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  
24  import java.io.BufferedReader;
25  import java.io.Closeable;
26  import java.io.File;
27  import java.io.FileReader;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.lang.reflect.Field;
31  import java.lang.reflect.Modifier;
32  import java.net.HttpURLConnection;
33  import java.net.IDN;
34  import java.net.URL;
35  import java.nio.file.Files;
36  import java.nio.file.StandardCopyOption;
37  import java.text.SimpleDateFormat;
38  import java.util.Date;
39  import java.util.HashMap;
40  import java.util.HashSet;
41  import java.util.Locale;
42  import java.util.Map;
43  import java.util.Map.Entry;
44  import java.util.Set;
45  import java.util.TreeMap;
46  import java.util.regex.Matcher;
47  import java.util.regex.Pattern;
48  
49  import org.apache.commons.validator.routines.DomainValidator.ArrayType;
50  import org.junit.jupiter.api.BeforeEach;
51  import org.junit.jupiter.api.Disabled;
52  import org.junit.jupiter.api.Test;
53  
54  /**
55   * Tests for the DomainValidator.
56   */
57  public class DomainValidatorTest {
58  // Must be public, because it has a main method.
59      private static void closeQuietly(final Closeable in) {
60          if (in != null) {
61              try {
62                  in.close();
63              } catch (final IOException ignore) {
64                  // ignore
65              }
66          }
67      }
68  
69      /*
70       * Download a file if it is more recent than our cached copy. Unfortunately the server does not seem to honor If-Modified-Since for the Html page, so we
71       * check if it is newer than the txt file and skip download if so
72       */
73      private static long download(final File file, final String tldUrl, final long timestamp) throws IOException {
74          final int hour = 60 * 60 * 1000; // an hour in ms
75          final long modTime;
76          // For testing purposes, don't download files more than once an hour
77          if (file.canRead()) {
78              modTime = file.lastModified();
79              if (modTime > System.currentTimeMillis() - hour) {
80                  System.out.println("Skipping download - found recent " + file);
81                  return modTime;
82              }
83          } else {
84              modTime = 0;
85          }
86          final HttpURLConnection hc = (HttpURLConnection) new URL(tldUrl).openConnection();
87          if (modTime > 0) {
88              final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); // Sun, 06 Nov 1994 08:49:37 GMT
89              final String since = sdf.format(new Date(modTime));
90              hc.addRequestProperty("If-Modified-Since", since);
91              System.out.println("Found " + file + " with date " + since);
92          }
93          if (hc.getResponseCode() == 304) {
94              System.out.println("Already have most recent " + tldUrl);
95          } else {
96              System.out.println("Downloading " + tldUrl);
97              try (InputStream is = hc.getInputStream()) {
98                  Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
99              }
100             System.out.println("Done");
101         }
102         return file.lastModified();
103     }
104 
105     private static Map<String, String[]> getHtmlInfo(final File f) throws IOException {
106         final Map<String, String[]> info = new HashMap<>();
107 
108 //        <td><span class="domain tld"><a href="/domains/root/db/ax.html">.ax</a></span></td>
109         final Pattern domain = Pattern.compile(".*<a href=\"/domains/root/db/([^.]+)\\.html");
110 //        <td>country-code</td>
111         final Pattern type = Pattern.compile("\\s+<td>([^<]+)</td>");
112 //        <!-- <td>Åland Islands<br/><span class="tld-table-so">Ålands landskapsregering</span></td> </td> -->
113 //        <td>Ålands landskapsregering</td>
114         final Pattern comment = Pattern.compile("\\s+<td>([^<]+)</td>");
115 
116         try (BufferedReader br = new BufferedReader(new FileReader(f))) {
117             String line;
118             while ((line = br.readLine()) != null) {
119                 final Matcher m = domain.matcher(line);
120                 if (m.lookingAt()) {
121                     final String dom = m.group(1);
122                     String typ = "??";
123                     String com = "??";
124                     line = br.readLine();
125                     while (line.matches("^\\s*$")) { // extra blank lines introduced
126                         line = br.readLine();
127                     }
128                     final Matcher t = type.matcher(line);
129                     if (t.lookingAt()) {
130                         typ = t.group(1);
131                         line = br.readLine();
132                         if (line.matches("\\s+<!--.*")) {
133                             while (!line.matches(".*-->.*")) {
134                                 line = br.readLine();
135                             }
136                             line = br.readLine();
137                         }
138                         // Should have comment; is it wrapped?
139                         while (!line.matches(".*</td>.*")) {
140                             line += " " + br.readLine();
141                         }
142                         final Matcher n = comment.matcher(line);
143                         if (n.lookingAt()) {
144                             com = n.group(1);
145                         }
146                         // Don't save unused entries
147                         if (com.contains("Not assigned") || com.contains("Retired") || typ.equals("test")) {
148 //                        System.out.println("Ignored: " + typ + " " + dom + " " +com);
149                         } else {
150                             info.put(dom.toLowerCase(Locale.ENGLISH), new String[] { typ, com });
151 //                        System.out.println("Storing: " + typ + " " + dom + " " +com);
152                         }
153                     } else {
154                         System.err.println("Unexpected type: " + line);
155                     }
156                 }
157             }
158         }
159         return info;
160     }
161 
162     // isInIanaList and isSorted are split into two methods.
163     // If/when access to the arrays is possible without reflection, the intermediate
164     // methods can be dropped
165     private static boolean isInIanaList(final String arrayName, final Set<String> ianaTlds) throws Exception {
166         final Field f = DomainValidator.class.getDeclaredField(arrayName);
167         final boolean isPrivate = Modifier.isPrivate(f.getModifiers());
168         if (isPrivate) {
169             f.setAccessible(true);
170         }
171         final String[] array = (String[]) f.get(null);
172         try {
173             return isInIanaList(arrayName, array, ianaTlds);
174         } finally {
175             if (isPrivate) {
176                 f.setAccessible(false);
177             }
178         }
179     }
180 
181     private static boolean isInIanaList(final String name, final String[] array, final Set<String> ianaTlds) {
182         for (final String element : array) {
183             if (!ianaTlds.contains(element)) {
184                 System.out.println(name + " contains unexpected value: " + element);
185                 return false;
186             }
187         }
188         return true;
189     }
190 
191     private static boolean isLowerCase(final String string) {
192         return string.equals(string.toLowerCase(Locale.ENGLISH));
193     }
194 
195     /**
196      * Check whether the domain is in the root zone currently. Reads the URL https://www.iana.org/domains/root/db/*domain*.html (using a local disk cache) and
197      * checks for the string "This domain is not present in the root zone at this time."
198      *
199      * @param domain the domain to check
200      * @return true if the string is found
201      */
202     private static boolean isNotInRootZone(final String domain) {
203         final String tldUrl = "https://www.iana.org/domains/root/db/" + domain + ".html";
204         final File rootCheck = new File("target", "tld_" + domain + ".html");
205         BufferedReader in = null;
206         try {
207             download(rootCheck, tldUrl, 0L);
208             in = new BufferedReader(new FileReader(rootCheck));
209             String inputLine;
210             while ((inputLine = in.readLine()) != null) {
211                 if (inputLine.contains("This domain is not present in the root zone at this time.")) {
212                     return true;
213                 }
214             }
215             in.close();
216         } catch (final IOException ignore) {
217             // ignore
218         } finally {
219             closeQuietly(in);
220         }
221         return false;
222     }
223 
224     private static boolean isSortedLowerCase(final String arrayName) throws Exception {
225         final Field f = DomainValidator.class.getDeclaredField(arrayName);
226         final boolean isPrivate = Modifier.isPrivate(f.getModifiers());
227         if (isPrivate) {
228             f.setAccessible(true);
229         }
230         final String[] array = (String[]) f.get(null);
231         try {
232             return isSortedLowerCase(arrayName, array);
233         } finally {
234             if (isPrivate) {
235                 f.setAccessible(false);
236             }
237         }
238     }
239 
240     // Check if an array is strictly sorted - and lowerCase
241     private static boolean isSortedLowerCase(final String name, final String[] array) {
242         boolean sorted = true;
243         boolean strictlySorted = true;
244         final int length = array.length;
245         boolean lowerCase = isLowerCase(array[length - 1]); // Check the last entry
246         for (int i = 0; i < length - 1; i++) { // compare all but last entry with next
247             final String entry = array[i];
248             final String nextEntry = array[i + 1];
249             final int cmp = entry.compareTo(nextEntry);
250             if (cmp > 0) { // out of order
251                 System.out.println("Out of order entry: " + entry + " < " + nextEntry + " in " + name);
252                 sorted = false;
253             } else if (cmp == 0) {
254                 strictlySorted = false;
255                 System.out.println("Duplicated entry: " + entry + " in " + name);
256             }
257             if (!isLowerCase(entry)) {
258                 System.out.println("Non lowerCase entry: " + entry + " in " + name);
259                 lowerCase = false;
260             }
261         }
262         return sorted && strictlySorted && lowerCase;
263     }
264 
265     // Download and process local copy of https://data.iana.org/TLD/tlds-alpha-by-domain.txt
266     // Check if the internal TLD table is up to date
267     // Check if the internal TLD tables have any spurious entries
268     // Can be invoked as: mvn -PDomainValidatorTest
269     public static void main(final String[] a) throws Exception {
270         // Check the arrays first as this affects later checks
271         // Doing this here makes it easier when updating the lists
272         boolean ok = true;
273         for (final String list : new String[] { "INFRASTRUCTURE_TLDS", "COUNTRY_CODE_TLDS", "GENERIC_TLDS", "LOCAL_TLDS" }) {
274             ok &= isSortedLowerCase(list);
275         }
276         if (!ok) {
277             System.out.println("Fix arrays before retrying; cannot continue");
278             return;
279         }
280         final Set<String> ianaTlds = new HashSet<>(); // keep for comparison with array contents
281         final DomainValidator dv = DomainValidator.getInstance();
282         final File txtFile = new File("target/tlds-alpha-by-domain.txt");
283         final long timestamp = download(txtFile, "https://data.iana.org/TLD/tlds-alpha-by-domain.txt", 0L);
284         final File htmlFile = new File("target/tlds-alpha-by-domain.html");
285         // Sometimes the html file may be updated a day or so after the txt file
286         // if the txt file contains entries not found in the html file, try again in a day or two
287         download(htmlFile, "https://www.iana.org/domains/root/db", timestamp);
288 
289         final BufferedReader br = new BufferedReader(new FileReader(txtFile));
290         String line;
291         final String header;
292         line = br.readLine(); // header
293         if (!line.startsWith("# Version ")) {
294             br.close();
295             throw new IOException("File does not have expected Version header");
296         }
297         header = line.substring(2);
298         final boolean generateUnicodeTlds = false; // Change this to generate Unicode TLDs as well
299 
300         // Parse html page to get entries
301         final Map<String, String[]> htmlInfo = getHtmlInfo(htmlFile);
302         final Map<String, String> missingTLD = new TreeMap<>(); // stores entry and comments as String[]
303         final Map<String, String> missingCC = new TreeMap<>();
304         while ((line = br.readLine()) != null) {
305             if (!line.startsWith("#")) {
306                 final String unicodeTld; // only different from asciiTld if that was punycode
307                 final String asciiTld = line.toLowerCase(Locale.ENGLISH);
308                 if (line.startsWith("XN--")) {
309                     unicodeTld = IDN.toUnicode(line);
310                 } else {
311                     unicodeTld = asciiTld;
312                 }
313                 if (!dv.isValidTld(asciiTld)) {
314                     final String[] info = htmlInfo.get(asciiTld);
315                     if (info != null) {
316                         final String type = info[0];
317                         final String comment = info[1];
318                         if ("country-code".equals(type)) { // Which list to use?
319                             missingCC.put(asciiTld, unicodeTld + " " + comment);
320                             if (generateUnicodeTlds) {
321                                 missingCC.put(unicodeTld, asciiTld + " " + comment);
322                             }
323                         } else {
324                             missingTLD.put(asciiTld, unicodeTld + " " + comment);
325                             if (generateUnicodeTlds) {
326                                 missingTLD.put(unicodeTld, asciiTld + " " + comment);
327                             }
328                         }
329                     } else {
330                         System.err.println("Expected to find HTML info for " + asciiTld);
331                     }
332                 }
333                 ianaTlds.add(asciiTld);
334                 // Don't merge these conditions; generateUnicodeTlds is final so needs to be separate to avoid a warning
335                 if (generateUnicodeTlds && !unicodeTld.equals(asciiTld)) {
336                     ianaTlds.add(unicodeTld);
337                 }
338             }
339         }
340         br.close();
341         int errorsDetected = 0;
342         // List html entries not in TLD text list
343         for (final String key : new TreeMap<>(htmlInfo).keySet()) {
344             if (!ianaTlds.contains(key)) {
345                 if (isNotInRootZone(key)) {
346                     System.out.println("INFO: HTML entry not yet in root zone: " + key);
347                 } else {
348                     errorsDetected ++;
349                     System.err.println("WARN: Expected to find text entry for html: " + key);
350                 }
351             }
352         }
353         if (!missingTLD.isEmpty()) {
354             errorsDetected ++;
355             printMap(header, missingTLD, "GENERIC_TLDS");
356         }
357         if (!missingCC.isEmpty()) {
358             errorsDetected ++;
359             printMap(header, missingCC, "COUNTRY_CODE_TLDS");
360         }
361         // Check if internal tables contain any additional entries
362         if (!isInIanaList("INFRASTRUCTURE_TLDS", ianaTlds)) {
363             errorsDetected ++;
364         }
365         if (!isInIanaList("COUNTRY_CODE_TLDS", ianaTlds)) {
366             errorsDetected ++;
367         }
368         if (!isInIanaList("GENERIC_TLDS", ianaTlds)) {
369             errorsDetected ++;
370         }
371         // Don't check local TLDS isInIanaList("LOCAL_TLDS", ianaTlds);
372         System.out.println("Finished checks");
373         if (errorsDetected > 0) {
374             throw new RuntimeException("Errors detected: " + errorsDetected);
375         }
376     }
377 
378     private static void printMap(final String header, final Map<String, String> map, final String string) {
379         System.out.println("Entries missing from " + string + " List\n");
380         if (header != null) {
381             System.out.println("        // Taken from " + header);
382         }
383         for (final Entry<String, String> me : map.entrySet()) {
384             System.out.println("        \"" + me.getKey() + "\", // " + me.getValue());
385         }
386         System.out.println("\nDone");
387     }
388 
389     private DomainValidator validator;
390 
391     @BeforeEach
392     public void setUp() {
393         validator = DomainValidator.getInstance();
394     }
395 
396     // Check array is sorted and is lower-case
397     @Test
398     public void tesLocalTldsSortedAndLowerCase() throws Exception {
399         final boolean sorted = isSortedLowerCase("LOCAL_TLDS");
400         assertTrue(sorted);
401     }
402 
403     @Test
404     void testAllowLocal() {
405         final DomainValidator noLocal = DomainValidator.getInstance(false);
406         final DomainValidator allowLocal = DomainValidator.getInstance(true);
407 
408         // Default is false, and should use singletons
409         assertEquals(noLocal, validator);
410 
411         // Default won't allow local
412         assertFalse(noLocal.isValid("localhost.localdomain"), "localhost.localdomain should validate");
413         assertFalse(noLocal.isValid("localhost"), "localhost should validate");
414 
415         // But it may be requested
416         assertTrue(allowLocal.isValid("localhost.localdomain"), "localhost.localdomain should validate");
417         assertTrue(allowLocal.isValid("localhost"), "localhost should validate");
418         assertTrue(allowLocal.isValid("hostname"), "hostname should validate");
419         assertTrue(allowLocal.isValid("machinename"), "machinename should validate");
420 
421         // Check the localhost one with a few others
422         assertTrue(allowLocal.isValid("apache.org"), "apache.org should validate");
423         assertFalse(allowLocal.isValid(" apache.org "), "domain name with spaces shouldn't validate");
424     }
425 
426     // Check array is sorted and is lower-case
427     @Test
428     void testCountryCodeTldsSortedAndLowerCase() throws Exception {
429         final boolean sorted = isSortedLowerCase("COUNTRY_CODE_TLDS");
430         assertTrue(sorted);
431     }
432 
433     @Test
434     void testDomainNoDots() { // rfc1123
435         assertTrue(validator.isValidDomainSyntax("a"), "a (alpha) should validate");
436         assertTrue(validator.isValidDomainSyntax("9"), "9 (alphanum) should validate");
437         assertTrue(validator.isValidDomainSyntax("c-z"), "c-z (alpha - alpha) should validate");
438 
439         assertFalse(validator.isValidDomainSyntax("c-"), "c- (alpha -) should fail");
440         assertFalse(validator.isValidDomainSyntax("-c"), "-c (- alpha) should fail");
441         assertFalse(validator.isValidDomainSyntax("-"), "- (-) should fail");
442     }
443 
444     @Test
445     void testEnumIsPublic() {
446         assertTrue(Modifier.isPublic(DomainValidator.ArrayType.class.getModifiers()));
447     }
448 
449     // Check array is sorted and is lower-case
450     @Test
451     void testGenericTldsSortedAndLowerCase() throws Exception {
452         final boolean sorted = isSortedLowerCase("GENERIC_TLDS");
453         assertTrue(sorted);
454     }
455 
456     @Test
457     void testGetArray() {
458         assertNotNull(DomainValidator.getTLDEntries(ArrayType.COUNTRY_CODE_MINUS));
459         assertNotNull(DomainValidator.getTLDEntries(ArrayType.COUNTRY_CODE_PLUS));
460         assertNotNull(DomainValidator.getTLDEntries(ArrayType.GENERIC_MINUS));
461         assertNotNull(DomainValidator.getTLDEntries(ArrayType.GENERIC_PLUS));
462         assertNotNull(DomainValidator.getTLDEntries(ArrayType.LOCAL_MINUS));
463         assertNotNull(DomainValidator.getTLDEntries(ArrayType.LOCAL_PLUS));
464         assertNotNull(DomainValidator.getTLDEntries(ArrayType.COUNTRY_CODE_RO));
465         assertNotNull(DomainValidator.getTLDEntries(ArrayType.GENERIC_RO));
466         assertNotNull(DomainValidator.getTLDEntries(ArrayType.INFRASTRUCTURE_RO));
467         assertNotNull(DomainValidator.getTLDEntries(ArrayType.LOCAL_RO));
468     }
469 
470     @Test
471     void testIDN() {
472         assertTrue(validator.isValid("www.xn--bcher-kva.ch"), "b\u00fccher.ch in IDN should validate");
473     }
474 
475     @Test
476     void testIDNJava6OrLater() {
477         // xn--d1abbgf6aiiy.xn--p1ai http://президент.рф
478         assertTrue(validator.isValid("www.b\u00fccher.ch"), "b\u00fccher.ch should validate");
479         assertTrue(validator.isValid("xn--d1abbgf6aiiy.xn--p1ai"), "xn--d1abbgf6aiiy.xn--p1ai should validate");
480         assertTrue(validator.isValid("президент.рф"), "президент.рф should validate");
481         assertFalse(validator.isValid("www.\uFFFD.ch"), "www.\uFFFD.ch FFFD should fail");
482     }
483 
484     // Check array is sorted and is lower-case
485     @Test
486     void testInfrastructureTldsSortedAndLowerCase() throws Exception {
487         final boolean sorted = isSortedLowerCase("INFRASTRUCTURE_TLDS");
488         assertTrue(sorted);
489     }
490 
491     @Test
492     void testInvalidDomains() {
493         assertFalse(validator.isValid(".org"), "bare TLD .org shouldn't validate");
494         assertFalse(validator.isValid(" apache.org "), "domain name with spaces shouldn't validate");
495         assertFalse(validator.isValid("apa che.org"), "domain name containing spaces shouldn't validate");
496         assertFalse(validator.isValid("-testdomain.name"), "domain name starting with dash shouldn't validate");
497         assertFalse(validator.isValid("testdomain-.name"), "domain name ending with dash shouldn't validate");
498         assertFalse(validator.isValid("---c.com"), "domain name starting with multiple dashes shouldn't validate");
499         assertFalse(validator.isValid("c--.com"), "domain name ending with multiple dashes shouldn't validate");
500         assertFalse(validator.isValid("apache.rog"), "domain name with invalid TLD shouldn't validate");
501         assertFalse(validator.isValid("http://www.apache.org"), "URL shouldn't validate");
502         assertFalse(validator.isValid(" "), "Empty string shouldn't validate as domain name");
503         assertFalse(validator.isValid(null), "Null shouldn't validate as domain name");
504         // VALIDATOR-501
505         assertFalse(validator.isValid("-test.fr"));
506         assertFalse(validator.isValid("test-.fr"));
507     }
508 
509     @Test
510     @Disabled
511     void testInvalidDomains501() {
512         // VALIDATOR-501
513         assertFalse(validator.isValid("-tést.fr"));
514         assertFalse(validator.isValid("tést-.fr"));
515     }
516 
517     // Check if IDN.toASCII is broken or not
518     @Test
519     void testIsIDNtoASCIIBroken() {
520         final String input = ".";
521         if (!input.equals(IDN.toASCII(input))) {
522             System.out.println(">>DomainValidatorTest.testIsIDNtoASCIIBroken()");
523             System.out.println("IDN.toASCII is BROKEN");
524             final String[] props = { "java.version", // Java Runtime Environment version
525                     "java.vendor", // Java Runtime Environment vendor
526                     "java.vm.specification.version", // Java Virtual Machine specification version
527                     "java.vm.specification.vendor", // Java Virtual Machine specification vendor
528                     "java.vm.specification.name", // Java Virtual Machine specification name
529                     "java.vm.version", // Java Virtual Machine implementation version
530                     "java.vm.vendor", // Java Virtual Machine implementation vendor
531                     "java.vm.name", // Java Virtual Machine implementation name
532                     "java.specification.version", // Java Runtime Environment specification version
533                     "java.specification.vendor", // Java Runtime Environment specification vendor
534                     "java.specification.name", // Java Runtime Environment specification name
535                     "java.class.version", // Java class format version number
536             };
537             for (final String t : props) {
538                 System.out.println(t + "=" + System.getProperty(t));
539             }
540             System.out.println("<<DomainValidatorTest.testIsIDNtoASCIIBroken()");
541         }
542         assertTrue(true); // dummy assertion to satisfy lint
543     }
544 
545     // RFC2396: domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
546     @Test
547     void testRFC2396domainlabel() { // use fixed valid TLD
548         assertTrue(validator.isValid("a.ch"), "a.ch should validate");
549         assertTrue(validator.isValid("9.ch"), "9.ch should validate");
550         assertTrue(validator.isValid("az.ch"), "az.ch should validate");
551         assertTrue(validator.isValid("09.ch"), "09.ch should validate");
552         assertTrue(validator.isValid("9-1.ch"), "9-1.ch should validate");
553         assertFalse(validator.isValid("91-.ch"), "91-.ch should not validate");
554         assertFalse(validator.isValid("-.ch"), "-.ch should not validate");
555     }
556 
557     // RFC2396 toplabel = alpha | alpha *( alphanum | "-" ) alphanum
558     @Test
559     void testRFC2396toplabel() {
560         // These tests use non-existent TLDs so currently need to use a package protected method
561         assertTrue(validator.isValidDomainSyntax("a.c"), "a.c (alpha) should validate");
562         assertTrue(validator.isValidDomainSyntax("a.cc"), "a.cc (alpha alpha) should validate");
563         assertTrue(validator.isValidDomainSyntax("a.c9"), "a.c9 (alpha alphanum) should validate");
564         assertTrue(validator.isValidDomainSyntax("a.c-9"), "a.c-9 (alpha - alphanum) should validate");
565         assertTrue(validator.isValidDomainSyntax("a.c-z"), "a.c-z (alpha - alpha) should validate");
566 
567         assertFalse(validator.isValidDomainSyntax("a.9c"), "a.9c (alphanum alpha) should fail");
568         assertFalse(validator.isValidDomainSyntax("a.c-"), "a.c- (alpha -) should fail");
569         assertFalse(validator.isValidDomainSyntax("a.-"), "a.- (-) should fail");
570         assertFalse(validator.isValidDomainSyntax("a.-9"), "a.-9 (- alphanum) should fail");
571     }
572 
573     @Test
574     void testTopLevelDomains() {
575         // infrastructure TLDs
576         assertTrue(validator.isValidInfrastructureTld(".arpa"), ".arpa should validate as iTLD");
577         assertFalse(validator.isValidInfrastructureTld(".com"), ".com shouldn't validate as iTLD");
578 
579         // generic TLDs
580         assertTrue(validator.isValidGenericTld(".name"), ".name should validate as gTLD");
581         assertFalse(validator.isValidGenericTld(".us"), ".us shouldn't validate as gTLD");
582 
583         // country code TLDs
584         assertTrue(validator.isValidCountryCodeTld(".uk"), ".uk should validate as ccTLD");
585         assertFalse(validator.isValidCountryCodeTld(".org"), ".org shouldn't validate as ccTLD");
586 
587         // case-insensitive
588         assertTrue(validator.isValidTld(".COM"), ".COM should validate as TLD");
589         assertTrue(validator.isValidTld(".BiZ"), ".BiZ should validate as TLD");
590 
591         // corner cases
592         assertFalse(validator.isValid(".nope"), "invalid TLD shouldn't validate"); // TODO this is not guaranteed invalid forever
593         assertFalse(validator.isValid(""), "empty string shouldn't validate as TLD");
594         assertFalse(validator.isValid(null), "null shouldn't validate as TLD");
595     }
596 
597     // Check that IDN.toASCII behaves as it should (when wrapped by DomainValidator.unicodeToASCII)
598     // Tests show that method incorrectly trims a trailing "." character
599     @Test
600     void testUnicodeToASCII() {
601         final String[] asciidots = { "", ",", ".", // fails IDN.toASCII, but should pass wrapped version
602                 "a.", // ditto
603                 "a.b", "a..b", "a...b", ".a", "..a", };
604         for (final String s : asciidots) {
605             assertEquals(s, DomainValidator.unicodeToASCII(s));
606         }
607         // RFC3490 3.1. 1)
608 //      Whenever dots are used as label separators, the following
609 //      characters MUST be recognized as dots: U+002E (full stop), U+3002
610 //      (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61
611 //      (halfwidth ideographic full stop).
612         final String[][] otherDots = { { "b\u3002", "b.", }, { "b\uFF0E", "b.", }, { "b\uFF61", "b.", }, { "\u3002", ".", }, { "\uFF0E", ".", },
613                 { "\uFF61", ".", }, };
614         for (final String[] s : otherDots) {
615             assertEquals(s[1], DomainValidator.unicodeToASCII(s[0]));
616         }
617     }
618 
619     @Test
620     void testValidator297() {
621         assertTrue(validator.isValid("xn--d1abbgf6aiiy.xn--p1ai"), "xn--d1abbgf6aiiy.xn--p1ai should validate"); // This uses a valid TLD
622     }
623 
624     // labels are a max of 63 chars and domains 253
625     @Test
626     void testValidator306() {
627         final String longString = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789A";
628         assertEquals(63, longString.length()); // 26 * 2 + 11
629 
630         assertTrue(validator.isValidDomainSyntax(longString + ".com"), "63 chars label should validate");
631         assertFalse(validator.isValidDomainSyntax(longString + "x.com"), "64 chars label should fail");
632 
633         assertTrue(validator.isValidDomainSyntax("test." + longString), "63 chars TLD should validate");
634         assertFalse(validator.isValidDomainSyntax("test.x" + longString), "64 chars TLD should fail");
635 
636         final String longDomain = longString + "." + longString + "." + longString + "." + longString.substring(0, 61);
637         assertEquals(253, longDomain.length());
638         assertTrue(validator.isValidDomainSyntax(longDomain), "253 chars domain should validate");
639         assertFalse(validator.isValidDomainSyntax(longDomain + "x"), "254 chars domain should fail");
640     }
641 
642     @Test
643     void testValidDomains() {
644         assertTrue(validator.isValid("apache.org"), "apache.org should validate");
645         assertTrue(validator.isValid("www.google.com"), "www.google.com should validate");
646 
647         assertTrue(validator.isValid("test-domain.com"), "test-domain.com should validate");
648         assertTrue(validator.isValid("test---domain.com"), "test---domain.com should validate");
649         assertTrue(validator.isValid("test-d-o-m-ain.com"), "test-d-o-m-ain.com should validate");
650         assertTrue(validator.isValid("as.uk"), "two-letter domain label should validate");
651 
652         assertTrue(validator.isValid("ApAchE.Org"), "case-insensitive ApAchE.Org should validate");
653 
654         assertTrue(validator.isValid("z.com"), "single-character domain label should validate");
655 
656         assertTrue(validator.isValid("i.have.an-example.domain.name"), "i.have.an-example.domain.name should validate");
657     }
658 }