Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 12 additions & 1 deletion lib/mock/mock-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ function getResponseData (data) {
return data
} else if (data instanceof ArrayBuffer) {
return data
} else if (ArrayBuffer.isView(data)) {
// A DataView, or any non-Uint8Array typed array, is a byte container
// rather than a plain object. Buffer.from() cannot read one directly, so
// expose the bytes it covers instead of letting it reach JSON.stringify.
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
} else if (typeof data === 'object') {
return JSON.stringify(data)
} else if (data) {
Expand Down Expand Up @@ -225,9 +230,15 @@ function deleteMockDispatch (mockDispatches, key) {
}

/**
* @param {string} path Path to remove trailing slash from
* @param {string|RegExp|Function} path Path, or path matcher, to remove trailing slash from
*/
function removeTrailingSlash (path) {
// Registered path matchers may be a RegExp or a function, which have no
// trailing slash to strip; hand those back for matchValue to apply.
if (typeof path !== 'string') {
return path
}

while (path.endsWith('/')) {
path = path.slice(0, -1)
}
Expand Down
64 changes: 64 additions & 0 deletions test/mock-interceptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,61 @@ describe('https://github.com/nodejs/undici/issues/3649', () => {
})
})

describe('MockInterceptor - ignoreTrailingSlash with non-string path matchers', () => {
[
['RegExp', /^\/api\/some-path$/],
['Function', (path) => path === '/api/some-path']
].forEach(([matcherType, path]) => {
['/api/some-path', '/api/some-path/', '/api/some-path///'].forEach((fetchedPath) => {
test(`a ${matcherType} path matcher matches '${fetchedPath}' as a MockAgent option`, async (t) => {
t.plan(1)

const mockAgent = new MockAgent({ ignoreTrailingSlash: true })
mockAgent.disableNetConnect()
t.after(() => mockAgent.close())
mockAgent
.get('https://localhost')
.intercept({ path }).reply(200, { ok: true })

const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })

t.assert.deepStrictEqual(await res.json(), { ok: true })
})

test(`a ${matcherType} path matcher matches '${fetchedPath}' as an intercept option`, async (t) => {
t.plan(1)

const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
t.after(() => mockAgent.close())
mockAgent
.get('https://localhost')
.intercept({ path, ignoreTrailingSlash: true }).reply(200, { ok: true })

const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })

t.assert.deepStrictEqual(await res.json(), { ok: true })
})
})

test(`a ${matcherType} path matcher still rejects a non-matching path`, async (t) => {
t.plan(1)

const mockAgent = new MockAgent({ ignoreTrailingSlash: true })
mockAgent.disableNetConnect()
t.after(() => mockAgent.close())
mockAgent
.get('https://localhost')
.intercept({ path }).reply(200, { ok: true })

await t.assert.rejects(
fetch(new URL('/api/other-path', 'https://localhost'), { dispatcher: mockAgent }),
(err) => err.cause instanceof MockNotMatchedError
)
})
})
})

describe('MockInterceptor - different payloads', () => {
[
// Buffer
Expand All @@ -537,6 +592,15 @@ describe('MockInterceptor - different payloads', () => {
['bytes', 'string', 'Uint8Array', '{"test":true}', new TextEncoder().encode('{"test":true}')],
['text', 'string', 'string', '{"test":true}', '{"test":true}'],

// DataView
['arrayBuffer', 'DataView', 'ArrayBuffer', new DataView(new TextEncoder().encode('{"test":true}').buffer), new TextEncoder().encode('{"test":true}').buffer],
['json', 'DataView', 'Object', new DataView(new TextEncoder().encode('{"test":true}').buffer), { test: true }],
['bytes', 'DataView', 'Uint8Array', new DataView(new TextEncoder().encode('{"test":true}').buffer), new TextEncoder().encode('{"test":true}')],
['text', 'DataView', 'string', new DataView(new TextEncoder().encode('{"test":true}').buffer), '{"test":true}'],

// DataView covering only part of its backing ArrayBuffer
['text', 'DataView with an offset', 'string', new DataView(new TextEncoder().encode('xx{"test":true}yy').buffer, 2, 13), '{"test":true}'],

// object
['arrayBuffer', 'Object', 'ArrayBuffer', { test: true }, new TextEncoder().encode('{"test":true}').buffer],
['json', 'Object', 'Object', { test: true }, { test: true }],
Expand Down
65 changes: 65 additions & 0 deletions test/mock-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,44 @@ describe('getMockDispatch', () => {
}), new MockNotMatchedError('Mock dispatch not matched for body \'wrong\' on path \'path\''))
})

test('it should match a RegExp path with ignoreTrailingSlash', (t) => {
t.plan(2)
const dispatch = {
path: /^\/path$/,
method: 'method',
ignoreTrailingSlash: true,
consumed: false
}

t.assert.deepStrictEqual(getMockDispatch([dispatch], {
path: '/path',
method: 'method'
}), dispatch)
t.assert.deepStrictEqual(getMockDispatch([dispatch], {
path: '/path/',
method: 'method'
}), dispatch)
})

test('it should match a function path with ignoreTrailingSlash', (t) => {
t.plan(2)
const dispatch = {
path: (path) => path === '/path',
method: 'method',
ignoreTrailingSlash: true,
consumed: false
}

t.assert.deepStrictEqual(getMockDispatch([dispatch], {
path: '/path',
method: 'method'
}), dispatch)
t.assert.deepStrictEqual(getMockDispatch([dispatch], {
path: '/path/',
method: 'method'
}), dispatch)
})

test('it should throw if no dispatch matches headers', (t) => {
t.plan(1)
const dispatches = [
Expand Down Expand Up @@ -176,11 +214,38 @@ describe('getResponseData', () => {
t.assert.ok(responseData instanceof ArrayBuffer)
})

test('it should return the bytes of a DataView', (t) => {
t.plan(2)
const responseData = getResponseData(new DataView(new TextEncoder().encode('{"test":true}').buffer))
t.assert.ok(responseData instanceof Uint8Array)
t.assert.strictEqual(Buffer.from(responseData).toString('utf8'), '{"test":true}')
})

test('it should return only the bytes a DataView covers', (t) => {
t.plan(1)
const buffer = new TextEncoder().encode('xx{"test":true}yy').buffer
const responseData = getResponseData(new DataView(buffer, 2, 13))
t.assert.strictEqual(Buffer.from(responseData).toString('utf8'), '{"test":true}')
})

test('it should return the bytes of a typed array that is not a Uint8Array', (t) => {
t.plan(2)
const responseData = getResponseData(new Uint8ClampedArray([1, 2, 3]))
t.assert.ok(responseData instanceof Uint8Array)
t.assert.deepStrictEqual([...responseData], [1, 2, 3])
})

test('it should handle undefined', (t) => {
t.plan(1)
const responseData = getResponseData(undefined)
t.assert.strictEqual(responseData, '')
})

test('it should handle null', (t) => {
t.plan(1)
const responseData = getResponseData(null)
t.assert.strictEqual(responseData, 'null')
})
})

test('getStatusText', (t) => {
Expand Down
Loading