1 /* eslint-disable capitalized-comments */
5 const assert
= require('assert');
6 const sinon
= require('sinon'); // eslint-disable-line node/no-unpublished-require
7 const fs
= require('fs');
9 const Dingus
= require('../../lib/dingus');
10 const { DingusError
, RouterNoMethodError
} = require('../../lib/errors');
11 const Enum
= require('../../lib/enum');
13 const noExpectedException
= 'did not get expected exception';
15 const _nop
= () => {};
16 const _logFn
= (process
.env
['VERBOSE_TESTS'] && console
.log
) || _nop
;
21 sinon
.spy(noLogger
, 'debug');
22 sinon
.spy(noLogger
, 'error');
24 describe('Dingus', function () {
26 beforeEach(function () {
27 dingus
= new Dingus(noLogger
, {});
29 afterEach(function () {
33 describe('constructor', function () {
34 it('covers', function () {
35 const d
= new Dingus();
40 describe('_normalizePath', function () {
41 it('returns normal path', function () {
43 const r
= dingus
._normalizePath(p
);
44 assert
.strictEqual(r
, p
);
46 it('returns normal path', function () {
47 const p
= '////a///b/./bar/..///c';
48 const expected
= '/a/b/c';
49 const r
= dingus
._normalizePath(p
);
50 assert
.strictEqual(r
, expected
);
54 describe('_splitUrl', function () {
55 const nullObject
= Object
.create(null);
57 it('splits a simple path', function () {
61 queryParams: nullObject
,
63 const r
= dingus
._splitUrl(p
);
64 assert
.deepStrictEqual(r
, expected
);
66 it('splits a path with trailing slash preserved', function () {
70 queryParams: nullObject
,
72 const r
= dingus
._splitUrl(p
);
73 assert
.deepStrictEqual(r
, expected
);
75 it('splits a path with trailing slash ignored', function () {
79 queryParams: nullObject
,
81 dingus
.ignoreTrailingSlash
= true;
82 const r
= dingus
._splitUrl(p
);
83 assert
.deepStrictEqual(r
, expected
);
85 it('splits a path with empty query string', function () {
89 queryParams: nullObject
,
91 const r
= dingus
._splitUrl(p
);
92 assert
.deepStrictEqual(r
, expected
);
94 it('splits a path with query string', function () {
95 const p
= '/a/b/c?x=1&y=2&z';
98 queryParams: Object
.assign(Object
.create(null), {
101 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.
104 const r
= dingus
._splitUrl(p
);
105 assert
.deepStrictEqual(r
, expected
);
109 describe('tagContext', function () {
111 beforeEach(function () {
113 getHeader: sinon
.stub(),
114 setHeader: sinon
.stub(),
117 getHeader: sinon
.stub(),
118 setHeader: sinon
.stub(),
122 it ('sets id in context', function () {
123 const result
= Dingus
.tagContext(req
, res
, ctx
);
124 assert
.strictEqual(ctx
.requestId
, result
);
125 assert(res
.setHeader
.called
);
127 it ('sets provided header', function () {
128 req
.getHeader
.onCall(0).returns('abc'); // X-Request-ID
129 const result
= Dingus
.tagContext(req
, res
, ctx
);
130 assert
.strictEqual(ctx
.requestId
, result
);
131 assert
.strictEqual(res
.setHeader
.getCall(0).args
[0], 'Request-ID');
132 assert
.strictEqual(res
.setHeader
.getCall(1).args
[0], 'X-Request-ID');
133 assert
.strictEqual(res
.setHeader
.getCall(1).args
[1], 'abc');
134 assert
.strictEqual(res
.setHeader
.callCount
, 2);
138 describe('clientAddressContext', function () {
142 _tp
= dingus
.trustProxy
;
145 dingus
.trustProxy
= _tp
;
147 beforeEach(function () {
149 getHeader: sinon
.stub(),
150 setHeader: sinon
.stub(),
154 getHeader: sinon
.stub(),
155 setHeader: sinon
.stub(),
159 it ('covers untrusted proxy', function () {
160 dingus
.trustProxy
= false;
163 clientProtocol: 'http',
165 dingus
.clientAddressContext(req
, res
, ctx
);
166 assert
.deepStrictEqual(ctx
, expected
);
167 assert(!req
.getHeader
.called
);
169 it ('covers missing', function () {
170 dingus
.trustProxy
= true;
172 clientAddress: '::1',
173 clientProtocol: 'https',
175 req
.connection
.remoteAddress
= '::1';
176 req
.connection
.encrypted
= true;
177 dingus
.clientAddressContext(req
, res
, ctx
);
178 assert(req
.getHeader
.called
);
179 assert
.deepStrictEqual(ctx
, expected
);
181 }); // clientAddressContext
183 describe('getRequestContentType', function () {
185 beforeEach(function () {
187 getHeader: sinon
.stub(),
188 setHeader: sinon
.stub(),
191 it('handles missing header', function () {
192 const result
= Dingus
.getRequestContentType(req
);
193 assert
.strictEqual(result
, '');
195 it('parses simple type', function () {
196 req
.getHeader
.onCall(0).returns(Enum
.ContentType
.ApplicationJson
);
197 const result
= Dingus
.getRequestContentType(req
);
198 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
200 it('parses complex type', function () {
201 req
.getHeader
.onCall(0).returns('application/json ; charset=UTF-8');
202 const result
= Dingus
.getRequestContentType(req
);
203 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
205 }); // getRequestContentType
207 describe('setResponseContentType', function () {
208 let req
, responseTypes
;
209 beforeEach(function () {
212 setHeader: sinon
.stub(),
213 getHeader: sinon
.stub(),
216 it('handles missing header', function () {
217 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
218 assert
.strictEqual(result
, undefined);
220 it('behaves as expected', function () {
221 responseTypes
.push(Enum
.ContentType
.ApplicationJson
);
222 req
.getHeader
.onCall(0).returns('text, image/png;q=0.5, application/*;q=0.2, audio;q=0.1');
223 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
224 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
226 }); // setResponseContentType
228 describe('on', function () {
230 beforeEach(function () {
231 stubOn
= sinon
.stub(dingus
.router
, 'on');
233 it('covers', function () {
234 dingus
.on('GET', '/', () => {});
235 assert(stubOn
.called
);
239 describe('setEndBodyHandler', function () {
240 let req
, res
, ctx
, handler
, origEnd
, origWrite
;
241 beforeEach(function () {
242 origEnd
= sinon
.stub();
243 origWrite
= sinon
.stub();
250 handler
= sinon
.stub();
252 it('collects body and handles', function () {
253 Dingus
.setEndBodyHandler(req
, res
, ctx
, handler
);
254 res
.write(Buffer
.from('foo'));
258 assert(origWrite
.called
);
259 assert(origEnd
.called
);
260 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
261 assert(handler
.called
);
263 }); // setEndBodyHandler
265 describe('setHeadHandler', function () {
266 let req
, res
, ctx
, origEnd
, origWrite
;
267 beforeEach(function () {
268 origEnd
= sinon
.stub();
269 origWrite
= sinon
.stub();
276 setHeader: sinon
.stub(),
280 it('collects response without writing', function () {
281 Dingus
.setHeadHandler(req
, res
, ctx
);
282 res
.write(Buffer
.from('foo'));
286 assert(!origWrite
.called
);
287 assert(origEnd
.called
);
288 assert
.deepStrictEqual(ctx
.responseBody
, undefined);
290 it('collects response without writing, persists written data', function () {
291 Dingus
.setHeadHandler(req
, res
, ctx
, true);
292 res
.write(Buffer
.from('foo'));
296 assert(!origWrite
.called
);
297 assert(origEnd
.called
);
298 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
300 it('ignores non-head method', function () {
302 Dingus
.setHeadHandler(req
, res
, ctx
);
303 res
.write(Buffer
.from('foo'));
305 assert(origWrite
.called
);
306 assert(origEnd
.called
);
308 }); // setHeadHandler
310 describe('addEncodingHeader', function () {
312 beforeEach(function () {
315 // eslint-disable-next-line security/detect-object-injection
316 getHeader: (h
) => res
._headers
[h
],
317 // eslint-disable-next-line security/detect-object-injection
318 setHeader: (h
, v
) => res
._headers
[h
] = v
,
321 it('adds', function () {
323 Dingus
.addEncodingHeader(res
, encoding
);
324 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip');
326 it('extends', function () {
328 Dingus
.addEncodingHeader(res
, encoding
);
329 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'utf8');
331 Dingus
.addEncodingHeader(res
, encoding
);
332 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip, utf8');
334 }); // addEncodingHeader
336 describe('dispatch', function () {
337 let pathsByLengthOrig
;
341 beforeEach(function () {
345 setHeader: sinon
.stub(),
346 getHeader: sinon
.stub(),
351 setHeader: sinon
.stub(),
352 hasHeader: sinon
.stub(),
353 getHeader: sinon
.stub(),
354 getHeaders: sinon
.stub(),
357 pathsByLengthOrig
= dingus
.pathsByLength
;
358 sinon
.spy(dingus
, 'handlerMethodNotAllowed');
359 sinon
.spy(dingus
, 'handlerNotFound');
360 sinon
.spy(dingus
, 'handlerBadRequest');
361 sinon
.spy(dingus
, 'handlerInternalServerError');
362 sinon
.spy(Dingus
, 'setHeadHandler');
363 stubHandler
= sinon
.stub();
365 afterEach(function () {
366 dingus
.pathsByLength
= pathsByLengthOrig
;
369 it('calls handler', async
function () {
370 const urlPath
= '/:id';
371 const method
= 'GET';
372 dingus
.on(method
, urlPath
, stubHandler
);
376 await dingus
.dispatch(req
, res
, ctx
);
377 assert(stubHandler
.called
);
378 assert(!dingus
.handlerMethodNotAllowed
.called
);
379 assert(!dingus
.handlerNotFound
.called
);
381 it('calls handler without context', async
function () {
382 const urlPath
= '/:id';
383 const method
= 'GET';
384 dingus
.on(method
, urlPath
, stubHandler
);
388 await dingus
.dispatch(req
, res
);
389 assert(stubHandler
.called
);
390 assert(!dingus
.handlerMethodNotAllowed
.called
);
391 assert(!dingus
.handlerNotFound
.called
);
393 it('calls fallback handler', async
function () {
394 const urlPath
= '/abc/:id';
395 const method
= 'GET';
396 dingus
.on('*', urlPath
, stubHandler
);
397 req
.url
= '/abc/def';
400 await dingus
.dispatch(req
, res
, ctx
);
401 assert(stubHandler
.called
);
402 assert(!dingus
.handlerMethodNotAllowed
.called
);
403 assert(!dingus
.handlerNotFound
.called
);
405 it('handles error in handler', async
function () {
406 const urlPath
= '/:id';
407 const method
= 'GET';
408 dingus
.on(method
, urlPath
, stubHandler
);
411 stubHandler
.rejects(new Error('blah'));
413 await dingus
.dispatch(req
, res
, ctx
);
414 assert(stubHandler
.called
);
415 assert(!dingus
.handlerMethodNotAllowed
.called
);
416 assert(!dingus
.handlerNotFound
.called
);
418 it('calls unsupported method', async
function () {
419 const urlPath
= '/:id';
420 const method
= 'POST';
421 dingus
.on('GET', urlPath
, stubHandler
);
425 await dingus
.dispatch(req
, res
, ctx
);
426 assert(!stubHandler
.called
);
427 assert(dingus
.handlerMethodNotAllowed
.called
);
428 assert(!dingus
.handlerNotFound
.called
);
430 it('does not lookup nonexistent path', async
function () {
431 req
.url
= '/foo/bar';
434 await dingus
.dispatch(req
, res
, ctx
);
435 assert(!stubHandler
.called
);
436 assert(!dingus
.handlerMethodNotAllowed
.called
);
437 assert(dingus
.handlerNotFound
.called
);
439 it('covers unhandled dingus exception', async
function () {
440 const expectedException
= new DingusError('blah');
441 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
443 await dingus
.dispatch(req
, res
, ctx
);
444 assert(!stubHandler
.called
);
445 assert(dingus
.handlerInternalServerError
.called
);
447 it('covers other exception', async
function () {
448 const expectedException
= new Error('blah');
449 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
451 await dingus
.dispatch(req
, res
, ctx
);
452 assert(!stubHandler
.called
);
453 assert(dingus
.handlerInternalServerError
.called
);
455 it('covers bad uri', async
function () {
458 await dingus
.dispatch(req
, res
, ctx
);
459 assert(dingus
.handlerBadRequest
.called
);
461 it('calls handler with additional arguments', async
function () {
462 dingus
.on('GET', '/', stubHandler
, 'foo', 'bar');
463 await dingus
.dispatch(req
, res
, ctx
);
464 assert(stubHandler
.called
);
465 assert
.strictEqual(stubHandler
.args
[0][3], 'foo');
466 assert
.strictEqual(stubHandler
.args
[0][4], 'bar');
468 describe('intrinsic HEAD handling', function () {
469 it('covers no intrinsic HEAD handling', async
function () {
470 dingus
.intrinsicHeadMethod
= false;
471 dingus
.on('GET', '/', stubHandler
);
473 await dingus
.dispatch(req
, res
, ctx
);
474 assert(!stubHandler
.called
);
475 assert(dingus
.handlerMethodNotAllowed
.called
);
477 it('calls HEAD setup and GET handler', async
function () {
478 dingus
.on('GET', '/', stubHandler
);
480 await dingus
.dispatch(req
, res
, ctx
);
481 assert(Dingus
.setHeadHandler
.called
);
482 assert(stubHandler
.called
);
484 it('covers no GET handler', async
function () {
485 dingus
.on('POST', '/', stubHandler
);
487 await dingus
.dispatch(req
, res
, ctx
);
488 assert(!stubHandler
.called
);
489 assert(dingus
.handlerMethodNotAllowed
.called
);
491 it('covers unexpected router error', async
function () {
492 sinon
.stub(dingus
.router
, 'lookup')
493 .onFirstCall().throws(new RouterNoMethodError())
494 .onSecondCall().throws(new DingusError())
496 dingus
.on('GET', '/', stubHandler
);
498 await dingus
.dispatch(req
, res
, ctx
);
499 assert(dingus
.handlerInternalServerError
.called
);
504 describe('parseBody', function () {
506 beforeEach(function () {
509 it('does not parse unknown type', function () {
511 dingus
.parseBody('unknown/type', ctx
);
512 assert
.fail(noExpectedException
);
514 assert
.strictEqual(e
.statusCode
, 415);
517 it('parses json', function () {
518 const src
= { foo: 'bar' };
519 const rawBody
= JSON
.stringify(src
);
520 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
, rawBody
);
521 assert
.deepStrictEqual(ctx
.parsedBody
, src
);
523 it('handles unparsable json', function () {
524 const rawBody
= 'not json';
526 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
, rawBody
);
527 assert
.fail(noExpectedException
);
529 assert
.strictEqual(e
.statusCode
, 400);
532 it('parses form', function () {
533 const expected
= Object
.assign(Object
.create(null), {
536 const rawBody
= 'foo=bar';
537 dingus
.parseBody('application/x-www-form-urlencoded', ctx
, rawBody
);
538 assert
.deepStrictEqual(ctx
.parsedBody
, expected
);
543 describe('bodyData', function () {
545 beforeEach(function () {
548 // eslint-disable-next-line security/detect-object-injection
549 on: (ev
, fn
) => resEvents
[ev
] = fn
,
552 it('provides data', async
function () {
553 const p
= dingus
.bodyData(res
);
554 resEvents
['data'](Buffer
.from('foo'));
555 resEvents
['data'](Buffer
.from('bar'));
557 const result
= await p
;
558 assert
.strictEqual(result
, 'foobar');
560 it('handles error', async
function () {
561 const p
= dingus
.bodyData(res
);
562 resEvents
['error']('foo');
565 assert
.fail(noExpectedException
);
567 assert
.strictEqual(e
, 'foo');
570 it('limits size', async
function () {
571 const p
= dingus
.bodyData(res
, 8);
572 resEvents
['data'](Buffer
.from('foobar'));
573 resEvents
['data'](Buffer
.from('bazquux'));
576 assert
.fail(noExpectedException
);
578 assert
.strictEqual(e
.statusCode
, 413);
581 it('provides buffer', async
function () {
582 const p
= dingus
.bodyData(res
, 0, false);
583 const expected
= Buffer
.from('bleat');
584 resEvents
['data'](expected
);
586 const result
= await p
;
587 assert
.deepStrictEqual(result
, expected
);
591 describe('ingestBody', function () {
592 it('ingests json', async
function () {
596 sinon
.stub(dingus
, 'bodyData').resolves('{"foo":"bar"}');
597 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
598 await dingus
.ingestBody(req
, res
, ctx
);
599 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
600 assert
.deepStrictEqual(ctx
.rawBody
, undefined);
602 it('persists rawBody', async
function () {
606 const body
= '{"foo":"bar"}';
607 sinon
.stub(dingus
, 'bodyData').resolves(body
);
608 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
609 await dingus
.ingestBody(req
, res
, ctx
, { persistRawBody: true });
610 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
611 assert
.deepStrictEqual(ctx
.rawBody
, body
);
613 it('skips parsing empty body', async
function () {
618 sinon
.stub(dingus
, 'bodyData').resolves(body
);
619 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
620 sinon
.spy(dingus
, 'parseBody');
621 await dingus
.ingestBody(req
, res
, ctx
, { parseEmptyBody: false });
622 assert
.deepStrictEqual(ctx
.parsedBody
, undefined);
623 assert(dingus
.parseBody
.notCalled
);
627 describe('setResponseType', function () {
629 let _sa
; // Preserve strictAccept
631 _sa
= dingus
.strictAccept
;
634 dingus
.strictAccept
= _sa
;
636 beforeEach(function () {
640 setHeader: sinon
.stub(),
642 sinon
.stub(Dingus
, 'getResponseContentType').returns();
644 it('rejects missing', function () {
645 dingus
.strictAccept
= true;
647 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
648 assert
.fail(noExpectedException
);
650 assert
.strictEqual(e
.statusCode
, 406, 'did not get expected status code');
653 it('accepts missing', function () {
654 dingus
.strictAccept
= false;
655 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
656 assert
.strictEqual(ctx
.responseType
, 'my/type');
659 }); // setResponseType
661 describe('_readFileInfo', function () {
662 let stat
, data
, statRes
, dataRes
, filename
;
663 beforeEach(function () {
664 sinon
.stub(fs
.promises
, 'stat');
665 sinon
.stub(fs
.promises
, 'readFile');
667 mtimeMs:1612553697186,
670 filename
= 'dummy.txt';
672 it('succeeds', async
function () {
673 fs
.promises
.stat
.resolves(statRes
);
674 fs
.promises
.readFile
.resolves('data');
675 [stat
, data
] = await dingus
._readFileInfo(filename
);
676 assert
.deepStrictEqual(stat
, statRes
);
677 assert
.deepStrictEqual(data
, dataRes
);
679 it('returns null for non-existant file', async
function () {
683 fs
.promises
.stat
.rejects(noEnt
);
684 fs
.promises
.readFile
.rejects(noEnt
);
685 [stat
, data
] = await dingus
._readFileInfo(filename
);
686 assert
.strictEqual(stat
, null);
687 assert
.strictEqual(data
, null);
689 it('throws unexpected error', async
function () {
690 const expectedException
= new Error('blah');
691 fs
.promises
.stat
.rejects(expectedException
);
692 await assert
.rejects(async () => {
693 await dingus
._readFileInfo(filename
);
694 }, expectedException
);
698 describe('_serveFileMetaHeaders', function () {
699 let res
, directory
, fileName
;
700 beforeEach(function () {
701 sinon
.stub(dingus
, '_readFileInfo');
703 setHeader: sinon
.stub(),
706 fileName
= 'filename';
708 it('covers no meta file', async
function() {
709 dingus
._readFileInfo
.resolves([null, null]);
710 await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
711 assert(!res
.setHeader
.called
);
713 it('adds extra headers', async
function () {
714 dingus
._readFileInfo
.resolves([{}, Buffer
.from(`Link: <https://example.com/>; rel="relation"
715 X-Folded-Header: data
718 Content-Type: image/sgi
720 await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
721 assert(res
.setHeader
.called
);
723 }); // _serveFileMetaHeaders
725 describe('serveFile', function () {
726 const path
= require('path');
727 let ctx
, req
, res
, directory
, fileName
, filestats
;
728 beforeEach(function () {
729 directory
= path
.join(__dirname
, '..', 'test-data');
730 fileName
= 'example.html';
734 [Enum
.Header
.Accept
]: undefined,
735 [Enum
.Header
.IfModifiedSince
]: undefined,
736 [Enum
.Header
.AcceptEncoding
]: undefined,
737 [Enum
.Header
.IfNoneMatch
]: undefined,
739 getHeader: (header
) => {
740 if (header
in req
._headers
) {
741 // eslint-disable-next-line security/detect-object-injection
742 return req
._headers
[header
];
744 assert
.fail(`unexpected getHeader ${header}`);
749 getHeader: sinon
.stub(),
750 getHeaders: sinon
.stub(),
751 hasHeader: sinon
.stub().returns(true),
752 setHeader: sinon
.stub(),
765 atimeMs: 1613253436842.815,
766 mtimeMs: 1603485933192.861,
767 ctimeMs: 1603485933192.861,
769 atime: '2021-02-13T21:57:16.843Z',
770 mtime: '2020-10-23T13:45:33.193Z',
771 ctime: '2020-10-23T13:45:33.193Z',
772 birthtime: '1970-01-01T00:00:00.000Z',
774 sinon
.stub(dingus
, 'handlerNotFound');
775 sinon
.stub(fs
.promises
, 'stat').resolves(filestats
);
776 sinon
.spy(fs
.promises
, 'readFile');
778 it('serves a file', async
function () {
779 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
780 assert(fs
.promises
.readFile
.called
);
781 assert(!dingus
.handlerNotFound
.called
);
783 it('covers no meta headers', async
function () {
784 dingus
.staticMetadata
= false;
785 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
786 assert(fs
.promises
.readFile
.called
);
787 assert(!dingus
.handlerNotFound
.called
);
789 it('does not serve dot-file', async
function () {
790 fileName
= '.example';
791 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
792 assert(!fs
.promises
.readFile
.called
);
793 assert(dingus
.handlerNotFound
.called
);
795 it('does not serve encoded navigation', async
function () {
796 fileName
= '/example.html';
797 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
798 assert(!fs
.promises
.readFile
.called
);
799 assert(dingus
.handlerNotFound
.called
);
801 it('does not serve missing file', async
function () {
802 fileName
= 'no-file.here';
803 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
804 assert(dingus
.handlerNotFound
.called
);
806 it('requires directory be specified', async
function () {
807 await dingus
.serveFile(req
, res
, ctx
, '', fileName
);
808 assert(!fs
.promises
.readFile
.called
);
809 assert(dingus
.handlerNotFound
.called
);
811 it('covers fs error', async
function () {
812 const expectedException
= new Error('blah');
813 fs
.promises
.stat
.restore();
814 sinon
.stub(fs
.promises
, 'stat').rejects(expectedException
);
816 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
817 assert
.fail('should have thrown');
819 assert
.strictEqual(e
, expectedException
);
822 it('caches by modified', async
function () {
823 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 23:11:16 GMT';
824 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
825 assert
.strictEqual(res
.statusCode
, 304);
827 it('does not cache old modified', async
function () {
828 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 01:11:16 GMT';
829 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
830 assert
.notStrictEqual(res
.statusCode
, 304);
831 assert(!dingus
.handlerNotFound
.called
);
833 it('caches ETag match', async
function () {
834 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
835 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
836 assert
.strictEqual(res
.statusCode
, 304);
838 it('does not cache ETag non-match', async
function () {
839 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"foo", "bar"';
840 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
841 assert
.notStrictEqual(res
.statusCode
, 304);
842 assert(!dingus
.handlerNotFound
.called
);
844 it('handles no possible encodings', async
function () {
845 req
._headers
[Enum
.Header
.AcceptEncoding
] = '*;q=0';
846 await assert
.rejects(async () => {
847 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
849 name: 'ResponseError',
852 it('handles a valid encoding', async
function () {
853 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'gzip';
854 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
855 assert(res
.end
.called
);
857 it('handles a valid encoding among others', async
function () {
858 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, br, gzip';
859 fs
.promises
.stat
.restore();
860 sinon
.stub(fs
.promises
, 'stat')
861 .onCall(0).resolves(filestats
) // identity file
862 .onCall(1).resolves(null) // br encoding
863 .onCall(2).resolves(filestats
); // gzip encoding
864 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
865 assert(res
.end
.called
);
867 it('handles misconfigured encoding', async
function () {
868 Enum
.EncodingType
.Flarp
= 'flarp';
869 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, gzip';
870 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
871 delete Enum
.EncodingType
.Flarp
;
872 assert(res
.end
.called
);
876 describe('renderError', function () {
878 beforeEach(function () {
882 details: 'hunkydorey',
885 it('renders unknown type', function () {
886 const contentType
= 'unknown/type';
887 const result
= dingus
.renderError(contentType
, err
);
888 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
890 it('renders text', function () {
891 const contentType
= 'text/plain';
892 const result
= dingus
.renderError(contentType
, err
);
893 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
895 it('renders json', function () {
896 const contentType
= Enum
.ContentType
.ApplicationJson
;
897 const result
= dingus
.renderError(contentType
, err
);
898 assert
.deepStrictEqual(result
, JSON
.stringify(err
));
900 it('renders html without details', function () {
903 errorMessage: 'Created',
905 const contentType
= 'text/html';
906 const result
= dingus
.renderError(contentType
, err
);
907 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
910 <title>${err.statusCode} ${err.errorMessage}</title>
913 <h1>${err.errorMessage}</h1>
917 it('renders html', function () {
918 const contentType
= 'text/html';
919 const result
= dingus
.renderError(contentType
, err
);
920 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
923 <title>${err.statusCode} ${err.errorMessage}</title>
926 <h1>${err.errorMessage}</h1>
927 <p>${err.details}</p>
931 it('renders html, multiple details', function () {
932 const contentType
= 'text/html';
933 err
.details
= ['one detail', 'two detail'];
934 const result
= dingus
.renderError(contentType
, err
);
935 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
938 <title>${err.statusCode} ${err.errorMessage}</title>
941 <h1>${err.errorMessage}</h1>
949 describe('sendErrorResponse', function () {
951 beforeEach(function () {
956 getHeader: sinon
.stub(),
957 getHeaders: sinon
.stub(),
958 hasHeader: sinon
.stub().returns(true),
959 setHeader: sinon
.stub(),
961 sinon
.stub(dingus
, 'renderError');
963 it('covers', function () {
967 dingus
.sendErrorResponse(err
, req
, res
, ctx
);
968 assert(res
.end
.called
);
970 }); // sendErrorResponse
972 describe('proxyPrefix', function () {
973 let req
, res
, ctx
, stubHandler
, pfxDingus
;
976 beforeEach(function () {
977 pfxDingus
= new Dingus(console
, { proxyPrefix: pfx
});
979 setHeader: sinon
.stub(),
980 getHeader: sinon
.stub(),
985 setHeader: sinon
.stub(),
986 getHeader: sinon
.stub(),
989 sinon
.stub(pfxDingus
, 'handlerMethodNotAllowed');
990 sinon
.stub(pfxDingus
, 'handlerNotFound');
991 stubHandler
= sinon
.stub();
993 afterEach(function () {
997 it('handles prefixed route', async
function () {
998 const urlPath
= '/:id';
999 const method
= 'GET';
1000 pfxDingus
.on(method
, urlPath
, stubHandler
);
1001 req
.url
= pfx
+ '/abc';
1002 req
.method
= method
;
1004 await pfxDingus
.dispatch(req
, res
, ctx
);
1005 assert(stubHandler
.called
);
1006 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1007 assert(!pfxDingus
.handlerNotFound
.called
);
1009 it('does not handle prefixed route', async
function () {
1010 const urlPath
= '/:id';
1011 const method
= 'GET';
1012 pfxDingus
.on(method
, urlPath
, stubHandler
);
1013 req
.url
= '/wrongpfx/abc';
1014 req
.method
= method
;
1016 await pfxDingus
.dispatch(req
, res
, ctx
);
1017 assert(!stubHandler
.called
);
1018 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1019 assert(pfxDingus
.handlerNotFound
.called
);
1023 describe('handlerRedirect', function () {
1025 beforeEach(function () {
1027 getHeader: sinon
.stub(),
1030 setHeader: sinon
.stub(),
1035 it('covers', async
function () {
1036 await dingus
.handlerRedirect(req
, res
, ctx
);
1037 assert(res
.setHeader
.called
);
1038 assert(res
.end
.called
);
1040 it('covers non-defaults', async
function () {
1041 await dingus
.handlerRedirect(req
, res
, ctx
, 308);
1042 assert(res
.setHeader
.called
);
1043 assert(res
.end
.called
);
1045 }); // handlerRedirect
1047 describe('handlerGetStaticFile', function () {
1049 beforeEach(function () {
1051 getHeader: sinon
.stub(),
1054 setHeader: sinon
.stub(),
1061 sinon
.stub(dingus
, 'serveFile');
1063 it('covers', async
function () {
1064 await dingus
.handlerGetStaticFile(req
, res
, ctx
);
1065 assert(dingus
.serveFile
.called
);
1067 it('covers specified file', async
function () {
1068 await dingus
.handlerGetStaticFile(req
, res
, ctx
, 'file.txt');
1069 assert(dingus
.serveFile
.called
);
1071 }); // handlerGetStaticFile