489 lines
11 KiB
JavaScript
489 lines
11 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const assert = require('assert');
|
||
|
const { transcode } = require('buffer');
|
||
|
const { inspect } = require('util');
|
||
|
|
||
|
const busboy = require('..');
|
||
|
|
||
|
const active = new Map();
|
||
|
|
||
|
const tests = [
|
||
|
{ source: ['foo'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Unassigned value'
|
||
|
},
|
||
|
{ source: ['foo=bar'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'bar',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Assigned value'
|
||
|
},
|
||
|
{ source: ['foo&bar=baz'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['bar',
|
||
|
'baz',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Unassigned and assigned value'
|
||
|
},
|
||
|
{ source: ['foo=bar&baz'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'bar',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['baz',
|
||
|
'',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Assigned and unassigned value'
|
||
|
},
|
||
|
{ source: ['foo=bar&baz=bla'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'bar',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['baz',
|
||
|
'bla',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Two assigned values'
|
||
|
},
|
||
|
{ source: ['foo&bar'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['bar',
|
||
|
'',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Two unassigned values'
|
||
|
},
|
||
|
{ source: ['foo&bar&'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['bar',
|
||
|
'',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Two unassigned values and ampersand'
|
||
|
},
|
||
|
{ source: ['foo+1=bar+baz%2Bquux'],
|
||
|
expected: [
|
||
|
['foo 1',
|
||
|
'bar baz+quux',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Assigned key and value with (plus) space'
|
||
|
},
|
||
|
{ source: ['foo=bar%20baz%21'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'bar baz!',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Assigned value with encoded bytes'
|
||
|
},
|
||
|
{ source: ['foo%20bar=baz%20bla%21'],
|
||
|
expected: [
|
||
|
['foo bar',
|
||
|
'baz bla!',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Assigned value with encoded bytes #2'
|
||
|
},
|
||
|
{ source: ['foo=bar%20baz%21&num=1000'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'bar baz!',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['num',
|
||
|
'1000',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Two assigned values, one with encoded bytes'
|
||
|
},
|
||
|
{ source: [
|
||
|
Array.from(transcode(Buffer.from('foo'), 'utf8', 'utf16le')).map(
|
||
|
(n) => `%${n.toString(16).padStart(2, '0')}`
|
||
|
).join(''),
|
||
|
'=',
|
||
|
Array.from(transcode(Buffer.from('😀!'), 'utf8', 'utf16le')).map(
|
||
|
(n) => `%${n.toString(16).padStart(2, '0')}`
|
||
|
).join(''),
|
||
|
],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'😀!',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'UTF-16LE',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
charset: 'UTF-16LE',
|
||
|
what: 'Encoded value with multi-byte charset'
|
||
|
},
|
||
|
{ source: [
|
||
|
'foo=<',
|
||
|
Array.from(transcode(Buffer.from('©:^þ'), 'utf8', 'latin1')).map(
|
||
|
(n) => `%${n.toString(16).padStart(2, '0')}`
|
||
|
).join(''),
|
||
|
],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'<©:^þ',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'ISO-8859-1',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
charset: 'ISO-8859-1',
|
||
|
what: 'Encoded value with single-byte, ASCII-compatible, non-UTF8 charset'
|
||
|
},
|
||
|
{ source: ['foo=bar&baz=bla'],
|
||
|
expected: [],
|
||
|
what: 'Limits: zero fields',
|
||
|
limits: { fields: 0 }
|
||
|
},
|
||
|
{ source: ['foo=bar&baz=bla'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'bar',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Limits: one field',
|
||
|
limits: { fields: 1 }
|
||
|
},
|
||
|
{ source: ['foo=bar&baz=bla'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'bar',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['baz',
|
||
|
'bla',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Limits: field part lengths match limits',
|
||
|
limits: { fieldNameSize: 3, fieldSize: 3 }
|
||
|
},
|
||
|
{ source: ['foo=bar&baz=bla'],
|
||
|
expected: [
|
||
|
['fo',
|
||
|
'bar',
|
||
|
{ nameTruncated: true,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['ba',
|
||
|
'bla',
|
||
|
{ nameTruncated: true,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Limits: truncated field name',
|
||
|
limits: { fieldNameSize: 2 }
|
||
|
},
|
||
|
{ source: ['foo=bar&baz=bla'],
|
||
|
expected: [
|
||
|
['foo',
|
||
|
'ba',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: true,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['baz',
|
||
|
'bl',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: true,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Limits: truncated field value',
|
||
|
limits: { fieldSize: 2 }
|
||
|
},
|
||
|
{ source: ['foo=bar&baz=bla'],
|
||
|
expected: [
|
||
|
['fo',
|
||
|
'ba',
|
||
|
{ nameTruncated: true,
|
||
|
valueTruncated: true,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['ba',
|
||
|
'bl',
|
||
|
{ nameTruncated: true,
|
||
|
valueTruncated: true,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Limits: truncated field name and value',
|
||
|
limits: { fieldNameSize: 2, fieldSize: 2 }
|
||
|
},
|
||
|
{ source: ['foo=bar&baz=bla'],
|
||
|
expected: [
|
||
|
['fo',
|
||
|
'',
|
||
|
{ nameTruncated: true,
|
||
|
valueTruncated: true,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['ba',
|
||
|
'',
|
||
|
{ nameTruncated: true,
|
||
|
valueTruncated: true,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Limits: truncated field name and zero value limit',
|
||
|
limits: { fieldNameSize: 2, fieldSize: 0 }
|
||
|
},
|
||
|
{ source: ['foo=bar&baz=bla'],
|
||
|
expected: [
|
||
|
['',
|
||
|
'',
|
||
|
{ nameTruncated: true,
|
||
|
valueTruncated: true,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
['',
|
||
|
'',
|
||
|
{ nameTruncated: true,
|
||
|
valueTruncated: true,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Limits: truncated zero field name and zero value limit',
|
||
|
limits: { fieldNameSize: 0, fieldSize: 0 }
|
||
|
},
|
||
|
{ source: ['&'],
|
||
|
expected: [],
|
||
|
what: 'Ampersand'
|
||
|
},
|
||
|
{ source: ['&&&&&'],
|
||
|
expected: [],
|
||
|
what: 'Many ampersands'
|
||
|
},
|
||
|
{ source: ['='],
|
||
|
expected: [
|
||
|
['',
|
||
|
'',
|
||
|
{ nameTruncated: false,
|
||
|
valueTruncated: false,
|
||
|
encoding: 'utf-8',
|
||
|
mimeType: 'text/plain' },
|
||
|
],
|
||
|
],
|
||
|
what: 'Assigned value, empty name and value'
|
||
|
},
|
||
|
{ source: [''],
|
||
|
expected: [],
|
||
|
what: 'Nothing'
|
||
|
},
|
||
|
];
|
||
|
|
||
|
for (const test of tests) {
|
||
|
active.set(test, 1);
|
||
|
|
||
|
const { what } = test;
|
||
|
const charset = test.charset || 'utf-8';
|
||
|
const bb = busboy({
|
||
|
limits: test.limits,
|
||
|
headers: {
|
||
|
'content-type': `application/x-www-form-urlencoded; charset=${charset}`,
|
||
|
},
|
||
|
});
|
||
|
const results = [];
|
||
|
|
||
|
bb.on('field', (key, val, info) => {
|
||
|
results.push([key, val, info]);
|
||
|
});
|
||
|
|
||
|
bb.on('file', () => {
|
||
|
throw new Error(`[${what}] Unexpected file`);
|
||
|
});
|
||
|
|
||
|
bb.on('close', () => {
|
||
|
active.delete(test);
|
||
|
|
||
|
assert.deepStrictEqual(
|
||
|
results,
|
||
|
test.expected,
|
||
|
`[${what}] Results mismatch.\n`
|
||
|
+ `Parsed: ${inspect(results)}\n`
|
||
|
+ `Expected: ${inspect(test.expected)}`
|
||
|
);
|
||
|
});
|
||
|
|
||
|
for (const src of test.source) {
|
||
|
const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src);
|
||
|
bb.write(buf);
|
||
|
}
|
||
|
bb.end();
|
||
|
}
|
||
|
|
||
|
// Byte-by-byte versions
|
||
|
for (let test of tests) {
|
||
|
test = { ...test };
|
||
|
test.what += ' (byte-by-byte)';
|
||
|
active.set(test, 1);
|
||
|
|
||
|
const { what } = test;
|
||
|
const charset = test.charset || 'utf-8';
|
||
|
const bb = busboy({
|
||
|
limits: test.limits,
|
||
|
headers: {
|
||
|
'content-type': `application/x-www-form-urlencoded; charset="${charset}"`,
|
||
|
},
|
||
|
});
|
||
|
const results = [];
|
||
|
|
||
|
bb.on('field', (key, val, info) => {
|
||
|
results.push([key, val, info]);
|
||
|
});
|
||
|
|
||
|
bb.on('file', () => {
|
||
|
throw new Error(`[${what}] Unexpected file`);
|
||
|
});
|
||
|
|
||
|
bb.on('close', () => {
|
||
|
active.delete(test);
|
||
|
|
||
|
assert.deepStrictEqual(
|
||
|
results,
|
||
|
test.expected,
|
||
|
`[${what}] Results mismatch.\n`
|
||
|
+ `Parsed: ${inspect(results)}\n`
|
||
|
+ `Expected: ${inspect(test.expected)}`
|
||
|
);
|
||
|
});
|
||
|
|
||
|
for (const src of test.source) {
|
||
|
const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src);
|
||
|
for (let i = 0; i < buf.length; ++i)
|
||
|
bb.write(buf.slice(i, i + 1));
|
||
|
}
|
||
|
bb.end();
|
||
|
}
|
||
|
|
||
|
{
|
||
|
let exception = false;
|
||
|
process.once('uncaughtException', (ex) => {
|
||
|
exception = true;
|
||
|
throw ex;
|
||
|
});
|
||
|
process.on('exit', () => {
|
||
|
if (exception || active.size === 0)
|
||
|
return;
|
||
|
process.exitCode = 1;
|
||
|
console.error('==========================');
|
||
|
console.error(`${active.size} test(s) did not finish:`);
|
||
|
console.error('==========================');
|
||
|
console.error(Array.from(active.keys()).map((v) => v.what).join('\n'));
|
||
|
});
|
||
|
}
|