Skip to content

Commit

Permalink
feat(MockInterceptor): add support for callbacks on reply (nodejs#1200
Browse files Browse the repository at this point in the history
)
  • Loading branch information
suneettipirneni authored and metcoder95 committed Dec 26, 2022
1 parent 0afa06d commit c6e6c28
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 4 deletions.
37 changes: 35 additions & 2 deletions docs/api/MockPool.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Returns: `MockInterceptor` corresponding to the input options.

We can define the behaviour of an intercepted request with the following options.

* **reply** `(statusCode: number, replyData: string | Buffer | object, responseOptions?: MockResponseOptions) => MockScope` - define a reply for a matching request. Default for `responseOptions` is `{}`.
* **reply** `(statusCode: number, replyData: string | Buffer | object | MockInterceptor.MockResponseDataHandler, responseOptions?: MockResponseOptions) => MockScope` - define a reply for a matching request. You can define this as a callback to read incoming request data. Default for `responseOptions` is `{}`.
* **replyWithError** `(error: Error) => MockScope` - define an error for a matching request to throw.
* **defaultReplyHeaders** `(headers: Record<string, string>) => MockInterceptor` - define default headers to be included in subsequent replies. These are in addition to headers on a specific reply.
* **defaultReplyTrailers** `(trailers: Record<string, string>) => MockInterceptor` - define default trailers to be included in subsequent replies. These are in addition to trailers on a specific reply.
Expand Down Expand Up @@ -113,6 +113,39 @@ for await (const data of body) {
}
```

#### Example - Mocked request using reply callbacks

```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

const mockPool = mockAgent.get('http://localhost:3000')

mockPool.intercept({
path: '/echo',
method: 'GET',
headers: {
'User-Agent': 'undici',
Host: 'example.com'
}
}).reply(200, ({ headers }) => ({ message: headers.get('message') }))

const { statusCode, body, headers } = await request('http://localhost:3000', {
headers: {
message: 'hello world!'
}
})

console.log('response received', statusCode) // response received 200
console.log('headers', headers) // { 'content-type': 'application/json' }

for await (const data of body) {
console.log('data', data.toString('utf8')) // { "message":"hello world!" }
}
```

#### Example - Basic Mocked requests with multiple intercepts

```js
Expand All @@ -130,7 +163,7 @@ mockPool.intercept({

mockPool.intercept({
path: '/hello',
method: 'GET'
method: 'GET',
}).reply(200, 'hello')

const result1 = await request('http://localhost:3000/foo')
Expand Down
2 changes: 1 addition & 1 deletion lib/mock/mock-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function mockDispatch (opts, handler) {
}

function handleReply (mockDispatches) {
const responseData = getResponseData(data)
const responseData = getResponseData(typeof data === 'function' ? data(opts) : data);
const responseHeaders = generateKeyValues(headers)
const responseTrailers = generateKeyValues(trailers)

Expand Down
25 changes: 25 additions & 0 deletions test/mock-interceptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,31 @@ test('MockInterceptor - reply', t => {
})
})

test('MockInterceptor - reply callback', t => {
t.plan(2)

t.test('should return MockScope', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.reply(200, () => 'hello')
t.type(result, MockScope)
})

t.test('should error if passed options invalid', t => {
t.plan(2)

const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.throws(() => mockInterceptor.reply(), new InvalidArgumentError('statusCode must be defined'))
t.throws(() => mockInterceptor.reply(200, () => {}, 'hello'), new InvalidArgumentError('responseOptions must be an object'))
})
})

test('MockInterceptor - replyWithError', t => {
t.plan(2)

Expand Down
5 changes: 5 additions & 0 deletions test/types/mock-interceptor.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ import { MockInterceptor, MockScope } from '../../types/mock-interceptor'
expectAssignable<MockScope>(mockInterceptor.reply(200, ''))
expectAssignable<MockScope>(mockInterceptor.reply(200, Buffer))
expectAssignable<MockScope>(mockInterceptor.reply(200, {}))
expectAssignable<MockScope>(mockInterceptor.reply(200, () => ({})))
expectAssignable<MockScope>(mockInterceptor.reply(200, {}, {}))
expectAssignable<MockScope>(mockInterceptor.reply(200, () => ({}), {}))
expectAssignable<MockScope>(mockInterceptor.reply(200, {}, { headers: { foo: 'bar' }}))
expectAssignable<MockScope>(mockInterceptor.reply(200, () => ({}), { headers: { foo: 'bar' }}))
expectAssignable<MockScope>(mockInterceptor.reply(200, {}, { trailers: { foo: 'bar' }}))
expectAssignable<MockScope>(mockInterceptor.reply(200, () => ({}), { trailers: { foo: 'bar' }}))
expectAssignable<MockScope<{ foo: string }>>(mockInterceptor.reply<{ foo: string }>(200, { foo: 'bar' }))
expectAssignable<MockScope<{ foo: string }>>(mockInterceptor.reply<{ foo: string }>(200, () => ({ foo: 'bar' })))

// replyWithError
class CustomError extends Error {
Expand Down
19 changes: 18 additions & 1 deletion types/mock-interceptor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ declare class MockScope<TData extends object = object> {
declare class MockInterceptor {
constructor(options: MockInterceptor.Options, mockDispatches: MockInterceptor.MockDispatch[]);
/** Mock an undici request with the defined reply. */
reply<TData extends object = object>(statusCode: number, data: TData | Buffer| string , responseOptions?: MockInterceptor.MockResponseOptions): MockScope<TData>;
reply<TData extends object = object>(
statusCode: number,
data: TData | Buffer | string | MockInterceptor.MockResponseDataHandler<TData>,
responseOptions?: MockInterceptor.MockResponseOptions
): MockScope<TData>;
/** Mock an undici request by throwing the defined reply error. */
replyWithError<TError extends Error = Error>(error: TError): MockScope;
/** Set default reply headers on the interceptor for subsequent mocked replies. */
Expand Down Expand Up @@ -59,6 +63,19 @@ declare namespace MockInterceptor {
headers?: IncomingHttpHeaders;
trailers?: Record<string, string>;
}

export interface MockResponseCallbackOptions {
path: string;
origin: string;
method: string;
body?: string;
headers: Headers;
maxRedirections: number;
}

export type MockResponseDataHandler<TData extends object = object> = (
opts: MockResponseCallbackOptions
) => TData | Buffer | string;
}

interface Interceptable {
Expand Down

0 comments on commit c6e6c28

Please sign in to comment.