@gjsify/querystring 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -3
- package/lib/esm/index.js +19 -21
- package/lib/types/error.d.ts +12 -0
- package/lib/types/index.d.ts +86 -0
- package/package.json +13 -19
- package/src/error.ts +2 -0
- package/src/index.spec.ts +568 -47
- package/src/index.ts +18 -14
- package/tsconfig.json +22 -10
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/error.js +0 -25
- package/lib/cjs/index.js +0 -1035
- package/test.gjs.js +0 -33178
- package/test.gjs.mjs +0 -35804
- package/test.gjs.mjs.meta.json +0 -1
- package/test.node.js +0 -231
- package/test.node.mjs +0 -353
- package/tsconfig.types.json +0 -8
package/src/index.spec.ts
CHANGED
|
@@ -1,67 +1,588 @@
|
|
|
1
1
|
import { describe, it, expect } from '@gjsify/unit';
|
|
2
|
-
import * as qs from 'querystring';
|
|
2
|
+
import * as qs from 'node:querystring';
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// Ported from refs/node/test/parallel/test-querystring.js,
|
|
5
|
+
// test-querystring-escape.js, test-querystring-maxKeys-non-finite.js,
|
|
6
|
+
// test-querystring-multichar-separator.js
|
|
7
|
+
// Original: MIT license, Node.js contributors
|
|
8
|
+
|
|
9
|
+
// Helper: create object with null prototype (like Node.js qs.parse results)
|
|
10
|
+
function createWithNoPrototype(properties: { key: string; value: string | string[] }[]) {
|
|
11
|
+
const noProto: Record<string, string | string[]> = Object.create(null);
|
|
12
|
+
properties.forEach((property) => {
|
|
13
|
+
noProto[property.key] = property.value;
|
|
14
|
+
});
|
|
15
|
+
return noProto;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Helper: deep check parse result (no prototype, matching keys+values)
|
|
19
|
+
function check(actual: Record<string, unknown>, expected: Record<string, unknown>) {
|
|
20
|
+
const actualKeys = Object.keys(actual).sort();
|
|
21
|
+
const expectedKeys = Object.keys(expected).sort();
|
|
22
|
+
// Compare key arrays element by element since toEqual uses ==
|
|
23
|
+
expect(actualKeys.length).toBe(expectedKeys.length);
|
|
24
|
+
for (let i = 0; i < expectedKeys.length; i++) {
|
|
25
|
+
expect(actualKeys[i]).toBe(expectedKeys[i]);
|
|
26
|
+
}
|
|
27
|
+
expectedKeys.forEach((key) => {
|
|
28
|
+
const a = actual[key];
|
|
29
|
+
const e = expected[key];
|
|
30
|
+
if (Array.isArray(a) && Array.isArray(e)) {
|
|
31
|
+
expect(a.length).toBe(e.length);
|
|
32
|
+
for (let i = 0; i < e.length; i++) {
|
|
33
|
+
expect(a[i]).toBe(e[i]);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
expect(a).toBe(e);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// [ wonkyQS, canonicalQS, obj ]
|
|
42
|
+
const qsTestCases: [string | null | undefined, string, Record<string, string | string[]>][] = [
|
|
43
|
+
['__proto__=1', '__proto__=1',
|
|
44
|
+
createWithNoPrototype([{ key: '__proto__', value: '1' }])],
|
|
45
|
+
['__defineGetter__=asdf', '__defineGetter__=asdf',
|
|
46
|
+
JSON.parse('{"__defineGetter__":"asdf"}')],
|
|
47
|
+
['foo=918854443121279438895193', 'foo=918854443121279438895193',
|
|
48
|
+
{ 'foo': '918854443121279438895193' }],
|
|
49
|
+
['foo=bar', 'foo=bar', { 'foo': 'bar' }],
|
|
50
|
+
['foo=bar&foo=quux', 'foo=bar&foo=quux', { 'foo': ['bar', 'quux'] }],
|
|
51
|
+
['foo=1&bar=2', 'foo=1&bar=2', { 'foo': '1', 'bar': '2' }],
|
|
52
|
+
['my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F',
|
|
53
|
+
'my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F',
|
|
54
|
+
{ 'my weird field': 'q1!2"\'w$5&7/z8)?' }],
|
|
55
|
+
['foo%3Dbaz=bar', 'foo%3Dbaz=bar', { 'foo=baz': 'bar' }],
|
|
56
|
+
['foo=baz=bar', 'foo=baz%3Dbar', { 'foo': 'baz=bar' }],
|
|
57
|
+
['str=foo&arr=1&arr=2&arr=3&somenull=&undef=',
|
|
58
|
+
'str=foo&arr=1&arr=2&arr=3&somenull=&undef=',
|
|
59
|
+
{ 'str': 'foo', 'arr': ['1', '2', '3'], 'somenull': '', 'undef': '' }],
|
|
60
|
+
[' foo = bar ', '%20foo%20=%20bar%20', { ' foo ': ' bar ' }],
|
|
61
|
+
['foo=%zx', 'foo=%25zx', { 'foo': '%zx' }],
|
|
62
|
+
['foo=%EF%BF%BD', 'foo=%EF%BF%BD', { 'foo': '\ufffd' }],
|
|
63
|
+
['hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz',
|
|
64
|
+
'hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz',
|
|
65
|
+
{ hasOwnProperty: 'x', toString: 'foo', valueOf: 'bar', __defineGetter__: 'baz' }],
|
|
66
|
+
['foo&bar=baz', 'foo=&bar=baz', { foo: '', bar: 'baz' }],
|
|
67
|
+
['a=b&c&d=e', 'a=b&c=&d=e', { a: 'b', c: '', d: 'e' }],
|
|
68
|
+
['a=b&c=&d=e', 'a=b&c=&d=e', { a: 'b', c: '', d: 'e' }],
|
|
69
|
+
['a=b&=c&d=e', 'a=b&=c&d=e', { 'a': 'b', '': 'c', 'd': 'e' }],
|
|
70
|
+
['a=b&=&c=d', 'a=b&=&c=d', { 'a': 'b', '': '', 'c': 'd' }],
|
|
71
|
+
['&&foo=bar&&', 'foo=bar', { foo: 'bar' }],
|
|
72
|
+
['&', '', {}],
|
|
73
|
+
['&&&&', '', {}],
|
|
74
|
+
['&=&', '=', { '': '' }],
|
|
75
|
+
['&=&=', '=&=', { '': ['', ''] }],
|
|
76
|
+
['=', '=', { '': '' }],
|
|
77
|
+
['+', '%20=', { ' ': '' }],
|
|
78
|
+
['+=', '%20=', { ' ': '' }],
|
|
79
|
+
['+&', '%20=', { ' ': '' }],
|
|
80
|
+
['=+', '=%20', { '': ' ' }],
|
|
81
|
+
['+=&', '%20=', { ' ': '' }],
|
|
82
|
+
['a&&b', 'a=&b=', { 'a': '', 'b': '' }],
|
|
83
|
+
['a=a&&b=b', 'a=a&b=b', { 'a': 'a', 'b': 'b' }],
|
|
84
|
+
['&a', 'a=', { 'a': '' }],
|
|
85
|
+
['&=', '=', { '': '' }],
|
|
86
|
+
['a&a&', 'a=&a=', { a: ['', ''] }],
|
|
87
|
+
['a&a&a&', 'a=&a=&a=', { a: ['', '', ''] }],
|
|
88
|
+
['a&a&a&a&', 'a=&a=&a=&a=', { a: ['', '', '', ''] }],
|
|
89
|
+
['a=&a=value&a=', 'a=&a=value&a=', { a: ['', 'value', ''] }],
|
|
90
|
+
['foo+bar=baz+quux', 'foo%20bar=baz%20quux', { 'foo bar': 'baz quux' }],
|
|
91
|
+
['+foo=+bar', '%20foo=%20bar', { ' foo': ' bar' }],
|
|
92
|
+
['a+', 'a%20=', { 'a ': '' }],
|
|
93
|
+
['=a+', '=a%20', { '': 'a ' }],
|
|
94
|
+
['a+&', 'a%20=', { 'a ': '' }],
|
|
95
|
+
['=a+&', '=a%20', { '': 'a ' }],
|
|
96
|
+
['%20+', '%20%20=', { ' ': '' }],
|
|
97
|
+
['=%20+', '=%20%20', { '': ' ' }],
|
|
98
|
+
['%20+&', '%20%20=', { ' ': '' }],
|
|
99
|
+
['=%20+&', '=%20%20', { '': ' ' }],
|
|
100
|
+
[null, '', {}],
|
|
101
|
+
[undefined, '', {}],
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// [ wonkyQS, canonicalQS, obj ] with colon separator
|
|
105
|
+
const qsColonTestCases: [string, string, Record<string, string | string[]>][] = [
|
|
106
|
+
['foo:bar', 'foo:bar', { 'foo': 'bar' }],
|
|
107
|
+
['foo:bar;foo:quux', 'foo:bar;foo:quux', { 'foo': ['bar', 'quux'] }],
|
|
108
|
+
['foo:1&bar:2;baz:quux', 'foo:1%26bar%3A2;baz:quux',
|
|
109
|
+
{ 'foo': '1&bar:2', 'baz': 'quux' }],
|
|
110
|
+
['foo%3Abaz:bar', 'foo%3Abaz:bar', { 'foo:baz': 'bar' }],
|
|
111
|
+
['foo:baz:bar', 'foo:baz%3Abar', { 'foo': 'baz:bar' }],
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
// [wonkyObj, qs, canonicalObj]
|
|
115
|
+
function extendedFunction() { /* noop */ }
|
|
116
|
+
extendedFunction.prototype = { a: 'b' };
|
|
117
|
+
|
|
118
|
+
const qsWeirdObjects: [Record<string, unknown>, string, Record<string, string | string[]>][] = [
|
|
119
|
+
[{ regexp: /./g }, 'regexp=', { 'regexp': '' }],
|
|
120
|
+
[{ regexp: new RegExp('.', 'g') }, 'regexp=', { 'regexp': '' }],
|
|
121
|
+
[{ fn: () => { /* noop */ } }, 'fn=', { 'fn': '' }],
|
|
122
|
+
[{ math: Math }, 'math=', { 'math': '' }],
|
|
123
|
+
[{ e: extendedFunction }, 'e=', { 'e': '' }],
|
|
124
|
+
[{ d: new Date() }, 'd=', { 'd': '' }],
|
|
125
|
+
[{ d: Date }, 'd=', { 'd': '' }],
|
|
126
|
+
[{ f: new Boolean(false), t: new Boolean(true) }, 'f=&t=', { 'f': '', 't': '' }],
|
|
127
|
+
[{ f: false, t: true }, 'f=false&t=true', { 'f': 'false', 't': 'true' }],
|
|
128
|
+
[{ n: null }, 'n=', { 'n': '' }],
|
|
129
|
+
[{ nan: NaN }, 'nan=', { 'nan': '' }],
|
|
130
|
+
[{ inf: Infinity }, 'inf=', { 'inf': '' }],
|
|
131
|
+
[{ a: [], b: [] }, '', {}],
|
|
132
|
+
[{ a: 1, b: [] }, 'a=1', { 'a': '1' }],
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const qsNoMungeTestCases: [string, Record<string, string | string[]>][] = [
|
|
136
|
+
['', {}],
|
|
137
|
+
['foo=bar&foo=baz', { 'foo': ['bar', 'baz'] }],
|
|
138
|
+
['blah=burp', { 'blah': 'burp' }],
|
|
139
|
+
['a=!-._~\'()*', { 'a': '!-._~\'()*' }],
|
|
140
|
+
['a=abcdefghijklmnopqrstuvwxyz', { 'a': 'abcdefghijklmnopqrstuvwxyz' }],
|
|
141
|
+
['a=ABCDEFGHIJKLMNOPQRSTUVWXYZ', { 'a': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' }],
|
|
142
|
+
['a=0123456789', { 'a': '0123456789' }],
|
|
143
|
+
['gragh=1&gragh=3&goo=2', { 'gragh': ['1', '3'], 'goo': '2' }],
|
|
144
|
+
['frappucino=muffin&goat%5B%5D=scone&pond=moose',
|
|
145
|
+
{ 'frappucino': 'muffin', 'goat[]': 'scone', 'pond': 'moose' }],
|
|
146
|
+
['trololol=yes&lololo=no', { 'trololol': 'yes', 'lololo': 'no' }],
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const qsUnescapeTestCases: [string, string][] = [
|
|
150
|
+
['there is nothing to unescape here', 'there is nothing to unescape here'],
|
|
151
|
+
['there%20are%20several%20spaces%20that%20need%20to%20be%20unescaped',
|
|
152
|
+
'there are several spaces that need to be unescaped'],
|
|
153
|
+
['there%2Qare%0-fake%escaped values in%%%%this%9Hstring',
|
|
154
|
+
'there%2Qare%0-fake%escaped values in%%%%this%9Hstring'],
|
|
155
|
+
['%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37',
|
|
156
|
+
' !"#$%&\'()*+,-./01234567'],
|
|
157
|
+
['%%2a', '%*'],
|
|
158
|
+
['%2sf%2a', '%2sf*'],
|
|
159
|
+
['%2%2af%2a', '%2*f*'],
|
|
160
|
+
];
|
|
5
161
|
|
|
6
162
|
export default async () => {
|
|
7
|
-
|
|
8
|
-
|
|
163
|
+
// ==================== parse ====================
|
|
164
|
+
|
|
165
|
+
await describe('querystring.parse', async () => {
|
|
166
|
+
await it('should parse basic id string', async () => {
|
|
9
167
|
expect(qs.parse('id=918854443121279438895193').id).toBe('918854443121279438895193');
|
|
10
168
|
});
|
|
169
|
+
|
|
170
|
+
await describe('qsTestCases', async () => {
|
|
171
|
+
for (const [input, , expected] of qsTestCases) {
|
|
172
|
+
const label = input === null ? 'null' : input === undefined ? 'undefined' : `"${input}"`;
|
|
173
|
+
await it(`should parse ${label}`, async () => {
|
|
174
|
+
check(qs.parse(input as string), expected);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await describe('colon separator', async () => {
|
|
180
|
+
for (const [input, , expected] of qsColonTestCases) {
|
|
181
|
+
await it(`should parse "${input}" with sep=; eq=:`, async () => {
|
|
182
|
+
check(qs.parse(input, ';', ':'), expected);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await describe('weird objects round-trip', async () => {
|
|
188
|
+
for (const [, input, expected] of qsWeirdObjects) {
|
|
189
|
+
await it(`should parse "${input}"`, async () => {
|
|
190
|
+
check(qs.parse(input), expected);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await it('should parse nested qs-in-qs', async () => {
|
|
196
|
+
const f = qs.parse('a=b&q=x%3Dy%26y%3Dz');
|
|
197
|
+
expect(f.a).toBe('b');
|
|
198
|
+
expect(f.q).toBe('x=y&y=z');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await it('should parse nested qs-in-qs with colon', async () => {
|
|
202
|
+
const f = qs.parse('a:b;q:x%3Ay%3By%3Az', ';', ':');
|
|
203
|
+
expect(f.a).toBe('b');
|
|
204
|
+
expect(f.q).toBe('x:y;y:z');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await it('should not throw on undefined input', async () => {
|
|
208
|
+
const result = qs.parse(undefined as unknown as string);
|
|
209
|
+
expect(Object.keys(result).length).toBe(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await it('should handle empty sep (array)', async () => {
|
|
213
|
+
check(qs.parse('a', [] as unknown as string), { a: '' });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await it('should handle empty eq (array)', async () => {
|
|
217
|
+
check(qs.parse('a', null as unknown as string, [] as unknown as string), { '': 'a' });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await it('should handle separator and equals parsing order', async () => {
|
|
221
|
+
check(qs.parse('foo&bar', '&', '&'), { foo: '', bar: '' });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await it('should handle invalid encoded string', async () => {
|
|
225
|
+
check(qs.parse('%\u0100=%\u0101'), { '%\u0100': '%\u0101' });
|
|
226
|
+
});
|
|
11
227
|
});
|
|
12
228
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
229
|
+
// ==================== parse — maxKeys ====================
|
|
230
|
+
|
|
231
|
+
await describe('querystring.parse maxKeys', async () => {
|
|
232
|
+
await it('should limit keys with maxKeys=1', async () => {
|
|
233
|
+
const result = qs.parse('a=1&b=1&c=1', undefined, undefined, { maxKeys: 1 });
|
|
234
|
+
expect(Object.keys(result).length).toBe(1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await it('should limit keys starting from &', async () => {
|
|
238
|
+
const result = qs.parse('&a', undefined, undefined, { maxKeys: 1 });
|
|
239
|
+
expect(Object.keys(result).length).toBe(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await it('should remove limit with maxKeys=0', async () => {
|
|
243
|
+
const query: Record<string, number> = {};
|
|
244
|
+
for (let i = 0; i < 2000; i++) query[i] = i;
|
|
245
|
+
const url = qs.stringify(query as unknown as Record<string, unknown>);
|
|
246
|
+
const result = qs.parse(url, undefined, undefined, { maxKeys: 0 });
|
|
247
|
+
expect(Object.keys(result).length).toBe(2000);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await it('should handle maxKeys=Infinity', async () => {
|
|
251
|
+
const count = 10000;
|
|
252
|
+
let str = '0=0';
|
|
253
|
+
for (let i = 1; i < count; i++) {
|
|
254
|
+
const n = i.toString(36);
|
|
255
|
+
str += `&${n}=${n}`;
|
|
256
|
+
}
|
|
257
|
+
const result = qs.parse(str, undefined, undefined, { maxKeys: Infinity });
|
|
258
|
+
expect(Object.keys(result).length).toBe(count);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await it('should handle maxKeys=NaN', async () => {
|
|
262
|
+
const count = 10000;
|
|
263
|
+
let str = '0=0';
|
|
264
|
+
for (let i = 1; i < count; i++) {
|
|
265
|
+
const n = i.toString(36);
|
|
266
|
+
str += `&${n}=${n}`;
|
|
267
|
+
}
|
|
268
|
+
const result = qs.parse(str, undefined, undefined, { maxKeys: NaN });
|
|
269
|
+
expect(Object.keys(result).length).toBe(count);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await it('should treat string "Infinity" as default maxKeys', async () => {
|
|
273
|
+
const count = 10000;
|
|
274
|
+
let str = '0=0';
|
|
275
|
+
for (let i = 1; i < count; i++) {
|
|
276
|
+
const n = i.toString(36);
|
|
277
|
+
str += `&${n}=${n}`;
|
|
278
|
+
}
|
|
279
|
+
const result = qs.parse(str, undefined, undefined,
|
|
280
|
+
{ maxKeys: 'Infinity' as unknown as number });
|
|
281
|
+
expect(Object.keys(result).length).toBe(1000);
|
|
282
|
+
});
|
|
17
283
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
284
|
+
await it('should treat string "NaN" as default maxKeys', async () => {
|
|
285
|
+
const count = 10000;
|
|
286
|
+
let str = '0=0';
|
|
287
|
+
for (let i = 1; i < count; i++) {
|
|
288
|
+
const n = i.toString(36);
|
|
289
|
+
str += `&${n}=${n}`;
|
|
290
|
+
}
|
|
291
|
+
const result = qs.parse(str, undefined, undefined,
|
|
292
|
+
{ maxKeys: 'NaN' as unknown as number });
|
|
293
|
+
expect(Object.keys(result).length).toBe(1000);
|
|
294
|
+
});
|
|
23
295
|
});
|
|
24
296
|
|
|
297
|
+
// ==================== parse — custom decode ====================
|
|
25
298
|
|
|
26
|
-
await
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
299
|
+
await describe('querystring.parse custom decode', async () => {
|
|
300
|
+
await it('should use custom decodeURIComponent', async () => {
|
|
301
|
+
function demoDecode(str: string) { return str + str; }
|
|
302
|
+
check(
|
|
303
|
+
qs.parse('a=a&b=b&c=c', undefined, undefined, { decodeURIComponent: demoDecode }),
|
|
304
|
+
{ aa: 'aa', bb: 'bb', cc: 'cc' },
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await it('should use custom decode with custom eq', async () => {
|
|
309
|
+
check(
|
|
310
|
+
qs.parse('a=a&b=b&c=c', undefined, '==', { decodeURIComponent: (str: string) => str }),
|
|
311
|
+
{ 'a=a': '', 'b=b': '', 'c=c': '' },
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await it('should fall back when decode throws', async () => {
|
|
316
|
+
function errDecode(_str: string): string {
|
|
317
|
+
throw new Error('To jump to the catch scope');
|
|
318
|
+
}
|
|
319
|
+
check(qs.parse('a=a', undefined, undefined, { decodeURIComponent: errDecode }), { a: 'a' });
|
|
320
|
+
});
|
|
33
321
|
});
|
|
34
322
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
323
|
+
// ==================== stringify ====================
|
|
324
|
+
|
|
325
|
+
await describe('querystring.stringify', async () => {
|
|
326
|
+
await describe('qsTestCases round-trip', async () => {
|
|
327
|
+
for (const [, canonical, obj] of qsTestCases) {
|
|
328
|
+
await it(`should stringify to "${canonical}"`, async () => {
|
|
329
|
+
expect(qs.stringify(obj as unknown as Record<string, unknown>)).toBe(canonical);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await describe('colon separator round-trip', async () => {
|
|
335
|
+
for (const [, canonical, obj] of qsColonTestCases) {
|
|
336
|
+
await it(`should stringify to "${canonical}" with sep=; eq=:`, async () => {
|
|
337
|
+
expect(qs.stringify(obj as unknown as Record<string, unknown>, ';', ':')).toBe(canonical);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await describe('weird objects', async () => {
|
|
343
|
+
for (const [obj, expected] of qsWeirdObjects) {
|
|
344
|
+
await it(`should stringify ${JSON.stringify(obj)} to "${expected}"`, async () => {
|
|
345
|
+
expect(qs.stringify(obj as unknown as Record<string, unknown>)).toBe(expected);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await describe('noMunge round-trip', async () => {
|
|
351
|
+
for (const [expected, obj] of qsNoMungeTestCases) {
|
|
352
|
+
await it(`should stringify to "${expected}"`, async () => {
|
|
353
|
+
expect(qs.stringify(obj as unknown as Record<string, unknown>, '&', '=')).toBe(expected);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await it('should coerce numbers to string', async () => {
|
|
359
|
+
expect(qs.stringify({ foo: 0 })).toBe('foo=0');
|
|
360
|
+
expect(qs.stringify({ foo: -0 })).toBe('foo=0');
|
|
361
|
+
expect(qs.stringify({ foo: 3 })).toBe('foo=3');
|
|
362
|
+
expect(qs.stringify({ foo: -72.42 })).toBe('foo=-72.42');
|
|
363
|
+
expect(qs.stringify({ foo: NaN })).toBe('foo=');
|
|
364
|
+
expect(qs.stringify({ foo: Infinity })).toBe('foo=');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await it('should escape scientific notation (1e21)', async () => {
|
|
368
|
+
expect(qs.stringify({ foo: 1e21 })).toBe('foo=1e%2B21');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await it('should stringify BigInt values', async () => {
|
|
372
|
+
expect(qs.stringify({ foo: 2n ** 1023n })).toBe('foo=' + (2n ** 1023n));
|
|
373
|
+
expect(qs.stringify([0n, 1n, 2n] as unknown as Record<string, unknown>)).toBe('0=0&1=1&2=2');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await it('should stringify BigInt with custom encode', async () => {
|
|
377
|
+
expect(qs.stringify({ foo: 2n ** 1023n }, undefined, undefined,
|
|
378
|
+
{ encodeURIComponent: (c: string) => c })).toBe('foo=' + (2n ** 1023n));
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
await it('should throw URIError on invalid surrogate pair', async () => {
|
|
382
|
+
expect(() => {
|
|
383
|
+
qs.stringify({ foo: '\udc00' });
|
|
384
|
+
}).toThrow();
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
qs.stringify({ foo: '\udc00' });
|
|
388
|
+
} catch (error) {
|
|
389
|
+
expect(error instanceof URIError).toBeTruthy();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await it('should stringify nested qs-in-qs', async () => {
|
|
394
|
+
const f = qs.stringify({
|
|
395
|
+
a: 'b',
|
|
396
|
+
q: qs.stringify({ x: 'y', y: 'z' }),
|
|
397
|
+
});
|
|
398
|
+
expect(f).toBe('a=b&q=x%3Dy%26y%3Dz');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await it('should stringify nested in colon', async () => {
|
|
402
|
+
const f = qs.stringify({
|
|
403
|
+
a: 'b',
|
|
404
|
+
q: qs.stringify({ x: 'y', y: 'z' }, ';', ':'),
|
|
405
|
+
}, ';', ':');
|
|
406
|
+
expect(f).toBe('a:b;q:x%3Ay%3By%3Az');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await it('should return empty string for non-objects', async () => {
|
|
410
|
+
expect(qs.stringify()).toBe('');
|
|
411
|
+
expect(qs.stringify(0 as unknown as Record<string, unknown>)).toBe('');
|
|
412
|
+
expect(qs.stringify([] as unknown as Record<string, unknown>)).toBe('');
|
|
413
|
+
expect(qs.stringify(null as unknown as Record<string, unknown>)).toBe('');
|
|
414
|
+
expect(qs.stringify(true as unknown as Record<string, unknown>)).toBe('');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await it('should use custom encodeURIComponent', async () => {
|
|
418
|
+
function demoEncode(str: string) { return str[0]; }
|
|
419
|
+
const obj = { aa: 'aa', bb: 'bb', cc: 'cc' };
|
|
420
|
+
expect(qs.stringify(obj, undefined, undefined, { encodeURIComponent: demoEncode }))
|
|
421
|
+
.toBe('a=a&b=b&c=c');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
await it('should use custom encode for different types', async () => {
|
|
425
|
+
const obj = { number: 1, bigint: 2n, true: true, false: false, object: {} };
|
|
426
|
+
expect(qs.stringify(obj as unknown as Record<string, unknown>, undefined, undefined,
|
|
427
|
+
{ encodeURIComponent: (v: string) => v }))
|
|
428
|
+
.toBe('number=1&bigint=2&true=true&false=false&object=');
|
|
42
429
|
});
|
|
43
|
-
expect(f).toBe('a=b&q=x%3Dy%26y%3Dz');
|
|
44
430
|
});
|
|
45
431
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
432
|
+
// ==================== escape ====================
|
|
433
|
+
|
|
434
|
+
await describe('querystring.escape', async () => {
|
|
435
|
+
await it('should escape numbers', async () => {
|
|
436
|
+
expect(qs.escape(5 as unknown as string)).toBe('5');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await it('should not escape safe strings', async () => {
|
|
440
|
+
expect(qs.escape('test')).toBe('test');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await it('should escape objects via toString', async () => {
|
|
444
|
+
expect(qs.escape({} as unknown as string)).toBe('%5Bobject%20Object%5D');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
await it('should escape arrays', async () => {
|
|
448
|
+
expect(qs.escape([5, 10] as unknown as string)).toBe('5%2C10');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await it('should escape unicode strings', async () => {
|
|
452
|
+
expect(qs.escape('Ŋōđĕ')).toBe('%C5%8A%C5%8D%C4%91%C4%95');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
await it('should escape mixed ascii+unicode', async () => {
|
|
456
|
+
expect(qs.escape('testŊōđĕ')).toBe('test%C5%8A%C5%8D%C4%91%C4%95');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
await it('should escape valid surrogate pair with trailing chars', async () => {
|
|
460
|
+
expect(qs.escape(`${String.fromCharCode(0xD800 + 1)}test`))
|
|
461
|
+
.toBe('%F0%90%91%B4est');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
await it('should throw URIError on lone surrogate', async () => {
|
|
465
|
+
expect(() => qs.escape(String.fromCharCode(0xD800 + 1))).toThrow();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
await it('should prefer toString over valueOf for objects', async () => {
|
|
469
|
+
expect(qs.escape({ test: 5, toString: () => 'test', valueOf: () => 10 } as unknown as string))
|
|
470
|
+
.toBe('test');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
await it('should throw TypeError when toString is not callable', async () => {
|
|
474
|
+
expect(() => qs.escape({ toString: 5 } as unknown as string)).toThrow();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await it('should use valueOf when toString is not callable', async () => {
|
|
478
|
+
expect(qs.escape({ toString: 5, valueOf: () => 'test' } as unknown as string)).toBe('test');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
await it('should throw TypeError on Symbol', async () => {
|
|
482
|
+
expect(() => qs.escape(Symbol('test') as unknown as string)).toThrow();
|
|
483
|
+
});
|
|
55
484
|
});
|
|
56
485
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// expect((qs as any).parse()).toBeEmptyObject();
|
|
486
|
+
// ==================== unescape / unescapeBuffer ====================
|
|
487
|
+
|
|
488
|
+
await describe('querystring.unescape', async () => {
|
|
489
|
+
for (const [input, expected] of qsUnescapeTestCases) {
|
|
490
|
+
await it(`should unescape "${input.substring(0, 40)}..."`, async () => {
|
|
491
|
+
expect(qs.unescape(input)).toBe(expected);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
66
494
|
});
|
|
67
|
-
|
|
495
|
+
|
|
496
|
+
await describe('querystring.unescapeBuffer', async () => {
|
|
497
|
+
for (const [input, expected] of qsUnescapeTestCases) {
|
|
498
|
+
await it(`should unescapeBuffer "${input.substring(0, 40)}..."`, async () => {
|
|
499
|
+
expect(qs.unescapeBuffer(input).toString()).toBe(expected);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
await it('should decode + as space when decodeSpaces=true', async () => {
|
|
504
|
+
expect(qs.unescapeBuffer('a+b', true).toString()).toBe('a b');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
await it('should not decode + without decodeSpaces', async () => {
|
|
508
|
+
expect(qs.unescapeBuffer('a+b').toString()).toBe('a+b');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await it('should handle trailing %', async () => {
|
|
512
|
+
expect(qs.unescapeBuffer('a%').toString()).toBe('a%');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
await it('should handle incomplete hex %2', async () => {
|
|
516
|
+
expect(qs.unescapeBuffer('a%2').toString()).toBe('a%2');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
await it('should decode %20 as space', async () => {
|
|
520
|
+
expect(qs.unescapeBuffer('a%20').toString()).toBe('a ');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await it('should not decode invalid hex %2g', async () => {
|
|
524
|
+
expect(qs.unescapeBuffer('a%2g').toString()).toBe('a%2g');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
await it('should handle double %', async () => {
|
|
528
|
+
expect(qs.unescapeBuffer('a%%').toString()).toBe('a%%');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
await it('should decode hex bytes correctly', async () => {
|
|
532
|
+
const b = qs.unescapeBuffer('%d3%f2Ug%1f6v%24%5e%98%cb%0d%ac%a2%2f%9d%eb%d8%a2%e6');
|
|
533
|
+
expect(b[0]).toBe(0xd3);
|
|
534
|
+
expect(b[1]).toBe(0xf2);
|
|
535
|
+
expect(b[2]).toBe(0x55); // 'U'
|
|
536
|
+
expect(b[3]).toBe(0x67); // 'g'
|
|
537
|
+
expect(b[4]).toBe(0x1f);
|
|
538
|
+
expect(b[5]).toBe(0x36); // '6'
|
|
539
|
+
expect(b[6]).toBe(0x76); // 'v'
|
|
540
|
+
expect(b[7]).toBe(0x24);
|
|
541
|
+
expect(b[8]).toBe(0x5e);
|
|
542
|
+
expect(b[9]).toBe(0x98);
|
|
543
|
+
expect(b[10]).toBe(0xcb);
|
|
544
|
+
expect(b[11]).toBe(0x0d);
|
|
545
|
+
expect(b[12]).toBe(0xac);
|
|
546
|
+
expect(b[13]).toBe(0xa2);
|
|
547
|
+
expect(b[14]).toBe(0x2f);
|
|
548
|
+
expect(b[15]).toBe(0x9d);
|
|
549
|
+
expect(b[16]).toBe(0xeb);
|
|
550
|
+
expect(b[17]).toBe(0xd8);
|
|
551
|
+
expect(b[18]).toBe(0xa2);
|
|
552
|
+
expect(b[19]).toBe(0xe6);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// ==================== multichar separator ====================
|
|
557
|
+
|
|
558
|
+
await describe('querystring multichar separator', async () => {
|
|
559
|
+
await it('should parse with && and =>', async () => {
|
|
560
|
+
check(qs.parse('foo=>bar&&bar=>baz', '&&', '=>'), { foo: 'bar', bar: 'baz' });
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await it('should stringify with && and =>', async () => {
|
|
564
|
+
expect(qs.stringify({ foo: 'bar', bar: 'baz' }, '&&', '=>')).toBe('foo=>bar&&bar=>baz');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await it('should parse with ", " and "==>"', async () => {
|
|
568
|
+
check(qs.parse('foo==>bar, bar==>baz', ', ', '==>'), { foo: 'bar', bar: 'baz' });
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
await it('should stringify with ", " and "==>"', async () => {
|
|
572
|
+
expect(qs.stringify({ foo: 'bar', bar: 'baz' }, ', ', '==>'))
|
|
573
|
+
.toBe('foo==>bar, bar==>baz');
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// ==================== decode / encode aliases ====================
|
|
578
|
+
|
|
579
|
+
await describe('querystring aliases', async () => {
|
|
580
|
+
await it('decode should be an alias for parse', async () => {
|
|
581
|
+
expect(qs.decode).toBe(qs.parse);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await it('encode should be an alias for stringify', async () => {
|
|
585
|
+
expect(qs.encode).toBe(qs.stringify);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
};
|