tldts-tests 7.2.1 → 7.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -370,6 +370,25 @@ export default function test(
370
370
  expect(tldts.getDomain('s_pf.google.com')).to.equal('google.com'); // middle '_' (reference case)
371
371
  });
372
372
 
373
+ // A label must not BEGIN with a hyphen (RFC 1034 §3.5 / RFC 1035 §2.3.1 LDH;
374
+ // cf. WHATWG "valid domain" / UTS #46 CheckHyphens). validateHostname is on by
375
+ // default, so an invalid host yields a null parse — exercised here at the
376
+ // public-API level (the inline-validation fast path). @see issue #2395.
377
+ it('should reject labels that begin with a hyphen', () => {
378
+ expect(tldts.getHostname('foo.-example.com')).to.equal(null); // interior label
379
+ expect(tldts.getHostname('a.b.-c.com')).to.equal(null); // deeper interior label
380
+ expect(tldts.getHostname('foo.-com')).to.equal(null); // final label
381
+ expect(tldts.getDomain('foo.-example.com')).to.equal(null);
382
+ expect(tldts.getHostname('-example.com')).to.equal(null); // first label (already rejected)
383
+ // Interior hyphens and underscore-leading labels stay valid.
384
+ expect(tldts.getHostname('foo.ex-ample.com')).to.equal(
385
+ 'foo.ex-ample.com',
386
+ );
387
+ expect(tldts.getHostname('_dmarc.example.com')).to.equal(
388
+ '_dmarc.example.com',
389
+ );
390
+ });
391
+
373
392
  // @see https://github.com/oncletom/tld.js/issues/53
374
393
  it('should correctly extract domain from paths including "@" in the path', () => {
375
394
  const domain = tldts.getDomain(
@@ -731,6 +750,264 @@ export default function test(
731
750
  });
732
751
  });
733
752
 
