1import test from 'node:test';
2import assert from 'node:assert/strict';
3import {
4 parseReferrerPolicy,
5 classifyPosture,
6 extractMetaReferrer,
7 scoreReferrerPolicy,
8 normalizeAndValidateUrl,
9 auditReferrerPolicy,
10} from '../src/main.js';
11
12
13
14test('parseReferrerPolicy parses a single valid directive', () => {
15 const r = parseReferrerPolicy('strict-origin-when-cross-origin');
16 assert.equal(r.effective, 'strict-origin-when-cross-origin');
17 assert.equal(r.valid.length, 1);
18 assert.equal(r.invalid.length, 0);
19});
20
21test('parseReferrerPolicy handles empty input', () => {
22 const r = parseReferrerPolicy('');
23 assert.equal(r.effective, null);
24 assert.equal(r.valid.length, 0);
25});
26
27test('parseReferrerPolicy handles null input', () => {
28 const r = parseReferrerPolicy(null);
29 assert.equal(r.effective, null);
30 assert.equal(r.valid.length, 0);
31});
32
33test('parseReferrerPolicy is case-insensitive', () => {
34 const r = parseReferrerPolicy('No-Referrer');
35 assert.equal(r.effective, 'no-referrer');
36});
37
38test('parseReferrerPolicy trims whitespace', () => {
39 const r = parseReferrerPolicy(' strict-origin ');
40 assert.equal(r.effective, 'strict-origin');
41});
42
43test('parseReferrerPolicy flags invalid directives', () => {
44 const r = parseReferrerPolicy('none');
45 assert.equal(r.valid.length, 0);
46 assert.equal(r.invalid.length, 1);
47 assert.equal(r.invalid[0], 'none');
48 assert.equal(r.effective, null);
49});
50
51test('parseReferrerPolicy handles comma-separated directives (last valid wins)', () => {
52 const r = parseReferrerPolicy('no-referrer, strict-origin-when-cross-origin');
53 assert.equal(r.valid.length, 2);
54 assert.equal(r.effective, 'strict-origin-when-cross-origin');
55});
56
57test('parseReferrerPolicy handles mixed valid and invalid directives', () => {
58 const r = parseReferrerPolicy('invalid-val, no-referrer, another-bad');
59 assert.equal(r.valid.length, 1);
60 assert.equal(r.invalid.length, 2);
61 assert.equal(r.effective, 'no-referrer');
62});
63
64test('parseReferrerPolicy marks unsafe-url as deprecated', () => {
65 const r = parseReferrerPolicy('unsafe-url');
66 assert.equal(r.deprecated.length, 1);
67 assert.equal(r.deprecated[0], 'unsafe-url');
68});
69
70test('parseReferrerPolicy marks no-referrer-when-downgrade as deprecated', () => {
71 const r = parseReferrerPolicy('no-referrer-when-downgrade');
72 assert.equal(r.deprecated.length, 1);
73});
74
75test('parseReferrerPolicy does not mark strict-origin-when-cross-origin as deprecated', () => {
76 const r = parseReferrerPolicy('strict-origin-when-cross-origin');
77 assert.equal(r.deprecated.length, 0);
78});
79
80
81
82test('classifyPosture returns none for null policy', () => {
83 assert.equal(classifyPosture(null), 'none');
84});
85
86test('classifyPosture returns strong for no-referrer', () => {
87 assert.equal(classifyPosture('no-referrer'), 'strong');
88});
89
90test('classifyPosture returns strong for same-origin', () => {
91 assert.equal(classifyPosture('same-origin'), 'strong');
92});
93
94test('classifyPosture returns strong for strict-origin', () => {
95 assert.equal(classifyPosture('strict-origin'), 'strong');
96});
97
98test('classifyPosture returns strong for strict-origin-when-cross-origin', () => {
99 assert.equal(classifyPosture('strict-origin-when-cross-origin'), 'strong');
100});
101
102test('classifyPosture returns balanced for origin', () => {
103 assert.equal(classifyPosture('origin'), 'balanced');
104});
105
106test('classifyPosture returns weak for unsafe-url', () => {
107 assert.equal(classifyPosture('unsafe-url'), 'weak');
108});
109
110test('classifyPosture returns weak for origin-when-cross-origin', () => {
111 assert.equal(classifyPosture('origin-when-cross-origin'), 'weak');
112});
113
114test('classifyPosture returns weak for no-referrer-when-downgrade', () => {
115 assert.equal(classifyPosture('no-referrer-when-downgrade'), 'weak');
116});
117
118
119
120test('extractMetaReferrer extracts a meta referrer tag with double quotes', () => {
121 const html = '<html><head><meta name="referrer" content="no-referrer"></head><body></body></html>';
122 const values = extractMetaReferrer(html);
123 assert.equal(values.length, 1);
124 assert.equal(values[0], 'no-referrer');
125});
126
127test('extractMetaReferrer extracts a meta referrer tag with single quotes', () => {
128 const html = "<html><head><meta name='referrer' content='strict-origin-when-cross-origin'></head></html>";
129 const values = extractMetaReferrer(html);
130 assert.equal(values.length, 1);
131 assert.equal(values[0], 'strict-origin-when-cross-origin');
132});
133
134test('extractMetaReferrer handles reversed attribute order (content before name)', () => {
135 const html = '<meta content="same-origin" name="referrer">';
136 const values = extractMetaReferrer(html);
137 assert.equal(values.length, 1);
138 assert.equal(values[0], 'same-origin');
139});
140
141test('extractMetaReferrer is case-insensitive', () => {
142 const html = '<META NAME="Referrer" CONTENT="origin">';
143 const values = extractMetaReferrer(html);
144 assert.equal(values.length, 1);
145 assert.equal(values[0], 'origin');
146});
147
148test('extractMetaReferrer returns empty array for null input', () => {
149 assert.equal(extractMetaReferrer(null).length, 0);
150});
151
152test('extractMetaReferrer returns empty array for HTML without meta referrer', () => {
153 const html = '<html><head><meta name="viewport" content="width=device-width"></head></html>';
154 assert.equal(extractMetaReferrer(html).length, 0);
155});
156
157test('extractMetaReferrer extracts multiple meta referrer tags', () => {
158 const html = '<meta name="referrer" content="no-referrer"><meta name="referrer" content="origin">';
159 const values = extractMetaReferrer(html);
160 assert.equal(values.length, 2);
161 assert.equal(values[0], 'no-referrer');
162 assert.equal(values[1], 'origin');
163});
164
165
166
167test('scoreReferrerPolicy returns low score when no policy is set', () => {
168 const analysis = {
169 hasHeader: false,
170 headerParsed: { valid: [], invalid: [], deprecated: [], effective: null },
171 hasMetaReferrer: false,
172 metaParsed: { valid: [], invalid: [], deprecated: [], effective: null },
173 effectivePolicy: null,
174 privacyPosture: 'none',
175 hasInvalidHeaderDirective: false,
176 hasDeprecatedHeaderDirective: false,
177 hasConflictingHeaders: false,
178 hasInvalidMetaDirective: false,
179 hasDeprecatedMetaDirective: false,
180 };
181 const scored = scoreReferrerPolicy(analysis);
182 assert.ok(scored.score <= 40);
183 assert.ok(['D', 'E', 'F'].includes(scored.grade));
184 assert.ok(scored.issues.length > 0);
185});
186
187test('scoreReferrerPolicy returns high score for strict-origin-when-cross-origin header', () => {
188 const analysis = {
189 hasHeader: true,
190 headerParsed: { valid: ['strict-origin-when-cross-origin'], invalid: [], deprecated: [], effective: 'strict-origin-when-cross-origin' },
191 hasMetaReferrer: false,
192 metaParsed: { valid: [], invalid: [], deprecated: [], effective: null },
193 effectivePolicy: 'strict-origin-when-cross-origin',
194 privacyPosture: 'strong',
195 hasInvalidHeaderDirective: false,
196 hasDeprecatedHeaderDirective: false,
197 hasConflictingHeaders: false,
198 hasInvalidMetaDirective: false,
199 hasDeprecatedMetaDirective: false,
200 };
201 const scored = scoreReferrerPolicy(analysis);
202 assert.ok(scored.score >= 95);
203 assert.equal(scored.grade, 'A+');
204});
205
206test('scoreReferrerPolicy penalizes unsafe-url', () => {
207 const analysis = {
208 hasHeader: true,
209 headerParsed: { valid: ['unsafe-url'], invalid: [], deprecated: ['unsafe-url'], effective: 'unsafe-url' },
210 hasMetaReferrer: false,
211 metaParsed: { valid: [], invalid: [], deprecated: [], effective: null },
212 effectivePolicy: 'unsafe-url',
213 privacyPosture: 'weak',
214 hasInvalidHeaderDirective: false,
215 hasDeprecatedHeaderDirective: true,
216 hasConflictingHeaders: false,
217 hasInvalidMetaDirective: false,
218 hasDeprecatedMetaDirective: false,
219 };
220 const scored = scoreReferrerPolicy(analysis);
221 assert.ok(scored.score < 60);
222 assert.ok(scored.issues.some((i) => i.includes('unsafe-url')));
223});
224
225test('scoreReferrerPolicy penalizes meta-only without header', () => {
226 const analysis = {
227 hasHeader: false,
228 headerParsed: { valid: [], invalid: [], deprecated: [], effective: null },
229 hasMetaReferrer: true,
230 metaParsed: { valid: ['strict-origin-when-cross-origin'], invalid: [], deprecated: [], effective: 'strict-origin-when-cross-origin' },
231 effectivePolicy: 'strict-origin-when-cross-origin',
232 privacyPosture: 'strong',
233 hasInvalidHeaderDirective: false,
234 hasDeprecatedHeaderDirective: false,
235 hasConflictingHeaders: false,
236 hasInvalidMetaDirective: false,
237 hasDeprecatedMetaDirective: false,
238 };
239 const scored = scoreReferrerPolicy(analysis);
240 assert.ok(scored.score < 95);
241 assert.ok(scored.issues.some((i) => i.includes('meta tag only')));
242});
243
244test('scoreReferrerPolicy penalizes conflicting header and meta values', () => {
245 const analysis = {
246 hasHeader: true,
247 headerParsed: { valid: ['no-referrer'], invalid: [], deprecated: [], effective: 'no-referrer' },
248 hasMetaReferrer: true,
249 metaParsed: { valid: ['unsafe-url'], invalid: [], deprecated: ['unsafe-url'], effective: 'unsafe-url' },
250 effectivePolicy: 'no-referrer',
251 privacyPosture: 'strong',
252 hasInvalidHeaderDirective: false,
253 hasDeprecatedHeaderDirective: false,
254 hasConflictingHeaders: false,
255 hasInvalidMetaDirective: false,
256 hasDeprecatedMetaDirective: true,
257 };
258 const scored = scoreReferrerPolicy(analysis);
259 assert.ok(scored.issues.some((i) => i.includes('different values')));
260});
261
262test('scoreReferrerPolicy gives bonus for consistent header and meta', () => {
263 const analysis = {
264 hasHeader: true,
265 headerParsed: { valid: ['strict-origin-when-cross-origin'], invalid: [], deprecated: [], effective: 'strict-origin-when-cross-origin' },
266 hasMetaReferrer: true,
267 metaParsed: { valid: ['strict-origin-when-cross-origin'], invalid: [], deprecated: [], effective: 'strict-origin-when-cross-origin' },
268 effectivePolicy: 'strict-origin-when-cross-origin',
269 privacyPosture: 'strong',
270 hasInvalidHeaderDirective: false,
271 hasDeprecatedHeaderDirective: false,
272 hasConflictingHeaders: false,
273 hasInvalidMetaDirective: false,
274 hasDeprecatedMetaDirective: false,
275 };
276 const scored = scoreReferrerPolicy(analysis);
277 assert.equal(scored.score, 100);
278 assert.equal(scored.grade, 'A+');
279});
280
281test('scoreReferrerPolicy penalizes conflicting multiple header directives', () => {
282 const analysis = {
283 hasHeader: true,
284 headerParsed: { valid: ['no-referrer', 'strict-origin-when-cross-origin'], invalid: [], deprecated: [], effective: 'strict-origin-when-cross-origin' },
285 hasMetaReferrer: false,
286 metaParsed: { valid: [], invalid: [], deprecated: [], effective: null },
287 effectivePolicy: 'strict-origin-when-cross-origin',
288 privacyPosture: 'strong',
289 hasInvalidHeaderDirective: false,
290 hasDeprecatedHeaderDirective: false,
291 hasConflictingHeaders: true,
292 hasInvalidMetaDirective: false,
293 hasDeprecatedMetaDirective: false,
294 };
295 const scored = scoreReferrerPolicy(analysis);
296 assert.ok(scored.issues.some((i) => i.includes('Multiple Referrer-Policy')));
297});
298
299test('scoreReferrerPolicy penalizes invalid header directives', () => {
300 const analysis = {
301 hasHeader: true,
302 headerParsed: { valid: ['no-referrer'], invalid: ['none', 'whatever'], deprecated: [], effective: 'no-referrer' },
303 hasMetaReferrer: false,
304 metaParsed: { valid: [], invalid: [], deprecated: [], effective: null },
305 effectivePolicy: 'no-referrer',
306 privacyPosture: 'strong',
307 hasInvalidHeaderDirective: true,
308 hasDeprecatedHeaderDirective: false,
309 hasConflictingHeaders: false,
310 hasInvalidMetaDirective: false,
311 hasDeprecatedMetaDirective: false,
312 };
313 const scored = scoreReferrerPolicy(analysis);
314 assert.ok(scored.score < 90);
315 assert.ok(scored.issues.some((i) => i.includes('invalid directive')));
316});
317
318
319
320test('normalizeAndValidateUrl blocks private IPv4 literals', async () => {
321 await assert.rejects(() => normalizeAndValidateUrl('http://10.0.0.1/'), /Private IPv4/);
322 await assert.rejects(() => normalizeAndValidateUrl('http://192.168.1.1/'), /Private IPv4/);
323});
324
325test('normalizeAndValidateUrl blocks localhost DNS resolution', async () => {
326 await assert.rejects(() => normalizeAndValidateUrl('http://localhost/'), /private|Private|DNS/);
327});
328
329test('normalizeAndValidateUrl rejects credentials in URL', async () => {
330 await assert.rejects(() => normalizeAndValidateUrl('https://user:pass@example.com/'), /credentials/);
331});
332
333test('normalizeAndValidateUrl rejects non-HTTP schemes', async () => {
334 await assert.rejects(() => normalizeAndValidateUrl('file:///etc/passwd'), /Only HTTP/);
335});
336
337test('normalizeAndValidateUrl defaults missing scheme to https', async () => {
338 const url = await normalizeAndValidateUrl('apify.com');
339 assert.equal(url.protocol, 'https:');
340 assert.equal(url.hostname, 'apify.com');
341});
342
343
344
345test('auditReferrerPolicy returns an F-grade error object when the fetch fails', async () => {
346 const result = await auditReferrerPolicy({ startUrl: 'https://this-host-does-not-exist-cron-test.invalid/' });
347 assert.equal(result.ok, false);
348 assert.equal(result.score, 0);
349 assert.equal(result.grade, 'F');
350 assert.equal(result.hasHeader, false);
351 assert.ok(Array.isArray(result.issues));
352 assert.ok(result.issues.length > 0);
353});