• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3var test = require('tape');
4var qs = require('../');
5var utils = require('../lib/utils');
6var iconv = require('iconv-lite');
7var SaferBuffer = require('safer-buffer').Buffer;
8
9test('stringify()', function (t) {
10    t.test('stringifies a querystring object', function (st) {
11        st.equal(qs.stringify({ a: 'b' }), 'a=b');
12        st.equal(qs.stringify({ a: 1 }), 'a=1');
13        st.equal(qs.stringify({ a: 1, b: 2 }), 'a=1&b=2');
14        st.equal(qs.stringify({ a: 'A_Z' }), 'a=A_Z');
15        st.equal(qs.stringify({ a: '€' }), 'a=%E2%82%AC');
16        st.equal(qs.stringify({ a: '' }), 'a=%EE%80%80');
17        st.equal(qs.stringify({ a: 'א' }), 'a=%D7%90');
18        st.equal(qs.stringify({ a: '��' }), 'a=%F0%90%90%B7');
19        st.end();
20    });
21
22    t.test('adds query prefix', function (st) {
23        st.equal(qs.stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b');
24        st.end();
25    });
26
27    t.test('with query prefix, outputs blank string given an empty object', function (st) {
28        st.equal(qs.stringify({}, { addQueryPrefix: true }), '');
29        st.end();
30    });
31
32    t.test('stringifies a nested object', function (st) {
33        st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
34        st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e');
35        st.end();
36    });
37
38    t.test('stringifies a nested object with dots notation', function (st) {
39        st.equal(qs.stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c');
40        st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e');
41        st.end();
42    });
43
44    t.test('stringifies an array value', function (st) {
45        st.equal(
46            qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' }),
47            'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
48            'indices => indices'
49        );
50        st.equal(
51            qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' }),
52            'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d',
53            'brackets => brackets'
54        );
55        st.equal(
56            qs.stringify({ a: ['b', 'c', 'd'] }),
57            'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
58            'default => indices'
59        );
60        st.end();
61    });
62
63    t.test('omits nulls when asked', function (st) {
64        st.equal(qs.stringify({ a: 'b', c: null }, { skipNulls: true }), 'a=b');
65        st.end();
66    });
67
68    t.test('omits nested nulls when asked', function (st) {
69        st.equal(qs.stringify({ a: { b: 'c', d: null } }, { skipNulls: true }), 'a%5Bb%5D=c');
70        st.end();
71    });
72
73    t.test('omits array indices when asked', function (st) {
74        st.equal(qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d');
75        st.end();
76    });
77
78    t.test('stringifies a nested array value', function (st) {
79        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { arrayFormat: 'indices' }), 'a%5Bb%5D%5B0%5D=c&a%5Bb%5D%5B1%5D=d');
80        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { arrayFormat: 'brackets' }), 'a%5Bb%5D%5B%5D=c&a%5Bb%5D%5B%5D=d');
81        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }), 'a%5Bb%5D%5B0%5D=c&a%5Bb%5D%5B1%5D=d');
82        st.end();
83    });
84
85    t.test('stringifies a nested array value with dots notation', function (st) {
86        st.equal(
87            qs.stringify(
88                { a: { b: ['c', 'd'] } },
89                { allowDots: true, encode: false, arrayFormat: 'indices' }
90            ),
91            'a.b[0]=c&a.b[1]=d',
92            'indices: stringifies with dots + indices'
93        );
94        st.equal(
95            qs.stringify(
96                { a: { b: ['c', 'd'] } },
97                { allowDots: true, encode: false, arrayFormat: 'brackets' }
98            ),
99            'a.b[]=c&a.b[]=d',
100            'brackets: stringifies with dots + brackets'
101        );
102        st.equal(
103            qs.stringify(
104                { a: { b: ['c', 'd'] } },
105                { allowDots: true, encode: false }
106            ),
107            'a.b[0]=c&a.b[1]=d',
108            'default: stringifies with dots + indices'
109        );
110        st.end();
111    });
112
113    t.test('stringifies an object inside an array', function (st) {
114        st.equal(
115            qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices' }),
116            'a%5B0%5D%5Bb%5D=c',
117            'indices => brackets'
118        );
119        st.equal(
120            qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets' }),
121            'a%5B%5D%5Bb%5D=c',
122            'brackets => brackets'
123        );
124        st.equal(
125            qs.stringify({ a: [{ b: 'c' }] }),
126            'a%5B0%5D%5Bb%5D=c',
127            'default => indices'
128        );
129
130        st.equal(
131            qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices' }),
132            'a%5B0%5D%5Bb%5D%5Bc%5D%5B0%5D=1',
133            'indices => indices'
134        );
135
136        st.equal(
137            qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets' }),
138            'a%5B%5D%5Bb%5D%5Bc%5D%5B%5D=1',
139            'brackets => brackets'
140        );
141
142        st.equal(
143            qs.stringify({ a: [{ b: { c: [1] } }] }),
144            'a%5B0%5D%5Bb%5D%5Bc%5D%5B0%5D=1',
145            'default => indices'
146        );
147
148        st.end();
149    });
150
151    t.test('stringifies an array with mixed objects and primitives', function (st) {
152        st.equal(
153            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encode: false, arrayFormat: 'indices' }),
154            'a[0][b]=1&a[1]=2&a[2]=3',
155            'indices => indices'
156        );
157        st.equal(
158            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encode: false, arrayFormat: 'brackets' }),
159            'a[][b]=1&a[]=2&a[]=3',
160            'brackets => brackets'
161        );
162        st.equal(
163            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encode: false }),
164            'a[0][b]=1&a[1]=2&a[2]=3',
165            'default => indices'
166        );
167
168        st.end();
169    });
170
171    t.test('stringifies an object inside an array with dots notation', function (st) {
172        st.equal(
173            qs.stringify(
174                { a: [{ b: 'c' }] },
175                { allowDots: true, encode: false, arrayFormat: 'indices' }
176            ),
177            'a[0].b=c',
178            'indices => indices'
179        );
180        st.equal(
181            qs.stringify(
182                { a: [{ b: 'c' }] },
183                { allowDots: true, encode: false, arrayFormat: 'brackets' }
184            ),
185            'a[].b=c',
186            'brackets => brackets'
187        );
188        st.equal(
189            qs.stringify(
190                { a: [{ b: 'c' }] },
191                { allowDots: true, encode: false }
192            ),
193            'a[0].b=c',
194            'default => indices'
195        );
196
197        st.equal(
198            qs.stringify(
199                { a: [{ b: { c: [1] } }] },
200                { allowDots: true, encode: false, arrayFormat: 'indices' }
201            ),
202            'a[0].b.c[0]=1',
203            'indices => indices'
204        );
205        st.equal(
206            qs.stringify(
207                { a: [{ b: { c: [1] } }] },
208                { allowDots: true, encode: false, arrayFormat: 'brackets' }
209            ),
210            'a[].b.c[]=1',
211            'brackets => brackets'
212        );
213        st.equal(
214            qs.stringify(
215                { a: [{ b: { c: [1] } }] },
216                { allowDots: true, encode: false }
217            ),
218            'a[0].b.c[0]=1',
219            'default => indices'
220        );
221
222        st.end();
223    });
224
225    t.test('does not omit object keys when indices = false', function (st) {
226        st.equal(qs.stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c');
227        st.end();
228    });
229
230    t.test('uses indices notation for arrays when indices=true', function (st) {
231        st.equal(qs.stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c');
232        st.end();
233    });
234
235    t.test('uses indices notation for arrays when no arrayFormat is specified', function (st) {
236        st.equal(qs.stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c');
237        st.end();
238    });
239
240    t.test('uses indices notation for arrays when no arrayFormat=indices', function (st) {
241        st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c');
242        st.end();
243    });
244
245    t.test('uses repeat notation for arrays when no arrayFormat=repeat', function (st) {
246        st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c');
247        st.end();
248    });
249
250    t.test('uses brackets notation for arrays when no arrayFormat=brackets', function (st) {
251        st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c');
252        st.end();
253    });
254
255    t.test('stringifies a complicated object', function (st) {
256        st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e');
257        st.end();
258    });
259
260    t.test('stringifies an empty value', function (st) {
261        st.equal(qs.stringify({ a: '' }), 'a=');
262        st.equal(qs.stringify({ a: null }, { strictNullHandling: true }), 'a');
263
264        st.equal(qs.stringify({ a: '', b: '' }), 'a=&b=');
265        st.equal(qs.stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b=');
266
267        st.equal(qs.stringify({ a: { b: '' } }), 'a%5Bb%5D=');
268        st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D');
269        st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D=');
270
271        st.end();
272    });
273
274    t.test('stringifies a null object', { skip: !Object.create }, function (st) {
275        var obj = Object.create(null);
276        obj.a = 'b';
277        st.equal(qs.stringify(obj), 'a=b');
278        st.end();
279    });
280
281    t.test('returns an empty string for invalid input', function (st) {
282        st.equal(qs.stringify(undefined), '');
283        st.equal(qs.stringify(false), '');
284        st.equal(qs.stringify(null), '');
285        st.equal(qs.stringify(''), '');
286        st.end();
287    });
288
289    t.test('stringifies an object with a null object as a child', { skip: !Object.create }, function (st) {
290        var obj = { a: Object.create(null) };
291
292        obj.a.b = 'c';
293        st.equal(qs.stringify(obj), 'a%5Bb%5D=c');
294        st.end();
295    });
296
297    t.test('drops keys with a value of undefined', function (st) {
298        st.equal(qs.stringify({ a: undefined }), '');
299
300        st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), 'a%5Bc%5D');
301        st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), 'a%5Bc%5D=');
302        st.equal(qs.stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D=');
303        st.end();
304    });
305
306    t.test('url encodes values', function (st) {
307        st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
308        st.end();
309    });
310
311    t.test('stringifies a date', function (st) {
312        var now = new Date();
313        var str = 'a=' + encodeURIComponent(now.toISOString());
314        st.equal(qs.stringify({ a: now }), str);
315        st.end();
316    });
317
318    t.test('stringifies the weird object from qs', function (st) {
319        st.equal(qs.stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F');
320        st.end();
321    });
322
323    t.test('skips properties that are part of the object prototype', function (st) {
324        Object.prototype.crash = 'test';
325        st.equal(qs.stringify({ a: 'b' }), 'a=b');
326        st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
327        delete Object.prototype.crash;
328        st.end();
329    });
330
331    t.test('stringifies boolean values', function (st) {
332        st.equal(qs.stringify({ a: true }), 'a=true');
333        st.equal(qs.stringify({ a: { b: true } }), 'a%5Bb%5D=true');
334        st.equal(qs.stringify({ b: false }), 'b=false');
335        st.equal(qs.stringify({ b: { c: false } }), 'b%5Bc%5D=false');
336        st.end();
337    });
338
339    t.test('stringifies buffer values', function (st) {
340        st.equal(qs.stringify({ a: SaferBuffer.from('test') }), 'a=test');
341        st.equal(qs.stringify({ a: { b: SaferBuffer.from('test') } }), 'a%5Bb%5D=test');
342        st.end();
343    });
344
345    t.test('stringifies an object using an alternative delimiter', function (st) {
346        st.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
347        st.end();
348    });
349
350    t.test('doesn\'t blow up when Buffer global is missing', function (st) {
351        var tempBuffer = global.Buffer;
352        delete global.Buffer;
353        var result = qs.stringify({ a: 'b', c: 'd' });
354        global.Buffer = tempBuffer;
355        st.equal(result, 'a=b&c=d');
356        st.end();
357    });
358
359    t.test('selects properties when filter=array', function (st) {
360        st.equal(qs.stringify({ a: 'b' }, { filter: ['a'] }), 'a=b');
361        st.equal(qs.stringify({ a: 1 }, { filter: [] }), '');
362
363        st.equal(
364            qs.stringify(
365                { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
366                { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }
367            ),
368            'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
369            'indices => indices'
370        );
371        st.equal(
372            qs.stringify(
373                { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
374                { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }
375            ),
376            'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3',
377            'brackets => brackets'
378        );
379        st.equal(
380            qs.stringify(
381                { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
382                { filter: ['a', 'b', 0, 2] }
383            ),
384            'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
385            'default => indices'
386        );
387
388        st.end();
389    });
390
391    t.test('supports custom representations when filter=function', function (st) {
392        var calls = 0;
393        var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
394        var filterFunc = function (prefix, value) {
395            calls += 1;
396            if (calls === 1) {
397                st.equal(prefix, '', 'prefix is empty');
398                st.equal(value, obj);
399            } else if (prefix === 'c') {
400                return void 0;
401            } else if (value instanceof Date) {
402                st.equal(prefix, 'e[f]');
403                return value.getTime();
404            }
405            return value;
406        };
407
408        st.equal(qs.stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000');
409        st.equal(calls, 5);
410        st.end();
411    });
412
413    t.test('can disable uri encoding', function (st) {
414        st.equal(qs.stringify({ a: 'b' }, { encode: false }), 'a=b');
415        st.equal(qs.stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c');
416        st.equal(qs.stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), 'a=b&c');
417        st.end();
418    });
419
420    t.test('can sort the keys', function (st) {
421        var sort = function (a, b) {
422            return a.localeCompare(b);
423        };
424        st.equal(qs.stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y');
425        st.equal(qs.stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a');
426        st.end();
427    });
428
429    t.test('can sort the keys at depth 3 or more too', function (st) {
430        var sort = function (a, b) {
431            return a.localeCompare(b);
432        };
433        st.equal(
434            qs.stringify(
435                { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
436                { sort: sort, encode: false }
437            ),
438            'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'
439        );
440        st.equal(
441            qs.stringify(
442                { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
443                { sort: null, encode: false }
444            ),
445            'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'
446        );
447        st.end();
448    });
449
450    t.test('can stringify with custom encoding', function (st) {
451        st.equal(qs.stringify({ : '大阪府', '': '' }, {
452            encoder: function (str) {
453                if (str.length === 0) {
454                    return '';
455                }
456                var buf = iconv.encode(str, 'shiftjis');
457                var result = [];
458                for (var i = 0; i < buf.length; ++i) {
459                    result.push(buf.readUInt8(i).toString(16));
460                }
461                return '%' + result.join('%');
462            }
463        }), '%8c%a7=%91%e5%8d%e3%95%7b&=');
464        st.end();
465    });
466
467    t.test('receives the default encoder as a second argument', function (st) {
468        st.plan(2);
469        qs.stringify({ a: 1 }, {
470            encoder: function (str, defaultEncoder) {
471                st.equal(defaultEncoder, utils.encode);
472            }
473        });
474        st.end();
475    });
476
477    t.test('throws error with wrong encoder', function (st) {
478        st['throws'](function () {
479            qs.stringify({}, { encoder: 'string' });
480        }, new TypeError('Encoder has to be a function.'));
481        st.end();
482    });
483
484    t.test('can use custom encoder for a buffer object', { skip: typeof Buffer === 'undefined' }, function (st) {
485        st.equal(qs.stringify({ a: SaferBuffer.from([1]) }, {
486            encoder: function (buffer) {
487                if (typeof buffer === 'string') {
488                    return buffer;
489                }
490                return String.fromCharCode(buffer.readUInt8(0) + 97);
491            }
492        }), 'a=b');
493        st.end();
494    });
495
496    t.test('serializeDate option', function (st) {
497        var date = new Date();
498        st.equal(
499            qs.stringify({ a: date }),
500            'a=' + date.toISOString().replace(/:/g, '%3A'),
501            'default is toISOString'
502        );
503
504        var mutatedDate = new Date();
505        mutatedDate.toISOString = function () {
506            throw new SyntaxError();
507        };
508        st['throws'](function () {
509            mutatedDate.toISOString();
510        }, SyntaxError);
511        st.equal(
512            qs.stringify({ a: mutatedDate }),
513            'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'),
514            'toISOString works even when method is not locally present'
515        );
516
517        var specificDate = new Date(6);
518        st.equal(
519            qs.stringify(
520                { a: specificDate },
521                { serializeDate: function (d) { return d.getTime() * 7; } }
522            ),
523            'a=42',
524            'custom serializeDate function called'
525        );
526
527        st.end();
528    });
529
530    t.test('RFC 1738 spaces serialization', function (st) {
531        st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC1738 }), 'a=b+c');
532        st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC1738 }), 'a+b=c+d');
533        st.end();
534    });
535
536    t.test('RFC 3986 spaces serialization', function (st) {
537        st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC3986 }), 'a=b%20c');
538        st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC3986 }), 'a%20b=c%20d');
539        st.end();
540    });
541
542    t.test('Backward compatibility to RFC 3986', function (st) {
543        st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
544        st.end();
545    });
546
547    t.test('Edge cases and unknown formats', function (st) {
548        ['UFO1234', false, 1234, null, {}, []].forEach(
549            function (format) {
550                st['throws'](
551                    function () {
552                        qs.stringify({ a: 'b c' }, { format: format });
553                    },
554                    new TypeError('Unknown format option provided.')
555                );
556            }
557        );
558        st.end();
559    });
560
561    t.test('encodeValuesOnly', function (st) {
562        st.equal(
563            qs.stringify(
564                { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
565                { encodeValuesOnly: true }
566            ),
567            'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'
568        );
569        st.equal(
570            qs.stringify(
571                { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }
572            ),
573            'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h'
574        );
575        st.end();
576    });
577
578    t.test('encodeValuesOnly - strictNullHandling', function (st) {
579        st.equal(
580            qs.stringify(
581                { a: { b: null } },
582                { encodeValuesOnly: true, strictNullHandling: true }
583            ),
584            'a[b]'
585        );
586        st.end();
587    });
588
589    t.test('does not mutate the options argument', function (st) {
590        var options = {};
591        qs.stringify({}, options);
592        st.deepEqual(options, {});
593        st.end();
594    });
595
596    t.end();
597});
598