1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
56
57 public class DomainValidatorTest {
58
59 private static void closeQuietly(final Closeable in) {
60 if (in != null) {
61 try {
62 in.close();
63 } catch (final IOException ignore) {
64
65 }
66 }
67 }
68
69
70
71
72
73 private static long download(final File file, final String tldUrl, final long timestamp) throws IOException {
74 final int hour = 60 * 60 * 1000;
75 final long modTime;
76
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");
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
109 final Pattern domain = Pattern.compile(".*<a href=\"/domains/root/db/([^.]+)\\.html");
110
111 final Pattern type = Pattern.compile("\\s+<td>([^<]+)</td>");
112
113
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*$")) {
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
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
147 if (com.contains("Not assigned") || com.contains("Retired") || typ.equals("test")) {
148
149 } else {
150 info.put(dom.toLowerCase(Locale.ENGLISH), new String[] { typ, com });
151
152 }
153 } else {
154 System.err.println("Unexpected type: " + line);
155 }
156 }
157 }
158 }
159 return info;
160 }
161
162
163
164
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
197
198
199
200
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
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
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]);
246 for (int i = 0; i < length - 1; i++) {
247 final String entry = array[i];
248 final String nextEntry = array[i + 1];
249 final int cmp = entry.compareTo(nextEntry);
250 if (cmp > 0) {
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
266
267
268
269 public static void main(final String[] a) throws Exception {
270
271
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<>();
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
286
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();
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;
299
300
301 final Map<String, String[]> htmlInfo = getHtmlInfo(htmlFile);
302 final Map<String, String> missingTLD = new TreeMap<>();
303 final Map<String, String> missingCC = new TreeMap<>();
304 while ((line = br.readLine()) != null) {
305 if (!line.startsWith("#")) {
306 final String unicodeTld;
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)) {
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
335 if (generateUnicodeTlds && !unicodeTld.equals(asciiTld)) {
336 ianaTlds.add(unicodeTld);
337 }
338 }
339 }
340 br.close();
341 int errorsDetected = 0;
342
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
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
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
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
409 assertEquals(noLocal, validator);
410
411
412 assertFalse(noLocal.isValid("localhost.localdomain"), "localhost.localdomain should validate");
413 assertFalse(noLocal.isValid("localhost"), "localhost should validate");
414
415
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
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
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() {
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
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
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
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
505 assertFalse(validator.isValid("-test.fr"));
506 assertFalse(validator.isValid("test-.fr"));
507 }
508
509 @Test
510 @Disabled
511 void testInvalidDomains501() {
512
513 assertFalse(validator.isValid("-tést.fr"));
514 assertFalse(validator.isValid("tést-.fr"));
515 }
516
517
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",
525 "java.vendor",
526 "java.vm.specification.version",
527 "java.vm.specification.vendor",
528 "java.vm.specification.name",
529 "java.vm.version",
530 "java.vm.vendor",
531 "java.vm.name",
532 "java.specification.version",
533 "java.specification.vendor",
534 "java.specification.name",
535 "java.class.version",
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);
543 }
544
545
546 @Test
547 void testRFC2396domainlabel() {
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
558 @Test
559 void testRFC2396toplabel() {
560
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
576 assertTrue(validator.isValidInfrastructureTld(".arpa"), ".arpa should validate as iTLD");
577 assertFalse(validator.isValidInfrastructureTld(".com"), ".com shouldn't validate as iTLD");
578
579
580 assertTrue(validator.isValidGenericTld(".name"), ".name should validate as gTLD");
581 assertFalse(validator.isValidGenericTld(".us"), ".us shouldn't validate as gTLD");
582
583
584 assertTrue(validator.isValidCountryCodeTld(".uk"), ".uk should validate as ccTLD");
585 assertFalse(validator.isValidCountryCodeTld(".org"), ".org shouldn't validate as ccTLD");
586
587
588 assertTrue(validator.isValidTld(".COM"), ".COM should validate as TLD");
589 assertTrue(validator.isValidTld(".BiZ"), ".BiZ should validate as TLD");
590
591
592 assertFalse(validator.isValid(".nope"), "invalid TLD shouldn't validate");
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
598
599 @Test
600 void testUnicodeToASCII() {
601 final String[] asciidots = { "", ",", ".",
602 "a.",
603 "a.b", "a..b", "a...b", ".a", "..a", };
604 for (final String s : asciidots) {
605 assertEquals(s, DomainValidator.unicodeToASCII(s));
606 }
607
608
609
610
611
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");
622 }
623
624
625 @Test
626 void testValidator306() {
627 final String longString = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789A";
628 assertEquals(63, longString.length());
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 }