11import { test } from 'node:test' ;
22import assert from 'node:assert/strict' ;
3- import { normalizeUrlForCompare , normalizeCanonicalUrl , canonicalAllowsIndex } from '../dist/util/url.js' ;
3+ import { readFileSync } from 'node:fs' ;
4+ import {
5+ canonicalizeUrl ,
6+ normalizeUrlForCompare ,
7+ normalizeCanonicalUrl ,
8+ canonicalAllowsIndex ,
9+ } from '../dist/util/url.js' ;
410
5- // The reported bug: the request carries %3A (encoded colon) while the page's canonical uses
6- // the literal ':'. They name the same resource, so the page is self-canonical → indexable.
11+ // The single shared vector, asserted by BOTH the browser (this file) and the plugin suite,
12+ // so the two canonicalizeUrl copies (TS here, JS in the plugin) cannot drift.
13+ const VECTORS : { url : string ; allowlist : string [ ] ; expected : string } [ ] = JSON . parse (
14+ readFileSync ( new URL ( '../../../test-vectors/canonicalize-url.json' , import . meta. url ) , 'utf8' )
15+ ) ;
16+
17+ test ( 'canonicalizeUrl matches every shared cache-key vector (must equal the plugin)' , ( ) => {
18+ for ( const { url, allowlist, expected } of VECTORS ) {
19+ assert . equal ( canonicalizeUrl ( url , allowlist ) , expected , `vector: ${ url } @ ${ JSON . stringify ( allowlist ) } ` ) ;
20+ }
21+ } ) ;
22+
23+ // Redirect detection compares canonicalizeUrl(page.url()) vs canonicalizeUrl(job.url). A page
24+ // that did not redirect must NOT be flagged just because Chrome's page.url() re-encodes, adds a
25+ // trailing slash, or reorders params relative to the stored key.
26+ test ( 'canonicalizeUrl does not flag a non-redirect (encoding / trailing slash / order)' , ( ) => {
27+ const jobUrl = 'https://example.com/p?b=2&a=1' ;
28+ assert . equal ( canonicalizeUrl ( 'https://example.com/p/?a=1&b=2' ) , canonicalizeUrl ( jobUrl ) ) ; // slash + order
29+ assert . equal ( canonicalizeUrl ( 'https://example.com/p?a=1&b=2#frag' ) , canonicalizeUrl ( jobUrl ) ) ; // hash
30+ // ':' literal (Chrome's form) matches a stored key that decoded '%3A' to ':'
31+ assert . equal ( canonicalizeUrl ( 'https://example.com/a?f=X:Y' ) , canonicalizeUrl ( 'https://example.com/a?f=X%3AY' ) ) ;
32+ } ) ;
33+
34+ // --- indexability (self-canonical) comparison — separate from the cache key ---
35+
36+ // The request carries %3A (encoded colon) while the page's canonical uses the literal ':'.
37+ // They name the same resource, so the page is self-canonical → indexable.
738test ( 'canonical with a decoded reserved char (: vs %3A) counts as self-canonical' , ( ) => {
8- const requested = 'https://www.kohls. com/catalog/mens-clothing .jsp?CN=Gender%3AMens+Department%3AClothing ' ;
9- const canonical = 'https://www.kohls. com/catalog/mens-clothing .jsp?CN=Gender:Mens+Department:Clothing ' ;
39+ const requested = 'https://example. com/c/page .jsp?f=A%3AB+g%3AC ' ;
40+ const canonical = 'https://example. com/c/page .jsp?f=A:B+g:C ' ;
1041 assert . equal ( canonicalAllowsIndex ( canonical , requested ) , true ) ;
1142} ) ;
1243
1344test ( 'a genuinely different canonical is NOT self-canonical (stays non-indexable)' , ( ) => {
14- const requested = 'https://www.kohls. com/catalog/mens-clothing .jsp?CN=Gender%3AMens ' ;
15- const canonical = 'https://www.kohls. com/catalog/womens-clothing .jsp?CN=Gender%3AWomens ' ;
45+ const requested = 'https://example. com/c/page-a .jsp?f=A%3AB ' ;
46+ const canonical = 'https://example. com/c/page-b .jsp?f=X%3AY ' ;
1647 assert . equal ( canonicalAllowsIndex ( canonical , requested ) , false ) ;
1748} ) ;
1849
1950test ( 'no canonical → indexable' , ( ) => {
20- assert . equal ( canonicalAllowsIndex ( null , 'https://www.kohls .com/x' ) , true ) ;
21- assert . equal ( canonicalAllowsIndex ( undefined , 'https://www.kohls .com/x' ) , true ) ;
22- assert . equal ( canonicalAllowsIndex ( '' , 'https://www.kohls .com/x' ) , true ) ;
51+ assert . equal ( canonicalAllowsIndex ( null , 'https://example .com/x' ) , true ) ;
52+ assert . equal ( canonicalAllowsIndex ( undefined , 'https://example .com/x' ) , true ) ;
53+ assert . equal ( canonicalAllowsIndex ( '' , 'https://example .com/x' ) , true ) ;
2354} ) ;
2455
2556test ( 'param order, trailing slash, and hash differences do not break self-canonical' , ( ) => {
26- const current = 'https://www.kohls .com/p?b=2&a=1' ;
27- assert . equal ( canonicalAllowsIndex ( 'https://www.kohls .com/p?a=1&b=2' , current ) , true ) ; // order
28- assert . equal ( canonicalAllowsIndex ( 'https://www.kohls .com/p/?b=2&a=1' , current ) , true ) ; // trailing slash
29- assert . equal ( canonicalAllowsIndex ( 'https://www.kohls .com/p?b=2&a=1#frag' , current ) , true ) ; // hash
57+ const current = 'https://example .com/p?b=2&a=1' ;
58+ assert . equal ( canonicalAllowsIndex ( 'https://example .com/p?a=1&b=2' , current ) , true ) ; // order
59+ assert . equal ( canonicalAllowsIndex ( 'https://example .com/p/?b=2&a=1' , current ) , true ) ; // trailing slash
60+ assert . equal ( canonicalAllowsIndex ( 'https://example .com/p?b=2&a=1#frag' , current ) , true ) ; // hash
3061} ) ;
3162
3263test ( 'a relative canonical resolves against the current URL' , ( ) => {
33- const current = 'https://www.kohls. com/catalog/mens-clothing .jsp?CN=Gender%3AMens ' ;
34- assert . equal ( canonicalAllowsIndex ( '/catalog/mens-clothing .jsp?CN=Gender:Mens ' , current ) , true ) ;
64+ const current = 'https://example. com/c/page .jsp?f=A%3AB ' ;
65+ assert . equal ( canonicalAllowsIndex ( '/c/page .jsp?f=A:B ' , current ) , true ) ;
3566} ) ;
3667
3768test ( 'a malformed canonical fails open (does not drop indexability)' , ( ) => {
38- assert . equal ( canonicalAllowsIndex ( 'http://[bad' , 'https://www.kohls .com/x' ) , true ) ;
69+ assert . equal ( canonicalAllowsIndex ( 'http://[bad' , 'https://example .com/x' ) , true ) ;
3970} ) ;
4071
4172test ( 'normalizeCanonicalUrl canonicalizes encoding, param order, hash, and trailing slash' , ( ) => {
4273 assert . equal (
43- normalizeCanonicalUrl ( 'https://x.com/a/?c=2&b=Gender:Mens #h' ) ,
44- normalizeCanonicalUrl ( 'https://x.com/a?b=Gender%3AMens &c=2' )
74+ normalizeCanonicalUrl ( 'https://x.com/a/?c=2&b=A:B #h' ) ,
75+ normalizeCanonicalUrl ( 'https://x.com/a?b=A%3AB &c=2' )
4576 ) ;
4677} ) ;
4778
4879test ( 'normalizeCanonicalUrl is idempotent' , ( ) => {
49- const once = normalizeCanonicalUrl ( 'https://www.kohls. com/p/?b=2&a=Gender:Mens #h' ) ;
80+ const once = normalizeCanonicalUrl ( 'https://example. com/p/?b=2&a=A:B #h' ) ;
5081 assert . equal ( normalizeCanonicalUrl ( once ) , once ) ;
5182} ) ;
5283
@@ -60,6 +91,7 @@ test('malformed percent-encoding falls back to raw instead of throwing', () => {
6091 for ( const bad of [ 'https://x.com/%E0%A0/a' , 'https://x.com/p?x=%E0%A0' ] ) {
6192 assert . doesNotThrow ( ( ) => normalizeUrlForCompare ( bad ) ) ;
6293 assert . doesNotThrow ( ( ) => normalizeCanonicalUrl ( bad ) ) ;
94+ assert . doesNotThrow ( ( ) => canonicalizeUrl ( bad ) ) ;
6395 }
6496 assert . doesNotThrow ( ( ) => canonicalAllowsIndex ( 'https://x.com/a' , 'https://x.com/%E0%A0/a' ) ) ;
6597} ) ;
0 commit comments