From 33753828a5fb23acded3c90d5bb5cd4744203e3e Mon Sep 17 00:00:00 2001 From: Ryan Wheale Date: Tue, 10 Jan 2023 13:33:55 -0700 Subject: [PATCH] feat: support for WHATWG URLs (#2437) Co-authored-by: Ryan Wheale Co-authored-by: Matt R. Wilson --- README.md | 8 +++++++- lib/scope.js | 47 +++++++++++++++++++++++++++++++++++++++++---- tests/test_scope.js | 34 ++++++++++++++++++++++++++------ 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6d7e7f3b3..19aba35c6 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ If you don’t want interceptors to be removed as they are used, you can use the ### Specifying hostname -The request hostname can be a string or a RegExp. +The request hostname can be a string, URL, or a RegExp. ```js const scope = nock('http://www.example.com') @@ -167,6 +167,12 @@ const scope = nock('http://www.example.com') .reply(200, 'domain matched') ``` +```js +const scope = nock(new URL('http://www.example.com')) + .get('/resource') + .reply(200, 'domain matched') +``` + ```js const scope = nock(/example\.com/) .get('/resource') diff --git a/lib/scope.js b/lib/scope.js index 505c2fd52..8e32c618e 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -11,6 +11,7 @@ const debug = require('debug')('nock.scope') const { EventEmitter } = require('events') const Interceptor = require('./interceptor') +const { URL, Url: LegacyUrl } = url let fs try { @@ -20,7 +21,46 @@ try { } /** - * @param {string|RegExp|url.url} basePath + * Normalizes the passed url for consistent internal processing + * @param {string|LegacyUrl|URL} u + */ +function normalizeUrl(u) { + if (!(u instanceof URL)) { + if (u instanceof LegacyUrl) { + return normalizeUrl(new URL(url.format(u))) + } + // If the url is invalid, let the URL library report it + return normalizeUrl(new URL(u)) + } + + if (!/https?:/.test(u.protocol)) { + throw new TypeError( + `Protocol '${u.protocol}' not recognized. This commonly occurs when a hostname and port are included without a protocol, producing a URL that is valid but confusing, and probably not what you want.` + ) + } + + return { + href: u.href, + origin: u.origin, + protocol: u.protocol, + username: u.username, + password: u.password, + host: u.host, + hostname: + // strip brackets from IPv6 + typeof u.hostname === 'string' && u.hostname.startsWith('[') + ? u.hostname.slice(1, -1) + : u.hostname, + port: u.port || (u.protocol === 'http:' ? 80 : 443), + pathname: u.pathname, + search: u.search, + searchParams: u.searchParams, + hash: u.hash, + } +} + +/** + * @param {string|RegExp|LegacyUrl|URL} basePath * @param {Object} options * @param {boolean} options.allowUnmocked * @param {string[]} options.badheaders @@ -52,9 +92,8 @@ class Scope extends EventEmitter { let logNamespace = String(basePath) if (!(basePath instanceof RegExp)) { - this.urlParts = url.parse(basePath) - this.port = - this.urlParts.port || (this.urlParts.protocol === 'http:' ? 80 : 443) + this.urlParts = normalizeUrl(basePath) + this.port = this.urlParts.port this.basePathname = this.urlParts.pathname.replace(/\/$/, '') this.basePath = `${this.urlParts.protocol}//${this.urlParts.hostname}:${this.port}` logNamespace = this.urlParts.host diff --git a/tests/test_scope.js b/tests/test_scope.js index 3c80447f9..1e04becf7 100644 --- a/tests/test_scope.js +++ b/tests/test_scope.js @@ -34,8 +34,7 @@ describe('`Scope#constructor`', () => { scope.done() }) - // TODO: https://github.com/nock/nock/pull/1879 - it.skip('accepts a WHATWG URL instance', async () => { + it('accepts a WHATWG URL instance', async () => { const scope = nock(new url.URL('http://example.test')).get('/').reply() const { statusCode } = await got('http://example.test') @@ -43,10 +42,33 @@ describe('`Scope#constructor`', () => { scope.done() }) - it('fails when provided a WHATWG URL instance', () => { - // This test just proves the lack of current support. When this feature is added, - // this test should be removed and the test above un-skipped. - expect(() => nock(new url.URL('http://example.test'))).to.throw() + it('throws on invalid or omitted protocol', async () => { + expect(() => nock('ws://example.test')).to.throw() + expect(() => nock('localhost/foo')).to.throw() + expect(() => nock('foo.com:1234')).to.throw() + }) + + it('throws on invalid URL format', async () => { + expect(() => nock(['This is not a url'])).to.throw() + // The following contains all valid properties of WHATWG URL, but is not an + // instance of URL. Maybe we should support object literals some day? A + // simple duck-type validator would suffice. + expect(() => + nock({ + href: 'http://google.com/foo', + origin: 'http://google.com', + protocol: 'http:', + username: '', + password: '', + host: 'google.com', + hostname: 'google.com', + port: 80, + pathname: '/foo', + search: '', + searchParams: {}, + hash: '', + }) + ).to.throw() }) })