update
This commit is contained in:
+365
@@ -14,6 +14,12 @@ var hasProto = require('has-proto')();
|
||||
var qs = require('../');
|
||||
var utils = require('../lib/utils');
|
||||
|
||||
var characterizeParse = function characterizeParse(st, input, opts, expected, label) {
|
||||
var result;
|
||||
st.doesNotThrow(function () { result = qs.parse(input, opts); }, label + ': does not throw');
|
||||
st.deepEqual(result, expected, label + ': parses to the current lenient output');
|
||||
};
|
||||
|
||||
test('parse()', function (t) {
|
||||
t.test('parses a simple string', function (st) {
|
||||
st.deepEqual(qs.parse('0=foo'), { 0: 'foo' });
|
||||
@@ -210,6 +216,21 @@ test('parse()', function (t) {
|
||||
t.test('uses original key when depth = 0', function (st) {
|
||||
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: 0 }), { 'a[0]': 'b', 'a[1]': 'c' });
|
||||
st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: 0 }), { 'a[0][0]': 'b', 'a[0][1]': 'c', 'a[1]': 'd', e: '2' });
|
||||
st.deepEqual(qs.parse('a.b=c', { depth: 0, allowDots: true }), { 'a[b]': 'c' }, 'normalizes dots before applying depth-0 behavior');
|
||||
st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {}, 'respects prototype guard at depth 0');
|
||||
st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' }, 'allows prototypes at depth 0 when enabled');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('ignores prototype keys when depth = 0 and allowPrototypes is false', function (st) {
|
||||
st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {});
|
||||
st.deepEqual(qs.parse('hasOwnProperty=bar', { depth: 0 }), {});
|
||||
st.deepEqual(qs.parse('toString=foo&a=b', { depth: 0 }), { a: 'b' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows prototype keys when depth = 0 and allowPrototypes is true', function (st) {
|
||||
st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -251,6 +272,94 @@ test('parse()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses keys with literal [] inside a bracket group (#493)', function (st) {
|
||||
// A bracket pair inside a bracket group should be treated literally as part of the key
|
||||
st.deepEqual(
|
||||
qs.parse('search[withbracket[]]=foobar'),
|
||||
{ search: { 'withbracket[]': 'foobar' } },
|
||||
'treats inner [] literally when inside a bracket group'
|
||||
);
|
||||
|
||||
// Single-level variant
|
||||
st.deepEqual(
|
||||
qs.parse('a[b[]]=c'),
|
||||
{ a: { 'b[]': 'c' } },
|
||||
'keeps "b[]" as a literal key'
|
||||
);
|
||||
|
||||
// Nested with an array push on the outer level
|
||||
st.deepEqual(
|
||||
qs.parse('list[][x[]]=y'),
|
||||
{ list: [{ 'x[]': 'y' }] },
|
||||
'preserves inner [] while still treating outer [] as array push'
|
||||
);
|
||||
|
||||
// Multiple nested bracket pairs: inner [] remains literal as part of the key
|
||||
st.deepEqual(
|
||||
qs.parse('a[b[c[]]]=d'),
|
||||
{ a: { 'b[c[]]': 'd' } },
|
||||
'treats "b[c[]]" as a literal key inside the bracket group'
|
||||
);
|
||||
|
||||
// Depth limits with literal brackets: preserve inner [] while limiting bracket-group parsing
|
||||
st.deepEqual(
|
||||
qs.parse('a[b[c[]]][d]=e', { depth: 1 }),
|
||||
{ a: { 'b[c[]]': { '[d]': 'e' } } },
|
||||
'respects depth: 1 and preserves literal inner [] in the parsed key'
|
||||
);
|
||||
|
||||
// Unterminated inner bracket group is wrapped as a literal remainder segment
|
||||
st.deepEqual(
|
||||
qs.parse('a[[]b=c'),
|
||||
{ a: { '[[]b': 'c' } },
|
||||
'handles unterminated inner bracket groups without throwing'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('currently parses unbalanced bracket keys after a parent leniently to literal segments (issue #558)', function (st) {
|
||||
characterizeParse(st, 'a[bc=v', undefined, { a: { '[bc': 'v' } }, 'unclosed group after a parent');
|
||||
characterizeParse(st, 'a[=v', undefined, { a: { '[': 'v' } }, 'bare unclosed bracket after a parent');
|
||||
characterizeParse(st, 'a[b][c=v', undefined, { a: { b: { '[c': 'v' } } }, 'unclosed group after a valid one');
|
||||
characterizeParse(st, 'a[b]c[d=v', undefined, { a: { b: { '[d': 'v' } } }, 'unclosed group after text following a valid one');
|
||||
characterizeParse(st, 'filters[customtags:Env: Prod=v', undefined, { filters: { '[customtags:Env: Prod': 'v' } }, 'the issue #558 reproduction');
|
||||
characterizeParse(st, '][a=v', undefined, { ']': { '[a': 'v' } }, 'stray close bracket before an unclosed group');
|
||||
characterizeParse(st, 'a][b=v', undefined, { 'a]': { '[b': 'v' } }, 'stray close bracket inside the parent');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('currently parses unbalanced bracket keys containing inner brackets leniently (issue #558)', function (st) {
|
||||
characterizeParse(st, 'a[b[c=v', undefined, { a: { '[b[c': 'v' } }, 'unclosed group containing an inner bracket');
|
||||
characterizeParse(st, 'a[b[c]=v', undefined, { a: { '[b[c]': 'v' } }, 'unbalanced group with an inner bracket and one close');
|
||||
characterizeParse(st, 'a[b][c[d=v', undefined, { a: { b: { '[c[d': 'v' } } }, 'unclosed inner-bracket group after a valid one');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('currently parses bracket-prefixed unbalanced keys leniently (issue #558)', function (st) {
|
||||
characterizeParse(st, '[abc=v', undefined, { '[abc': 'v' }, 'key starting with an unclosed bracket');
|
||||
characterizeParse(st, '[[]b=v', undefined, { '[[]b': 'v' }, 'key starting with an unbalanced bracket group');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('lenient unbalanced-bracket handling currently depends on the depth option (issue #558)', function (st) {
|
||||
characterizeParse(st, 'a[b]c[d]e[f=v', { depth: 5 }, { a: { b: { d: { '[f': 'v' } } } }, 'consumes groups up to the depth budget then keeps the unclosed remainder literal');
|
||||
characterizeParse(st, 'a[b]c[d]e[f=v', { depth: 1 }, { a: { b: { '[d]e[f': 'v' } } }, 'a lower depth keeps more of the unclosed remainder literal');
|
||||
characterizeParse(st, 'a[bc=v', { depth: 0 }, { 'a[bc': 'v' }, 'depth 0 keeps the entire key literal');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('currently parses an allowDots key with a trailing unclosed bracket leniently (issue #558)', function (st) {
|
||||
characterizeParse(st, 'a.b[c=v', { allowDots: true }, { a: { b: { '[c': 'v' } } }, 'allowDots expands the dot then keeps the unclosed bracket literal');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('valid and stray-close bracket keys are unaffected by unbalanced-bracket handling', function (st) {
|
||||
characterizeParse(st, 'a]b=v', undefined, { 'a]b': 'v' }, 'stray close bracket with no open bracket stays a flat key');
|
||||
characterizeParse(st, 'a[b]extra=v', undefined, { a: { b: 'v' } }, 'text after a balanced group is ignored');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows to specify array indices', function (st) {
|
||||
st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] });
|
||||
st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] });
|
||||
@@ -665,6 +774,21 @@ test('parse()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not crash on multi-step circular references', function (st) {
|
||||
var a = {};
|
||||
a.b = { c: { d: a } };
|
||||
|
||||
var parsed;
|
||||
|
||||
st.doesNotThrow(function () {
|
||||
parsed = qs.parse({ foo: a });
|
||||
});
|
||||
|
||||
st.equal('foo' in parsed, true, 'parsed has "foo" property');
|
||||
st.equal(parsed.foo.b.c.d, parsed.foo, 'the multi-step cycle is preserved');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not crash when parsing deep objects', function (st) {
|
||||
var parsed;
|
||||
var str = 'foo';
|
||||
@@ -898,6 +1022,22 @@ test('parse()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('object-valued input with own `__proto__` does not mutate sub-object [[Prototype]]', function (st) {
|
||||
// JSON.parse creates own data `__proto__` properties (via CreateDataProperty),
|
||||
// which would trigger the Object.prototype.__proto__ accessor if merged via `acc[key] = value`.
|
||||
var out = qs.parse({
|
||||
'user[name]': 'alice',
|
||||
user: JSON.parse('{"__proto__":{"isAdmin":true}}')
|
||||
}, { allowPrototypes: false });
|
||||
|
||||
st.equal(out.user.name, 'alice', 'name from bracket key is preserved');
|
||||
st.equal(out.user.isAdmin, undefined, 'attacker-controlled inherited property is not exposed');
|
||||
st.equal(Object.getPrototypeOf(out.user), Object.prototype, 'sub-object [[Prototype]] is unchanged');
|
||||
st.equal(Object.prototype.isAdmin, undefined, 'Object.prototype is not polluted');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can return null objects', { skip: !hasProto }, function (st) {
|
||||
var expected = {
|
||||
__proto__: null,
|
||||
@@ -1074,6 +1214,15 @@ test('parse()', function (t) {
|
||||
};
|
||||
|
||||
st.deepEqual(qs.parse('KeY=vAlUe', { decoder: decoder }), { key: 'VALUE' });
|
||||
|
||||
var noopDecoder = function () { return 'x'; };
|
||||
noopDecoder();
|
||||
st['throws'](
|
||||
function () { decoder('x', noopDecoder, 'utf-8', 'unknown'); },
|
||||
'this should never happen! type: unknown',
|
||||
'decoder throws for unexpected type'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1103,6 +1252,14 @@ test('parse()', function (t) {
|
||||
new RangeError('Parameter limit exceeded. Only 3 parameters allowed.'),
|
||||
'throws error when parameter limit is exceeded'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1&b=2', { parameterLimit: 1, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Parameter limit exceeded. Only 1 parameter allowed.'),
|
||||
'throws error with singular "parameter" when parameterLimit is 1'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
@@ -1124,6 +1281,12 @@ test('parse()', function (t) {
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('allows unlimited parameters when parameterLimit is Infinity and throwOnLimitExceeded is true', function (sst) {
|
||||
var result = qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: Infinity, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: '1', b: '2', c: '3', d: '4', e: '5', f: '6' }, 'parses all parameters without truncation or throwing');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1189,6 +1352,14 @@ test('parse()', function (t) {
|
||||
'throws error when a sparse index exceeds arrayLimit'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[2]=b', { arrayLimit: 1, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'throws error with singular "element" when arrayLimit is 1'
|
||||
);
|
||||
|
||||
sst.end();
|
||||
});
|
||||
|
||||
@@ -1206,6 +1377,168 @@ test('parse()', function (t) {
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws when duplicate bracket keys exceed arrayLimit with throwOnLimitExceeded', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[]=1&a[]=2&a[]=3&a[]=4&a[]=5&a[]=6', { arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'throws error when duplicate bracket notation exceeds array limit'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws when cumulative comma + duplicate-key combine exceeds arrayLimit', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1,2,3&a=4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'throws when comma groups within the limit cumulatively exceed it across duplicate keys'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=v,v,v,v,v&a=v,v,v,v,v&a=v,v,v,v,v', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'throws on a subsequent part once the cumulative array is already over the limit'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws when plain duplicate keys combine past arrayLimit at the boundary', function (sst) {
|
||||
sst['throws'](
|
||||
function () { qs.parse('a=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'duplicate scalar keys'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () { qs.parse('a[]=x&a[]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'duplicate bracket keys'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws when mixed index and key notation merge past arrayLimit', function (sst) {
|
||||
sst['throws'](
|
||||
function () { qs.parse('a=x&a[0]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'scalar then index that overflows on merge'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () { qs.parse('a[0]=1&a[1]=2&a=3', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'indexed array then scalar that overflows on merge'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('enforces arrayLimit on merge at the boundary, consistently with combine', function (sst) {
|
||||
sst['throws'](
|
||||
function () { qs.parse('a[0]=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'a trailing scalar merged into an at-limit array throws'
|
||||
);
|
||||
sst.deepEqual(
|
||||
qs.parse('a[0]=x&a=y', { arrayLimit: 1 }),
|
||||
{ a: { 0: 'x', 1: 'y' } },
|
||||
'and converts to an overflow object without throwOnLimitExceeded'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () { qs.parse('a[0]=x&a[]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'mixed index and bracket notation merged past the limit throws'
|
||||
);
|
||||
sst.deepEqual(
|
||||
qs.parse('a[0]=x&a[]=y', { arrayLimit: 1 }),
|
||||
{ a: { 0: 'x', 1: 'y' } },
|
||||
'mixed index and bracket notation converts like duplicate-bracket combine'
|
||||
);
|
||||
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('does not throw when cumulative comma combine stays within arrayLimit', function (sst) {
|
||||
var result = qs.parse('a=1,2,3&a=4', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: ['1', '2', '3', '4'] }, 'combined array within limit is preserved');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('silently combines to an overflow object when throwOnLimitExceeded is not set', function (sst) {
|
||||
var result = qs.parse('a=1,2,3&a=4,5,6', { comma: true, arrayLimit: 5 });
|
||||
sst.deepEqual(result, { a: { 0: '1', 1: '2', 2: '3', 3: '4', 4: '5', 5: '6' } }, 'converts to object without throwing');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('does not throw for comma groups nested under bracket notation, counting each group as one element', function (sst) {
|
||||
var result = qs.parse('a[]=1,2,3&a[]=4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: [['1', '2', '3'], ['4', '5', '6']] }, 'nested comma groups count as one element each');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws before splitting when a single comma value exceeds arrayLimit', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'a flat comma value over the limit throws'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1,2', { comma: true, arrayLimit: 1, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'singular message at arrayLimit 1'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[b]=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'a non-bracket nested key comma value over the limit throws'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('does not throw for a single comma value within arrayLimit', function (sst) {
|
||||
sst.deepEqual(
|
||||
qs.parse('a=1,2,3', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true }),
|
||||
{ a: ['1', '2', '3'] },
|
||||
'within the limit'
|
||||
);
|
||||
sst.deepEqual(
|
||||
qs.parse('a=1,2,3,4,5', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true }),
|
||||
{ a: ['1', '2', '3', '4', '5'] },
|
||||
'exactly at the limit'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('does not throw for a bracketed comma group within arrayLimit', function (sst) {
|
||||
var result = qs.parse('a[]=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: [['1', '2', '3', '4', '5', '6']] }, 'a bracketed comma group is a single element');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws for a bracketed comma group when arrayLimit is 0', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[]=1,2,3', { comma: true, arrayLimit: 0, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
|
||||
'a single bracketed element still exceeds arrayLimit 0'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1462,6 +1795,14 @@ test('comma + arrayLimit', function (t) {
|
||||
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
|
||||
'throws error when comma-split exceeds array limit'
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.parse('a=1,2,3', { comma: true, arrayLimit: 1, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'throws error with singular "element" when arrayLimit is 1'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1564,5 +1905,29 @@ test('mixed array and object notation', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('uses existing array length for currentArrayLength when parsing object input with bracket keys', function (st) {
|
||||
var input = {};
|
||||
var arr = ['x', 'y'];
|
||||
arr.a = ['z', 'w'];
|
||||
input['a[]'] = arr;
|
||||
st.deepEqual(qs.parse(input), { a: ['x', 'y'] }, 'parses object input with bracket keys using existing array values');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws with singular message when object input bracket key exceeds arrayLimit of 1', function (st) {
|
||||
var input = {};
|
||||
var arr = ['x'];
|
||||
arr.a = ['z', 'w'];
|
||||
input['a[]'] = arr;
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.parse(input, { throwOnLimitExceeded: true, arrayLimit: 1 });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'throws singular error for object input exceeding arrayLimit 1'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
+138
@@ -651,6 +651,49 @@ test('stringify()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not crash on null/undefined entries in arrayFormat=comma with encodeValuesOnly', function (st) {
|
||||
st.doesNotThrow(
|
||||
function () { qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
|
||||
'does not pass a raw null array entry to the encoder'
|
||||
);
|
||||
st.doesNotThrow(
|
||||
function () { qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
|
||||
'does not pass a raw undefined array entry to the encoder'
|
||||
);
|
||||
st.doesNotThrow(
|
||||
function () { qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
|
||||
'does not crash on a single-null array'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
|
||||
'a=,b',
|
||||
'null entry joins as empty, comma stays unencoded under encodeValuesOnly'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
|
||||
'a=,b',
|
||||
'undefined entry joins as empty, comma stays unencoded under encodeValuesOnly'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
|
||||
'a=',
|
||||
'single-null array stringifies as empty value'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, strictNullHandling: true }),
|
||||
'a',
|
||||
'strictNullHandling drops the equals sign for a single-null array'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, skipNulls: true }),
|
||||
'',
|
||||
'skipNulls drops a single-null array entirely'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies a null object', { skip: !hasProto }, function (st) {
|
||||
st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
|
||||
st.end();
|
||||
@@ -825,6 +868,35 @@ test('stringify()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('skips null/undefined entries in filter=array', function (st) {
|
||||
st.doesNotThrow(
|
||||
function () { qs.stringify({ a: 'b', undefined: 'x' }, { filter: ['a', undefined] }); },
|
||||
'does not pass a raw undefined filter entry to the encoder'
|
||||
);
|
||||
st.doesNotThrow(
|
||||
function () { qs.stringify({ a: 'b', 'null': 'x' }, { filter: ['a', null] }); },
|
||||
'does not pass a raw null filter entry to the encoder'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ a: 'b', undefined: 'x', c: 'd' }, { filter: ['a', undefined, 'c'] }),
|
||||
'a=b&c=d',
|
||||
'undefined filter entry is skipped, remaining keys are kept'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: 'b', 'null': 'x', c: 'd' }, { filter: ['a', null, 'c'] }),
|
||||
'a=b&c=d',
|
||||
'null filter entry is skipped, remaining keys are kept'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: 'b', 'null': 'x' }, { filter: [null] }),
|
||||
'',
|
||||
'filter array containing only null yields empty string'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('supports custom representations when filter=function', function (st) {
|
||||
var calls = 0;
|
||||
var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
|
||||
@@ -1111,6 +1183,28 @@ test('stringify()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('strictNullHandling: applies the formatter to the encoded key (RFC1738)', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'a b': null, 'c d': 'e f' },
|
||||
{ strictNullHandling: false, format: 'RFC1738' }
|
||||
),
|
||||
'a+b=&c+d=e+f',
|
||||
'without: as expected'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'a b': null, 'c d': 'e f' },
|
||||
{ strictNullHandling: true, format: 'RFC1738' }
|
||||
),
|
||||
'a+b&c+d=e+f',
|
||||
'with: as expected'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws if an invalid charset is specified', function (st) {
|
||||
st['throws'](function () {
|
||||
qs.stringify({ a: 'b' }, { charset: 'foobar' });
|
||||
@@ -1146,6 +1240,12 @@ test('stringify()', function (t) {
|
||||
'adds the right sentinel when instructed to and the charset is iso-8859-1'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ a: 1, b: 2 }, { charsetSentinel: true, delimiter: ';' }),
|
||||
'utf8=%E2%9C%93;a=1;b=2',
|
||||
'uses the configured delimiter after the sentinel'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1188,6 +1288,15 @@ test('stringify()', function (t) {
|
||||
};
|
||||
|
||||
st.deepEqual(qs.stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE');
|
||||
|
||||
var noopEncoder = function () { return 'x'; };
|
||||
noopEncoder();
|
||||
st['throws'](
|
||||
function () { encoder('x', noopEncoder, 'utf-8', 'unknown'); },
|
||||
'this should never happen! type: unknown',
|
||||
'encoder throws for unexpected type'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1307,4 +1416,33 @@ test('stringifies empty keys', function (t) {
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('round-trips keys containing percent-encoded bracket text', function (st) {
|
||||
var cases = [
|
||||
{ 'a%5Bb': 'c' },
|
||||
{ 'a%5Db': 'c' },
|
||||
{ 'a%255Bb': 'c' },
|
||||
{ 'a%255Db': 'c' },
|
||||
{ a: { 'b%5Bc': 'd' } },
|
||||
{ a: { 'b%255Bc': 'd' } },
|
||||
{ 'a%5B%255Bb': 'c' }
|
||||
];
|
||||
for (var i = 0; i < cases.length; i++) {
|
||||
st.deepEqual(
|
||||
qs.parse(qs.stringify(cases[i])),
|
||||
cases[i],
|
||||
'round-trips ' + JSON.stringify(cases[i])
|
||||
);
|
||||
}
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses input containing percent-encoded bracket text without mangling', function (st) {
|
||||
st.deepEqual(qs.parse('a%25255Bb=c'), { 'a%255Bb': 'c' }, 'a%25255Bb decodes to a%255Bb, not a%5Bb');
|
||||
st.deepEqual(qs.parse('a%25255Db=c'), { 'a%255Db': 'c' }, 'a%25255Db decodes to a%255Db, not a%5Db');
|
||||
st.deepEqual(qs.parse('a%5Bb%25255Bc%5D=d'), { a: { 'b%255Bc': 'd' } }, 'nested %25255B decodes to %255B inside segment, not %5B');
|
||||
|
||||
st.end();
|
||||
});
|
||||
});
|
||||
|
||||
+225
-3
@@ -31,6 +31,7 @@ test('merge()', function (t) {
|
||||
t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true });
|
||||
|
||||
var func = function f() {};
|
||||
func();
|
||||
t.deepEqual(
|
||||
utils.merge(func, { foo: 'bar' }),
|
||||
[func, { foo: 'bar' }],
|
||||
@@ -64,6 +65,7 @@ test('merge()', function (t) {
|
||||
observed[0] = observed[0]; // eslint-disable-line no-self-assign
|
||||
st.equal(setCount, 1);
|
||||
st.equal(getCount, 2);
|
||||
|
||||
st.end();
|
||||
}
|
||||
);
|
||||
@@ -77,6 +79,7 @@ test('merge()', function (t) {
|
||||
s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
|
||||
var merged = utils.merge(overflow, 'd');
|
||||
s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'adds primitive at next numeric index');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
@@ -92,6 +95,7 @@ test('merge()', function (t) {
|
||||
var obj = { foo: 'bar' };
|
||||
var merged = utils.merge(obj, 'baz');
|
||||
s2t.deepEqual(merged, { foo: 'bar', baz: true }, 'adds primitive as key with value true');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
@@ -109,6 +113,17 @@ test('merge()', function (t) {
|
||||
var merged = utils.merge('c', overflow);
|
||||
s2t.ok(utils.isOverflow(merged), 'result is also marked as overflow');
|
||||
s2t.deepEqual(merged, { 0: 'c', 1: 'a', 2: 'b' }, 'creates object with primitive at 0, source values shifted');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('merges overflow object into primitive with plainObjects', function (s2t) {
|
||||
var overflow = utils.combine(['a'], 'b', 0, false);
|
||||
s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
|
||||
var merged = utils.merge('c', overflow, { plainObjects: true });
|
||||
s2t.ok(utils.isOverflow(merged), 'result is also marked as overflow');
|
||||
s2t.deepEqual(merged, { __proto__: null, 0: 'c', 1: 'a', 2: 'b' }, 'creates null-proto object with primitive at 0');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
@@ -118,6 +133,7 @@ test('merge()', function (t) {
|
||||
s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
|
||||
var merged = utils.merge('a', overflow);
|
||||
s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'shifts all source indices by 1');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
@@ -125,6 +141,95 @@ test('merge()', function (t) {
|
||||
var obj = { foo: 'bar' };
|
||||
var merged = utils.merge('a', obj);
|
||||
s2t.deepEqual(merged, ['a', { foo: 'bar' }], 'creates array with primitive and object');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('merges primitive into array that exceeds arrayLimit', function (s2t) {
|
||||
var arr = ['a', 'b', 'c'];
|
||||
var merged = utils.merge(arr, 'd', { arrayLimit: 1 });
|
||||
s2t.ok(utils.isOverflow(merged), 'result is marked as overflow');
|
||||
s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'converts to overflow object with primitive appended');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('merges array into primitive that exceeds arrayLimit', function (s2t) {
|
||||
var merged = utils.merge('a', ['b', 'c'], { arrayLimit: 1 });
|
||||
s2t.ok(utils.isOverflow(merged), 'result is marked as overflow');
|
||||
s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c' }, 'converts to overflow object');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('merges primitive into array at the arrayLimit boundary, consistently with combine', function (s2t) {
|
||||
var merged = utils.merge(['a'], 'b', { arrayLimit: 1 });
|
||||
s2t.ok(utils.isOverflow(merged), 'result is marked as overflow at the boundary');
|
||||
s2t.deepEqual(merged, { 0: 'a', 1: 'b' }, 'converts to overflow object instead of a length-2 array');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('merges two arrays that exceed arrayLimit into an overflow object', function (s2t) {
|
||||
var merged = utils.merge(['a'], ['b'], { arrayLimit: 1 });
|
||||
s2t.ok(utils.isOverflow(merged), 'result is marked as overflow');
|
||||
s2t.deepEqual(merged, { 0: 'a', 1: 'b' }, 'array-into-array merge enforces arrayLimit like combine');
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('throws at the arrayLimit boundary when merging a primitive into an array with throwOnLimitExceeded', function (s2t) {
|
||||
s2t['throws'](
|
||||
function () { utils.merge(['a'], 'b', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'throws when the resulting length would exceed arrayLimit'
|
||||
);
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('throws when merging two arrays past arrayLimit with throwOnLimitExceeded', function (s2t) {
|
||||
s2t['throws'](
|
||||
function () { utils.merge(['a'], ['b'], { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'array-into-array merge throws rather than silently exceeding arrayLimit'
|
||||
);
|
||||
s2t['throws'](
|
||||
function () { utils.merge(['a', 'b', 'c'], ['d', 'e', 'f'], { arrayLimit: 2, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
|
||||
'uses the plural message when arrayLimit is not 1'
|
||||
);
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('throws instead of merging primitive into over-limit array when throwOnLimitExceeded is set', function (s2t) {
|
||||
s2t['throws'](
|
||||
function () { utils.merge(['a', 'b', 'c'], 'd', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'throws rather than converting to an overflow object'
|
||||
);
|
||||
s2t['throws'](
|
||||
function () { utils.merge(['a', 'b', 'c'], 'd', { arrayLimit: 2, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
|
||||
'uses the plural message when arrayLimit is not 1'
|
||||
);
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('throws instead of merging array into primitive when throwOnLimitExceeded is set', function (s2t) {
|
||||
s2t['throws'](
|
||||
function () { utils.merge('a', ['b', 'c'], { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'throws rather than converting to an overflow object'
|
||||
);
|
||||
s2t['throws'](
|
||||
function () { utils.merge('a', ['b', 'c', 'd'], { arrayLimit: 2, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
|
||||
'uses the plural message when arrayLimit is not 1'
|
||||
);
|
||||
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
@@ -249,6 +354,47 @@ test('combine()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('with throwOnLimitExceeded', function (st) {
|
||||
st.test('throws when concatenation exceeds the limit', function (s2t) {
|
||||
s2t['throws'](
|
||||
function () { utils.combine(['a', 'b', 'c'], 'd', 3, false, true); },
|
||||
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
|
||||
'throws instead of converting to an overflow object'
|
||||
);
|
||||
s2t['throws'](
|
||||
function () { utils.combine([], 'a', 0, false, true); },
|
||||
new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
|
||||
'throws with the correct count at arrayLimit 0'
|
||||
);
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('throws when adding to an existing overflow object', function (s2t) {
|
||||
var overflow = utils.combine(['a', 'b'], 'c', 0, false);
|
||||
s2t.ok(utils.isOverflow(overflow), 'initial object is marked as overflow');
|
||||
|
||||
s2t['throws'](
|
||||
function () { utils.combine(overflow, 'd', 0, false, true); },
|
||||
new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
|
||||
'throws rather than appending to the overflow object'
|
||||
);
|
||||
s2t['throws'](
|
||||
function () { utils.combine(overflow, 'd', 1, false, true); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'uses the singular message at arrayLimit 1'
|
||||
);
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.test('does not throw when within the limit', function (s2t) {
|
||||
var combined = utils.combine(['a'], 'b', 5, false, true);
|
||||
s2t.deepEqual(combined, ['a', 'b'], 'returns the array unchanged when under the limit');
|
||||
s2t.end();
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('with existing overflow object', function (st) {
|
||||
st.test('adds to existing overflow object at next index', function (s2t) {
|
||||
// Create overflow object first via combine: 3 elements (indices 0-2) with limit 0
|
||||
@@ -347,6 +493,79 @@ test('encode', function (t) {
|
||||
'encodes a long string'
|
||||
);
|
||||
|
||||
var boundary = '';
|
||||
var expected = '';
|
||||
for (var j = 0; j < 1023; j++) {
|
||||
boundary += 'a';
|
||||
expected += 'a';
|
||||
}
|
||||
boundary += '😀';
|
||||
expected += '%F0%9F%98%80';
|
||||
|
||||
t.equal(
|
||||
utils.encode(boundary),
|
||||
expected,
|
||||
'encodes a surrogate pair split across long-string chunks'
|
||||
);
|
||||
|
||||
var laterBoundary = '';
|
||||
var laterExpected = '';
|
||||
for (var k = 0; k < 2047; k++) {
|
||||
laterBoundary += 'a';
|
||||
laterExpected += 'a';
|
||||
}
|
||||
laterBoundary += '😀';
|
||||
laterExpected += '%F0%9F%98%80';
|
||||
|
||||
t.equal(
|
||||
utils.encode(laterBoundary),
|
||||
laterExpected,
|
||||
'encodes a surrogate pair split across a later chunk boundary'
|
||||
);
|
||||
|
||||
var twoPairs = '';
|
||||
for (k = 0; k < 1023; k++) {
|
||||
twoPairs += 'a';
|
||||
}
|
||||
twoPairs += '😀';
|
||||
for (k = 0; k < 1022; k++) {
|
||||
twoPairs += 'b';
|
||||
}
|
||||
twoPairs += '😀';
|
||||
|
||||
t.equal(
|
||||
(utils.encode(twoPairs).match(/%F0%9F%98%80/g) || []).length,
|
||||
2,
|
||||
'encodes two surrogate pairs each split across a chunk boundary'
|
||||
);
|
||||
|
||||
var roundTrip = '';
|
||||
for (k = 0; k < 1023; k++) {
|
||||
roundTrip += 'a';
|
||||
}
|
||||
roundTrip += '😀';
|
||||
|
||||
t.equal(
|
||||
decodeURIComponent(utils.encode(roundTrip)),
|
||||
roundTrip,
|
||||
'a boundary-split surrogate pair round-trips through decodeURIComponent'
|
||||
);
|
||||
|
||||
var loneBoundary = '';
|
||||
var loneExpected = '';
|
||||
for (k = 0; k < 1023; k++) {
|
||||
loneBoundary += 'a';
|
||||
loneExpected += 'a';
|
||||
}
|
||||
loneBoundary += '\uD83DX';
|
||||
loneExpected += '%F0%9F%91%98';
|
||||
|
||||
t.equal(
|
||||
utils.encode(loneBoundary),
|
||||
loneExpected,
|
||||
'a lone high surrogate at a chunk boundary encodes the same as mid-chunk'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
utils.encode('\x28\x29'),
|
||||
'%28%29',
|
||||
@@ -376,7 +595,9 @@ test('encode', function (t) {
|
||||
});
|
||||
|
||||
test('isBuffer()', function (t) {
|
||||
forEach([null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], function () {}, /a/g], function (x) {
|
||||
var fn = function () {};
|
||||
fn();
|
||||
forEach([null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], fn, /a/g], function (x) {
|
||||
t.equal(utils.isBuffer(x), false, inspect(x) + ' is not a buffer');
|
||||
});
|
||||
|
||||
@@ -386,8 +607,9 @@ test('isBuffer()', function (t) {
|
||||
var saferBuffer = SaferBuffer.from('abc');
|
||||
t.equal(utils.isBuffer(saferBuffer), true, 'SaferBuffer instance is a buffer');
|
||||
|
||||
var buffer = Buffer.from && Buffer.alloc ? Buffer.from('abc') : new Buffer('abc');
|
||||
t.equal(utils.isBuffer(buffer), true, 'real Buffer instance is a buffer');
|
||||
var buffer = SaferBuffer.from('abc');
|
||||
t.notEqual(saferBuffer, buffer, 'different buffer instances');
|
||||
t.equal(utils.isBuffer(buffer), true, 'another Buffer instance is a buffer');
|
||||
t.end();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user