1 | 'use strict';
|
2 |
|
3 | const assert = require('assert');
|
4 | const { transcode } = require('buffer');
|
5 | const { inspect } = require('util');
|
6 |
|
7 | const busboy = require('..');
|
8 |
|
9 | const active = new Map();
|
10 |
|
11 | const tests = [
|
12 | { source: ['foo'],
|
13 | expected: [
|
14 | ['foo',
|
15 | '',
|
16 | { nameTruncated: false,
|
17 | valueTruncated: false,
|
18 | encoding: 'utf-8',
|
19 | mimeType: 'text/plain' },
|
20 | ],
|
21 | ],
|
22 | what: 'Unassigned value'
|
23 | },
|
24 | { source: ['foo=bar'],
|
25 | expected: [
|
26 | ['foo',
|
27 | 'bar',
|
28 | { nameTruncated: false,
|
29 | valueTruncated: false,
|
30 | encoding: 'utf-8',
|
31 | mimeType: 'text/plain' },
|
32 | ],
|
33 | ],
|
34 | what: 'Assigned value'
|
35 | },
|
36 | { source: ['foo&bar=baz'],
|
37 | expected: [
|
38 | ['foo',
|
39 | '',
|
40 | { nameTruncated: false,
|
41 | valueTruncated: false,
|
42 | encoding: 'utf-8',
|
43 | mimeType: 'text/plain' },
|
44 | ],
|
45 | ['bar',
|
46 | 'baz',
|
47 | { nameTruncated: false,
|
48 | valueTruncated: false,
|
49 | encoding: 'utf-8',
|
50 | mimeType: 'text/plain' },
|
51 | ],
|
52 | ],
|
53 | what: 'Unassigned and assigned value'
|
54 | },
|
55 | { source: ['foo=bar&baz'],
|
56 | expected: [
|
57 | ['foo',
|
58 | 'bar',
|
59 | { nameTruncated: false,
|
60 | valueTruncated: false,
|
61 | encoding: 'utf-8',
|
62 | mimeType: 'text/plain' },
|
63 | ],
|
64 | ['baz',
|
65 | '',
|
66 | { nameTruncated: false,
|
67 | valueTruncated: false,
|
68 | encoding: 'utf-8',
|
69 | mimeType: 'text/plain' },
|
70 | ],
|
71 | ],
|
72 | what: 'Assigned and unassigned value'
|
73 | },
|
74 | { source: ['foo=bar&baz=bla'],
|
75 | expected: [
|
76 | ['foo',
|
77 | 'bar',
|
78 | { nameTruncated: false,
|
79 | valueTruncated: false,
|
80 | encoding: 'utf-8',
|
81 | mimeType: 'text/plain' },
|
82 | ],
|
83 | ['baz',
|
84 | 'bla',
|
85 | { nameTruncated: false,
|
86 | valueTruncated: false,
|
87 | encoding: 'utf-8',
|
88 | mimeType: 'text/plain' },
|
89 | ],
|
90 | ],
|
91 | what: 'Two assigned values'
|
92 | },
|
93 | { source: ['foo&bar'],
|
94 | expected: [
|
95 | ['foo',
|
96 | '',
|
97 | { nameTruncated: false,
|
98 | valueTruncated: false,
|
99 | encoding: 'utf-8',
|
100 | mimeType: 'text/plain' },
|
101 | ],
|
102 | ['bar',
|
103 | '',
|
104 | { nameTruncated: false,
|
105 | valueTruncated: false,
|
106 | encoding: 'utf-8',
|
107 | mimeType: 'text/plain' },
|
108 | ],
|
109 | ],
|
110 | what: 'Two unassigned values'
|
111 | },
|
112 | { source: ['foo&bar&'],
|
113 | expected: [
|
114 | ['foo',
|
115 | '',
|
116 | { nameTruncated: false,
|
117 | valueTruncated: false,
|
118 | encoding: 'utf-8',
|
119 | mimeType: 'text/plain' },
|
120 | ],
|
121 | ['bar',
|
122 | '',
|
123 | { nameTruncated: false,
|
124 | valueTruncated: false,
|
125 | encoding: 'utf-8',
|
126 | mimeType: 'text/plain' },
|
127 | ],
|
128 | ],
|
129 | what: 'Two unassigned values and ampersand'
|
130 | },
|
131 | { source: ['foo+1=bar+baz%2Bquux'],
|
132 | expected: [
|
133 | ['foo 1',
|
134 | 'bar baz+quux',
|
135 | { nameTruncated: false,
|
136 | valueTruncated: false,
|
137 | encoding: 'utf-8',
|
138 | mimeType: 'text/plain' },
|
139 | ],
|
140 | ],
|
141 | what: 'Assigned key and value with (plus) space'
|
142 | },
|
143 | { source: ['foo=bar%20baz%21'],
|
144 | expected: [
|
145 | ['foo',
|
146 | 'bar baz!',
|
147 | { nameTruncated: false,
|
148 | valueTruncated: false,
|
149 | encoding: 'utf-8',
|
150 | mimeType: 'text/plain' },
|
151 | ],
|
152 | ],
|
153 | what: 'Assigned value with encoded bytes'
|
154 | },
|
155 | { source: ['foo%20bar=baz%20bla%21'],
|
156 | expected: [
|
157 | ['foo bar',
|
158 | 'baz bla!',
|
159 | { nameTruncated: false,
|
160 | valueTruncated: false,
|
161 | encoding: 'utf-8',
|
162 | mimeType: 'text/plain' },
|
163 | ],
|
164 | ],
|
165 | what: 'Assigned value with encoded bytes #2'
|
166 | },
|
167 | { source: ['foo=bar%20baz%21&num=1000'],
|
168 | expected: [
|
169 | ['foo',
|
170 | 'bar baz!',
|
171 | { nameTruncated: false,
|
172 | valueTruncated: false,
|
173 | encoding: 'utf-8',
|
174 | mimeType: 'text/plain' },
|
175 | ],
|
176 | ['num',
|
177 | '1000',
|
178 | { nameTruncated: false,
|
179 | valueTruncated: false,
|
180 | encoding: 'utf-8',
|
181 | mimeType: 'text/plain' },
|
182 | ],
|
183 | ],
|
184 | what: 'Two assigned values, one with encoded bytes'
|
185 | },
|
186 | { source: [
|
187 | Array.from(transcode(Buffer.from('foo'), 'utf8', 'utf16le')).map(
|
188 | (n) => `%${n.toString(16).padStart(2, '0')}`
|
189 | ).join(''),
|
190 | '=',
|
191 | Array.from(transcode(Buffer.from('😀!'), 'utf8', 'utf16le')).map(
|
192 | (n) => `%${n.toString(16).padStart(2, '0')}`
|
193 | ).join(''),
|
194 | ],
|
195 | expected: [
|
196 | ['foo',
|
197 | '😀!',
|
198 | { nameTruncated: false,
|
199 | valueTruncated: false,
|
200 | encoding: 'UTF-16LE',
|
201 | mimeType: 'text/plain' },
|
202 | ],
|
203 | ],
|
204 | charset: 'UTF-16LE',
|
205 | what: 'Encoded value with multi-byte charset'
|
206 | },
|
207 | { source: [
|
208 | 'foo=<',
|
209 | Array.from(transcode(Buffer.from('©:^þ'), 'utf8', 'latin1')).map(
|
210 | (n) => `%${n.toString(16).padStart(2, '0')}`
|
211 | ).join(''),
|
212 | ],
|
213 | expected: [
|
214 | ['foo',
|
215 | '<©:^þ',
|
216 | { nameTruncated: false,
|
217 | valueTruncated: false,
|
218 | encoding: 'ISO-8859-1',
|
219 | mimeType: 'text/plain' },
|
220 | ],
|
221 | ],
|
222 | charset: 'ISO-8859-1',
|
223 | what: 'Encoded value with single-byte, ASCII-compatible, non-UTF8 charset'
|
224 | },
|
225 | { source: ['foo=bar&baz=bla'],
|
226 | expected: [],
|
227 | what: 'Limits: zero fields',
|
228 | limits: { fields: 0 }
|
229 | },
|
230 | { source: ['foo=bar&baz=bla'],
|
231 | expected: [
|
232 | ['foo',
|
233 | 'bar',
|
234 | { nameTruncated: false,
|
235 | valueTruncated: false,
|
236 | encoding: 'utf-8',
|
237 | mimeType: 'text/plain' },
|
238 | ],
|
239 | ],
|
240 | what: 'Limits: one field',
|
241 | limits: { fields: 1 }
|
242 | },
|
243 | { source: ['foo=bar&baz=bla'],
|
244 | expected: [
|
245 | ['foo',
|
246 | 'bar',
|
247 | { nameTruncated: false,
|
248 | valueTruncated: false,
|
249 | encoding: 'utf-8',
|
250 | mimeType: 'text/plain' },
|
251 | ],
|
252 | ['baz',
|
253 | 'bla',
|
254 | { nameTruncated: false,
|
255 | valueTruncated: false,
|
256 | encoding: 'utf-8',
|
257 | mimeType: 'text/plain' },
|
258 | ],
|
259 | ],
|
260 | what: 'Limits: field part lengths match limits',
|
261 | limits: { fieldNameSize: 3, fieldSize: 3 }
|
262 | },
|
263 | { source: ['foo=bar&baz=bla'],
|
264 | expected: [
|
265 | ['fo',
|
266 | 'bar',
|
267 | { nameTruncated: true,
|
268 | valueTruncated: false,
|
269 | encoding: 'utf-8',
|
270 | mimeType: 'text/plain' },
|
271 | ],
|
272 | ['ba',
|
273 | 'bla',
|
274 | { nameTruncated: true,
|
275 | valueTruncated: false,
|
276 | encoding: 'utf-8',
|
277 | mimeType: 'text/plain' },
|
278 | ],
|
279 | ],
|
280 | what: 'Limits: truncated field name',
|
281 | limits: { fieldNameSize: 2 }
|
282 | },
|
283 | { source: ['foo=bar&baz=bla'],
|
284 | expected: [
|
285 | ['foo',
|
286 | 'ba',
|
287 | { nameTruncated: false,
|
288 | valueTruncated: true,
|
289 | encoding: 'utf-8',
|
290 | mimeType: 'text/plain' },
|
291 | ],
|
292 | ['baz',
|
293 | 'bl',
|
294 | { nameTruncated: false,
|
295 | valueTruncated: true,
|
296 | encoding: 'utf-8',
|
297 | mimeType: 'text/plain' },
|
298 | ],
|
299 | ],
|
300 | what: 'Limits: truncated field value',
|
301 | limits: { fieldSize: 2 }
|
302 | },
|
303 | { source: ['foo=bar&baz=bla'],
|
304 | expected: [
|
305 | ['fo',
|
306 | 'ba',
|
307 | { nameTruncated: true,
|
308 | valueTruncated: true,
|
309 | encoding: 'utf-8',
|
310 | mimeType: 'text/plain' },
|
311 | ],
|
312 | ['ba',
|
313 | 'bl',
|
314 | { nameTruncated: true,
|
315 | valueTruncated: true,
|
316 | encoding: 'utf-8',
|
317 | mimeType: 'text/plain' },
|
318 | ],
|
319 | ],
|
320 | what: 'Limits: truncated field name and value',
|
321 | limits: { fieldNameSize: 2, fieldSize: 2 }
|
322 | },
|
323 | { source: ['foo=bar&baz=bla'],
|
324 | expected: [
|
325 | ['fo',
|
326 | '',
|
327 | { nameTruncated: true,
|
328 | valueTruncated: true,
|
329 | encoding: 'utf-8',
|
330 | mimeType: 'text/plain' },
|
331 | ],
|
332 | ['ba',
|
333 | '',
|
334 | { nameTruncated: true,
|
335 | valueTruncated: true,
|
336 | encoding: 'utf-8',
|
337 | mimeType: 'text/plain' },
|
338 | ],
|
339 | ],
|
340 | what: 'Limits: truncated field name and zero value limit',
|
341 | limits: { fieldNameSize: 2, fieldSize: 0 }
|
342 | },
|
343 | { source: ['foo=bar&baz=bla'],
|
344 | expected: [
|
345 | ['',
|
346 | '',
|
347 | { nameTruncated: true,
|
348 | valueTruncated: true,
|
349 | encoding: 'utf-8',
|
350 | mimeType: 'text/plain' },
|
351 | ],
|
352 | ['',
|
353 | '',
|
354 | { nameTruncated: true,
|
355 | valueTruncated: true,
|
356 | encoding: 'utf-8',
|
357 | mimeType: 'text/plain' },
|
358 | ],
|
359 | ],
|
360 | what: 'Limits: truncated zero field name and zero value limit',
|
361 | limits: { fieldNameSize: 0, fieldSize: 0 }
|
362 | },
|
363 | { source: ['&'],
|
364 | expected: [],
|
365 | what: 'Ampersand'
|
366 | },
|
367 | { source: ['&&&&&'],
|
368 | expected: [],
|
369 | what: 'Many ampersands'
|
370 | },
|
371 | { source: ['='],
|
372 | expected: [
|
373 | ['',
|
374 | '',
|
375 | { nameTruncated: false,
|
376 | valueTruncated: false,
|
377 | encoding: 'utf-8',
|
378 | mimeType: 'text/plain' },
|
379 | ],
|
380 | ],
|
381 | what: 'Assigned value, empty name and value'
|
382 | },
|
383 | { source: [''],
|
384 | expected: [],
|
385 | what: 'Nothing'
|
386 | },
|
387 | ];
|
388 |
|
389 | for (const test of tests) {
|
390 | active.set(test, 1);
|
391 |
|
392 | const { what } = test;
|
393 | const charset = test.charset || 'utf-8';
|
394 | const bb = busboy({
|
395 | limits: test.limits,
|
396 | headers: {
|
397 | 'content-type': `application/x-www-form-urlencoded; charset=${charset}`,
|
398 | },
|
399 | });
|
400 | const results = [];
|
401 |
|
402 | bb.on('field', (key, val, info) => {
|
403 | results.push([key, val, info]);
|
404 | });
|
405 |
|
406 | bb.on('file', () => {
|
407 | throw new Error(`[${what}] Unexpected file`);
|
408 | });
|
409 |
|
410 | bb.on('close', () => {
|
411 | active.delete(test);
|
412 |
|
413 | assert.deepStrictEqual(
|
414 | results,
|
415 | test.expected,
|
416 | `[${what}] Results mismatch.\n`
|
417 | + `Parsed: ${inspect(results)}\n`
|
418 | + `Expected: ${inspect(test.expected)}`
|
419 | );
|
420 | });
|
421 |
|
422 | for (const src of test.source) {
|
423 | const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src);
|
424 | bb.write(buf);
|
425 | }
|
426 | bb.end();
|
427 | }
|
428 |
|
429 |
|
430 | for (let test of tests) {
|
431 | test = { ...test };
|
432 | test.what += ' (byte-by-byte)';
|
433 | active.set(test, 1);
|
434 |
|
435 | const { what } = test;
|
436 | const charset = test.charset || 'utf-8';
|
437 | const bb = busboy({
|
438 | limits: test.limits,
|
439 | headers: {
|
440 | 'content-type': `application/x-www-form-urlencoded; charset="${charset}"`,
|
441 | },
|
442 | });
|
443 | const results = [];
|
444 |
|
445 | bb.on('field', (key, val, info) => {
|
446 | results.push([key, val, info]);
|
447 | });
|
448 |
|
449 | bb.on('file', () => {
|
450 | throw new Error(`[${what}] Unexpected file`);
|
451 | });
|
452 |
|
453 | bb.on('close', () => {
|
454 | active.delete(test);
|
455 |
|
456 | assert.deepStrictEqual(
|
457 | results,
|
458 | test.expected,
|
459 | `[${what}] Results mismatch.\n`
|
460 | + `Parsed: ${inspect(results)}\n`
|
461 | + `Expected: ${inspect(test.expected)}`
|
462 | );
|
463 | });
|
464 |
|
465 | for (const src of test.source) {
|
466 | const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src);
|
467 | for (let i = 0; i < buf.length; ++i)
|
468 | bb.write(buf.slice(i, i + 1));
|
469 | }
|
470 | bb.end();
|
471 | }
|
472 |
|
473 | {
|
474 | let exception = false;
|
475 | process.once('uncaughtException', (ex) => {
|
476 | exception = true;
|
477 | throw ex;
|
478 | });
|
479 | process.on('exit', () => {
|
480 | if (exception || active.size === 0)
|
481 | return;
|
482 | process.exitCode = 1;
|
483 | console.error('==========================');
|
484 | console.error(`${active.size} test(s) did not finish:`);
|
485 | console.error('==========================');
|
486 | console.error(Array.from(active.keys()).map((v) => v.what).join('\n'));
|
487 | });
|
488 | }
|