753
+ // The default (validating) path fuses host validation into the extraction
754
+ // scan: for a "simple" authority (no userinfo / port / brackets / trailing
755
+ // dot / control char) parseImpl skips the separate isValidHostname pass and
756
+ // trusts the verdict accumulated during extraction. These cases pin the
757
+ // user-facing contract of that fusion — exercised end-to-end through the
758
+ // public API — at the structural boundaries (label/total length, hyphen, dot,
759
+ // underscore positions) and across the simple-vs-complex authority split. All
760
+ // expected values were cross-checked against the v7.2.0 pre-fusion baseline
761
+ // (0 divergences), so an accidental behaviour change here is a regression.
762
+ describe('inline hostname validation (extract/validate fusion)', () => {
763
+ const rep = (s: string, n: number): string => {
764
+ let r = '';
765
+ for (let i = 0; i < n; i += 1) {
766
+ r += s;
767
+ }
768
+ return r;
769
+ };
770
+
771
+ describe('label-length boundary (63 valid / 64 invalid)', () => {
772
+ it('accepts a 63-char first label, rejects 64', () => {
773
+ expect(tldts.getHostname('http://' + rep('a', 63) + '.com/p')).to.equal(
774
+ rep('a', 63) + '.com',
775
+ );
776
+ expect(tldts.getHostname('http://' + rep('a', 64) + '.com/p')).to.equal(
777
+ null,
778
+ );
779
+ });
780
+
781
+ it('accepts a 63-char last label, rejects 64', () => {
782
+ expect(tldts.getHostname('http://ok.' + rep('a', 63))).to.equal(
783
+ 'ok.' + rep('a', 63),
784
+ );
785
+ expect(tldts.getHostname('http://ok.' + rep('a', 64))).to.equal(null);
786
+ });
787
+
788
+ it('accepts a single 63-char label, rejects 64 (no dots)', () => {
789
+ // Public suffix of a bare single label is the label itself => no domain,
790
+ // but the hostname must still be extracted (or rejected at 64).
791
+ expect(tldts.getHostname('http://' + rep('a', 63) + '/p')).to.equal(
792
+ rep('a', 63),
793
+ );
794
+ expect(tldts.getHostname('http://' + rep('a', 64) + '/p')).to.equal(
795
+ null,
796
+ );
797
+ });
798
+
799
+ it('agrees between URL and bare-hostname forms at the boundary', () => {
800
+ // The bare form takes the urlIsValidHostname reference path; the URL
801
+ // form takes the inline-validation path. Both must converge.
802
+ expect(tldts.getHostname(rep('a', 63) + '.com')).to.equal(
803
+ rep('a', 63) + '.com',
804
+ );
805
+ expect(tldts.getHostname(rep('a', 64) + '.com')).to.equal(null);
806
+ });
807
+ });
808
+
809
+ describe('total-length boundary (255 valid / 256 invalid)', () => {
810
+ // 'aaa.' * 63 + 'aaa' = 255 chars with every label <= 63, so this isolates
811
+ // the total-length guard from the per-label guard. +1 char = 256.
812
+ const h255 = rep('aaa.', 63) + 'aaa';
813
+ const h256 = rep('aaa.', 63) + 'aaaa';
814
+
815
+ it('precondition: lengths are exactly 255 and 256', () => {
816
+ expect(h255.length).to.equal(255);
817
+ expect(h256.length).to.equal(256);
818
+ });
819
+
820
+ it('accepts a 255-char host, rejects 256 (URL form)', () => {
821
+ expect(tldts.getHostname('http://' + h255 + '/p')).to.equal(h255);
822
+ expect(tldts.getHostname('http://' + h256 + '/p')).to.equal(null);
823
+ });
824
+
825
+ it('accepts a 255-char host, rejects 256 (bare form)', () => {
826
+ expect(tldts.getHostname(h255)).to.equal(h255);
827
+ expect(tldts.getHostname(h256)).to.equal(null);
828
+ });
829
+ });
830
+
831
+ describe('dot placement', () => {
832
+ it('accepts a single leading dot', () => {
833
+ expect(tldts.getHostname('http://.example.com/p')).to.equal(
834
+ '.example.com',
835
+ );
836
+ });
837
+
838
+ it('strips a single and multiple trailing dots', () => {
839
+ expect(tldts.getHostname('http://example.com./p')).to.equal(
840
+ 'example.com',
841
+ );
842
+ expect(tldts.getHostname('http://example.com.../p')).to.equal(
843
+ 'example.com',
844
+ );
845
+ });
846
+
847
+ it('rejects consecutive dots (empty label)', () => {
848
+ expect(tldts.getHostname('http://a..b.com/p')).to.equal(null);
849
+ expect(tldts.getHostname('a..b.com')).to.equal(null);
850
+ });
851
+ });
852
+
853
+ describe('hyphen placement', () => {
854
+ it('rejects a leading hyphen on the host', () => {
855
+ expect(tldts.getHostname('http://-example.com/p')).to.equal(null);
856
+ expect(tldts.getHostname('-example.com')).to.equal(null);
857
+ });
858
+
859
+ it('rejects a trailing hyphen on the whole host', () => {
860
+ expect(tldts.getHostname('http://example.com-/p')).to.equal(null);
861
+ expect(tldts.getHostname('example.com-')).to.equal(null);
862
+ });
863
+
864
+ it('rejects a label that ends with a hyphen', () => {
865
+ expect(tldts.getHostname('http://foo-.bar.com/p')).to.equal(null);
866
+ });
867
+
868
+ it('accepts a hyphen mid-label', () => {
869
+ expect(tldts.getHostname('http://ex-ample.com/p')).to.equal(
870
+ 'ex-ample.com',
871
+ );
872
+ expect(tldts.getHostname('ex-ample.com')).to.equal('ex-ample.com');
873
+ });
874
+ });
875
+
876
+ describe('underscore placement (DNS-lenient, RFC 2181 §11)', () => {
877
+ it('accepts a leading underscore', () => {
878
+ expect(tldts.getHostname('http://_example.com/p')).to.equal(
879
+ '_example.com',
880
+ );
881
+ });
882
+
883
+ it('accepts a mid-label underscore', () => {
884
+ expect(tldts.getHostname('http://ex_ample.com/p')).to.equal(
885
+ 'ex_ample.com',
886
+ );
887
+ });
888
+
889
+ it('accepts a label-final underscore', () => {
890
+ expect(tldts.getHostname('http://spf_.example.com/p')).to.equal(
891
+ 'spf_.example.com',
892
+ );
893
+ });
894
+ });
895
+
896
+ describe('character classes', () => {
897
+ it('lower-cases an uppercase host then validates it (URL and bare)', () => {
898
+ expect(tldts.getHostname('HTTP://SUB.EXAMPLE.COM/P')).to.equal(
899
+ 'sub.example.com',
900
+ );
901
+ expect(tldts.getHostname('EXAMPLE.COM')).to.equal('example.com');
902
+ });
903
+
904
+ it('keeps a non-ASCII / IDN host without punycode (URL and bare)', () => {
905
+ expect(tldts.getHostname('http://bücher.example/p')).to.equal(
906
+ 'bücher.example',
907
+ );
908
+ expect(tldts.getHostname('ящик-с-апельсинами.рф')).to.equal(
909
+ 'ящик-с-апельсинами.рф',
910
+ );
911
+ });
912
+
913
+ it('accepts a digits-only host (no IPv4 normalisation)', () => {
914
+ expect(tldts.getHostname('http://123.456/p')).to.equal('123.456');
915
+ expect(tldts.getHostname('123.456')).to.equal('123.456');
916
+ });
917
+
918
+ it('rejects a forbidden host character (%, space)', () => {
919
+ expect(tldts.getHostname('http://a%b.com/p')).to.equal(null);
920
+ expect(tldts.getHostname('http://a b.com/p')).to.equal(null);
921
+ });
922
+ });
923
+
924
+ describe('simple-vs-complex authority split (the fusion gate)', () => {
925
+ // The fusion is ACTIVE only for a simple authority; a complex one
926
+ // (userinfo / port / brackets / trailing dot) falls back to the separate
927
+ // isValidHostname pass. Both branches must yield the identical host.
928
+ it('extracts the same host with and without userinfo', () => {
929
+ expect(tldts.getHostname('http://host.com/p')).to.equal('host.com');
930
+ expect(tldts.getHostname('http://user@host.com/p')).to.equal(
931
+ 'host.com',
932
+ );
933
+ expect(tldts.getHostname('http://user:pass@host.com/p')).to.equal(
934
+ 'host.com',
935
+ );
936
+ });
937
+
938
+ it('extracts the same host with and without a port', () => {
939
+ expect(tldts.getHostname('http://host.com:8080/p')).to.equal(
940
+ 'host.com',
941
+ );
942
+ });
943
+
944
+ it('extracts the same host with a trailing-dot (complex) authority', () => {
945
+ expect(tldts.getHostname('http://host.com./p')).to.equal('host.com');
946
+ });
947
+
948
+ it('extracts a bracketed IPv6 host (complex authority)', () => {
949
+ expect(tldts.getHostname('http://[::1]/p')).to.equal('::1');
950
+ });
951
+ });
952
+
953
+ describe('full parse() result parity at the boundary', () => {
954
+ // The fused verdict gates parseImpl BEFORE public-suffix/domain
955
+ // extraction, so a valid simple host must still produce a full result and
956
+ // an invalid one must zero every field.
957
+ it('produces a full result for a valid simple host', () => {
958
+ expect(tldts.parse('http://sub.example.com/p')).to.deep.include({
959
+ hostname: 'sub.example.com',
960
+ domain: 'example.com',
961
+ publicSuffix: 'com',
962
+ });
963
+ });
964
+
965
+ it('zeroes domain and publicSuffix for an invalid simple host', () => {
966
+ expect(tldts.parse('http://-example.com/p')).to.deep.include({
967
+ hostname: null,
968
+ domain: null,
969
+ publicSuffix: null,
970
+ });
971
+ });
972
+ });
973
+
974
+ describe('fusion-inactive controls (must match the validating path)', () => {
975
+ it('validateHostname:false keeps a host the fused path would reject', () => {
976
+ // Fusion never runs; the raw host is returned verbatim.
977
+ expect(
978
+ tldts.parse('http://-example.com/p', { validateHostname: false })
979
+ .hostname,
980
+ ).to.equal('-example.com');
981
+ expect(
982
+ tldts.parse('http://' + rep('a', 64) + '.com/p', {
983
+ validateHostname: false,
984
+ }).hostname,
985
+ ).to.equal(rep('a', 64) + '.com');
986
+ });
987
+
988
+ it('mixedInputs:false (URL-only path) matches the default for a valid host', () => {
989
+ const url = 'http://sub.example.com/p';
990
+ expect(tldts.parse(url, { mixedInputs: false })).to.deep.equal(
991
+ tldts.parse(url),
992
+ );
993
+ });
994
+
995
+ it('mixedInputs:false rejects an invalid simple host like the default', () => {
996
+ const url = 'http://-example.com/p';
997
+ expect(tldts.parse(url, { mixedInputs: false }).hostname).to.equal(
998
+ null,
999
+ );
1000
+ });
1001
+
1002
+ it('a bare valid hostname (reference path) parses identically to its URL form', () => {
1003
+ expect(tldts.parse('sub.example.com').hostname).to.equal(
1004
+ 'sub.example.com',
1005
+ );
1006
+ expect(tldts.parse('sub.example.com').domain).to.equal('example.com');
1007
+ });
1008
+ });
1009
+ });
1010
+
734
1011
  describe('getDomainWithoutSuffix method', () => {
735
1012
  it('should return null if the domain cannot be found', () => {
736
1013
  expect(tldts.getDomainWithoutSuffix('not-a-validHost')).to.equal(null);