@@ -13,6 +13,7 @@ import {
1313 isStrongPassword ,
1414 isIPv4 ,
1515 isIPv6 ,
16+ isHostname ,
1617 isCreditCard ,
1718 isValidJSON ,
1819 isArray ,
@@ -302,6 +303,231 @@ describe('ValidationUtils', () => {
302303 } ) ;
303304 } ) ;
304305
306+ describe ( 'isHostname' , ( ) => {
307+ describe ( 'IPv4 addresses' , ( ) => {
308+ it ( 'accepts valid IPv4 addresses' , ( ) => {
309+ expect ( isHostname ( '127.0.0.1' ) ) . toBe ( true ) ;
310+ expect ( isHostname ( '0.0.0.0' ) ) . toBe ( true ) ;
311+ expect ( isHostname ( '192.168.1.1' ) ) . toBe ( true ) ;
312+ expect ( isHostname ( '10.0.0.1' ) ) . toBe ( true ) ;
313+ expect ( isHostname ( '255.255.255.255' ) ) . toBe ( true ) ;
314+ expect ( isHostname ( '8.8.8.8' ) ) . toBe ( true ) ;
315+ } ) ;
316+
317+ it ( 'rejects invalid IPv4 addresses' , ( ) => {
318+ expect ( isHostname ( '256.0.0.1' ) ) . toBe ( false ) ;
319+ expect ( isHostname ( '192.168.1.256' ) ) . toBe ( false ) ;
320+ expect ( isHostname ( '192.168.1.1.1' ) ) . toBe ( false ) ;
321+ } ) ;
322+ } ) ;
323+
324+ describe ( 'IPv6 addresses' , ( ) => {
325+ it ( 'accepts valid IPv6 addresses' , ( ) => {
326+ expect ( isHostname ( '::1' ) ) . toBe ( true ) ;
327+ expect ( isHostname ( '::' ) ) . toBe ( true ) ;
328+ expect ( isHostname ( '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ) ) . toBe ( true ) ;
329+ expect ( isHostname ( '2001:db8::1' ) ) . toBe ( true ) ;
330+ expect ( isHostname ( 'fe80::1' ) ) . toBe ( true ) ;
331+ expect ( isHostname ( '::ffff:192.0.2.1' ) ) . toBe ( true ) ; // IPv4-mapped IPv6
332+ } ) ;
333+
334+ it ( 'rejects invalid IPv6 addresses' , ( ) => {
335+ expect ( isHostname ( 'gggg::1' ) ) . toBe ( false ) ;
336+ expect ( isHostname ( ':::' ) ) . toBe ( false ) ;
337+ expect ( isHostname ( 'not:valid:ipv6' ) ) . toBe ( false ) ;
338+ } ) ;
339+ } ) ;
340+
341+ describe ( 'localhost' , ( ) => {
342+ it ( 'accepts localhost' , ( ) => {
343+ expect ( isHostname ( 'localhost' ) ) . toBe ( true ) ;
344+ } ) ;
345+ } ) ;
346+
347+ describe ( 'valid domain names' , ( ) => {
348+ it ( 'accepts standard domain names' , ( ) => {
349+ expect ( isHostname ( 'example.com' ) ) . toBe ( true ) ;
350+ expect ( isHostname ( 'sub.example.com' ) ) . toBe ( true ) ;
351+ expect ( isHostname ( 'deep.sub.example.com' ) ) . toBe ( true ) ;
352+ expect ( isHostname ( 'api.github.com' ) ) . toBe ( true ) ;
353+ expect ( isHostname ( 'test-server.example.org' ) ) . toBe ( true ) ;
354+ } ) ;
355+
356+ it ( 'accepts single-label hostnames' , ( ) => {
357+ expect ( isHostname ( 'server1' ) ) . toBe ( true ) ;
358+ expect ( isHostname ( 'my-server' ) ) . toBe ( true ) ;
359+ expect ( isHostname ( 'app123' ) ) . toBe ( true ) ;
360+ } ) ;
361+
362+ it ( 'accepts hostnames with numbers' , ( ) => {
363+ expect ( isHostname ( 'server1.example.com' ) ) . toBe ( true ) ;
364+ expect ( isHostname ( 'api2.example.com' ) ) . toBe ( true ) ;
365+ expect ( isHostname ( '123server.com' ) ) . toBe ( true ) ;
366+ } ) ;
367+
368+ it ( 'accepts maximum length labels (63 chars)' , ( ) => {
369+ const maxLabel = 'a' . repeat ( 63 ) ;
370+ expect ( isHostname ( `${ maxLabel } .com` ) ) . toBe ( true ) ;
371+ } ) ;
372+
373+ it ( 'accepts exactly 255 character hostname' , ( ) => {
374+ const label = 'a' . repeat ( 63 ) ;
375+ const hostname = `${ label } .${ label } .${ label } .${ label } ` ;
376+ expect ( hostname . length ) . toBe ( 255 ) ;
377+ expect ( isHostname ( hostname ) ) . toBe ( true ) ;
378+ } ) ;
379+
380+ it ( 'accepts trailing dot (FQDN)' , ( ) => {
381+ expect ( isHostname ( 'example.com.' ) ) . toBe ( true ) ;
382+ } ) ;
383+
384+ it ( 'accepts punycode domains' , ( ) => {
385+ expect ( isHostname ( 'xn--d1acpjx3f.xn--p1ai' ) ) . toBe ( true ) ;
386+ } ) ;
387+
388+ it ( 'handles IPv6 with zone index' , ( ) => {
389+ expect ( isHostname ( 'fe80::1%eth0' ) ) . toBe ( true ) ;
390+ } ) ;
391+
392+ it ( 'accepts uppercase domains' , ( ) => {
393+ expect ( isHostname ( 'EXAMPLE.COM' ) ) . toBe ( true ) ;
394+ expect ( isHostname ( 'Sub.Domain.COM' ) ) . toBe ( true ) ;
395+ } ) ;
396+ } ) ;
397+
398+ describe ( 'invalid cases' , ( ) => {
399+ it ( 'rejects empty or whitespace strings' , ( ) => {
400+ expect ( isHostname ( '' ) ) . toBe ( false ) ;
401+ expect ( isHostname ( ' ' ) ) . toBe ( false ) ;
402+ expect ( isHostname ( ' ' ) ) . toBe ( false ) ;
403+ expect ( isHostname ( '\t' ) ) . toBe ( false ) ;
404+ } ) ;
405+
406+ it ( 'rejects null and undefined' , ( ) => {
407+ expect ( isHostname ( null as any ) ) . toBe ( false ) ;
408+ expect ( isHostname ( undefined as any ) ) . toBe ( false ) ;
409+ } ) ;
410+
411+ it ( 'rejects non-string values' , ( ) => {
412+ expect ( isHostname ( 123 as any ) ) . toBe ( false ) ;
413+ expect ( isHostname ( { } as any ) ) . toBe ( false ) ;
414+ expect ( isHostname ( [ ] as any ) ) . toBe ( false ) ;
415+ } ) ;
416+
417+ it ( 'rejects hostnames with consecutive dots' , ( ) => {
418+ expect ( isHostname ( 'example..com' ) ) . toBe ( false ) ;
419+ expect ( isHostname ( '..example.com' ) ) . toBe ( false ) ;
420+ expect ( isHostname ( 'example.com..' ) ) . toBe ( false ) ;
421+ expect ( isHostname ( 'sub..example.com' ) ) . toBe ( false ) ;
422+ } ) ;
423+
424+ it ( 'rejects hostnames starting or ending with hyphen' , ( ) => {
425+ expect ( isHostname ( '-example.com' ) ) . toBe ( false ) ;
426+ expect ( isHostname ( 'example-.com' ) ) . toBe ( false ) ;
427+ expect ( isHostname ( 'sub.-example.com' ) ) . toBe ( false ) ;
428+ expect ( isHostname ( 'sub.example-.com' ) ) . toBe ( false ) ;
429+ } ) ;
430+
431+ it ( 'rejects hostnames with invalid characters' , ( ) => {
432+ expect ( isHostname ( 'exa_mple.com' ) ) . toBe ( false ) ;
433+ expect ( isHostname ( 'exa!mple.com' ) ) . toBe ( false ) ;
434+ expect ( isHostname ( 'exa@mple.com' ) ) . toBe ( false ) ;
435+ expect ( isHostname ( 'exa#mple.com' ) ) . toBe ( false ) ;
436+ expect ( isHostname ( 'exa$mple.com' ) ) . toBe ( false ) ;
437+ expect ( isHostname ( 'exa%mple.com' ) ) . toBe ( false ) ;
438+ expect ( isHostname ( 'exa mple.com' ) ) . toBe ( false ) ;
439+ expect ( isHostname ( 'exa\tmple.com' ) ) . toBe ( false ) ;
440+ } ) ;
441+
442+ it ( 'rejects hostnames exceeding 255 characters' , ( ) => {
443+ const tooLong = 'a' . repeat ( 256 ) ;
444+ expect ( isHostname ( tooLong ) ) . toBe ( false ) ;
445+
446+ const longHostname = 'a' . repeat ( 200 ) + '.com' ;
447+ expect ( isHostname ( longHostname ) ) . toBe ( false ) ;
448+ } ) ;
449+
450+ it ( 'rejects labels exceeding 63 characters' , ( ) => {
451+ const tooLongLabel = 'a' . repeat ( 64 ) ;
452+ expect ( isHostname ( `${ tooLongLabel } .com` ) ) . toBe ( false ) ;
453+ expect ( isHostname ( `sub.${ tooLongLabel } .com` ) ) . toBe ( false ) ;
454+ } ) ;
455+
456+ it ( 'rejects double hyphen edge misuse (optional strict)' , ( ) => {
457+ expect ( isHostname ( 'a--.com' ) ) . toBe ( false ) ;
458+ } ) ;
459+
460+ it ( 'rejects hostnames starting with dots' , ( ) => {
461+ expect ( isHostname ( '.example.com' ) ) . toBe ( false ) ;
462+ expect ( isHostname ( '.com' ) ) . toBe ( false ) ;
463+ } ) ;
464+
465+ it ( 'rejects hostnames ending with more than one dot' , ( ) => {
466+ expect ( isHostname ( 'example.com..' ) ) . toBe ( false ) ;
467+ expect ( isHostname ( 'sub.example.com..' ) ) . toBe ( false ) ;
468+ } ) ;
469+
470+ it ( 'rejects empty labels' , ( ) => {
471+ expect ( isHostname ( 'example..com' ) ) . toBe ( false ) ;
472+ expect ( isHostname ( '.com' ) ) . toBe ( false ) ;
473+ expect ( isHostname ( 'example..' ) ) . toBe ( false ) ;
474+ } ) ;
475+
476+ it ( 'rejects hostnames with only special characters' , ( ) => {
477+ expect ( isHostname ( '...' ) ) . toBe ( false ) ;
478+ expect ( isHostname ( '___' ) ) . toBe ( false ) ;
479+ expect ( isHostname ( '-' ) ) . toBe ( false ) ;
480+ expect ( isHostname ( 'a-' ) ) . toBe ( false ) ; // ends with hyphen
481+ expect ( isHostname ( '-a' ) ) . toBe ( false ) ; // starts with hyphen
482+ } ) ;
483+
484+ it ( 'rejects invalid IPv4-like but numeric hostnames' , ( ) => {
485+ expect ( isHostname ( '256.256.256.256' ) ) . toBe ( false ) ;
486+ expect ( isHostname ( '999.1.1.1' ) ) . toBe ( false ) ;
487+ } ) ;
488+ } ) ;
489+
490+ describe ( 'edge cases' , ( ) => {
491+ it ( 'handles trimmed whitespace' , ( ) => {
492+ expect ( isHostname ( ' example.com ' ) ) . toBe ( true ) ;
493+ expect ( isHostname ( '\tlocalhost\t' ) ) . toBe ( true ) ;
494+ expect ( isHostname ( ' 192.168.1.1 ' ) ) . toBe ( true ) ;
495+ } ) ;
496+
497+ it ( 'accepts single character labels' , ( ) => {
498+ expect ( isHostname ( 'a.b.c' ) ) . toBe ( true ) ;
499+ expect ( isHostname ( 'x' ) ) . toBe ( true ) ;
500+ } ) ;
501+
502+ it ( 'accepts all-numeric single label (hostname, not domain)' , ( ) => {
503+ expect ( isHostname ( '123' ) ) . toBe ( true ) ;
504+ expect ( isHostname ( '456789' ) ) . toBe ( true ) ;
505+ } ) ;
506+
507+ it ( 'rejects label with only hyphens' , ( ) => {
508+ expect ( isHostname ( 'a-b' ) ) . toBe ( true ) ; // valid
509+ expect ( isHostname ( '-' ) ) . toBe ( false ) ; // only hyphen
510+ } ) ;
511+ } ) ;
512+
513+ describe ( 'real-world examples' , ( ) => {
514+ it ( 'accepts common server hostnames' , ( ) => {
515+ expect ( isHostname ( 'web-server-01' ) ) . toBe ( true ) ;
516+ expect ( isHostname ( 'db1.prod.internal' ) ) . toBe ( true ) ;
517+ expect ( isHostname ( 'api-gateway.eu-west-1.aws.com' ) ) . toBe ( true ) ;
518+ } ) ;
519+
520+ it ( 'accepts common TLDs' , ( ) => {
521+ expect ( isHostname ( 'example.com' ) ) . toBe ( true ) ;
522+ expect ( isHostname ( 'example.org' ) ) . toBe ( true ) ;
523+ expect ( isHostname ( 'example.net' ) ) . toBe ( true ) ;
524+ expect ( isHostname ( 'example.io' ) ) . toBe ( true ) ;
525+ expect ( isHostname ( 'example.dev' ) ) . toBe ( true ) ;
526+ expect ( isHostname ( 'example.app' ) ) . toBe ( true ) ;
527+ } ) ;
528+ } ) ;
529+ } ) ;
530+
305531 describe ( 'isCreditCard' , ( ) => {
306532 it ( 'returns true for valid credit card numbers' , ( ) => {
307533 expect ( isCreditCard ( '4111 1111 1111 1111' ) ) . toBe ( true ) ;
0 commit comments