diff options
Diffstat (limited to 'fastify-busboy/test')
61 files changed, 2965 insertions, 0 deletions
diff --git a/fastify-busboy/test/busboy-constructor.test.js b/fastify-busboy/test/busboy-constructor.test.js new file mode 100644 index 0000000..8607789 --- /dev/null +++ b/fastify-busboy/test/busboy-constructor.test.js @@ -0,0 +1,75 @@ +'use strict' + +const Busboy = require('../lib/main') +const { test } = require('tap') + +test('busboy-constructor - should throw an Error if no options are provided', t => { + t.plan(1) + + t.throws(() => new Busboy(), new Error('Busboy expected an options-Object.')) +}) + +test('busboy-constructor - should throw an Error if options does not contain headers', t => { + t.plan(1) + + t.throws(() => new Busboy({}), new Error('Busboy expected an options-Object with headers-attribute.')) +}) + +test('busboy-constructor - if busboy is called without new-operator, still creates a busboy instance', t => { + t.plan(1) + + const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.type(busboyInstance, Busboy) +}) + +test('busboy-constructor - should throw an Error if content-type is not set', t => { + t.plan(1) + + t.throws(() => new Busboy({ headers: {} }), new Error('Missing Content-Type-header.')) +}) + +test('busboy-constructor - should throw an Error if content-type is unsupported', t => { + t.plan(1) + + t.throws(() => new Busboy({ headers: { 'content-type': 'unsupported' } }), new Error('Unsupported Content-Type.')) +}) + +test('busboy-constructor - should not throw an Error if content-type is urlencoded', t => { + t.plan(1) + + t.doesNotThrow(() => new Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } })) +}) + +test('busboy-constructor - if busboy is called without stream options autoDestroy is set to false', t => { + t.plan(1) + + const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.equal(busboyInstance._writableState.autoDestroy, false) +}) + +test('busboy-constructor - if busboy is called with invalid value for stream option highWaterMark we should throw', t => { + t.plan(1) + + t.throws(() => Busboy({ highWaterMark: 'not_allowed_value_for_highWaterMark', headers: { 'content-type': 'application/x-www-form-urlencoded' } }), new Error('not_allowed_value_for_highWaterMark')) +}) + +test('busboy-constructor - if busboy is called with stream options and autoDestroy:true, autoDestroy should be set to true', t => { + t.plan(1) + + const busboyInstance = Busboy({ autoDestroy: true, headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.equal(busboyInstance._writableState.autoDestroy, true) +}) + +test('busboy-constructor - busboy should be initialized with private attribute _done set as false', t => { + t.plan(1) + + const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.equal(busboyInstance._done, false) +}) + +test('busboy-constructor - busboy should be initialized with private attribute _finished set as false', t => { + t.plan(1) + + const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.equal(busboyInstance._finished, false) +}) diff --git a/fastify-busboy/test/decoder.test.js b/fastify-busboy/test/decoder.test.js new file mode 100644 index 0000000..fa4ce69 --- /dev/null +++ b/fastify-busboy/test/decoder.test.js @@ -0,0 +1,98 @@ +'use strict' + +const { test } = require('tap') +const Decoder = require('../lib/utils/Decoder') + +test('Decoder', t => { + const tests = + [ + { + source: ['Hello world'], + expected: 'Hello world', + what: 'No encoded bytes' + }, + { + source: ['Hello%20world'], + expected: 'Hello world', + what: 'One full encoded byte' + }, + { + source: ['Hello%20world%21'], + expected: 'Hello world!', + what: 'Two full encoded bytes' + }, + { + source: ['Hello%', '20world'], + expected: 'Hello world', + what: 'One full encoded byte split #1' + }, + { + source: ['Hello%2', '0world'], + expected: 'Hello world', + what: 'One full encoded byte split #2' + }, + { + source: ['Hello%20', 'world'], + expected: 'Hello world', + what: 'One full encoded byte (concat)' + }, + { + source: ['Hello%2Qworld'], + expected: 'Hello%2Qworld', + what: 'Malformed encoded byte #1' + }, + { + source: ['Hello%world'], + expected: 'Hello%world', + what: 'Malformed encoded byte #2' + }, + { + source: ['Hello+world'], + expected: 'Hello world', + what: 'Plus to space' + }, + { + source: ['Hello+world%21'], + expected: 'Hello world!', + what: 'Plus and encoded byte' + }, + { + source: ['5%2B5%3D10'], + expected: '5+5=10', + what: 'Encoded plus' + }, + { + source: ['5+%2B+5+%3D+10'], + expected: '5 + 5 = 10', + what: 'Spaces and encoded plus' + } + ] + t.plan(tests.length + 1) + + tests.forEach((v) => { + t.test(v.what, t => { + t.plan(1) + + const dec = new Decoder() + let result = '' + v.source.forEach(function (s) { + result += dec.write(s) + }) + const msg = 'Decoded string mismatch.\n' + + 'Saw: ' + result + '\n' + + 'Expected: ' + v.expected + t.strictSame(result, v.expected, msg) + }) + }) + + t.test('reset sets internal buffer to undefined', t => { + t.plan(2) + + const dec = new Decoder() + dec.write('Hello+world%2') + + t.notSame(dec.buffer, undefined) + dec.reset() + t.equal(dec.buffer, undefined) + }) +}) diff --git a/fastify-busboy/test/dicer-constructor.test.js b/fastify-busboy/test/dicer-constructor.test.js new file mode 100644 index 0000000..e0e6a6c --- /dev/null +++ b/fastify-busboy/test/dicer-constructor.test.js @@ -0,0 +1,22 @@ +'use strict' + +const { test } = require('tap') +const Dicer = require('../deps/dicer/lib/Dicer') + +test('dicer-constructor', t => { + t.plan(2) + + t.test('should throw an Error when no options parameter is supplied to Dicer', t => { + t.plan(1) + + t.throws(() => new Dicer(), new Error('Boundary required')) + }) + + t.test('without new operator a new dicer instance will be initialized', t => { + t.plan(1) + + t.type(Dicer({ + boundary: '----boundary' + }), Dicer) + }) +}) diff --git a/fastify-busboy/test/dicer-endfinish.test.js b/fastify-busboy/test/dicer-endfinish.test.js new file mode 100644 index 0000000..4718076 --- /dev/null +++ b/fastify-busboy/test/dicer-endfinish.test.js @@ -0,0 +1,96 @@ +'use strict' + +const Dicer = require('../deps/dicer/lib/Dicer') +const { test } = require('tap') + +test('dicer-endfinish', t => { + t.plan(1) + + t.test('should properly handle finish', t => { + t.plan(4) + + const CRLF = '\r\n' + const boundary = 'boundary' + + const writeSep = '--' + boundary + + const writePart = [ + writeSep, + 'Content-Type: text/plain', + 'Content-Length: 0' + ].join(CRLF) + + CRLF + CRLF + + 'some data' + CRLF + + const writeEnd = '--' + CRLF + + let firedEnd = false + let firedFinish = false + + const dicer = new Dicer({ boundary }) + dicer.on('part', partListener) + dicer.on('finish', finishListener) + dicer.write(writePart + writeSep) + + function partListener (partReadStream) { + partReadStream.on('data', function () { }) + partReadStream.on('end', partEndListener) + } + function partEndListener () { + firedEnd = true + setImmediate(afterEnd) + } + function afterEnd () { + dicer.end(writeEnd) + setImmediate(afterWrite) + } + function finishListener () { + t.ok(firedEnd, 'end before finishing') + firedFinish = true + test2() + } + function afterWrite () { + t.ok(firedFinish, 'Failed to finish') + } + + let isPausePush = true + + let firedPauseCallback = false + let firedPauseFinish = false + + let dicer2 = null + + function test2 () { + dicer2 = new Dicer({ boundary }) + dicer2.on('part', pausePartListener) + dicer2.on('finish', pauseFinish) + dicer2.write(writePart + writeSep, 'utf8', pausePartCallback) + setImmediate(pauseAfterWrite) + } + function pausePartListener (partReadStream) { + partReadStream.on('data', function () { }) + partReadStream.on('end', function () { }) + const realPush = partReadStream.push + partReadStream.push = function fakePush () { + realPush.apply(partReadStream, arguments) + if (!isPausePush) { return true } + isPausePush = false + return false + } + } + function pauseAfterWrite () { + dicer2.end(writeEnd) + setImmediate(pauseAfterEnd) + } + function pauseAfterEnd () { + t.ok(firedPauseCallback, 'Called callback after pause') + t.ok(firedPauseFinish, 'Finish after pause') + } + function pauseFinish () { + firedPauseFinish = true + } + function pausePartCallback () { + firedPauseCallback = true + } + }) +}) diff --git a/fastify-busboy/test/dicer-export.test.js b/fastify-busboy/test/dicer-export.test.js new file mode 100644 index 0000000..05df4e6 --- /dev/null +++ b/fastify-busboy/test/dicer-export.test.js @@ -0,0 +1,24 @@ +'use strict' + +const { test } = require('tap') +const { Dicer } = require('../lib/main') + +test('dicer-export', t => { + t.plan(2) + + t.test('without new operator a new dicer instance will be initialized', t => { + t.plan(1) + + t.type(Dicer({ + boundary: '----boundary' + }), Dicer) + }) + + t.test('with new operator a new dicer instance will be initialized', t => { + t.plan(1) + + t.type(new Dicer({ + boundary: '----boundary' + }), Dicer) + }) +}) diff --git a/fastify-busboy/test/dicer-headerparser.test.js b/fastify-busboy/test/dicer-headerparser.test.js new file mode 100644 index 0000000..73da283 --- /dev/null +++ b/fastify-busboy/test/dicer-headerparser.test.js @@ -0,0 +1,192 @@ +'use strict' + +const { test } = require('tap') +const HeaderParser = require('../deps/dicer/lib/HeaderParser') + +test('dicer-headerparser', t => { + const DCRLF = '\r\n\r\n' + const MAXED_BUFFER = Buffer.allocUnsafe(128 * 1024) + MAXED_BUFFER.fill(0x41) // 'A' + + const tests = [ + { + source: DCRLF, + expected: {}, + what: 'No header' + }, + { + source: ['Content-Type:\t text/plain', + 'Content-Length:0' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'], 'content-length': ['0'] }, + what: 'Value spacing' + }, + { + source: ['Content-Type:\t text/plain', + 'Content-Length:0' + ].join('\r\n') + DCRLF, + cfg: { + maxHeaderPairs: 0 + }, + expected: {}, + what: 'should enforce maxHeaderPairs of 0' + }, + { + source: ['Content-Type:\t text/plain', + 'Content-Length:0' + ].join('\r\n') + DCRLF, + cfg: { + maxHeaderPairs: 1 + }, + expected: { 'content-type': [' text/plain'] }, + what: 'should enforce maxHeaderPairs of 1' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: {}, + cfg: { + maxHeaderSize: 0 + }, + what: 'should enforce maxHeaderSize of 0' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plai'] }, + cfg: { + maxHeaderSize: 25 + }, + what: 'should enforce maxHeaderSize of 25' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'] }, + cfg: { + maxHeaderSize: 31 + }, + what: 'should enforce maxHeaderSize of 31 and ignore the second header' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'], foo: [''] }, + cfg: { + maxHeaderSize: 32 + }, + what: 'should enforce maxHeaderSize of 32 and only add key of second header' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'], foo: ['\r'] }, + cfg: { + maxHeaderSize: 33 + }, + what: 'should enforce maxHeaderSize of 32 and get only first character of second pair' + }, + { + source: ['Content-Type:\r\n text/plain', + ' : ' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain : '] }, + what: 'should not break if invalid header pair (colon exists but empty key and value) is provided' + }, + { + source: ['Content-Type:\r\n text/plain', + 'FoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobaz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'] }, + what: 'should not break if invalid header pair (no distinctive colon) is provided' + }, + { + source: ['Content-Type:\r\n text/plain', + ':FoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobaz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'] }, + what: 'should not break if invalid header pair (no key) is provided' + }, + { + source: ['Content-Type:\t text/plain', + 'Content-Length:0' + ].join('\r\n') + DCRLF, + cfg: { + maxHeaderPairs: 2 + }, + expected: { 'content-type': [' text/plain'], 'content-length': ['0'] }, + what: 'should enforce maxHeaderPairs of 2' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'], foo: [' bar baz'] }, + what: 'Folded values' + }, + { + source: [ + 'Foo: bar', + 'Foo: baz' + ].join('\r\n') + DCRLF, + expected: { foo: ['bar', 'baz'] }, + what: 'Folded values' + }, + { + source: ['Content-Type:', + 'Foo: ' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [''], foo: [''] }, + what: 'Empty values' + }, + { + source: MAXED_BUFFER.toString('ascii') + DCRLF, + expected: {}, + what: 'Max header size (single chunk)' + }, + { + source: ['ABCDEFGHIJ', MAXED_BUFFER.toString('ascii'), DCRLF], + expected: {}, + what: 'Max header size (multiple chunks #1)' + }, + { + source: [MAXED_BUFFER.toString('ascii'), MAXED_BUFFER.toString('ascii'), DCRLF], + expected: {}, + what: 'Max header size (multiple chunk #2)' + } + ] + + t.plan(tests.length) + + tests.forEach(function (v) { + t.test(v.what, t => { + t.plan(4) + + const cfg = { + ...v.cfg + } + + const parser = Object.keys(cfg).length ? new HeaderParser(cfg) : new HeaderParser() + let fired = false + + parser.on('header', function (header) { + t.ok(!fired, `${v.what}: Header event fired more than once`) + fired = true + t.strictSame(header, + v.expected, + `${v.what}: Parsed result mismatch`) + }) + if (!Array.isArray(v.source)) { v.source = [v.source] } + v.source.forEach(function (s) { + parser.push(s) + }) + t.ok(fired, `${v.what}: Did not receive header from parser`) + t.pass() + }) + }) +}) diff --git a/fastify-busboy/test/dicer-malformed-header.test.js b/fastify-busboy/test/dicer-malformed-header.test.js new file mode 100644 index 0000000..c25ccdd --- /dev/null +++ b/fastify-busboy/test/dicer-malformed-header.test.js @@ -0,0 +1,29 @@ +'use strict' + +const { test } = require('tap') +const Dicer = require('../deps/dicer/lib/Dicer') + +test('dicer-malformed-header', t => { + t.plan(1) + + t.test('should gracefully handle headers with leading whitespace', t => { + t.plan(3) + const d = new Dicer({ boundary: '----WebKitFormBoundaryoo6vortfDzBsDiro' }) + + d.on('part', function (p) { + p.on('header', function (header) { + t.hasProp(header, ' content-disposition') + t.strictSame(header[' content-disposition'], ['form-data; name="bildbeschreibung"']) + }) + p.on('data', function (data) { + }) + p.on('end', function () { + }) + }) + d.on('finish', function () { + t.pass() + }) + + d.write(Buffer.from('------WebKitFormBoundaryoo6vortfDzBsDiro\r\n Content-Disposition: form-data; name="bildbeschreibung"\r\n\r\n\r\n------WebKitFormBoundaryoo6vortfDzBsDiro--')) + }) +}) diff --git a/fastify-busboy/test/dicer-multipart-extra-trailer.test.js b/fastify-busboy/test/dicer-multipart-extra-trailer.test.js new file mode 100644 index 0000000..335605a --- /dev/null +++ b/fastify-busboy/test/dicer-multipart-extra-trailer.test.js @@ -0,0 +1,82 @@ +'use strict' + +const { test } = require('tap') +const Dicer = require('../deps/dicer/lib/Dicer') +const fs = require('fs') +const path = require('path') + +const FIXTURES_ROOT = path.join(__dirname, 'fixtures/') + +test('dicer-multipart-extra-trailer', t => { + t.plan(1) + + t.test('Extra trailer data pushed after finished', t => { + t.plan(5) + const fixtureBase = FIXTURES_ROOT + 'many' + let n = 0 + const buffer = Buffer.allocUnsafe(16) + const state = { parts: [] } + + const fd = fs.openSync(fixtureBase + '/original', 'r') + + const dicer = new Dicer({ boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' }) + let error + let finishes = 0 + let trailerEmitted = false + + dicer.on('part', function (p) { + const part = { + body: undefined, + bodylen: 0, + error: undefined, + header: undefined + } + + p.on('header', function (h) { + part.header = h + }).on('data', function (data) { + // make a copy because we are using readSync which re-uses a buffer ... + const copy = Buffer.allocUnsafe(data.length) + data.copy(copy) + data = copy + if (!part.body) { part.body = [data] } else { part.body.push(data) } + part.bodylen += data.length + }).on('error', function (err) { + part.error = err + t.fail() + }).on('end', function () { + if (part.body) { part.body = Buffer.concat(part.body, part.bodylen) } + state.parts.push(part) + }) + }).on('error', function (err) { + error = err + }).on('trailer', function (data) { + trailerEmitted = true + t.equal(data.toString(), 'Extra', 'trailer should contain the extra data') + }).on('finish', function () { + t.ok(finishes++ === 0, makeMsg('Extra trailer data pushed after finished', 'finish emitted multiple times')) + t.ok(trailerEmitted, makeMsg('Extra trailer data pushed after finished', 'should have emitted trailer')) + + t.ok(error === undefined, makeMsg('Extra trailer data pushed after finished', 'Unexpected error')) + + t.pass() + }) + + while (true) { + n = fs.readSync(fd, buffer, 0, buffer.length, null) + if (n === 0) { + setTimeout(function () { + dicer.write('\r\n\r\n\r\n') + dicer.end() + }, 50) + break + } + dicer.write(n === buffer.length ? buffer : buffer.slice(0, n)) + } + fs.closeSync(fd) + }) +}) + +function makeMsg (what, msg) { + return what + ': ' + msg +} diff --git a/fastify-busboy/test/dicer-multipart-nolisteners.test.js b/fastify-busboy/test/dicer-multipart-nolisteners.test.js new file mode 100644 index 0000000..1e311ba --- /dev/null +++ b/fastify-busboy/test/dicer-multipart-nolisteners.test.js @@ -0,0 +1,44 @@ +'use strict' + +const Dicer = require('../deps/dicer/lib/Dicer') +const { test } = require('tap') +const fs = require('fs') +const path = require('path') + +const FIXTURES_ROOT = path.join(__dirname, 'fixtures/') + +test('dicer-multipart-nolisteners', t => { + t.plan(1) + + t.test('No preamble or part listeners', t => { + t.plan(3) + const fixtureBase = path.resolve(FIXTURES_ROOT, 'many') + let n = 0 + const buffer = Buffer.allocUnsafe(16) + + const fd = fs.openSync(fixtureBase + '/original', 'r') + + const dicer = new Dicer({ boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' }) + let error + let finishes = 0 + + dicer.on('error', function (err) { + error = err + }).on('finish', function () { + t.ok(finishes++ === 0, 'finish emitted multiple times') + + t.ok(error === undefined, `Unexpected error: ${error}`) + t.pass() + }) + + while (true) { + n = fs.readSync(fd, buffer, 0, buffer.length, null) + if (n === 0) { + dicer.end() + break + } + dicer.write(n === buffer.length ? buffer : buffer.slice(0, n)) + } + fs.closeSync(fd) + }) +}) diff --git a/fastify-busboy/test/dicer-multipart.test.js b/fastify-busboy/test/dicer-multipart.test.js new file mode 100644 index 0000000..c35c4d0 --- /dev/null +++ b/fastify-busboy/test/dicer-multipart.test.js @@ -0,0 +1,223 @@ +'use strict' + +const Dicer = require('../deps/dicer/lib/Dicer') +const assert = require('node:assert') +const fs = require('node:fs') +const path = require('node:path') +const inspect = require('node:util').inspect +const { test } = require('tap') + +const FIXTURES_ROOT = path.join(__dirname, 'fixtures/') + +test('dicer-multipart', t => { + const tests = + [ + { + source: 'nested', + opts: { boundary: 'AaB03x' }, + chsize: 32, + nparts: 2, + what: 'One nested multipart' + }, + { + source: 'many', + opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' }, + chsize: 16, + nparts: 7, + what: 'Many parts' + }, + { + source: 'many-wrongboundary', + opts: { boundary: 'LOLOLOL' }, + chsize: 8, + nparts: 0, + dicerError: true, + what: 'Many parts, wrong boundary' + }, + { + source: 'many-noend', + opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' }, + chsize: 16, + nparts: 7, + npartErrors: 1, + dicerError: true, + what: 'Many parts, end boundary missing, 1 file open' + }, + { + source: 'nested-full', + opts: { boundary: 'AaB03x', headerFirst: true }, + chsize: 32, + nparts: 2, + what: 'One nested multipart with preceding header' + }, + { + source: 'nested-full', + opts: { headerFirst: true }, + chsize: 32, + nparts: 2, + setBoundary: 'AaB03x', + what: 'One nested multipart with preceding header, using setBoundary' + } + ] + + t.plan(tests.length) + + tests.forEach(function (v) { + t.test(v.what, t => { + t.plan(1) + const fixtureBase = FIXTURES_ROOT + v.source + const state = { parts: [], preamble: undefined } + + const dicer = new Dicer(v.opts) + let error + let partErrors = 0 + let finishes = 0 + + dicer.on('preamble', function (p) { + const preamble = { + body: undefined, + bodylen: 0, + error: undefined, + header: undefined + } + + p.on('header', function (h) { + preamble.header = h + if (v.setBoundary) { dicer.setBoundary(v.setBoundary) } + }).on('data', function (data) { + // make a copy because we are using readSync which re-uses a buffer ... + const copy = Buffer.allocUnsafe(data.length) + data.copy(copy) + data = copy + if (!preamble.body) { preamble.body = [data] } else { preamble.body.push(data) } + preamble.bodylen += data.length + }).on('error', function (err) { + preamble.error = err + }).on('end', function () { + if (preamble.body) { preamble.body = Buffer.concat(preamble.body, preamble.bodylen) } + if (preamble.body || preamble.header) { state.preamble = preamble } + }) + }) + dicer.on('part', function (p) { + const part = { + body: undefined, + bodylen: 0, + error: undefined, + header: undefined + } + + p.on('header', function (h) { + part.header = h + }).on('data', function (data) { + if (!part.body) { part.body = [data] } else { part.body.push(data) } + part.bodylen += data.length + }).on('error', function (err) { + part.error = err + ++partErrors + }).on('end', function () { + if (part.body) { part.body = Buffer.concat(part.body, part.bodylen) } + state.parts.push(part) + }) + }).on('error', function (err) { + error = err + }).on('finish', function () { + assert(finishes++ === 0, makeMsg(v.what, 'finish emitted multiple times')) + + if (v.dicerError) { assert(error !== undefined, makeMsg(v.what, 'Expected error')) } else { assert(error === undefined, makeMsg(v.what, 'Unexpected error: ' + error)) } + + let preamble + if (fs.existsSync(fixtureBase + '/preamble')) { + const prebody = fs.readFileSync(fixtureBase + '/preamble') + if (prebody.length) { + preamble = { + body: prebody, + bodylen: prebody.length, + error: undefined, + header: undefined + } + } + } + if (fs.existsSync(fixtureBase + '/preamble.header')) { + const prehead = JSON.parse(fs.readFileSync(fixtureBase + + '/preamble.header', 'binary')) + if (!preamble) { + preamble = { + body: undefined, + bodylen: 0, + error: undefined, + header: prehead + } + } else { preamble.header = prehead } + } + if (fs.existsSync(fixtureBase + '/preamble.error')) { + const err = new Error(fs.readFileSync(fixtureBase + + '/preamble.error', 'binary')) + if (!preamble) { + preamble = { + body: undefined, + bodylen: 0, + error: err, + header: undefined + } + } else { preamble.error = err } + } + + assert.deepEqual(state.preamble, + preamble, + makeMsg(v.what, + 'Preamble mismatch:\nActual:' + + inspect(state.preamble) + + '\nExpected: ' + + inspect(preamble))) + + assert.equal(state.parts.length, + v.nparts, + makeMsg(v.what, + 'Part count mismatch:\nActual: ' + + state.parts.length + + '\nExpected: ' + + v.nparts)) + + if (!v.npartErrors) { v.npartErrors = 0 } + assert.equal(partErrors, + v.npartErrors, + makeMsg(v.what, + 'Part errors mismatch:\nActual: ' + + partErrors + + '\nExpected: ' + + v.npartErrors)) + + for (let i = 0, header, body; i < v.nparts; ++i) { + if (fs.existsSync(fixtureBase + '/part' + (i + 1))) { + body = fs.readFileSync(fixtureBase + '/part' + (i + 1)) + if (body.length === 0) { body = undefined } + } else { body = undefined } + assert.deepEqual(state.parts[i].body, + body, + makeMsg(v.what, + 'Part #' + (i + 1) + ' body mismatch')) + if (fs.existsSync(fixtureBase + '/part' + (i + 1) + '.header')) { + header = fs.readFileSync(fixtureBase + + '/part' + (i + 1) + '.header', 'binary') + header = JSON.parse(header) + } else { header = undefined } + assert.deepEqual(state.parts[i].header, + header, + makeMsg(v.what, + 'Part #' + (i + 1) + + ' parsed header mismatch:\nActual: ' + + inspect(state.parts[i].header) + + '\nExpected: ' + + inspect(header))) + } + t.pass() + }) + + fs.createReadStream(fixtureBase + '/original').pipe(dicer) + }) + }) +}) + +function makeMsg (what, msg) { + return what + ': ' + msg +} diff --git a/fastify-busboy/test/fixtures/many-noend/original b/fastify-busboy/test/fixtures/many-noend/original new file mode 100644 index 0000000..ad9f0cc --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/original @@ -0,0 +1,31 @@ +------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
diff --git a/fastify-busboy/test/fixtures/many-noend/part1 b/fastify-busboy/test/fixtures/many-noend/part1 new file mode 100644 index 0000000..a232311 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part1 @@ -0,0 +1 @@ +put
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part1.header b/fastify-busboy/test/fixtures/many-noend/part1.header new file mode 100644 index 0000000..5e6bbe5 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part1.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"_method\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part2 b/fastify-busboy/test/fixtures/many-noend/part2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part2 diff --git a/fastify-busboy/test/fixtures/many-noend/part2.header b/fastify-busboy/test/fixtures/many-noend/part2.header new file mode 100644 index 0000000..5b53966 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part2.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[blog]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part3 b/fastify-busboy/test/fixtures/many-noend/part3 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part3 diff --git a/fastify-busboy/test/fixtures/many-noend/part3.header b/fastify-busboy/test/fixtures/many-noend/part3.header new file mode 100644 index 0000000..579e16e --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part3.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[public_email]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part4 b/fastify-busboy/test/fixtures/many-noend/part4 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part4 diff --git a/fastify-busboy/test/fixtures/many-noend/part4.header b/fastify-busboy/test/fixtures/many-noend/part4.header new file mode 100644 index 0000000..b41be09 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part4.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[interests]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part5 b/fastify-busboy/test/fixtures/many-noend/part5 new file mode 100644 index 0000000..f2bb979 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part5 @@ -0,0 +1,3 @@ +hello
+
+"quote"
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part5.header b/fastify-busboy/test/fixtures/many-noend/part5.header new file mode 100644 index 0000000..92e417f --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part5.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[bio]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part6 b/fastify-busboy/test/fixtures/many-noend/part6 new file mode 100644 index 0000000..f0f5479 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part6 @@ -0,0 +1 @@ +Save
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part6.header b/fastify-busboy/test/fixtures/many-noend/part6.header new file mode 100644 index 0000000..65a68a9 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part6.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"commit\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part7.header b/fastify-busboy/test/fixtures/many-noend/part7.header new file mode 100644 index 0000000..25171e8 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part7.header @@ -0,0 +1,2 @@ +{"content-disposition": ["form-data; name=\"media\"; filename=\"\""],
+ "content-type": ["application/octet-stream"]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-wrongboundary/original b/fastify-busboy/test/fixtures/many-wrongboundary/original new file mode 100644 index 0000000..859770c --- /dev/null +++ b/fastify-busboy/test/fixtures/many-wrongboundary/original @@ -0,0 +1,32 @@ +------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-wrongboundary/preamble b/fastify-busboy/test/fixtures/many-wrongboundary/preamble new file mode 100644 index 0000000..6e4bcc6 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-wrongboundary/preamble @@ -0,0 +1,33 @@ +
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-wrongboundary/preamble.error b/fastify-busboy/test/fixtures/many-wrongboundary/preamble.error new file mode 100644 index 0000000..15f4c89 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-wrongboundary/preamble.error @@ -0,0 +1 @@ +Preamble terminated early due to unexpected end of multipart data
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/original b/fastify-busboy/test/fixtures/many/original new file mode 100644 index 0000000..779c5cb --- /dev/null +++ b/fastify-busboy/test/fixtures/many/original @@ -0,0 +1,32 @@ +------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR--Extra
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part1 b/fastify-busboy/test/fixtures/many/part1 new file mode 100644 index 0000000..a232311 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part1 @@ -0,0 +1 @@ +put
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part1.header b/fastify-busboy/test/fixtures/many/part1.header new file mode 100644 index 0000000..5e6bbe5 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part1.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"_method\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part2 b/fastify-busboy/test/fixtures/many/part2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part2 diff --git a/fastify-busboy/test/fixtures/many/part2.header b/fastify-busboy/test/fixtures/many/part2.header new file mode 100644 index 0000000..5b53966 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part2.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[blog]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part3 b/fastify-busboy/test/fixtures/many/part3 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part3 diff --git a/fastify-busboy/test/fixtures/many/part3.header b/fastify-busboy/test/fixtures/many/part3.header new file mode 100644 index 0000000..579e16e --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part3.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[public_email]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part4 b/fastify-busboy/test/fixtures/many/part4 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part4 diff --git a/fastify-busboy/test/fixtures/many/part4.header b/fastify-busboy/test/fixtures/many/part4.header new file mode 100644 index 0000000..b41be09 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part4.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[interests]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part5 b/fastify-busboy/test/fixtures/many/part5 new file mode 100644 index 0000000..f2bb979 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part5 @@ -0,0 +1,3 @@ +hello
+
+"quote"
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part5.header b/fastify-busboy/test/fixtures/many/part5.header new file mode 100644 index 0000000..92e417f --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part5.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[bio]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part6 b/fastify-busboy/test/fixtures/many/part6 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part6 diff --git a/fastify-busboy/test/fixtures/many/part6.header b/fastify-busboy/test/fixtures/many/part6.header new file mode 100644 index 0000000..25171e8 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part6.header @@ -0,0 +1,2 @@ +{"content-disposition": ["form-data; name=\"media\"; filename=\"\""],
+ "content-type": ["application/octet-stream"]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part7 b/fastify-busboy/test/fixtures/many/part7 new file mode 100644 index 0000000..f0f5479 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part7 @@ -0,0 +1 @@ +Save
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part7.header b/fastify-busboy/test/fixtures/many/part7.header new file mode 100644 index 0000000..65a68a9 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part7.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"commit\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/original b/fastify-busboy/test/fixtures/nested-full/original new file mode 100644 index 0000000..3044550 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/original @@ -0,0 +1,24 @@ +User-Agent: foo bar baz
+Content-Type: multipart/form-data; boundary=AaB03x
+
+--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="files"
+Content-Type: multipart/mixed, boundary=BbC04y
+
+--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
+--AaB03x--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/part1 b/fastify-busboy/test/fixtures/nested-full/part1 new file mode 100644 index 0000000..ba0e162 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/part1 @@ -0,0 +1 @@ +bar
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/part1.header b/fastify-busboy/test/fixtures/nested-full/part1.header new file mode 100644 index 0000000..03bd093 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/part1.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"foo\""]}
diff --git a/fastify-busboy/test/fixtures/nested-full/part2 b/fastify-busboy/test/fixtures/nested-full/part2 new file mode 100644 index 0000000..2d4deb5 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/part2 @@ -0,0 +1,12 @@ +--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/part2.header b/fastify-busboy/test/fixtures/nested-full/part2.header new file mode 100644 index 0000000..bbe4513 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/part2.header @@ -0,0 +1,2 @@ +{"content-disposition": ["form-data; name=\"files\""],
+ "content-type": ["multipart/mixed, boundary=BbC04y"]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/preamble.header b/fastify-busboy/test/fixtures/nested-full/preamble.header new file mode 100644 index 0000000..2815341 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/preamble.header @@ -0,0 +1,2 @@ +{"user-agent": ["foo bar baz"],
+ "content-type": ["multipart/form-data; boundary=AaB03x"]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested/original b/fastify-busboy/test/fixtures/nested/original new file mode 100644 index 0000000..380f451 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/original @@ -0,0 +1,21 @@ +--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="files"
+Content-Type: multipart/mixed, boundary=BbC04y
+
+--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
+--AaB03x--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested/part1 b/fastify-busboy/test/fixtures/nested/part1 new file mode 100644 index 0000000..ba0e162 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/part1 @@ -0,0 +1 @@ +bar
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested/part1.header b/fastify-busboy/test/fixtures/nested/part1.header new file mode 100644 index 0000000..03bd093 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/part1.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"foo\""]}
diff --git a/fastify-busboy/test/fixtures/nested/part2 b/fastify-busboy/test/fixtures/nested/part2 new file mode 100644 index 0000000..2d4deb5 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/part2 @@ -0,0 +1,12 @@ +--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested/part2.header b/fastify-busboy/test/fixtures/nested/part2.header new file mode 100644 index 0000000..bbe4513 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/part2.header @@ -0,0 +1,2 @@ +{"content-disposition": ["form-data; name=\"files\""],
+ "content-type": ["multipart/mixed, boundary=BbC04y"]}
\ No newline at end of file diff --git a/fastify-busboy/test/get-limit.test.js b/fastify-busboy/test/get-limit.test.js new file mode 100644 index 0000000..76a2997 --- /dev/null +++ b/fastify-busboy/test/get-limit.test.js @@ -0,0 +1,34 @@ +'use strict' + +const getLimit = require('../lib/utils/getLimit') +const { test } = require('tap') + +test('Get limit', t => { + t.plan(2) + + t.test('Correctly resolves limits', t => { + t.plan(8) + t.strictSame(getLimit(undefined, 'fieldSize', 1), 1) + t.strictSame(getLimit(undefined, 'fileSize', Infinity), Infinity) + + t.strictSame(getLimit({}, 'fieldSize', 1), 1) + t.strictSame(getLimit({}, 'fileSize', Infinity), Infinity) + t.strictSame(getLimit({ fieldSize: null }, 'fieldSize', 1), 1) + t.strictSame(getLimit({ fileSize: null }, 'fileSize', Infinity), Infinity) + + t.strictSame(getLimit({ fieldSize: 0 }, 'fieldSize', 1), 0) + t.strictSame(getLimit({ fileSize: 2 }, 'fileSize', 1), 2) + }) + + t.test('Throws an error on incorrect limits', t => { + t.plan(2) + + t.throws(function () { + getLimit({ fieldSize: '1' }, 'fieldSize', 1) + }, new Error('Limit fieldSize is not a valid number')) + + t.throws(function () { + getLimit({ fieldSize: NaN }, 'fieldSize', 1) + }, new Error('Limit fieldSize is not a valid number')) + }) +}) diff --git a/fastify-busboy/test/multipart-stream-pause.test.js b/fastify-busboy/test/multipart-stream-pause.test.js new file mode 100644 index 0000000..856cf71 --- /dev/null +++ b/fastify-busboy/test/multipart-stream-pause.test.js @@ -0,0 +1,82 @@ +'use strict' + +const { inspect } = require('util') +const { test } = require('tap') + +const Busboy = require('..') + +const BOUNDARY = 'u2KxIV5yF1y+xUspOQCCZopaVgeV6Jxihv35XQJmuTx8X3sh' + +function formDataSection (key, value) { + return Buffer.from('\r\n--' + BOUNDARY + + '\r\nContent-Disposition: form-data; name="' + + key + '"\r\n\r\n' + value) +} +function formDataFile (key, filename, contentType) { + return Buffer.concat([ + Buffer.from('\r\n--' + BOUNDARY + '\r\n'), + Buffer.from('Content-Disposition: form-data; name="' + + key + '"; filename="' + filename + '"\r\n'), + Buffer.from('Content-Type: ' + contentType + '\r\n\r\n'), + Buffer.allocUnsafe(100000) + ]) +} + +test('multipart-stream-pause - processes stream correctly', t => { + t.plan(6) + const reqChunks = [ + Buffer.concat([ + formDataFile('file', 'file.bin', 'application/octet-stream'), + formDataSection('foo', 'foo value') + ]), + formDataSection('bar', 'bar value'), + Buffer.from('\r\n--' + BOUNDARY + '--\r\n') + ] + const busboy = new Busboy({ + headers: { + 'content-type': 'multipart/form-data; boundary=' + BOUNDARY + } + }) + let finishes = 0 + const results = [] + const expected = [ + ['file', 'file', 'file.bin', '7bit', 'application/octet-stream'], + ['field', 'foo', 'foo value', false, false, '7bit', 'text/plain'], + ['field', 'bar', 'bar value', false, false, '7bit', 'text/plain'] + ] + + busboy.on('field', function (key, val, keyTrunc, valTrunc, encoding, contype) { + results.push(['field', key, val, keyTrunc, valTrunc, encoding, contype]) + }) + busboy.on('file', function (fieldname, stream, filename, encoding, mimeType) { + results.push(['file', fieldname, filename, encoding, mimeType]) + // Simulate a pipe where the destination is pausing (perhaps due to waiting + // for file system write to finish) + setTimeout(function () { + stream.resume() + }, 10) + }) + busboy.on('finish', function () { + t.ok(finishes++ === 0, 'finish emitted multiple times') + t.strictSame(results.length, + expected.length, + 'Parsed result count mismatch. Saw ' + + results.length + + '. Expected: ' + expected.length) + + results.forEach(function (result, i) { + t.strictSame(result, + expected[i], + 'Result mismatch:\nParsed: ' + inspect(result) + + '\nExpected: ' + inspect(expected[i])) + }) + t.pass() + }).on('error', function (err) { + t.error(err) + }) + + reqChunks.forEach(function (buf) { + busboy.write(buf) + }) + busboy.end() +}) diff --git a/fastify-busboy/test/parse-params.test.js b/fastify-busboy/test/parse-params.test.js new file mode 100644 index 0000000..eea4768 --- /dev/null +++ b/fastify-busboy/test/parse-params.test.js @@ -0,0 +1,124 @@ +'use strict' + +const { inspect } = require('node:util') +const { test } = require('tap') +const parseParams = require('../lib/utils/parseParams') + +test('parse-params', t => { + const tests = [ + { + source: 'video/ogg', + expected: ['video/ogg'], + what: 'No parameters' + }, + { + source: 'video/ogg;', + expected: ['video/ogg'], + what: 'No parameters (with separator)' + }, + { + source: 'video/ogg; ', + expected: ['video/ogg'], + what: 'No parameters (with separator followed by whitespace)' + }, + { + source: ';video/ogg', + expected: ['', 'video/ogg'], + what: 'Empty parameter' + }, + { + source: 'video/*', + expected: ['video/*'], + what: 'Subtype with asterisk' + }, + { + source: 'text/plain; encoding=utf8', + expected: ['text/plain', ['encoding', 'utf8']], + what: 'Unquoted' + }, + { + source: 'text/plain; encoding=', + expected: ['text/plain', ['encoding', '']], + what: 'Unquoted empty string' + }, + { + source: 'text/plain; encoding="utf8"', + expected: ['text/plain', ['encoding', 'utf8']], + what: 'Quoted' + }, + { + source: 'text/plain; greeting="hello \\"world\\""', + expected: ['text/plain', ['greeting', 'hello "world"']], + what: 'Quotes within quoted' + }, + { + source: 'text/plain; encoding=""', + expected: ['text/plain', ['encoding', '']], + what: 'Quoted empty string' + }, + { + source: 'text/plain; encoding="utf8";\t foo=bar;test', + expected: ['text/plain', ['encoding', 'utf8'], ['foo', 'bar'], 'test'], + what: 'Multiple params with various spacing' + }, + { + source: "text/plain; filename*=iso-8859-1'en'%A3%20rates", + expected: ['text/plain', ['filename', '£ rates']], + what: 'Extended parameter (RFC 5987) with language' + }, + { + source: "text/plain; filename*=utf-8''%c2%a3%20and%20%e2%82%ac%20rates", + expected: ['text/plain', ['filename', '£ and € rates']], + what: 'Extended parameter (RFC 5987) without language' + }, + { + source: "text/plain; filename*=utf-8''%E6%B5%8B%E8%AF%95%E6%96%87%E6%A1%A3", + expected: ['text/plain', ['filename', '测试文档']], + what: 'Extended parameter (RFC 5987) without language #2' + }, + { + source: "text/plain; filename*=iso-8859-1'en'%A3%20rates; altfilename*=utf-8''%c2%a3%20and%20%e2%82%ac%20rates", + expected: ['text/plain', ['filename', '£ rates'], ['altfilename', '£ and € rates']], + what: 'Multiple extended parameters (RFC 5987) with mixed charsets' + }, + { + source: "text/plain; filename*=iso-8859-1'en'%A3%20rates; altfilename=\"foobarbaz\"", + expected: ['text/plain', ['filename', '£ rates'], ['altfilename', 'foobarbaz']], + what: 'Mixed regular and extended parameters (RFC 5987)' + }, + { + source: "text/plain; filename=\"foobarbaz\"; altfilename*=iso-8859-1'en'%A3%20rates", + expected: ['text/plain', ['filename', 'foobarbaz'], ['altfilename', '£ rates']], + what: 'Mixed regular and extended parameters (RFC 5987) #2' + }, + { + source: 'text/plain; filename="C:\\folder\\test.png"', + expected: ['text/plain', ['filename', 'C:\\folder\\test.png']], + what: 'Unescaped backslashes should be considered backslashes' + }, + { + source: 'text/plain; filename="John \\"Magic\\" Smith.png"', + expected: ['text/plain', ['filename', 'John "Magic" Smith.png']], + what: 'Escaped double-quotes should be considered double-quotes' + }, + { + source: 'multipart/form-data; charset=utf-8; boundary=0xKhTmLbOuNdArY', + expected: ['multipart/form-data', ['charset', 'utf-8'], ['boundary', '0xKhTmLbOuNdArY']], + what: 'Multiple non-quoted parameters' + } + ] + + t.plan(tests.length) + + tests.forEach((v) => { + t.test(v.what, t => { + t.plan(1) + + const result = parseParams(v.source) + t.strictSame( + result, + v.expected, + `parsed parameters match.\nSaw: ${inspect(result)}\nExpected: ${inspect(v.expected)}`) + }) + }) +}) diff --git a/fastify-busboy/test/streamsearch.test.js b/fastify-busboy/test/streamsearch.test.js new file mode 100644 index 0000000..968c7de --- /dev/null +++ b/fastify-busboy/test/streamsearch.test.js @@ -0,0 +1,396 @@ +'use strict' + +const { test } = require('tap') +const Streamsearch = require('../deps/streamsearch/sbmh') + +test('streamsearch', t => { + t.plan(17) + + t.test('should throw an error if the needle is not a String or Buffer', t => { + t.plan(1) + + t.throws(() => new Streamsearch(2), new Error('The needle has to be a String or a Buffer.')) + }) + t.test('should throw an error if the needle is an empty String', t => { + t.plan(1) + + t.throws(() => new Streamsearch(''), new Error('The needle cannot be an empty String/Buffer.')) + }) + t.test('should throw an error if the needle is an empty Buffer', t => { + t.plan(1) + + t.throws(() => new Streamsearch(Buffer.from('')), new Error('The needle cannot be an empty String/Buffer.')) + }) + t.test('should throw an error if the needle is bigger than 256 characters', t => { + t.plan(1) + + t.throws(() => new Streamsearch(Buffer.from(Array(257).fill('a').join(''))), new Error('The needle cannot have a length bigger than 256.')) + }) + + t.test('should process a Buffer without a needle', t => { + t.plan(5) + const expected = [ + [false, Buffer.from('bar hello'), 0, 9] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 1) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should cast a string without a needle', t => { + t.plan(5) + + const expected = [ + [false, Buffer.from('bar hello'), 0, 9] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + 'bar hello' + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 1) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process a chunk with a needle at the beginning', t => { + t.plan(9) + + const expected = [ + [true, undefined, undefined, undefined], + [false, Buffer.from('\r\nbar hello'), 2, 11] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('\r\nbar hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 2) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process a chunk with a needle in the middle', t => { + t.plan(9) + const expected = [ + [true, Buffer.from('bar\r\n hello'), 0, 3], + [false, Buffer.from('bar\r\n hello'), 5, 11] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r\n hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 2) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process a chunk with a needle at the end', t => { + t.plan(5) + const expected = [ + [true, Buffer.from('bar hello\r\n'), 0, 9] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar hello\r\n') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 1) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process a chunk with multiple needle at the end', t => { + t.plan(9) + const expected = [ + [true, Buffer.from('bar hello\r\n\r\n'), 0, 9], + [true, Buffer.from('bar hello\r\n\r\n'), 11, 11] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar hello\r\n\r\n') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 2) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process two chunks without a needle', t => { + t.plan(9) + const expected = [ + [false, Buffer.from('bar'), 0, 3], + [false, Buffer.from('hello'), 0, 5] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar'), + Buffer.from('hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 2) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + }) + + t.test('should process two chunks with an overflowing needle', t => { + t.plan(13) + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [true, undefined, undefined, undefined], + [false, Buffer.from('\nhello'), 1, 6] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\nhello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 3) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + }) + + t.test('should process two chunks with a potentially overflowing needle', t => { + t.plan(13) + + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [false, Buffer.from('\r\0\0'), 0, 1], + [false, Buffer.from('\n\r\nhello'), 0, 8] + ] + const needle = '\r\n\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\n\r\nhello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 3) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + }) + + t.test('should process three chunks with a overflowing needle', t => { + t.plan(13) + + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [true, undefined, undefined, undefined], + [false, Buffer.from('\nhello'), 1, 6] + ] + const needle = '\r\n\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\n'), + Buffer.from('\nhello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 3) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + s.push(chunks[2]) + }) + + t.test('should process four chunks with a overflowing needle', t => { + t.plan(13) + + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [true, undefined, undefined, undefined], + [false, Buffer.from('hello'), 0, 5] + ] + const needle = '\r\n\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\n'), + Buffer.from('\n'), + Buffer.from('hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 3) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + s.push(chunks[2]) + s.push(chunks[3]) + }) + + t.test('should process four chunks with a potentially overflowing needle', t => { + t.plan(17) + + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [false, Buffer.from('\r\n\0'), 0, 2], + [false, Buffer.from('\r\n\0'), 0, 1], + [false, Buffer.from('hello'), 0, 5] + ] + const needle = '\r\n\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\n'), + Buffer.from('\r'), + Buffer.from('hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 4) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + s.push(chunks[2]) + s.push(chunks[3]) + }) + + t.test('should reset the internal values if .reset() is called', t => { + t.plan(9) + + const s = new Streamsearch('test') + + t.strictSame(s._lookbehind_size, 0) + t.strictSame(s.matches, 0) + t.strictSame(s._bufpos, 0) + + s._lookbehind_size = 1 + s._bufpos = 1 + s.matches = 1 + + t.strictSame(s._lookbehind_size, 1) + t.strictSame(s.matches, 1) + t.strictSame(s._bufpos, 1) + + s.reset() + + t.strictSame(s._lookbehind_size, 0) + t.strictSame(s.matches, 0) + t.strictSame(s._bufpos, 0) + }) +}) diff --git a/fastify-busboy/test/types-multipart.test.js b/fastify-busboy/test/types-multipart.test.js new file mode 100644 index 0000000..dc7ae88 --- /dev/null +++ b/fastify-busboy/test/types-multipart.test.js @@ -0,0 +1,678 @@ +'use strict' + +const Busboy = require('..') + +const { test } = require('tap') +const { inspect } = require('util') + +const EMPTY_FN = function () { +} + +const tests = [ + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_1"', + '', + 'super beta file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_1"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'], + ['field', 'file_name_1', 'super beta file', false, false, '7bit', 'text/plain'], + ['file', 'upload_file_0', 1023, 0, '1k_a.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_1', 1023, 0, '1k_b.dat', '7bit', 'application/octet-stream'] + ], + what: 'Fields and files', + plan: 11 + }, + { + source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + '', + 'some random content', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="pass"', + '', + 'some random pass', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="bit"', + '', + '2', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--' + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + ['field', 'cont', 'some random content', false, false, '7bit', 'text/plain'], + ['field', 'pass', 'some random pass', false, false, '7bit', 'text/plain'], + ['field', 'bit', '2', false, false, '7bit', 'text/plain'] + ], + what: 'Fields only', + plan: 6 + }, + { + source: [ + '' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [], + shouldError: 'Unexpected end of multipart data', + what: 'No fields and no files', + plan: 3 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fileSize: 13, + fieldSize: 5 + }, + expected: [ + ['field', 'file_name_0', 'super', false, true, '7bit', 'text/plain'], + ['file', 'upload_file_0', 13, 2, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'Fields and files (limits)', + plan: 7 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fields: 0 + }, + events: ['file'], + expected: [ + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'should not emit fieldsLimit if no field was sent', + plan: 6 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fields: 0 + }, + events: ['file', 'fieldsLimit'], + expected: [ + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'should respect fields limit of 0', + plan: 6 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_1"', + '', + 'super beta file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fields: 1 + }, + events: ['field', 'file', 'fieldsLimit'], + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'], + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'should respect fields limit of 7', + plan: 7 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + files: 0 + }, + events: ['field'], + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'] + ], + what: 'should not emit filesLimit if no file was sent', + plan: 4 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + files: 0 + }, + events: ['field', 'filesLimit'], + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'] + ], + what: 'should respect fields limit of 0', + plan: 4 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_b"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + files: 1 + }, + events: ['field', 'file', 'filesLimit'], + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'], + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'should respect fields limit of 1', + plan: 7 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_1"', + '', + 'super beta file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_1"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'], + ['field', 'file_name_1', 'super beta file', false, false, '7bit', 'text/plain'] + ], + events: ['field'], + what: 'Fields and (ignored) files', + plan: 5 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="/tmp/1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_1"; filename="C:\\files\\1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_2"; filename="relative/1k_c.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_1', 26, 0, '1k_b.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_2', 26, 0, '1k_c.dat', '7bit', 'application/octet-stream'] + ], + what: 'Files with filenames containing paths', + plan: 12 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="/absolute/1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_1"; filename="C:\\absolute\\1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_2"; filename="relative/1k_c.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + preservePath: true, + expected: [ + ['file', 'upload_file_0', 26, 0, '/absolute/1k_a.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_1', 26, 0, 'C:\\absolute\\1k_b.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_2', 26, 0, 'relative/1k_c.dat', '7bit', 'application/octet-stream'] + ], + what: 'Paths to be preserved through the preservePath option', + plan: 12 + }, + { + source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + 'Content-Type: ', + '', + 'some random content', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: ', + '', + 'some random pass', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--' + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + ['field', 'cont', 'some random content', false, false, '7bit', 'text/plain'] + ], + what: 'Empty content-type and empty content-disposition', + plan: 4 + }, + { + config: { + isPartAFile: (fieldName) => (fieldName !== 'upload_file_0') + }, + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="blob"', + 'Content-Type: application/json', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['field', 'upload_file_0', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', false, false, '7bit', 'application/json'] + ], + what: 'Blob uploads should be handled as fields if isPartAFile is provided.', + plan: 4 + }, + { + config: { + isPartAFile: (fieldName) => (fieldName !== 'upload_file_0') + }, + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="blob"', + 'Content-Type: application/json', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file"; filename*=utf-8\'\'n%C3%A4me.txt', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['field', 'upload_file_0', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', false, false, '7bit', 'application/json'], + ['file', 'file', 26, 0, 'näme.txt', '7bit', 'application/octet-stream'] + ], + what: 'Blob uploads should be handled as fields if isPartAFile is provided. Other parts should be files.', + plan: 7 + }, + { + config: { + isPartAFile: (fieldName) => (fieldName === 'upload_file_0') + }, + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="blob"', + 'Content-Type: application/json', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file"; filename*=utf-8\'\'n%C3%A4me.txt', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['file', 'upload_file_0', 26, 0, 'blob', '7bit', 'application/json'], + ['field', 'file', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', false, false, '7bit', 'application/octet-stream'] + ], + what: 'Blob uploads sould be handled as files if corresponding isPartAFile is provided. Other parts should be fields.', + plan: 7 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file"; filename*=utf-8\'\'n%C3%A4me.txt', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['file', 'file', 26, 0, 'näme.txt', '7bit', 'application/octet-stream'] + ], + what: 'Unicode filenames', + plan: 6 + }, + { + source: [ + ['--asdasdasdasd\r\n', + 'Content-Type: text/plain\r\n', + 'Content-Disposition: form-data; name="foo"\r\n', + '\r\n', + 'asd\r\n', + '--asdasdasdasd--' + ].join(':)') + ], + boundary: 'asdasdasdasd', + expected: [], + shouldError: 'Unexpected end of multipart data', + what: 'Stopped mid-header', + plan: 3 + }, + { + source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + 'Content-Type: application/json', + '', + '{}', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--' + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + ['field', 'cont', '{}', false, false, '7bit', 'application/json'] + ], + what: 'content-type for fields', + plan: 4 + }, + { + source: [ + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [], + what: 'empty form', + plan: 3 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="field1"', + 'content-type: text/plain; charset=utf-8', + '', + 'Aufklärung ist der Ausgang des Menschen aus seiner selbstverschuldeten Unmündigkeit.', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="field2"', + 'content-type: text/plain; charset=iso-8859-1', + '', + 'sapere aude!', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['field', 'field1', 'Aufklärung ist der Ausgang des Menschen aus seiner selbstverschuldeten Unmündigkeit.', false, false, '7bit', 'text/plain'], + ['field', 'field2', 'sapere aude!', false, false, '7bit', 'text/plain'] + ], + what: 'Fields and files', + plan: 5 + }, + { + source: [[ + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="regsubmit"', + '', + 'yes', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="referer"', + '', + 'http://domainExample/./', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="activationauth"', + '', + '', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="seccodemodid"', + '', + 'member::register', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7--'].join('\r\n') + ], + boundary: '----WebKitFormBoundaryzca7IDMnT6QwqBp7', + expected: [ + ['field', 'regsubmit', 'yes', false, false, '7bit', 'text/plain'], + ['field', 'referer', 'http://domainExample/./', false, false, '7bit', 'text/plain'], + ['field', 'activationauth', '', false, false, '7bit', 'text/plain'], + ['field', 'seccodemodid', 'member::register', false, false, '7bit', 'text/plain'] + ], + what: 'one empty part should get ignored', + plan: 7 + }, + { + source: [ + ' ------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [], + shouldError: 'Unexpected end of multipart data', + what: 'empty form with preceding whitespace', + plan: 3 + }, + { + source: [ + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhYY', + expected: [], + shouldError: 'Unexpected end of multipart data', + what: 'empty form with wrong boundary (extra Y)', + plan: 3 + }, + { + source: [[ + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="regsubmit"', + '', + 'yes', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="referer"', + '', + 'http://domainExample/./', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="activationauth"', + '', + '', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="seccodemodid"', + '', + 'member::register', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7--'].join('\r\n') + ], + boundary: '----WebKitFormBoundaryzca7IDMnT6QwqBp7', + expected: [ + ['field', 'regsubmit', 'yes', false, false, '7bit', 'text/plain'], + ['field', 'referer', 'http://domainExample/./', false, false, '7bit', 'text/plain'], + ['field', 'activationauth', '', false, false, '7bit', 'text/plain'], + ['field', 'seccodemodid', 'member::register', false, false, '7bit', 'text/plain'] + ], + what: 'multiple empty parts should get ignored', + plan: 7 + } +] + +tests.forEach((v) => { + test(v.what, t => { + t.plan(v.plan) + const busboy = new Busboy({ + ...v.config, + limits: v.limits, + preservePath: v.preservePath, + headers: { + 'content-type': 'multipart/form-data; boundary=' + v.boundary + } + }) + let finishes = 0 + const results = [] + + if (v.events === undefined || v.events.indexOf('field') > -1) { + busboy.on('field', function (key, val, keyTrunc, valTrunc, encoding, contype) { + results.push(['field', key, val, keyTrunc, valTrunc, encoding, contype]) + }) + } + if (v.events === undefined || v.events.indexOf('file') > -1) { + busboy.on('file', function (fieldname, stream, filename, encoding, mimeType) { + let nb = 0 + const info = ['file', + fieldname, + nb, + 0, + filename, + encoding, + mimeType] + results.push(info) + stream.on('data', function (d) { + nb += d.length + }).on('limit', function () { + ++info[3] + }).on('end', function () { + info[2] = nb + t.ok(typeof (stream.bytesRead) === 'number', 'file.bytesRead is missing') + t.ok(stream.bytesRead === nb, 'file.bytesRead is not equal to filesize') + if (stream.truncated) { ++info[3] } + }) + }) + } + busboy.on('finish', function () { + t.ok(finishes++ === 0, 'finish emitted multiple times') + t.equal(results.length, + v.expected.length, + 'Parsed result count mismatch. Saw ' + + results.length + + '. Expected: ' + v.expected.length) + + results.forEach(function (result, i) { + t.strictSame(result, + v.expected[i], + 'Result mismatch:\nParsed: ' + inspect(result) + + '\nExpected: ' + inspect(v.expected[i]) + ) + }) + t.pass() + }).on('error', function (err) { + if (!v.shouldError || v.shouldError !== err.message) { t.error(err) } + }) + + v.source.forEach(function (s) { + busboy.write(Buffer.from(s, 'utf8'), EMPTY_FN) + }) + busboy.end() + }) +}) diff --git a/fastify-busboy/test/types-urlencoded.test.js b/fastify-busboy/test/types-urlencoded.test.js new file mode 100644 index 0000000..73cc286 --- /dev/null +++ b/fastify-busboy/test/types-urlencoded.test.js @@ -0,0 +1,210 @@ +'use strict' + +const { inspect } = require('util') +const Busboy = require('..') +const { test } = require('tap') + +const EMPTY_FN = function () { +} + +const tests = [ + { + source: ['foo'], + expected: [['foo', '', false, false]], + what: 'Unassigned value', + plan: 4 + }, + { + source: ['foo=bar'], + expected: [['foo', 'bar', false, false]], + what: 'Assigned value', + plan: 4 + }, + { + source: ['foo&bar=baz'], + expected: [['foo', '', false, false], + ['bar', 'baz', false, false]], + what: 'Unassigned and assigned value', + plan: 5 + }, + { + source: ['foo=bar&baz'], + expected: [['foo', 'bar', false, false], + ['baz', '', false, false]], + what: 'Assigned and unassigned value', + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['foo', 'bar', false, false], + ['baz', 'bla', false, false]], + what: 'Two assigned values', + plan: 5 + }, + { + source: ['foo&bar'], + expected: [['foo', '', false, false], + ['bar', '', false, false]], + what: 'Two unassigned values', + plan: 5 + }, + { + source: ['foo&bar&'], + expected: [['foo', '', false, false], + ['bar', '', false, false]], + what: 'Two unassigned values and ampersand', + plan: 5 + }, + { + source: ['foo=bar+baz%2Bquux'], + expected: [['foo', 'bar baz+quux', false, false]], + what: 'Assigned value with (plus) space', + plan: 4 + }, + { + source: ['foo=bar%20baz%21'], + expected: [['foo', 'bar baz!', false, false]], + what: 'Assigned value with encoded bytes', + plan: 4 + }, + { + source: ['foo%20bar=baz%20bla%21'], + expected: [['foo bar', 'baz bla!', false, false]], + what: 'Assigned value with encoded bytes #2', + plan: 4 + }, + { + source: ['foo=bar%20baz%21&num=1000'], + expected: [['foo', 'bar baz!', false, false], + ['num', '1000', false, false]], + what: 'Two assigned values, one with encoded bytes', + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [], + what: 'Limits: zero fields', + limits: { fields: 0 }, + plan: 3 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['foo', 'bar', false, false]], + what: 'Limits: one field', + limits: { fields: 1 }, + plan: 4 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['foo', 'bar', false, false], + ['baz', 'bla', false, false]], + what: 'Limits: field part lengths match limits', + limits: { fieldNameSize: 3, fieldSize: 3 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['fo', 'bar', true, false], + ['ba', 'bla', true, false]], + what: 'Limits: truncated field name', + limits: { fieldNameSize: 2 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['foo', 'ba', false, true], + ['baz', 'bl', false, true]], + what: 'Limits: truncated field value', + limits: { fieldSize: 2 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['fo', 'ba', true, true], + ['ba', 'bl', true, true]], + what: 'Limits: truncated field name and value', + limits: { fieldNameSize: 2, fieldSize: 2 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['fo', '', true, true], + ['ba', '', true, true]], + what: 'Limits: truncated field name and zero value limit', + limits: { fieldNameSize: 2, fieldSize: 0 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['', '', true, true], + ['', '', true, true]], + what: 'Limits: truncated zero field name and zero value limit', + limits: { fieldNameSize: 0, fieldSize: 0 }, + plan: 5 + }, + { + source: ['&'], + expected: [], + what: 'Ampersand', + plan: 3 + }, + { + source: ['&&&&&'], + expected: [], + what: 'Many ampersands', + plan: 3 + }, + { + source: ['='], + expected: [['', '', false, false]], + what: 'Assigned value, empty name and value', + plan: 4 + }, + { + source: [''], + expected: [], + what: 'Nothing', + plan: 3 + } +] + +tests.forEach((v) => { + test(v.what, t => { + t.plan(v.plan || 20) + const busboy = new Busboy({ + limits: v.limits, + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8' + } + }) + let finishes = 0 + const results = [] + + busboy.on('field', function (key, val, keyTrunc, valTrunc) { + results.push([key, val, keyTrunc, valTrunc]) + }) + busboy.on('file', function () { + throw new Error('Unexpected file') + }) + busboy.on('finish', function () { + t.ok(finishes++ === 0, 'finish emitted multiple times') + t.equal(results.length, v.expected.length) + + let i = 0 + results.forEach(function (result) { + t.strictSame(result, + v.expected[i], + 'Result mismatch:\nParsed: ' + inspect(result) + + '\nExpected: ' + inspect(v.expected[i]) + ) + ++i + }) + t.pass() + }) + + v.source.forEach(function (s) { + busboy.write(Buffer.from(s, 'utf8'), EMPTY_FN) + }) + busboy.end() + }) +}) diff --git a/fastify-busboy/test/types/dicer.test-d.ts b/fastify-busboy/test/types/dicer.test-d.ts new file mode 100644 index 0000000..466c1e1 --- /dev/null +++ b/fastify-busboy/test/types/dicer.test-d.ts @@ -0,0 +1,81 @@ +import { Dicer } from "../../lib/main"; +import * as fs from "fs"; +import * as stream from "stream"; + +function testDicerSyntax() { + const opts: Dicer.Config = { + boundary: "testing", + }; + const dicer = new Dicer(opts); + const opts2: Dicer.Config = { + headerFirst: true, + maxHeaderPairs: 1, + }; + const opts3: Dicer.Config = { + boundary: "more-testing", + headerFirst: false, + maxHeaderPairs: 8, + }; + dicer.setBoundary("new-testing-boundary"); + dicer.on("part", handleDicerPartStream); + dicer.on("finish", () => { + console.log("dicer parsing finished"); + }); + dicer.on("preamble", part => { + console.log("dicer preamble to new part"); + }); + dicer.on("trailer", data => { + console.log(`dicer trailing data found: ${data.length} bytes`); + }); + dicer.on("close", () => { + console.log("dicer close"); + }); + dicer.on("drain", () => { + console.log("dicer drain"); + }); + dicer.on("error", err => { + console.error(`dicer error: ${err.message || JSON.stringify(err)}`); + }); + dicer.on("finish", () => { + console.log("dicer finish"); + }); + dicer.on("pipe", (src: stream.Readable) => { + console.log("dicer pipe"); + }); + dicer.on("unpipe", (src: stream.Readable) => { + console.log("dicer unpipe"); + }); + const inputFileStream = fs.createReadStream("in-test-file.txt"); + inputFileStream.pipe(dicer); +} +/** + * Handle a part found by a Dicer parser + * + * @param part Part found + */ +function handleDicerPartStream(part: Dicer.PartStream) { + console.log("dicer part found"); + const outputFileStream = fs.createWriteStream("out-test-file.txt"); + part.on("readable", () => { + console.log("part readable"); + }); + part.on("header", header => { + console.log(`part header found:\n${JSON.stringify(header)}`); + }); + part.on("data", () => { + console.log("part data"); + }); + part.on("finish", () => { + console.log("part finished"); + }); + part.on("error", err => { + console.error(`part error: ${err.message || JSON.stringify(err)}`); + }); + part.on("end", () => { + console.log("part ended"); + }); + part.on("close", () => { + console.log("part closed"); + }); + part.pipe(outputFileStream); +}
\ No newline at end of file diff --git a/fastify-busboy/test/types/main.test-d.ts b/fastify-busboy/test/types/main.test-d.ts new file mode 100644 index 0000000..fb58b3f --- /dev/null +++ b/fastify-busboy/test/types/main.test-d.ts @@ -0,0 +1,241 @@ +import BusboyDefault, { BusboyConstructor, BusboyConfig, BusboyHeaders, Busboy, BusboyEvents, BusboyFileStream } from '../..'; +import {expectError, expectType} from "tsd"; +import BusboyESM from "../.."; + +// test type exports +type Constructor = BusboyConstructor; +type Config = BusboyConfig; +type Headers = BusboyHeaders; +type Events = BusboyEvents; +type BB = Busboy; + +expectType<Busboy>(new BusboyESM({ headers: { 'content-type': 'foo' } })); +expectType<Busboy>(new Busboy({ headers: { 'content-type': 'foo' } })); + +expectError(new BusboyDefault({})); +const busboy = BusboyDefault({ headers: { 'content-type': 'foo' } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, highWaterMark: 1000 }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, fileHwm: 1000 }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, defCharset: 'utf8' }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, preservePath: true }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fieldNameSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fieldSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fields: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fileSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { files: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { parts: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { headerPairs: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { headerSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, isPartAFile: (fieldName, contentType, fileName) => fieldName === 'my-special-field' || fileName !== 'not-so-special.txt' }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, isPartAFile: (fieldName, contentType, fileName) => fileName !== undefined }); // $ExpectType Busboy + +busboy.addListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname) + expectType<BusboyFileStream>(file); + expectType<string>(filename); + expectType<string>(encoding); + expectType<string>(mimetype); +}); +busboy.addListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.addListener('partsLimit', () => {}); +busboy.addListener('filesLimit', () => {}); +busboy.addListener('fieldsLimit', () => {}); +busboy.addListener('error', e => { + expectType<unknown> (e); +}); +busboy.addListener('finish', () => {}); +// test fallback +busboy.on('foo', foo => { + expectType<any> (foo); +}); +busboy.on(Symbol('foo'), foo => { + expectType<any>(foo); +}); + +busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.on('partsLimit', () => {}); +busboy.on('filesLimit', () => {}); +busboy.on('fieldsLimit', () => {}); +busboy.on('error', e => { + expectType<unknown> (e); +}); +busboy.on('finish', () => {}); +// test fallback +busboy.on('foo', foo => { + expectType<any> (foo); +}); +busboy.on(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.once('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.once('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.once('partsLimit', () => {}); +busboy.once('filesLimit', () => {}); +busboy.once('fieldsLimit', () => {}); +busboy.once('error', e => { + expectType<unknown> (e); +}); +busboy.once('finish', () => {}); +// test fallback +busboy.once('foo', foo => { + expectType<any> (foo); +}); +busboy.once(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.removeListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.removeListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.removeListener('partsLimit', () => {}); +busboy.removeListener('filesLimit', () => {}); +busboy.removeListener('fieldsLimit', () => {}); +busboy.removeListener('error', e => { + expectType<unknown> (e); +}); +busboy.removeListener('finish', () => {}); +// test fallback +busboy.removeListener('foo', foo => { + expectType<any> (foo); +}); +busboy.removeListener(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.off('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.off('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.off('partsLimit', () => {}); +busboy.off('filesLimit', () => {}); +busboy.off('fieldsLimit', () => {}); +busboy.off('error', e => { + expectType<unknown> (e); +}); +busboy.off('finish', () => {}); +// test fallback +busboy.off('foo', foo => { + expectType<any> (foo); +}); +busboy.off(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.prependListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.prependListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.prependListener('partsLimit', () => {}); +busboy.prependListener('filesLimit', () => {}); +busboy.prependListener('fieldsLimit', () => {}); +busboy.prependListener('error', e => { + expectType<unknown> (e); +}); +busboy.prependListener('finish', () => {}); +// test fallback +busboy.prependListener('foo', foo => { + expectType<any> (foo); +}); +busboy.prependListener(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.prependOnceListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.prependOnceListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.prependOnceListener('partsLimit', () => {}); +busboy.prependOnceListener('filesLimit', () => {}); +busboy.prependOnceListener('fieldsLimit', () => {}); +busboy.prependOnceListener('error', e => { + expectType<unknown> (e); +}); +busboy.prependOnceListener('finish', () => {}); +// test fallback +busboy.prependOnceListener('foo', foo => { + expectType<any> (foo); +}); +busboy.prependOnceListener(Symbol('foo'), foo => { + expectType<any> (foo); +}); |