All new
alle new
This commit is contained in:
1 parent
da64472251
commit
5b20adee48
1605 files changed
+396517
-1674
No files matched your search
+317
@@ -136,6 +136,144 @@ test('stringify()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('encodes dot in key of object with a primitive value when encodeDotInKeys and allowDots is provided', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj': 'John' },
|
||||
{ allowDots: true, encodeDotInKeys: true }
|
||||
),
|
||||
'name%252Eobj=John',
|
||||
'with allowDots true and encodeDotInKeys true'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj': 'John' },
|
||||
{ allowDots: false, encodeDotInKeys: true }
|
||||
),
|
||||
'name%252Eobj=John',
|
||||
'with allowDots false and encodeDotInKeys true'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ 'name.obj': 'John' },
|
||||
{ encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true }
|
||||
),
|
||||
'name%2Eobj=John',
|
||||
'with encodeValuesOnly true'
|
||||
);
|
||||
|
||||
// With `encodeValuesOnly` the key encoder is skipped, so the dot is encoded once
|
||||
// (`%2E`) rather than twice. That single `%2E` decodes back to a structural dot, so
|
||||
// unlike the default output below this does not round-trip to the original dotted key
|
||||
// (a pre-existing `encodeValuesOnly` + `encodeDotInKeys` limitation, not new here).
|
||||
st.deepEqual(
|
||||
qs.parse(
|
||||
qs.stringify(
|
||||
{ 'name.obj': 'John' },
|
||||
{ encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true }
|
||||
),
|
||||
{ allowDots: true, decodeDotInKeys: true }
|
||||
),
|
||||
{ name: { obj: 'John' } },
|
||||
'with encodeValuesOnly the single-encoded dot decodes structurally (does not round-trip)'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse(
|
||||
qs.stringify({ 'name.obj': 'John' }, { allowDots: true, encodeDotInKeys: true }),
|
||||
{ allowDots: true, decodeDotInKeys: true }
|
||||
),
|
||||
{ 'name.obj': 'John' },
|
||||
'round-trips a dotted key with a primitive value'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ 'a.b': null }, { allowDots: true, encodeDotInKeys: true, strictNullHandling: true }),
|
||||
'a%252Eb',
|
||||
'encodes a dotted key with a null value'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ 'a.b': '1', 'c.d': '2' }, { allowDots: true, encodeDotInKeys: true }),
|
||||
'a%252Eb=1&c%252Ed=2',
|
||||
'encodes dots in every top-level key, not just the first'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ 'a.b.c': 'x' }, { allowDots: true, encodeDotInKeys: true }),
|
||||
'a%252Eb%252Ec=x',
|
||||
'encodes every dot in a key'
|
||||
);
|
||||
var primitiveKey = qs.stringify({ 'a.b': 'c' }, { allowDots: true, encodeDotInKeys: true }).split('=')[0];
|
||||
st.equal(
|
||||
qs.stringify({ 'a.b': { x: 'c' } }, { allowDots: true, encodeDotInKeys: true }).indexOf(primitiveKey + '.'),
|
||||
0,
|
||||
'a top-level dotted key encodes identically for primitive and object values'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
// Characterization of the CURRENT (buggy) behavior: with `allowDots` + `encodeDotInKeys`,
|
||||
// `stringify` percent-encodes the structural `.` separators that `allowDots` inserts between
|
||||
// nested keys, not just the literal dots inside a key, which breaks the stringify -> parse
|
||||
// round-trip. These assertions pin that behavior so it cannot change silently: the fix in
|
||||
// https://github.com/ljharb/qs/pull/564 changes every output below, so landing that fix MUST
|
||||
// update this test (that is the point — it forces the behavior change to be explicit).
|
||||
t.test('[characterization] encodeDotInKeys currently over-encodes structural separator dots (pre-#564)', function (st) {
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
|
||||
'a%252Eb.c=d',
|
||||
'over-encodes the a->b separator (#564: should become a.b.c=d)'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true, encodeDotInKeys: true }),
|
||||
'a%252Eb%252Ec.d=e',
|
||||
'clobbers every separator but the last (#564: should become a.b.c.d=e)'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ 'a.b': { c: { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
|
||||
'a%252Eb%252Ec.d=e',
|
||||
'top-level dotted key: deeper separators also get encoded (#564: should become a%252Eb.c.d=e)'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
|
||||
'a%252Eb%252Ec.d=e',
|
||||
'nested dotted key + object value: the separator around it is encoded (#564: should become a.b%252Ec.d=e)'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: { 'c.d': { e: 'f' } } } }, { allowDots: true, encodeDotInKeys: true }),
|
||||
'a%252Eb%252Ec%252Ed.e=f',
|
||||
'deeply nested dotted key: all separators encoded (#564: should become a.b.c%252Ed.e=f)'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
|
||||
'a%252Eb%5B0%5D=1&a%252Eb%5B1%5D=2',
|
||||
'array value under nested keys: separator encoded (#564: should become a.b%5B0%5D=1&a.b%5B1%5D=2)'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: { 'b.c': [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
|
||||
'a%252Eb%252Ec%5B0%5D=1&a%252Eb%252Ec%5B1%5D=2',
|
||||
'array value under nested dotted key (#564: should become a.b%252Ec%5B0%5D=1&a.b%252Ec%5B1%5D=2)'
|
||||
);
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: false, encodeDotInKeys: true }),
|
||||
'a%5Bb%252Ec%5D%5Bd%5D=e',
|
||||
'allowDots:false object value encodes the nested-key dot, unlike the primitive-value case (#564: should become a%5Bb.c%5D%5Bd%5D=e)'
|
||||
);
|
||||
|
||||
st.deepEqual(
|
||||
qs.parse(
|
||||
qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
|
||||
{ allowDots: true, decodeDotInKeys: true }
|
||||
),
|
||||
{ 'a.b': { c: 'd' } },
|
||||
'the over-encoding breaks the round-trip: {a:{b:{c}}} comes back as {"a.b":{c}} (#564 restores it)'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function (st) {
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
@@ -328,6 +466,39 @@ test('stringify()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allowEmptyArrays only short-circuits arrays with no own keys', function (st) {
|
||||
st.equal(
|
||||
qs.stringify({ a: [] }, { allowEmptyArrays: true }),
|
||||
'a[]',
|
||||
'a genuinely empty array is still emitted as `[]`'
|
||||
);
|
||||
|
||||
var withKey = [];
|
||||
withKey.extra = 'x';
|
||||
st.equal(
|
||||
qs.stringify({ a: withKey }, { allowEmptyArrays: true }),
|
||||
'a%5Bextra%5D=x',
|
||||
'an own key on an empty array is serialized rather than dropped'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: withKey }, { allowEmptyArrays: true }),
|
||||
qs.stringify({ a: withKey }),
|
||||
'the allowEmptyArrays fast path matches the default path when the array has own keys'
|
||||
);
|
||||
|
||||
var container = {};
|
||||
var cyclic = [];
|
||||
cyclic.back = container;
|
||||
container.a = cyclic;
|
||||
st['throws'](
|
||||
function () { qs.stringify(container, { allowEmptyArrays: true }); },
|
||||
RangeError,
|
||||
'a cycle through an empty array with an own key is still detected'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an array value with one item vs multiple items', function (st) {
|
||||
st.test('non-array item', function (s2t) {
|
||||
s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a=c');
|
||||
@@ -761,6 +932,33 @@ test('stringify()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not throw on a non-callable `constructor.isBuffer` key', function (st) {
|
||||
var input = { a: { constructor: { isBuffer: 'x' } } };
|
||||
st.doesNotThrow(function () { qs.stringify(input); });
|
||||
st.equal(qs.stringify(input), 'a%5Bconstructor%5D%5BisBuffer%5D=x');
|
||||
st.equal(qs.stringify({ a: { constructor: { isBuffer: { b: 'c' } } } }), 'a%5Bconstructor%5D%5BisBuffer%5D%5Bb%5D=c');
|
||||
st.equal(qs.stringify({ a: { constructor: { isBuffer: ['b'] } } }), 'a%5Bconstructor%5D%5BisBuffer%5D%5B0%5D=b');
|
||||
st.equal(qs.stringify({ a: { constructor: { isBuffer: true } } }), 'a%5Bconstructor%5D%5BisBuffer%5D=true');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('round-trips a parsed `constructor[isBuffer]` key', function (st) {
|
||||
var str = 'x%5Bconstructor%5D%5BisBuffer%5D=y';
|
||||
|
||||
var plain = qs.parse(str, { plainObjects: true });
|
||||
st.deepEqual(plain, { __proto__: null, x: { __proto__: null, constructor: { __proto__: null, isBuffer: 'y' } } }, 'plainObjects keeps the key');
|
||||
st.doesNotThrow(function () { qs.stringify(plain); });
|
||||
st.equal(qs.stringify(plain), str, 'plainObjects round-trips');
|
||||
|
||||
var proto = qs.parse(str, { allowPrototypes: true });
|
||||
st.deepEqual(proto, { x: { constructor: { isBuffer: 'y' } } }, 'allowPrototypes keeps the key');
|
||||
st.doesNotThrow(function () { qs.stringify(proto); });
|
||||
st.equal(qs.stringify(proto), str, 'allowPrototypes round-trips');
|
||||
|
||||
st.equal(qs.stringify(qs.parse(str)), '', 'default options drop the key');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('stringifies an object using an alternative delimiter', function (st) {
|
||||
st.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
|
||||
st.end();
|
||||
@@ -919,6 +1117,66 @@ test('stringify()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('serializes Date values unless filter transforms them', function (st) {
|
||||
var obj = {
|
||||
a: new Date(Date.UTC(2023, 2, 7, 9, 2, 41, 260)),
|
||||
b: [new Date(Date.UTC(2023, 2, 7, 9, 2, 41, 260))]
|
||||
};
|
||||
var iso = '2023-03-07T09%3A02%3A41.260Z';
|
||||
st.equal(
|
||||
qs.stringify(obj, { filter: function (prefix, value) { return value; } }),
|
||||
'a=' + iso + '&b%5B0%5D=' + iso,
|
||||
'Dates returned unchanged by a filter are serialized'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: new Date(0) }, { filter: function (prefix, value) { return value instanceof Date ? 'kept' : value; } }),
|
||||
'a=kept',
|
||||
'a filter that transforms a Date wins over default serialization'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('applies default Date handling to whatever a filter function returns', function (st) {
|
||||
var date = new Date(Date.UTC(2023, 2, 7, 9, 2, 41, 260));
|
||||
var iso = '2023-03-07T09%3A02%3A41.260Z';
|
||||
var identity = function (prefix, value) { return value; };
|
||||
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: date } }, { filter: identity }),
|
||||
'a%5Bb%5D=' + iso,
|
||||
'a nested Date is serialized through a filter'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify(
|
||||
{ a: date },
|
||||
{ filter: identity, serializeDate: function (d) { return String(d.getTime()); } }
|
||||
),
|
||||
'a=1678179761260',
|
||||
'`serializeDate` is honored after a filter'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: [date, 'x'] }, { filter: identity, arrayFormat: 'comma' }),
|
||||
'a=' + iso + '%2Cx',
|
||||
'Dates inside a comma array are serialized through a filter, not stringified as locale text'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: 'x' }, { filter: function (prefix, value) { return prefix === 'a' ? date : value; } }),
|
||||
'a=' + iso,
|
||||
'a Date produced by the filter is serialized'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: date, b: 1 }, { filter: ['a'] }),
|
||||
'a=' + iso,
|
||||
'an array filter still serializes Dates'
|
||||
);
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: new Date(NaN) }, { filter: identity }); },
|
||||
RangeError,
|
||||
'an invalid Date through a filter throws like the no-filter path'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can disable uri encoding', function (st) {
|
||||
st.equal(qs.stringify({ a: 'b' }, { encode: false }), 'a=b');
|
||||
st.equal(qs.stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c');
|
||||
@@ -1445,4 +1703,63 @@ test('stringifies empty keys', function (t) {
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('limits nesting depth with the `depth` option', function (st) {
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: { c: 'd' } } }),
|
||||
'a%5Bb%5D%5Bc%5D=d',
|
||||
'the default depth is unlimited and does not alter output'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: { c: 'd' } } }, { depth: 2 }),
|
||||
'a%5Bb%5D%5Bc%5D=d',
|
||||
'does not throw when nesting is within the depth limit'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: 'c' } }, { depth: Infinity }),
|
||||
'a%5Bb%5D=c',
|
||||
'an explicit Infinity depth is unlimited'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: { b: { c: { d: 'e' } } } }, { depth: null }),
|
||||
qs.stringify({ a: { b: { c: { d: 'e' } } } }),
|
||||
'a non-number depth is ignored and treated as unlimited'
|
||||
);
|
||||
st.equal(
|
||||
qs.stringify({ a: 'b' }, { depth: 0 }),
|
||||
'a=b',
|
||||
'a flat object serializes with a depth of 0'
|
||||
);
|
||||
|
||||
var deep = {};
|
||||
var cursor = deep;
|
||||
for (var i = 0; i < 250; i++) {
|
||||
cursor.a = {};
|
||||
cursor = cursor.a;
|
||||
}
|
||||
cursor.b = 'c';
|
||||
st.equal(typeof qs.stringify(deep), 'string', 'the default depth serializes deeply nested objects without throwing');
|
||||
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: { b: { c: { d: 'e' } } } }, { depth: 2 }); },
|
||||
RangeError,
|
||||
'throws a RangeError when nesting exceeds the depth limit'
|
||||
);
|
||||
st['throws'](
|
||||
function () { qs.stringify({ a: { b: 'c' } }, { depth: 0 }); },
|
||||
RangeError,
|
||||
'a depth of 0 throws for any nested object'
|
||||
);
|
||||
|
||||
var caught;
|
||||
try {
|
||||
qs.stringify({ a: { b: { c: { d: 'e' } } } }, { depth: 2 });
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
st.ok(caught instanceof RangeError, 'the thrown value is a RangeError');
|
||||
st.equal(caught.message, 'Input depth exceeded depth option of 2', 'the RangeError names the exceeded depth');
|
||||
|
||||
st.end();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user