support maximum request body size
[squeep-api-dingus] / test / lib / dingus.js
1 /* eslint-disable capitalized-comments */
2 /* eslint-env mocha */
3 'use strict';
4
5 const assert = require('assert');
6 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
7 const fs = require('fs');
8
9 const Dingus = require('../../lib/dingus');
10 const { DingusError } = require('../../lib/errors');
11 const Enum = require('../../lib/enum');
12
13 const noExpectedException = 'did not get expected exception';
14
15 describe('Dingus', function () {
16 let dingus;
17 beforeEach(function () {
18 dingus = new Dingus();
19 });
20 afterEach(function () {
21 sinon.restore();
22 });
23
24 describe('constructor', function () {
25 it('covers', function () {
26 const d = new Dingus({}, {});
27 assert(d);
28 assert('log' in d.logger);
29 });
30 }); // constructor
31
32 describe('_normalizePath', function () {
33 it('returns normal path', function () {
34 const p = '/a/b/c';
35 const r = dingus._normalizePath(p);
36 assert.strictEqual(r, p);
37 });
38 it('returns normal path', function () {
39 const p = '////a///b/./bar/..///c';
40 const expected = '/a/b/c'
41 const r = dingus._normalizePath(p);
42 assert.strictEqual(r, expected);
43 });
44 }); // _normalizePath
45
46 describe('_splitUrl', function () {
47 const nullObject = Object.create(null);
48
49 it('splits a simple path', function () {
50 const p = '/a/b/c';
51 const expected = {
52 pathPart: p,
53 queryParams: nullObject,
54 };
55 const r = dingus._splitUrl(p);
56 assert.deepStrictEqual(r, expected);
57 });
58 it('splits a path with trailing slash preserved', function () {
59 const p = '/a/b/c/';
60 const expected = {
61 pathPart: p,
62 queryParams: nullObject,
63 };
64 const r = dingus._splitUrl(p);
65 assert.deepStrictEqual(r, expected);
66 });
67 it('splits a path with trailing slash ignored', function () {
68 const p = '/a/b/c/';
69 const expected = {
70 pathPart: p,
71 queryParams: nullObject,
72 };
73 dingus.ignoreTrailingSlash = true;
74 const r = dingus._splitUrl(p);
75 assert.deepStrictEqual(r, expected);
76 });
77 it('splits a path with empty query string', function () {
78 const p = '/a/b/c?';
79 const expected = {
80 pathPart: '/a/b/c',
81 queryParams: nullObject,
82 };
83 const r = dingus._splitUrl(p);
84 assert.deepStrictEqual(r, expected);
85 });
86 it('splits a path with query string', function () {
87 const p = '/a/b/c?x=1&y=2&z';
88 const expected = {
89 pathPart: '/a/b/c',
90 queryParams: Object.assign(Object.create(null), {
91 x: '1',
92 y: '2',
93 z: '', // Subjective Editorial: disagree with the default querystring parser behavior here: null would be better than empty string, esp as result is null-prototyped object.
94 }),
95 };
96 const r = dingus._splitUrl(p);
97 assert.deepStrictEqual(r, expected);
98 });
99 }); // _splitUrl
100
101 describe('tagContext', function () {
102 let req, res, ctx;
103 beforeEach(function () {
104 req = {
105 getHeader: sinon.stub(),
106 setHeader: sinon.stub(),
107 };
108 res = {
109 getHeader: sinon.stub(),
110 setHeader: sinon.stub(),
111 };
112 ctx = {};
113 });
114 it ('sets id in context', function () {
115 const result = Dingus.tagContext(req, res, ctx);
116 assert.strictEqual(ctx.requestId, result);
117 assert(res.setHeader.called);
118 });
119 it ('sets provided header', function () {
120 req.getHeader.onCall(0).returns('abc'); // X-Request-ID
121 const result = Dingus.tagContext(req, res, ctx);
122 assert.strictEqual(ctx.requestId, result);
123 assert.strictEqual(res.setHeader.getCall(0).args[0], 'Request-ID');
124 assert.strictEqual(res.setHeader.getCall(1).args[0], 'X-Request-ID');
125 assert.strictEqual(res.setHeader.getCall(1).args[1], 'abc');
126 assert.strictEqual(res.setHeader.callCount, 2);
127 });
128 }); // tagContext
129
130 describe('clientAddressContext', function () {
131 let req, res, ctx;
132 let _tp;
133 before(function () {
134 _tp = dingus.trustProxy;
135 });
136 after(function () {
137 dingus.trustProxy = _tp;
138 });
139 beforeEach(function () {
140 req = {
141 getHeader: sinon.stub(),
142 setHeader: sinon.stub(),
143 connection: {},
144 };
145 res = {
146 getHeader: sinon.stub(),
147 setHeader: sinon.stub(),
148 };
149 ctx = {};
150 });
151 it ('covers untrusted proxy', function () {
152 dingus.trustProxy = false;
153 const expected = {
154 clientAddress: '',
155 clientProtocol: 'http',
156 }
157 dingus.clientAddressContext(req, res, ctx);
158 assert.deepStrictEqual(ctx, expected);
159 assert(!req.getHeader.called);
160 });
161 it ('covers missing', function () {
162 dingus.trustProxy = true;
163 const expected = {
164 clientAddress: '::1',
165 clientProtocol: 'https',
166 }
167 req.connection.remoteAddress = '::1';
168 req.connection.encrypted = true;
169 dingus.clientAddressContext(req, res, ctx);
170 assert(req.getHeader.called);
171 assert.deepStrictEqual(ctx, expected);
172 });
173 }); // clientAddressContext
174
175 describe('getRequestContentType', function () {
176 let req;
177 beforeEach(function () {
178 req = {
179 getHeader: sinon.stub(),
180 setHeader: sinon.stub(),
181 };
182 });
183 it('handles missing header', function () {
184 const result = Dingus.getRequestContentType(req);
185 assert.strictEqual(result, '');
186 });
187 it('parses simple type', function () {
188 req.getHeader.onCall(0).returns(Enum.ContentType.ApplicationJson);
189 const result = Dingus.getRequestContentType(req);
190 assert.strictEqual(result, Enum.ContentType.ApplicationJson);
191 });
192 it('parses complex type', function () {
193 req.getHeader.onCall(0).returns('application/json ; charset=UTF-8');
194 const result = Dingus.getRequestContentType(req);
195 assert.strictEqual(result, Enum.ContentType.ApplicationJson);
196 });
197 }); // getRequestContentType
198
199 describe('setResponseContentType', function () {
200 let req, responseTypes;
201 beforeEach(function () {
202 responseTypes = [];
203 req = {
204 setHeader: sinon.stub(),
205 getHeader: sinon.stub(),
206 };
207 });
208 it('handles missing header', function () {
209 const result = Dingus.getResponseContentType(responseTypes, req);
210 assert.strictEqual(result, undefined);
211 });
212 it('behaves as expected', function () {
213 responseTypes.push(Enum.ContentType.ApplicationJson);
214 req.getHeader.onCall(0).returns('text, image/png;q=0.5, application/*;q=0.2, audio;q=0.1');
215 const result = Dingus.getResponseContentType(responseTypes, req);
216 assert.strictEqual(result, Enum.ContentType.ApplicationJson);
217 });
218 }); // setResponseContentType
219
220 describe('on', function () {
221 let stubOn;
222 beforeEach(function () {
223 stubOn = sinon.stub(dingus.router, 'on');
224 });
225 it('covers', function () {
226 dingus.on('GET', '/', () => {});
227 assert(stubOn.called);
228 });
229 }); // on
230
231 describe('setEndBodyHandler', function () {
232 let req, res, ctx, handler, origEnd, origWrite;
233 beforeEach(function () {
234 origEnd = sinon.stub();
235 origWrite = sinon.stub();
236 req = {};
237 res = {
238 write: origWrite,
239 end: origEnd,
240 };
241 ctx = {};
242 handler = sinon.stub();
243 });
244 it('collects body and handles', function () {
245 Dingus.setEndBodyHandler(req, res, ctx, handler);
246 res.write(Buffer.from('foo'));
247 res.write('baz');
248 res.write();
249 res.end('quux');
250 assert(origWrite.called);
251 assert(origEnd.called);
252 assert.deepStrictEqual(ctx.responseBody, Buffer.from('foobazquux'));
253 assert(handler.called);
254 });
255 }); // setEndBodyHandler
256
257 describe('setHeadHandler', function () {
258 let req, res, ctx, origEnd, origWrite;
259 beforeEach(function () {
260 origEnd = sinon.stub();
261 origWrite = sinon.stub();
262 req = {
263 method: 'HEAD',
264 };
265 res = {
266 end: origEnd,
267 write: origWrite,
268 setHeader: sinon.stub(),
269 };
270 ctx = {};
271 });
272 it('collects body without writing', function () {
273 Dingus.setHeadHandler(req, res, ctx);
274 res.write(Buffer.from('foo'));
275 res.write('baz');
276 res.write();
277 res.end('quux');
278 assert(!origWrite.called);
279 assert(origEnd.called);
280 assert.deepStrictEqual(ctx.responseBody, Buffer.from('foobazquux'));
281 });
282 it('ignores non-head method', function () {
283 req.method = 'GET';
284 Dingus.setHeadHandler(req, res, ctx);
285 res.write(Buffer.from('foo'));
286 res.end('bar');
287 assert(origWrite.called);
288 assert(origEnd.called);
289 });
290 }); // setHeadHandler
291
292 describe('addEncodingHeader', function () {
293 let res, encoding;
294 beforeEach(function () {
295 res = {
296 _headers: {},
297 // eslint-disable-next-line security/detect-object-injection
298 getHeader: (h) => res._headers[h],
299 // eslint-disable-next-line security/detect-object-injection
300 setHeader: (h, v) => res._headers[h] = v,
301 };
302 });
303 it('adds', function () {
304 encoding = 'gzip';
305 Dingus.addEncodingHeader(res, encoding);
306 assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'gzip');
307 });
308 it('extends', function () {
309 encoding = 'utf8';
310 Dingus.addEncodingHeader(res, encoding);
311 assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'utf8');
312 encoding = 'gzip';
313 Dingus.addEncodingHeader(res, encoding);
314 assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'gzip, utf8');
315 });
316 }); // addEncodingHeader
317
318 describe('dispatch', function () {
319 let pathsByLengthOrig;
320 let req, res, ctx;
321 let stubHandler;
322
323 beforeEach(function () {
324 req = {
325 url: '/',
326 method: 'GET',
327 setHeader: sinon.stub(),
328 getHeader: sinon.stub(),
329 };
330 res = {
331 statusCode: 200,
332 end: sinon.stub(),
333 setHeader: sinon.stub(),
334 hasHeader: sinon.stub(),
335 getHeader: sinon.stub(),
336 getHeaders: sinon.stub(),
337 };
338 ctx = {};
339 pathsByLengthOrig = dingus.pathsByLength;
340 sinon.spy(dingus, 'handlerMethodNotAllowed');
341 sinon.spy(dingus, 'handlerNotFound');
342 sinon.spy(dingus, 'handlerBadRequest');
343 sinon.spy(dingus, 'handlerInternalServerError');
344 stubHandler = sinon.stub();
345 });
346 afterEach(function () {
347 dingus.pathsByLength = pathsByLengthOrig;
348 });
349
350 it('calls handler', async function () {
351 const urlPath = '/:id';
352 const method = 'GET';
353 dingus.on(method, urlPath, stubHandler);
354 req.url = '/abc';
355 req.method = method;
356
357 await dingus.dispatch(req, res, ctx);
358 assert(stubHandler.called);
359 assert(!dingus.handlerMethodNotAllowed.called);
360 assert(!dingus.handlerNotFound.called);
361 });
362 it('calls handler without context', async function () {
363 const urlPath = '/:id';
364 const method = 'GET';
365 dingus.on(method, urlPath, stubHandler);
366 req.url = '/abc';
367 req.method = method;
368
369 await dingus.dispatch(req, res);
370 assert(stubHandler.called);
371 assert(!dingus.handlerMethodNotAllowed.called);
372 assert(!dingus.handlerNotFound.called);
373 });
374 it('calls fallback handler', async function () {
375 const urlPath = '/abc/:id';
376 const method = 'GET';
377 dingus.on('*', urlPath, stubHandler);
378 req.url = '/abc/def';
379 req.method = method;
380
381 await dingus.dispatch(req, res, ctx);
382 assert(stubHandler.called);
383 assert(!dingus.handlerMethodNotAllowed.called);
384 assert(!dingus.handlerNotFound.called);
385 });
386 it('handles error in handler', async function () {
387 const urlPath = '/:id';
388 const method = 'GET';
389 dingus.on(method, urlPath, stubHandler);
390 req.url = '/abc';
391 req.method = method;
392 stubHandler.rejects(new Error('blah'));
393
394 await dingus.dispatch(req, res, ctx);
395 assert(stubHandler.called);
396 assert(!dingus.handlerMethodNotAllowed.called);
397 assert(!dingus.handlerNotFound.called);
398 });
399 it('calls unsupported method', async function () {
400 const urlPath = '/:id';
401 const method = 'POST';
402 dingus.on('GET', urlPath, stubHandler);
403 req.url = '/abc';
404 req.method = method;
405
406 await dingus.dispatch(req, res, ctx);
407 assert(!stubHandler.called);
408 assert(dingus.handlerMethodNotAllowed.called);
409 assert(!dingus.handlerNotFound.called);
410 });
411 it('does not lookup nonexistent path', async function () {
412 req.url = '/foo/bar';
413 req.method = 'GET';
414
415 await dingus.dispatch(req, res, ctx);
416 assert(!stubHandler.called);
417 assert(!dingus.handlerMethodNotAllowed.called);
418 assert(dingus.handlerNotFound.called);
419 });
420 it('covers unhandled dingus exception', async function () {
421 const expectedException = new DingusError('blah');
422 sinon.stub(dingus.router, 'lookup').throws(expectedException);
423
424 await dingus.dispatch(req, res, ctx);
425 assert(!stubHandler.called);
426 assert(dingus.handlerInternalServerError.called);
427 });
428 it('covers other exception', async function () {
429 const expectedException = new Error('blah');
430 sinon.stub(dingus.router, 'lookup').throws(expectedException);
431
432 await dingus.dispatch(req, res, ctx);
433 assert(!stubHandler.called);
434 assert(dingus.handlerInternalServerError.called);
435 });
436 it('covers bad uri', async function () {
437 req.url = '/%f';
438
439 await dingus.dispatch(req, res, ctx);
440 assert(dingus.handlerBadRequest.called);
441 });
442 it('calls handler with additional arguments', async function () {
443 dingus.on('GET', '/', stubHandler, 'foo', 'bar');
444 await dingus.dispatch(req, res, ctx);
445 assert(stubHandler.called);
446 assert.strictEqual(stubHandler.args[0][3], 'foo');
447 assert.strictEqual(stubHandler.args[0][4], 'bar');
448 });
449 }); // dispatch
450
451 describe('parseBody', function () {
452 let ctx;
453 beforeEach(function () {
454 ctx = {};
455 });
456 it('does not parse unknown type', function () {
457 try {
458 dingus.parseBody('unknown/type', ctx);
459 assert.fail(noExpectedException);
460 } catch (e) {
461 assert.strictEqual(e.statusCode, 415);
462 }
463 });
464 it('parses json', function () {
465 const src = { foo: 'bar' };
466 ctx.rawBody = JSON.stringify(src);
467 dingus.parseBody(Enum.ContentType.ApplicationJson, ctx);
468 assert.deepStrictEqual(ctx.parsedBody, src);
469 });
470 it('handles unparsable json', function () {
471 ctx.rawBody = 'not json';
472 try {
473 dingus.parseBody(Enum.ContentType.ApplicationJson, ctx);
474 assert.fail(noExpectedException);
475 } catch (e) {
476 assert.strictEqual(e.statusCode, 400);
477 }
478 });
479 it('parses form', function () {
480 const expected = Object.assign(Object.create(null), {
481 foo: 'bar',
482 });
483 ctx.rawBody = 'foo=bar';
484 dingus.parseBody('application/x-www-form-urlencoded', ctx);
485 assert.deepStrictEqual(ctx.parsedBody, expected);
486 });
487
488 }); // parseBody
489
490 describe('bodyData', function () {
491 let res, resEvents;
492 beforeEach(function () {
493 resEvents = {};
494 res = {
495 // eslint-disable-next-line security/detect-object-injection
496 on: (ev, fn) => resEvents[ev] = fn,
497 };
498 });
499 it('provides data', async function () {
500 const p = dingus.bodyData(res);
501 resEvents['data'](Buffer.from('foo'));
502 resEvents['data'](Buffer.from('bar'));
503 resEvents['end']();
504 const result = await p;
505 assert.strictEqual(result, 'foobar');
506 });
507 it('handles error', async function () {
508 const p = dingus.bodyData(res);
509 resEvents['error']('foo');
510 try {
511 await p;
512 assert.fail(noExpectedException);
513 } catch (e) {
514 assert.strictEqual(e, 'foo');
515 }
516 });
517 it('limits size', async function () {
518 const p = dingus.bodyData(res, 8);
519 resEvents['data'](Buffer.from('foobar'));
520 resEvents['data'](Buffer.from('bazquux'));
521 try {
522 await p;
523 assert.fail(noExpectedException);
524 } catch (e) {
525 assert.strictEqual(e.statusCode, 413);
526 }
527 });
528 }); // bodyData
529
530 describe('ingestBody', function () {
531 it('covers', async function () {
532 const req = {};
533 const res = {};
534 const ctx = {};
535 sinon.stub(dingus, 'bodyData').resolves('{"foo":"bar"}')
536 sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
537 await dingus.ingestBody(req, res, ctx);
538 assert.deepStrictEqual(ctx.parsedBody, { foo: 'bar' });
539 });
540 }); // ingestBody
541
542 describe('setResponseType', function () {
543 let req, res, ctx;
544 let _sa; // Preserve strictAccept
545 before(function () {
546 _sa = dingus.strictAccept;
547 });
548 after(function () {
549 dingus.strictAccept = _sa;
550 });
551 beforeEach(function () {
552 ctx = {};
553 req = {};
554 res = {
555 setHeader: sinon.stub(),
556 };
557 sinon.stub(Dingus, 'getResponseContentType').returns();
558 });
559 it('rejects missing', function () {
560 dingus.strictAccept = true;
561 try {
562 dingus.setResponseType(['my/type'], req, res, ctx);
563 assert.fail(noExpectedException);
564 } catch (e) {
565 assert.strictEqual(e.statusCode, 406, 'did not get expected status code');
566 }
567 });
568 it('accepts missing', function () {
569 dingus.strictAccept = false;
570 dingus.setResponseType(['my/type'], req, res, ctx);
571 assert.strictEqual(ctx.responseType, 'my/type');
572 });
573
574 }); // setResponseType
575
576 describe('_readFileInfo', function () {
577 let stat, data, statRes, dataRes, filename;
578 beforeEach(function () {
579 sinon.stub(fs.promises, 'stat');
580 sinon.stub(fs.promises, 'readFile');
581 statRes = {
582 mtimeMs:1612553697186,
583 };
584 dataRes = 'data';
585 filename = 'dummy.txt';
586 });
587 it('succeeds', async function () {
588 fs.promises.stat.resolves(statRes);
589 fs.promises.readFile.resolves('data');
590 [stat, data] = await dingus._readFileInfo(filename);
591 assert.deepStrictEqual(stat, statRes);
592 assert.deepStrictEqual(data, dataRes);
593 });
594 it('returns null for non-existant file', async function () {
595 const noEnt = {
596 code: 'ENOENT',
597 };
598 fs.promises.stat.rejects(noEnt);
599 fs.promises.readFile.rejects(noEnt);
600 [stat, data] = await dingus._readFileInfo(filename);
601 assert.strictEqual(stat, null);
602 assert.strictEqual(data, null);
603 });
604 it('throws unexpected error', async function () {
605 const expectedException = new Error('blah');
606 fs.promises.stat.rejects(expectedException);
607 await assert.rejects(async () => {
608 await dingus._readFileInfo(filename);
609 }, expectedException);
610 });
611 }); // _readFileInfo
612
613 describe('_serveFileMetaHeaders', function () {
614 let res, directory, fileName;
615 beforeEach(function () {
616 sinon.stub(dingus, '_readFileInfo');
617 res = {
618 setHeader: sinon.stub(),
619 };
620 directory = '/path';
621 fileName = 'filename';
622 });
623 it('covers no meta file', async function() {
624 dingus._readFileInfo.resolves([null, null]);
625 await dingus._serveFileMetaHeaders(res, directory, fileName);
626 assert(!res.setHeader.called);
627 });
628 it('adds extra headers', async function () {
629 dingus._readFileInfo.resolves([{}, Buffer.from(`Link: <https://example.com/>; rel="relation"
630 X-Folded-Header: data
631 data under
632 the fold
633 Content-Type: image/sgi
634 `)]);
635 await dingus._serveFileMetaHeaders(res, directory, fileName);
636 assert(res.setHeader.called);
637 });
638 }); // _serveFileMetaHeaders
639
640 describe('serveFile', function () {
641 const path = require('path');
642 let ctx, req, res, directory, fileName, filestats;
643 beforeEach(function () {
644 directory = path.join(__dirname, '..', 'test-data');
645 fileName = 'example.html';
646 ctx = {};
647 req = {
648 _headers: {
649 [Enum.Header.Accept]: undefined,
650 [Enum.Header.IfModifiedSince]: undefined,
651 [Enum.Header.AcceptEncoding]: undefined,
652 [Enum.Header.IfNoneMatch]: undefined,
653 },
654 getHeader: (header) => {
655 if (header in req._headers) {
656 // eslint-disable-next-line security/detect-object-injection
657 return req._headers[header];
658 }
659 assert.fail(`unexpected getHeader ${header}`);
660 },
661 };
662 res = {
663 end: sinon.stub(),
664 getHeader: sinon.stub(),
665 getHeaders: sinon.stub(),
666 hasHeader: sinon.stub().returns(true),
667 setHeader: sinon.stub(),
668 };
669 filestats = {
670 dev: 39,
671 mode: 33188,
672 nlink: 1,
673 uid: 1002,
674 gid: 1002,
675 rdev: 0,
676 blksize: 512,
677 ino: 897653,
678 size: 8,
679 blocks: 17,
680 atimeMs: 1613253436842.815,
681 mtimeMs: 1603485933192.861,
682 ctimeMs: 1603485933192.861,
683 birthtimeMs: 0,
684 atime: '2021-02-13T21:57:16.843Z',
685 mtime: '2020-10-23T13:45:33.193Z',
686 ctime: '2020-10-23T13:45:33.193Z',
687 birthtime: '1970-01-01T00:00:00.000Z',
688 };
689 sinon.stub(dingus, 'handlerNotFound');
690 sinon.stub(fs.promises, 'stat').resolves(filestats);
691 sinon.spy(fs.promises, 'readFile');
692 });
693 it('serves a file', async function () {
694 await dingus.serveFile(req, res, ctx, directory, fileName);
695 assert(fs.promises.readFile.called);
696 assert(!dingus.handlerNotFound.called);
697 });
698 it('covers no meta headers', async function () {
699 dingus.staticMetadata = false;
700 await dingus.serveFile(req, res, ctx, directory, fileName);
701 assert(fs.promises.readFile.called);
702 assert(!dingus.handlerNotFound.called);
703 });
704 it('does not serve dot-file', async function () {
705 fileName = '.example';
706 await dingus.serveFile(req, res, ctx, directory, fileName);
707 assert(!fs.promises.readFile.called);
708 assert(dingus.handlerNotFound.called);
709 });
710 it('does not serve encoded navigation', async function () {
711 fileName = '/example.html';
712 await dingus.serveFile(req, res, ctx, directory, fileName);
713 assert(!fs.promises.readFile.called);
714 assert(dingus.handlerNotFound.called);
715 });
716 it('does not serve missing file', async function () {
717 fileName = 'no-file.here';
718 await dingus.serveFile(req, res, ctx, directory, fileName);
719 assert(dingus.handlerNotFound.called);
720 });
721 it('requires directory be specified', async function () {
722 await dingus.serveFile(req, res, ctx, '', fileName);
723 assert(!fs.promises.readFile.called);
724 assert(dingus.handlerNotFound.called);
725 });
726 it('covers fs error', async function () {
727 const expectedException = new Error('blah');
728 fs.promises.stat.restore();
729 sinon.stub(fs.promises, 'stat').rejects(expectedException);
730 try {
731 await dingus.serveFile(req, res, ctx, directory, fileName);
732 assert.fail('should have thrown');
733 } catch (e) {
734 assert.strictEqual(e, expectedException);
735 }
736 });
737 it('caches by modified', async function () {
738 req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 23:11:16 GMT';
739 await dingus.serveFile(req, res, ctx, directory, fileName);
740 assert.strictEqual(res.statusCode, 304);
741 });
742 it('does not cache old modified', async function () {
743 req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 01:11:16 GMT';
744 await dingus.serveFile(req, res, ctx, directory, fileName);
745 assert.notStrictEqual(res.statusCode, 304);
746 assert(!dingus.handlerNotFound.called);
747 });
748 it('caches ETag match', async function () {
749 req._headers[Enum.Header.IfNoneMatch] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
750 await dingus.serveFile(req, res, ctx, directory, fileName);
751 assert.strictEqual(res.statusCode, 304);
752 });
753 it('does not cache ETag non-match', async function () {
754 req._headers[Enum.Header.IfNoneMatch] = '"foo", "bar"';
755 await dingus.serveFile(req, res, ctx, directory, fileName);
756 assert.notStrictEqual(res.statusCode, 304);
757 assert(!dingus.handlerNotFound.called);
758 });
759 it('handles no possible encodings', async function () {
760 req._headers[Enum.Header.AcceptEncoding] = '*;q=0';
761 await assert.rejects(async () => {
762 await dingus.serveFile(req, res, ctx, directory, fileName);
763 }, {
764 name: 'ResponseError',
765 });
766 });
767 it('handles a valid encoding', async function () {
768 req._headers[Enum.Header.AcceptEncoding] = 'gzip';
769 await dingus.serveFile(req, res, ctx, directory, fileName);
770 assert(res.end.called);
771 });
772 it('handles a valid encoding among others', async function () {
773 req._headers[Enum.Header.AcceptEncoding] = 'flarp, br, gzip';
774 fs.promises.stat.restore();
775 sinon.stub(fs.promises, 'stat')
776 .onCall(0).resolves(filestats) // identity file
777 .onCall(1).resolves(null) // br encoding
778 .onCall(2).resolves(filestats); // gzip encoding
779 await dingus.serveFile(req, res, ctx, directory, fileName);
780 assert(res.end.called);
781 });
782 }); // serveFile
783
784 describe('renderError', function () {
785 let err;
786 beforeEach(function () {
787 err = {
788 statusCode: '200',
789 errorMessage: 'OK',
790 details: 'hunkydorey',
791 };
792 });
793 it('renders unknown type', function () {
794 const contentType = 'unknown/type';
795 const result = dingus.renderError(contentType, err);
796 assert.deepStrictEqual(result, 'OK\r\nhunkydorey');
797 });
798 it('renders text', function () {
799 const contentType = 'text/plain';
800 const result = dingus.renderError(contentType, err);
801 assert.deepStrictEqual(result, 'OK\r\nhunkydorey');
802 });
803 it('renders json', function () {
804 const contentType = Enum.ContentType.ApplicationJson;
805 const result = dingus.renderError(contentType, err);
806 assert.deepStrictEqual(result, JSON.stringify(err));
807 });
808 it('renders html without details', function () {
809 err = {
810 statusCode: '201',
811 errorMessage: 'Created',
812 };
813 const contentType = 'text/html';
814 const result = dingus.renderError(contentType, err);
815 assert.deepStrictEqual(result, `<!DOCTYPE html>
816 <html lang="en">
817 <head>
818 <title>${err.statusCode} ${err.errorMessage}</title>
819 </head>
820 <body>
821 <h1>${err.errorMessage}</h1>
822 </body>
823 </html>`);
824 });
825 it('renders html', function () {
826 const contentType = 'text/html';
827 const result = dingus.renderError(contentType, err);
828 assert.deepStrictEqual(result, `<!DOCTYPE html>
829 <html lang="en">
830 <head>
831 <title>${err.statusCode} ${err.errorMessage}</title>
832 </head>
833 <body>
834 <h1>${err.errorMessage}</h1>
835 <p>${err.details}</p>
836 </body>
837 </html>`);
838 });
839 it('renders html, multiple details', function () {
840 const contentType = 'text/html';
841 err.details = ['one detail', 'two detail'];
842 const result = dingus.renderError(contentType, err);
843 assert.deepStrictEqual(result, `<!DOCTYPE html>
844 <html lang="en">
845 <head>
846 <title>${err.statusCode} ${err.errorMessage}</title>
847 </head>
848 <body>
849 <h1>${err.errorMessage}</h1>
850 <p>one detail</p>
851 <p>two detail</p>
852 </body>
853 </html>`);
854 });
855 }); // renderError
856
857 describe('sendErrorResponse', function () {
858 let ctx, req, res;
859 beforeEach(function () {
860 ctx = {};
861 req = {};
862 res = {
863 end: sinon.stub(),
864 getHeader: sinon.stub(),
865 getHeaders: sinon.stub(),
866 hasHeader: sinon.stub().returns(true),
867 setHeader: sinon.stub(),
868 };
869 sinon.stub(dingus, 'renderError');
870 });
871 it('covers', function () {
872 const err = {
873 statusCode: 444,
874 };
875 dingus.sendErrorResponse(err, req, res, ctx);
876 assert(res.end.called);
877 });
878 }); // sendErrorResponse
879
880 describe('proxyPrefix', function () {
881 let req, res, ctx, stubHandler, pfxDingus;
882 const pfx = '/pfx';
883
884 beforeEach(function () {
885 pfxDingus = new Dingus(console, { proxyPrefix: pfx });
886 req = {
887 setHeader: sinon.stub(),
888 getHeader: sinon.stub(),
889 };
890 res = {
891 statusCode: 200,
892 end: sinon.stub(),
893 setHeader: sinon.stub(),
894 getHeader: sinon.stub(),
895 };
896 ctx = {};
897 sinon.stub(pfxDingus, 'handlerMethodNotAllowed');
898 sinon.stub(pfxDingus, 'handlerNotFound');
899 stubHandler = sinon.stub();
900 });
901 afterEach(function () {
902 sinon.restore();
903 });
904
905 it('handles prefixed route', async function () {
906 const urlPath = '/:id';
907 const method = 'GET';
908 pfxDingus.on(method, urlPath, stubHandler);
909 req.url = pfx + '/abc';
910 req.method = method;
911
912 await pfxDingus.dispatch(req, res, ctx);
913 assert(stubHandler.called);
914 assert(!pfxDingus.handlerMethodNotAllowed.called);
915 assert(!pfxDingus.handlerNotFound.called);
916 });
917 it('does not handle prefixed route', async function () {
918 const urlPath = '/:id';
919 const method = 'GET';
920 pfxDingus.on(method, urlPath, stubHandler);
921 req.url = '/wrongpfx/abc';
922 req.method = method;
923
924 await pfxDingus.dispatch(req, res, ctx);
925 assert(!stubHandler.called);
926 assert(!pfxDingus.handlerMethodNotAllowed.called);
927 assert(pfxDingus.handlerNotFound.called);
928 });
929 }); // proxyPrefix
930
931 describe('handlerRedirect', function () {
932 let req, res, ctx;
933 beforeEach(function () {
934 req = {
935 getHeader: sinon.stub(),
936 };
937 res = {
938 setHeader: sinon.stub(),
939 end: sinon.stub(),
940 };
941 ctx = {};
942 });
943 it('covers', async function () {
944 await dingus.handlerRedirect(req, res, ctx);
945 assert(res.setHeader.called);
946 assert(res.end.called);
947 });
948 it('covers non-defaults', async function () {
949 await dingus.handlerRedirect(req, res, ctx, 308);
950 assert(res.setHeader.called);
951 assert(res.end.called);
952 });
953 }); // handlerRedirect
954
955 describe('handlerGetStaticFile', function () {
956 let req, res, ctx;
957 beforeEach(function () {
958 req = {
959 getHeader: sinon.stub(),
960 };
961 res = {
962 setHeader: sinon.stub(),
963 };
964 ctx = {
965 params: {
966 file: '',
967 },
968 };
969 sinon.stub(dingus, 'serveFile');
970 });
971 it('covers', async function () {
972 await dingus.handlerGetStaticFile(req, res, ctx);
973 assert(dingus.serveFile.called);
974 });
975 it('covers specified file', async function () {
976 await dingus.handlerGetStaticFile(req, res, ctx, 'file.txt');
977 assert(dingus.serveFile.called);
978 });
979 }); // handlerGetStaticFile
980 });