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