Skip to content

Commit

Permalink
feat: add support for request params in mock client (nodejs#1466)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad authored and crysmags committed Feb 27, 2024
1 parent 07dfbae commit ac48e9d
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 7 deletions.
1 change: 1 addition & 0 deletions docs/api/MockPool.md
Expand Up @@ -57,6 +57,7 @@ Returns: `MockInterceptor` corresponding to the input options.
* **method** `string | RegExp | (method: string) => boolean` - a matcher for the HTTP request method.
* **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body.
* **headers** `Record<string, string | RegExp | (body: string) => boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way.
* **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params.

### Return: `MockInterceptor`

Expand Down
11 changes: 8 additions & 3 deletions lib/mock/mock-interceptor.js
Expand Up @@ -10,6 +10,7 @@ const {
kMockDispatch
} = require('./mock-symbols')
const { InvalidArgumentError } = require('../core/errors')
const { buildURL } = require('../core/util')

/**
* Defines the scope API for an interceptor reply
Expand Down Expand Up @@ -70,9 +71,13 @@ class MockInterceptor {
// As per RFC 3986, clients are not supposed to send URI
// fragments to servers when they retrieve a document,
if (typeof opts.path === 'string') {
// Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811
const parsedURL = new URL(opts.path, 'data://')
opts.path = parsedURL.pathname + parsedURL.search
if (opts.query) {
opts.path = buildURL(opts.path, opts.query)
} else {
// Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811
const parsedURL = new URL(opts.path, 'data://')
opts.path = parsedURL.pathname + parsedURL.search
}
}
if (typeof opts.method === 'string') {
opts.method = opts.method.toUpperCase()
Expand Down
12 changes: 8 additions & 4 deletions lib/mock/mock-utils.js
Expand Up @@ -8,6 +8,7 @@ const {
kOrigin,
kGetNetConnect
} = require('./mock-symbols')
const { buildURL } = require('../core/util')

function matchValue (match, value) {
if (typeof match === 'string') {
Expand Down Expand Up @@ -98,10 +99,12 @@ function getResponseData (data) {
}

function getMockDispatch (mockDispatches, key) {
const resolvedPath = key.query ? buildURL(key.path, key.query) : key.path

// Match path
let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(path, key.path))
let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(path, resolvedPath))
if (matchedMockDispatches.length === 0) {
throw new MockNotMatchedError(`Mock dispatch not matched for path '${key.path}'`)
throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`)
}

// Match method
Expand Down Expand Up @@ -146,12 +149,13 @@ function deleteMockDispatch (mockDispatches, key) {
}

function buildKey (opts) {
const { path, method, body, headers } = opts
const { path, method, body, headers, query } = opts
return {
path,
method,
body,
headers
headers,
query
}
}

Expand Down
81 changes: 81 additions & 0 deletions test/mock-client.js
Expand Up @@ -235,6 +235,87 @@ test('MockClient - should be able to set as globalDispatcher', async (t) => {
t.same(response, 'hello')
})

test('MockClient - should support query params', async (t) => {
t.plan(3)

const server = createServer((req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.fail('should not be called')
t.end()
})
t.teardown(server.close.bind(server))

await promisify(server.listen.bind(server))(0)

const baseUrl = `http://localhost:${server.address().port}`

const mockAgent = new MockAgent({ connections: 1 })
t.teardown(mockAgent.close.bind(mockAgent))

const mockClient = mockAgent.get(baseUrl)
t.type(mockClient, MockClient)
setGlobalDispatcher(mockClient)

const query = {
pageNum: 1
}
mockClient.intercept({
path: '/foo',
query,
method: 'GET'
}).reply(200, 'hello')

const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
query
})
t.equal(statusCode, 200)

const response = await getResponse(body)
t.same(response, 'hello')
})

test('MockClient - should intercept query params with hardcoded path', async (t) => {
t.plan(3)

const server = createServer((req, res) => {
res.setHeader('content-type', 'text/plain')
res.end('should not be called')
t.fail('should not be called')
t.end()
})
t.teardown(server.close.bind(server))

await promisify(server.listen.bind(server))(0)

const baseUrl = `http://localhost:${server.address().port}`

const mockAgent = new MockAgent({ connections: 1 })
t.teardown(mockAgent.close.bind(mockAgent))

const mockClient = mockAgent.get(baseUrl)
t.type(mockClient, MockClient)
setGlobalDispatcher(mockClient)

const query = {
pageNum: 1
}
mockClient.intercept({
path: '/foo?pageNum=1',
method: 'GET'
}).reply(200, 'hello')

const { statusCode, body } = await request(`${baseUrl}/foo`, {
method: 'GET',
query
})
t.equal(statusCode, 200)

const response = await getResponse(body)
t.same(response, 'hello')
})

test('MockClient - should be able to use as a local dispatcher', async (t) => {
t.plan(3)

Expand Down
1 change: 1 addition & 0 deletions test/mock-interceptor-unused-assertions.js
Expand Up @@ -204,6 +204,7 @@ test('returns unused interceptors', t => {
path: '/',
method: 'GET',
body: undefined,
query: undefined,
headers: undefined,
data: {
error: null,
Expand Down
1 change: 1 addition & 0 deletions test/types/mock-client.test-d.ts
Expand Up @@ -8,6 +8,7 @@ import { MockInterceptor } from '../../types/mock-interceptor'
// intercept
expectAssignable<MockInterceptor>(mockClient.intercept({ path: '', method: 'GET' }))
expectAssignable<MockInterceptor>(mockClient.intercept({ path: '', method: 'GET', body: '', headers: { 'User-Agent': '' } }))
expectAssignable<MockInterceptor>(mockClient.intercept({ path: '', method: 'GET', query: { id: 1 } }))
expectAssignable<MockInterceptor>(mockClient.intercept({ path: new RegExp(''), method: new RegExp(''), body: new RegExp(''), headers: { 'User-Agent': new RegExp('') } }))
expectAssignable<MockInterceptor>(mockClient.intercept({
path: (path) => {
Expand Down
2 changes: 2 additions & 0 deletions types/mock-interceptor.d.ts
Expand Up @@ -50,6 +50,8 @@ declare namespace MockInterceptor {
body?: string | RegExp | ((body: string) => boolean);
/** Headers to intercept on. */
headers?: Record<string, string | RegExp | ((body: string) => boolean)> | ((headers: Record<string, string>) => boolean);
/** Query params to intercept on */
query?: Record<string, any>;
}
export interface MockDispatch<TData extends object = object, TError extends Error = Error> extends Options {
times: number | null;
Expand Down

0 comments on commit ac48e9d

Please sign in to comment.