Skip to content
File

Blob: src/workerd/api/node/tests/url-nodejs-test.js

javascript518 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26import {
27 strictEqual,
28 throws,
29 ok as assert,
30 match,
31 deepStrictEqual,
32} from 'node:assert';
33import {
34 domainToASCII,
35 domainToUnicode,
36 URL as URLImpl,
37 URLSearchParams as URLSearchParamsImpl,
38 fileURLToPath,
39 pathToFileURL,
40} from 'node:url';
41 
42// Tests are taken from:
43// https://github.com/nodejs/node/blob/321a14b36d6b3304aedfd183e12ddba35dc704bd/test/parallel/test-url-domain-ascii-unicode.js
44export const testDomainAsciiUnicode = {
45 test() {
46 const domainWithASCII = [
47 ['ıíd', 'xn--d-iga7r'],
48 ['يٴ', 'xn--mhb8f'],
49 ['www.ϧƽəʐ.com', 'www.xn--cja62apfr6c.com'],
50 ['новини.com', 'xn--b1amarcd.com'],
51 ['名がドメイン.com', 'xn--v8jxj3d1dzdz08w.com'],
52 ['افغانستا.icom.museum', 'xn--mgbaal8b0b9b2b.icom.museum'],
53 ['الجزائر.icom.fake', 'xn--lgbbat1ad8j.icom.fake'],
54 ['भारत.org', 'xn--h2brj9c.org'],
55 ['aaa', 'aaa'],
56 ];
57 
58 for (const [domain, ascii] of domainWithASCII) {
59 strictEqual(domainToASCII(domain), ascii);
60 strictEqual(domainToUnicode(ascii), domain);
61 }
62 },
63};
64 
65// Tests are taken from:
66// https://github.com/nodejs/node/blob/321a14b36d6b3304aedfd183e12ddba35dc704bd/test/parallel/test-whatwg-url-custom-domainto.js
67export const whatwgURLCustomDomainTo = {
68 test() {
69 {
70 const expectedError = { code: 'ERR_MISSING_ARGS', name: 'TypeError' };
71 throws(() => domainToASCII(), expectedError);
72 throws(() => domainToUnicode(), expectedError);
73 strictEqual(domainToASCII(undefined), 'undefined');
74 strictEqual(domainToUnicode(undefined), 'undefined');
75 }
76 },
77};
78 
79export const urlAndSearchParams = {
80 test() {
81 assert(URLImpl, 'URL should be exported from node:url');
82 assert(
83 URLSearchParamsImpl,
84 'URLSearchParams should be exported by node:url'
85 );
86 },
87};
88 
89export const getBuiltinModule = {
90 async test() {
91 const bim = process.getBuiltinModule('node:url');
92 const url = await import('node:url');
93 strictEqual(bim, url.default);
94 },
95};
96 
97// Ref: https://github.com/nodejs/node/blob/e92446536ed4e268c9eef6ae6f911e384c98eecf/test/parallel/test-url-fileurltopath.js
98export const testFileURLToPath = {
99 async test() {
100 // invalid arguments
101 for (const arg of [null, undefined, 1, {}, true]) {
102 throws(() => fileURLToPath(arg), {
103 code: 'ERR_INVALID_ARG_TYPE',
104 });
105 }
106 
107 // input must be a file URL
108 throws(() => fileURLToPath('https://a/b/c'), {
109 code: 'ERR_INVALID_URL_SCHEME',
110 });
111 
112 {
113 // fileURLToPath with host
114 const withHost = new URL('file://host/a');
115 
116 throws(() => fileURLToPath(withHost), {
117 code: 'ERR_INVALID_FILE_URL_HOST',
118 });
119 }
120 
121 const windowsTestCases = [
122 // Lowercase ascii alpha
123 { path: 'C:\\foo', fileURL: 'file:///C:/foo' },
124 // Uppercase ascii alpha
125 { path: 'C:\\FOO', fileURL: 'file:///C:/FOO' },
126 // dir
127 { path: 'C:\\dir\\foo', fileURL: 'file:///C:/dir/foo' },
128 // trailing separator
129 { path: 'C:\\dir\\', fileURL: 'file:///C:/dir/' },
130 // dot
131 { path: 'C:\\foo.mjs', fileURL: 'file:///C:/foo.mjs' },
132 // space
133 { path: 'C:\\foo bar', fileURL: 'file:///C:/foo%20bar' },
134 // question mark
135 { path: 'C:\\foo?bar', fileURL: 'file:///C:/foo%3Fbar' },
136 // number sign
137 { path: 'C:\\foo#bar', fileURL: 'file:///C:/foo%23bar' },
138 // ampersand
139 { path: 'C:\\foo&bar', fileURL: 'file:///C:/foo&bar' },
140 // equals
141 { path: 'C:\\foo=bar', fileURL: 'file:///C:/foo=bar' },
142 // colon
143 { path: 'C:\\foo:bar', fileURL: 'file:///C:/foo:bar' },
144 // semicolon
145 { path: 'C:\\foo;bar', fileURL: 'file:///C:/foo;bar' },
146 // percent
147 { path: 'C:\\foo%bar', fileURL: 'file:///C:/foo%25bar' },
148 // backslash
149 { path: 'C:\\foo\\bar', fileURL: 'file:///C:/foo/bar' },
150 // backspace
151 { path: 'C:\\foo\bbar', fileURL: 'file:///C:/foo%08bar' },
152 // tab
153 { path: 'C:\\foo\tbar', fileURL: 'file:///C:/foo%09bar' },
154 // newline
155 { path: 'C:\\foo\nbar', fileURL: 'file:///C:/foo%0Abar' },
156 // carriage return
157 { path: 'C:\\foo\rbar', fileURL: 'file:///C:/foo%0Dbar' },
158 // latin1
159 { path: 'C:\\fóóbàr', fileURL: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' },
160 // Euro sign (BMP code point)
161 { path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' },
162 // Rocket emoji (non-BMP code point)
163 { path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' },
164 // UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows)
165 {
166 path: '\\\\nas\\My Docs\\File.doc',
167 fileURL: 'file://nas/My%20Docs/File.doc',
168 },
169 ];
170 
171 const posixTestCases = [
172 // Lowercase ascii alpha
173 { path: '/foo', fileURL: 'file:///foo' },
174 // Uppercase ascii alpha
175 { path: '/FOO', fileURL: 'file:///FOO' },
176 // dir
177 { path: '/dir/foo', fileURL: 'file:///dir/foo' },
178 // trailing separator
179 { path: '/dir/', fileURL: 'file:///dir/' },
180 // dot
181 { path: '/foo.mjs', fileURL: 'file:///foo.mjs' },
182 // space
183 { path: '/foo bar', fileURL: 'file:///foo%20bar' },
184 // question mark
185 { path: '/foo?bar', fileURL: 'file:///foo%3Fbar' },
186 // number sign
187 { path: '/foo#bar', fileURL: 'file:///foo%23bar' },
188 // ampersand
189 { path: '/foo&bar', fileURL: 'file:///foo&bar' },
190 // equals
191 { path: '/foo=bar', fileURL: 'file:///foo=bar' },
192 // colon
193 { path: '/foo:bar', fileURL: 'file:///foo:bar' },
194 // semicolon
195 { path: '/foo;bar', fileURL: 'file:///foo;bar' },
196 // percent
197 { path: '/foo%bar', fileURL: 'file:///foo%25bar' },
198 // backslash
199 { path: '/foo\\bar', fileURL: 'file:///foo%5Cbar' },
200 // backspace
201 { path: '/foo\bbar', fileURL: 'file:///foo%08bar' },
202 // tab
203 { path: '/foo\tbar', fileURL: 'file:///foo%09bar' },
204 // newline
205 { path: '/foo\nbar', fileURL: 'file:///foo%0Abar' },
206 // carriage return
207 { path: '/foo\rbar', fileURL: 'file:///foo%0Dbar' },
208 // latin1
209 { path: '/fóóbàr', fileURL: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
210 // Euro sign (BMP code point)
211 { path: '/€', fileURL: 'file:///%E2%82%AC' },
212 // Rocket emoji (non-BMP code point)
213 { path: '/🚀', fileURL: 'file:///%F0%9F%9A%80' },
214 ];
215 
216 // fileURLToPath with windows path
217 for (const { path, fileURL } of windowsTestCases) {
218 const fromString = fileURLToPath(fileURL, { windows: true });
219 strictEqual(fromString, path);
220 const fromURL = fileURLToPath(new URL(fileURL), { windows: true });
221 strictEqual(fromURL, path);
222 }
223 
224 // fileURLToPath with posix path
225 for (const { path, fileURL } of posixTestCases) {
226 const fromString = fileURLToPath(fileURL, { windows: false });
227 strictEqual(fromString, path);
228 const fromURL = fileURLToPath(new URL(fileURL), { windows: false });
229 strictEqual(fromURL, path);
230 }
231 
232 {
233 // options is null
234 const whenNullActual = fileURLToPath(
235 new URL(posixTestCases[0].fileURL),
236 null
237 );
238 strictEqual(whenNullActual, posixTestCases[0].path);
239 
240 // default test cases
241 for (const { path, fileURL } of posixTestCases) {
242 const fromString = fileURLToPath(fileURL);
243 strictEqual(fromString, path);
244 const fromURL = fileURLToPath(new URL(fileURL));
245 strictEqual(fromURL, path);
246 }
247 }
248 },
249};
250 
251// Ref: https://github.com/nodejs/node/blob/e92446536ed4e268c9eef6ae6f911e384c98eecf/test/parallel/test-url-pathtofileurl.js
252export const testPathToFileURL = {
253 async test() {
254 {
255 const fileURL = pathToFileURL('test/').href;
256 assert(fileURL.startsWith('file:///'));
257 assert(fileURL.endsWith('/'));
258 }
259 
260 {
261 const fileURL = pathToFileURL('test\\', { windows: false }).href;
262 assert(fileURL.startsWith('file:///'));
263 match(fileURL, /%5C$/);
264 }
265 
266 {
267 const fileURL = pathToFileURL('test\\', { windows: true }).href;
268 assert(fileURL.startsWith('file:///'));
269 match(fileURL, /\/$/);
270 }
271 
272 {
273 const fileURL = pathToFileURL('test/%').href;
274 assert(fileURL.includes('%25'));
275 }
276 
277 {
278 // UNC path: \\server\share\resource
279 // Missing server:
280 throws(() => pathToFileURL('\\\\\\no-server', { windows: true }), {
281 code: 'ERR_INVALID_ARG_VALUE',
282 });
283 
284 // Missing share or resource:
285 throws(() => pathToFileURL('\\\\host', { windows: true }), {
286 code: 'ERR_INVALID_ARG_VALUE',
287 });
288 
289 // Regression test for direct String.prototype.startsWith call
290 throws(
291 () =>
292 pathToFileURL(
293 ['\\\\', { [Symbol.toPrimitive]: () => 'blep\\blop' }],
294 { windows: true }
295 ),
296 {
297 code: 'ERR_INVALID_ARG_TYPE',
298 }
299 );
300 throws(() => pathToFileURL(['\\\\', 'blep\\blop'], { windows: true }), {
301 code: 'ERR_INVALID_ARG_TYPE',
302 });
303 throws(
304 () =>
305 pathToFileURL(
306 {
307 [Symbol.toPrimitive]: () => '\\\\blep\\blop',
308 },
309 { windows: true }
310 ),
311 {
312 code: 'ERR_INVALID_ARG_TYPE',
313 }
314 );
315 
316 // UNC paths on Posix are considered a single path that has backslashes:
317 const fileURL = pathToFileURL('\\\\nas\\share\\path.txt').href;
318 match(fileURL, /file:\/\/.+%5C%5Cnas%5Cshare%5Cpath\.txt$/);
319 }
320 
321 const windowsTestCases = [
322 // Lowercase ascii alpha
323 { path: 'C:\\foo', expected: 'file:///C:/foo' },
324 // Uppercase ascii alpha
325 { path: 'C:\\FOO', expected: 'file:///C:/FOO' },
326 // dir
327 { path: 'C:\\dir\\foo', expected: 'file:///C:/dir/foo' },
328 // trailing separator
329 { path: 'C:\\dir\\', expected: 'file:///C:/dir/' },
330 // dot
331 { path: 'C:\\foo.mjs', expected: 'file:///C:/foo.mjs' },
332 // space
333 { path: 'C:\\foo bar', expected: 'file:///C:/foo%20bar' },
334 // question mark
335 { path: 'C:\\foo?bar', expected: 'file:///C:/foo%3Fbar' },
336 // number sign
337 { path: 'C:\\foo#bar', expected: 'file:///C:/foo%23bar' },
338 // ampersand
339 { path: 'C:\\foo&bar', expected: 'file:///C:/foo&bar' },
340 // equals
341 { path: 'C:\\foo=bar', expected: 'file:///C:/foo=bar' },
342 // colon
343 { path: 'C:\\foo:bar', expected: 'file:///C:/foo:bar' },
344 // semicolon
345 { path: 'C:\\foo;bar', expected: 'file:///C:/foo;bar' },
346 // percent
347 { path: 'C:\\foo%bar', expected: 'file:///C:/foo%25bar' },
348 // backslash
349 { path: 'C:\\foo\\bar', expected: 'file:///C:/foo/bar' },
350 // backspace
351 { path: 'C:\\foo\bbar', expected: 'file:///C:/foo%08bar' },
352 // tab
353 { path: 'C:\\foo\tbar', expected: 'file:///C:/foo%09bar' },
354 // newline
355 { path: 'C:\\foo\nbar', expected: 'file:///C:/foo%0Abar' },
356 // carriage return
357 { path: 'C:\\foo\rbar', expected: 'file:///C:/foo%0Dbar' },
358 // latin1
359 { path: 'C:\\fóóbàr', expected: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' },
360 // Euro sign (BMP code point)
361 { path: 'C:\\€', expected: 'file:///C:/%E2%82%AC' },
362 // Rocket emoji (non-BMP code point)
363 { path: 'C:\\🚀', expected: 'file:///C:/%F0%9F%9A%80' },
364 // caret
365 { path: 'C:\\foo^bar', expected: 'file:///C:/foo%5Ebar' },
366 // left bracket
367 { path: 'C:\\foo[bar', expected: 'file:///C:/foo%5Bbar' },
368 // right bracket
369 { path: 'C:\\foo]bar', expected: 'file:///C:/foo%5Dbar' },
370 // Local extended path
371 {
372 path: '\\\\?\\C:\\path\\to\\file.txt',
373 expected: 'file:///C:/path/to/file.txt',
374 },
375 // UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows)
376 {
377 path: '\\\\nas\\My Docs\\File.doc',
378 expected: 'file://nas/My%20Docs/File.doc',
379 },
380 // Extended UNC path
381 {
382 path: '\\\\?\\UNC\\server\\share\\folder\\file.txt',
383 expected: 'file://server/share/folder/file.txt',
384 },
385 ];
386 const posixTestCases = [
387 // Lowercase ascii alpha
388 { path: '/foo', expected: 'file:///foo' },
389 // Uppercase ascii alpha
390 { path: '/FOO', expected: 'file:///FOO' },
391 // dir
392 { path: '/dir/foo', expected: 'file:///dir/foo' },
393 // trailing separator
394 { path: '/dir/', expected: 'file:///dir/' },
395 // dot
396 { path: '/foo.mjs', expected: 'file:///foo.mjs' },
397 // space
398 { path: '/foo bar', expected: 'file:///foo%20bar' },
399 // question mark
400 { path: '/foo?bar', expected: 'file:///foo%3Fbar' },
401 // number sign
402 { path: '/foo#bar', expected: 'file:///foo%23bar' },
403 // ampersand
404 { path: '/foo&bar', expected: 'file:///foo&bar' },
405 // equals
406 { path: '/foo=bar', expected: 'file:///foo=bar' },
407 // colon
408 { path: '/foo:bar', expected: 'file:///foo:bar' },
409 // semicolon
410 { path: '/foo;bar', expected: 'file:///foo;bar' },
411 // percent
412 { path: '/foo%bar', expected: 'file:///foo%25bar' },
413 // backslash
414 { path: '/foo\\bar', expected: 'file:///foo%5Cbar' },
415 // backspace
416 { path: '/foo\bbar', expected: 'file:///foo%08bar' },
417 // tab
418 { path: '/foo\tbar', expected: 'file:///foo%09bar' },
419 // newline
420 { path: '/foo\nbar', expected: 'file:///foo%0Abar' },
421 // carriage return
422 { path: '/foo\rbar', expected: 'file:///foo%0Dbar' },
423 // latin1
424 { path: '/fóóbàr', expected: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
425 // Euro sign (BMP code point)
426 { path: '/€', expected: 'file:///%E2%82%AC' },
427 // Rocket emoji (non-BMP code point)
428 { path: '/🚀', expected: 'file:///%F0%9F%9A%80' },
429 // "unsafe" chars
430 {
431 path: '/foo\r\n\t<>"#%{}|^[\\~]`?bar',
432 expected:
433 'file:///foo%0D%0A%09%3C%3E%22%23%25%7B%7D%7C%5E%5B%5C%7E%5D%60%3Fbar',
434 },
435 ];
436 
437 for (const { path, expected } of windowsTestCases) {
438 const actual = pathToFileURL(path, { windows: true }).href;
439 strictEqual(actual, expected);
440 }
441 
442 for (const { path, expected } of posixTestCases) {
443 const actual = pathToFileURL(path, { windows: false }).href;
444 strictEqual(actual, expected);
445 }
446 
447 for (const { path, expected } of windowsTestCases) {
448 const actual = pathToFileURL(path, { windows: true }).href;
449 strictEqual(actual, expected);
450 }
451 
452 for (const { path, expected } of posixTestCases) {
453 const actual = pathToFileURL(path, { windows: false }).href;
454 strictEqual(actual, expected);
455 }
456 
457 // Test for non-string parameter
458 {
459 for (const badPath of [
460 undefined,
461 null,
462 true,
463 42,
464 42n,
465 Symbol('42'),
466 NaN,
467 {},
468 [],
469 () => {},
470 Promise.resolve('foo'),
471 new Date(),
472 new String('notPrimitive'),
473 {
474 toString() {
475 return 'amObject';
476 },
477 },
478 { [Symbol.toPrimitive]: (hint) => 'amObject' },
479 ]) {
480 throws(() => pathToFileURL(badPath), {
481 code: 'ERR_INVALID_ARG_TYPE',
482 });
483 }
484 }
485 },
486};
487 
488// Regression test for: https://github.com/cloudflare/workerd/issues/5144
489export const testUrlPattern = {
490 async test(ctrl, env) {
491 const url = 'https://example.com/??';
492 const pattern = new URLPattern({ hostname: '*' });
493 const obj = new URL(url);
494 deepStrictEqual(pattern.exec(obj), {
495 inputs: [
496 {
497 protocol: 'https:',
498 username: '',
499 password: '',
500 hostname: 'example.com',
501 port: '',
502 pathname: '/',
503 search: '??',
504 hash: '',
505 },
506 ],
507 protocol: { input: 'https', groups: { 0: 'https' } },
508 username: { input: '', groups: { 0: '' } },
509 password: { input: '', groups: { 0: '' } },
510 hostname: { input: 'example.com', groups: { 0: 'example.com' } },
511 port: { input: '', groups: { 0: '' } },
512 pathname: { input: '/', groups: { 0: '/' } },
513 search: { input: '', groups: { 0: '' } },
514 hash: { input: '', groups: { 0: '' } },
515 });
516 },
517};