Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use @mswjs/interceptors for mocking #2517

Merged
merged 55 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
576b296
wip
mikicho Sep 11, 2023
94b9b90
wip
mikicho Sep 17, 2023
ef59cf6
wip
mikicho Sep 17, 2023
d7d17bd
fixes
mikicho Sep 18, 2023
855914e
fix
mikicho Sep 18, 2023
51dd0c6
wip
mikicho Sep 21, 2023
21cf9c2
more fixes
mikicho Sep 21, 2023
091d876
more fixes
mikicho Sep 23, 2023
79f5162
more fixes
mikicho Sep 24, 2023
2028d4e
more fixes
mikicho Sep 26, 2023
bba8bdb
small fix
mikicho Sep 26, 2023
5e521a1
more fixes
mikicho Sep 27, 2023
e1c3eb4
more fixes
mikicho Sep 29, 2023
e89acec
Merge remote-tracking branch 'upstream/main' into Michael/fetch-support
mikicho Oct 26, 2023
cbd31cb
wip
mikicho Dec 15, 2023
bc4327a
Merge remote-tracking branch 'upstream/main' into Michael/fetch-support
mikicho Dec 15, 2023
7df18f2
wip
mikicho Dec 16, 2023
53e3d2a
fix
mikicho Dec 17, 2023
ca9e616
fix
mikicho Dec 18, 2023
2c0e4f1
more-tests
mikicho Dec 19, 2023
bcbdd04
wip
mikicho Feb 3, 2024
f4d1b15
ci(test): use a single `test` job that we can require, independent of…
gr2m Feb 3, 2024
4ad60ba
add experimental fetch support notice (#2583)
mikicho Feb 6, 2024
8deca36
docs: add mikicho as a contributor for maintenance, code, and doc (#2…
allcontributors[bot] Feb 6, 2024
8bab28d
ci: add node 20 in ci (#2585)
VladimirChuprazov Feb 7, 2024
7e957b3
fix: remove duplicates from `activeMocks()` and `pendingMocks()` (#2356)
mbargiel Feb 17, 2024
4162fa8
fix: support literal query string (#2590)
mikicho Feb 17, 2024
1005698
chore(deps-dev): bump eslint-plugin-import from 2.29.0 to 2.29.1 (#2577)
dependabot[bot] Feb 26, 2024
08b2b09
chore(deps-dev): bump prettier from 3.1.0 to 3.2.4 (#2578)
dependabot[bot] Feb 26, 2024
81c20dd
chore(deps-dev): bump chai from 4.3.10 to 4.4.1 (#2576)
dependabot[bot] Feb 26, 2024
ba9fc42
fix: call `fs.createReadStream` lazily (#2357)
mbargiel Feb 26, 2024
cbb135d
chore(deps-dev): bump semantic-release from 22.0.6 to 23.0.2 (#2598)
dependabot[bot] Mar 2, 2024
3659e82
chore(deps-dev): bump prettier from 3.2.4 to 3.2.5 (#2596)
dependabot[bot] Mar 2, 2024
e44812b
chore(deps-dev): bump eslint from 8.56.0 to 8.57.0 (#2597)
dependabot[bot] Mar 2, 2024
ab8037d
wip
mikicho Mar 13, 2024
db0f76a
chore(deps-dev): bump eslint-plugin-mocha from 10.2.0 to 10.4.1
dependabot[bot] Apr 1, 2024
7a4badb
chore(deps-dev): bump semantic-release from 23.0.2 to 23.0.6
dependabot[bot] Apr 1, 2024
d013ed2
chore(deps-dev): bump typescript from 5.3.3 to 5.4.3
dependabot[bot] Apr 1, 2024
e7a6309
fix
mikicho Apr 13, 2024
dac5a26
fix
mikicho May 30, 2024
36f6778
fix
mikicho Jun 1, 2024
565e99a
chore(deps-dev): bump braces from 3.0.2 to 3.0.3 (#2752)
dependabot[bot] Jun 20, 2024
2b7836d
ci: exclude nodejs 10, 12 and 14 tests running on macos (#2753)
Uzlopak Jun 20, 2024
73e507d
fix
mikicho Jul 4, 2024
7c89896
fix
mikicho Jul 6, 2024
dfd069c
fix
mikicho Jul 6, 2024
cf3b522
fix
mikicho Jul 6, 2024
16453f7
Merge remote-tracking branch 'origin/main' into Michael/fetch-support
mikicho Jul 6, 2024
2746b60
fix
mikicho Jul 12, 2024
67c1481
fix
mikicho Jul 12, 2024
7baaf27
clean
mikicho Jul 12, 2024
6acc6df
fix
mikicho Jul 12, 2024
a083fdf
fix
mikicho Jul 13, 2024
7f6422d
Merge remote-tracking branch 'origin/beta' into Michael/fetch-support
mikicho Jul 13, 2024
e941ae7
fix
mikicho Jul 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 1 addition & 43 deletions lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,51 +67,9 @@ let requestOverrides = {}
* - callback - the callback of the issued request
*/
function overrideRequests(newRequest) {
debug('overriding requests')
;['http', 'https'].forEach(function (proto) {
debug('- overriding request for', proto)

const moduleName = proto // 1 to 1 match of protocol and module is fortunate :)
const module = {
http: require('http'),
https: require('https'),
}[moduleName]
const overriddenRequest = module.request
const overriddenGet = module.get

if (requestOverrides[moduleName]) {
throw new Error(
`Module's request already overridden for ${moduleName} protocol.`
)
}

// Store the properties of the overridden request so that it can be restored later on.
requestOverrides[moduleName] = {
module,
request: overriddenRequest,
get: overriddenGet,
}
// https://nodejs.org/api/http.html#http_http_request_url_options_callback
module.request = function (input, options, callback) {
return newRequest(proto, overriddenRequest.bind(module), [
input,
options,
callback,
])
}
// https://nodejs.org/api/http.html#http_http_get_options_callback
module.get = function (input, options, callback) {
const req = newRequest(proto, overriddenGet.bind(module), [
input,
options,
callback,
])
req.end()
return req
}

debug('- overridden request for', proto)
})

}

/**
Expand Down
147 changes: 90 additions & 57 deletions lib/intercept.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
const http = require('http')
const debug = require('debug')('nock.intercept')
const globalEmitter = require('./global_emitter')
const { BatchInterceptor } = require('@mswjs/interceptors')
const { FetchInterceptor } = require('@mswjs/interceptors/fetch')
const { default: nodeInterceptors } = require('@mswjs/interceptors/presets/node')

/**
* @name NetConnectNotAllowedError
Expand Down Expand Up @@ -365,75 +368,105 @@
return [].concat(...interceptorScopes().map(scope => scope.activeMocks()))
}

function activate() {
if (originalClientRequest) {
throw new Error('Nock already active')
}
function getRequestOptionsFromFetchRequest(fetchRequest) {

Check failure on line 371 in lib/intercept.js

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'getRequestOptionsFromFetchRequest' is defined but never used
const requestOptions = {
method: fetchRequest.method,
headers: {},
};

overrideClientRequest()
fetchRequest.headers.forEach((value, key) => {
requestOptions.headers[key] = value;
});

// ----- Overriding http.request and https.request:
const url = new URL(fetchRequest.url);

common.overrideRequests(function (proto, overriddenRequest, args) {
// NOTE: overriddenRequest is already bound to its module.
const options = {
...requestOptions,
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
proto: url.protocol === 'https:' ? 'https' : 'http',
};

const { options, callback } = common.normalizeClientRequestArgs(...args)

if (Object.keys(options).length === 0) {
// As weird as it is, it's possible to call `http.request` without
// options, and it makes a request to localhost or somesuch. We should
// support it too, for parity. However it doesn't work today, and fixing
// it seems low priority. Giving an explicit error is nicer than
// crashing with a weird stack trace. `new ClientRequest()`, nock's
// other client-facing entry point, makes a similar check.
// https://github.com/nock/nock/pull/1386
// https://github.com/nock/nock/pull/1440
throw Error(
'Making a request with empty `options` is not supported in Nock'
)
}
return options;
}

// The option per the docs is `protocol`. Its unclear if this line is meant to override that and is misspelled or if
// the intend is to explicitly keep track of which module was called using a separate name.
// Either way, `proto` is used as the source of truth from here on out.
options.proto = proto
function convertFetchRequestToClientRequest(fetchRequest) {
const url = new URL(fetchRequest.url);
const options = {
method: fetchRequest.method,
host: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
headers: fetchRequest.headers,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Node expects OutgoingHeaders here, which is not compatible with the Headers instance. You should be fine doing this:

headers: Object.fromEntries(fetchRequest.headers.entries())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch! 🙏

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just be cautious about the compilation target here. I'm not sure what's the support range is for Nock, but .fromEntries() and .entries() methods may not exist on super old versions of ECMAScript and Node.

proto: url.protocol.slice(0, -1),
headers: Object.fromEntries(fetchRequest.headers.entries())

Check failure on line 403 in lib/intercept.js

View workflow job for this annotation

GitHub Actions / Lint JavaScript

Duplicate key 'headers'
};

const clientRequest = new http.ClientRequest(options);
// Note: You won't have access to the request body data from the Fetch Request

return clientRequest;
}

const interceptors = interceptorsFor(options)
function activate() {
if (originalClientRequest) {
throw new Error('Nock already active')
}

if (isOn() && interceptors) {
const matches = interceptors.some(interceptor =>
interceptor.matchOrigin(options)
)
const allowUnmocked = interceptors.some(
interceptor => interceptor.options.allowUnmocked
)
overrideClientRequest()
const interceptor = new BatchInterceptor({
name: 'my-interceptor',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Giving this a meaningful name may improve the logging output, afaik. Maybe name: 'nock-interceptor'?

interceptors: [...nodeInterceptors, new FetchInterceptor()],
mikicho marked this conversation as resolved.
Show resolved Hide resolved
})
interceptor.apply();
interceptor.on('request', function ({ request, requestId }) {
return new Promise((resolve) => {
const { options, callback } = common.normalizeClientRequestArgs(request.url)
options.proto = options.protocol.slice(0, -1)
const interceptors = interceptorsFor(options)
if (isOn() && interceptors) {
const matches = interceptors.some(interceptor =>
interceptor.matchOrigin(options)
)
const allowUnmocked = interceptors.some(
interceptor => interceptor.options.allowUnmocked
)

if (!matches && allowUnmocked) {
let req
if (proto === 'https') {
const { ClientRequest } = http
http.ClientRequest = originalClientRequest
req = overriddenRequest(options, callback)
http.ClientRequest = ClientRequest
} else {
req = overriddenRequest(options, callback)
if (!matches && allowUnmocked) {
// TODO: implement unmocked
// let req
// if (proto === 'https') {
// const { ClientRequest } = http
// http.ClientRequest = originalClientRequest
// req = overriddenRequest(options, callback)
// http.ClientRequest = ClientRequest
// } else {
// req = overriddenRequest(options, callback)
// }
// globalEmitter.emit('no match', req)
// return req
throw new Error('TODO')
}
globalEmitter.emit('no match', req)
return req
}

// NOTE: Since we already overrode the http.ClientRequest we are in fact constructing
// our own OverriddenClientRequest.
return new http.ClientRequest(options, callback)
} else {
globalEmitter.emit('no match', options)
if (isOff() || isEnabledForNetConnect(options)) {
return overriddenRequest(options, callback)
const req = convertFetchRequestToClientRequest(request);
req.on('response', response => {
request.respondWith(new Response('test', { status: 200 }))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: we'd want to read the response stream to the ReadableStream of the response instance and call request.respondWith() on the end event of the original response here.

As I mentioned in the issue, let me know if exporting this existing utility from Interceptors would help here. I think it would.

Copy link
Contributor Author

@mikicho mikicho Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have an idea how we can wait for the nockResponse end event? I use a promise, but maybe you had something else in mind.

As I mentioned in the issue, let me know if exporting this existing utility from Interceptors would help here. I think it would.

Thanks!! For now, I copied the file. We can update this later.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, we can rely on the internal http.ClientRequest implementation to guarantee the order of event execution.

const stream = new Readable()
const fetchResponse = new Response(stream, {
  status: response.statusCode,
  statusText: response.statusMessage,
  headers: response.headers // pseudo-code
})

response.on('data', (chunk) => stream.write(chunk))
response.on('end', () => {
  stream.finish()
  request.respndWith(fetchResponse)
})

This is a gist. If you want the actual implementation example, take a look at this function.

resolve()
})

req.end()
} else {
const error = new NetConnectNotAllowedError(options.host, options.path)
return new ErroringClientRequest(error)
globalEmitter.emit('no match', options)
if (isOff() || isEnabledForNetConnect(options)) {
// TODO: implement unmocked
return overriddenRequest(options, callback)

Check failure on line 463 in lib/intercept.js

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'overriddenRequest' is not defined
} else {
const error = new NetConnectNotAllowedError(options.host, options.path)
return new ErroringClientRequest(error)
}
}
}
})
})
}

Expand Down
Loading
Loading