Skip to content

Commit 6d9cb9d

Browse files
fix: make sure array search params using different syntaxes are
supported
1 parent 8a5de7c commit 6d9cb9d

3 files changed

Lines changed: 119 additions & 4 deletions

File tree

lib/mock/mock-utils.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,38 @@ function matchHeaders (mockDispatch, headers) {
9292
return true
9393
}
9494

95+
function normalizeQueryParams (query) {
96+
if (typeof query !== 'string') {
97+
return query
98+
}
99+
100+
const originalQp = new URLSearchParams(query)
101+
const normalizedQp = new URLSearchParams()
102+
103+
for (let [key, value] of originalQp.entries()) {
104+
key = key.replace('[]', '')
105+
106+
const valueRepresentsString = /^(['"]).*\1$/.test(value)
107+
if (valueRepresentsString) {
108+
normalizedQp.append(key, value)
109+
continue
110+
}
111+
112+
if (value.includes(',')) {
113+
const values = value.split(',')
114+
for (const v of values) {
115+
normalizedQp.append(key, v)
116+
}
117+
continue
118+
}
119+
120+
normalizedQp.append(key, value)
121+
}
122+
123+
normalizedQp.sort()
124+
return normalizedQp
125+
}
126+
95127
function safeUrl (path) {
96128
if (typeof path !== 'string') {
97129
return path
@@ -103,8 +135,7 @@ function safeUrl (path) {
103135
return path
104136
}
105137

106-
const qp = new URLSearchParams(pathSegments.pop())
107-
qp.sort()
138+
const qp = normalizeQueryParams(pathSegments.pop())
108139
return [...pathSegments, qp.toString()].join('?')
109140
}
110141

@@ -395,5 +426,6 @@ module.exports = {
395426
checkNetConnect,
396427
buildAndValidateMockOptions,
397428
getHeaderByName,
398-
buildHeadersFromArray
429+
buildHeadersFromArray,
430+
normalizeQueryParams
399431
}

test/mock-agent.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2888,3 +2888,55 @@ test('MockAgent - headers should be array of strings (fetch)', async (t) => {
28882888

28892889
t.deepStrictEqual(response.headers.getSetCookie(), ['foo=bar', 'bar=baz', 'baz=qux'])
28902890
})
2891+
2892+
// https://github.com/nodejs/undici/issues/4146
2893+
;[
2894+
'/foo?array=item1&array=item2',
2895+
'/foo?array[]=item1&array[]=item2',
2896+
'/foo?array=item1,item2'
2897+
].forEach(path => {
2898+
test(`MockAgent - multi value query parameter "${path}"`, async (t) => {
2899+
t = tspl(t, { plan: 4 })
2900+
2901+
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
2902+
res.setHeader('content-type', 'text/plain')
2903+
res.end('should not be called')
2904+
t.fail('should not be called')
2905+
t.end()
2906+
})
2907+
after(() => server.close())
2908+
2909+
await promisify(server.listen.bind(server))(0)
2910+
2911+
const baseUrl = `http://localhost:${server.address().port}`
2912+
2913+
const mockAgent = new MockAgent()
2914+
after(() => mockAgent.close())
2915+
const mockPool = mockAgent.get(baseUrl)
2916+
2917+
mockPool.intercept({
2918+
path: '/foo',
2919+
method: 'GET',
2920+
query: {
2921+
array: ['item1', 'item2']
2922+
}
2923+
}).reply(200, { foo: 'bar' }, {
2924+
headers: { 'content-type': 'application/json' },
2925+
trailers: { 'Content-MD5': 'test' }
2926+
})
2927+
2928+
const { statusCode, headers, trailers, body } = await mockAgent.request({
2929+
origin: baseUrl,
2930+
path,
2931+
method: 'GET'
2932+
})
2933+
t.strictEqual(statusCode, 200)
2934+
t.strictEqual(headers['content-type'], 'application/json')
2935+
t.deepStrictEqual(trailers, { 'content-md5': 'test' })
2936+
2937+
const jsonResponse = JSON.parse(await getResponse(body))
2938+
t.deepStrictEqual(jsonResponse, {
2939+
foo: 'bar'
2940+
})
2941+
})
2942+
})

test/mock-utils.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const {
99
getResponseData,
1010
getStatusText,
1111
getHeaderByName,
12-
buildHeadersFromArray
12+
buildHeadersFromArray,
13+
normalizeQueryParams
1314
} = require('../lib/mock/mock-utils')
1415

1516
test('deleteMockDispatch - should do nothing if not able to find mock dispatch', (t) => {
@@ -242,3 +243,33 @@ describe('buildHeadersFromArray', () => {
242243
t.strictEqual(headers.key, 'value')
243244
})
244245
})
246+
247+
describe.only('normalizeQueryParams', () => {
248+
test('it should handle basic cases', (t) => {
249+
t = tspl(t, { plan: 4 })
250+
251+
t.deepStrictEqual(normalizeQueryParams('').toString(), '')
252+
t.deepStrictEqual(normalizeQueryParams('a').toString(), 'a=')
253+
t.deepStrictEqual(normalizeQueryParams('b=2&c=3&a=1').toString(), 'a=1&b=2&c=3')
254+
t.deepStrictEqual(normalizeQueryParams('lang=en_EN&id=123').toString(), 'id=123&lang=en_EN')
255+
})
256+
257+
// https://github.com/nodejs/undici/issues/4146
258+
test('it should handle multiple values set using different syntaxes', (t) => {
259+
t = tspl(t, { plan: 3 })
260+
261+
t.deepStrictEqual(normalizeQueryParams('a=1&a=2&a=3').toString(), 'a=1&a=2&a=3')
262+
t.deepStrictEqual(normalizeQueryParams('a[]=1&a[]=2&a[]=3').toString(), 'a=1&a=2&a=3')
263+
t.deepStrictEqual(normalizeQueryParams('a=1,2,3').toString(), 'a=1&a=2&a=3')
264+
})
265+
266+
test('should handle edge case scenarios', (t) => {
267+
t = tspl(t, { plan: 4 })
268+
269+
t.deepStrictEqual(normalizeQueryParams('a="b[]"').toString(), `a=${encodeURIComponent('"b[]"')}`)
270+
t.deepStrictEqual(normalizeQueryParams('a="1,2,3"').toString(), `a=${encodeURIComponent('"1,2,3"')}`)
271+
const encodedSingleQuote = '%27'
272+
t.deepStrictEqual(normalizeQueryParams("a='b[]'").toString(), `a=${encodedSingleQuote}${encodeURIComponent('b[]')}${encodedSingleQuote}`)
273+
t.deepStrictEqual(normalizeQueryParams("a='1,2,3'").toString(), `a=${encodedSingleQuote}${encodeURIComponent('1,2,3')}${encodedSingleQuote}`)
274+
})
275+
})

0 commit comments

Comments
 (0)