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
= () => undefined;
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('ingestCookie', function () {
185 beforeEach(function () {
187 getHeader: sinon
.stub(),
191 it('covers no header', function () {
193 Dingus
.ingestCookie(req
, res
, ctx
);
194 assert
.deepStrictEqual(ctx
.cookie
, expected
);
196 it('covers non variable', function () {
197 req
.getHeader
.returns('foo');
201 Dingus
.ingestCookie(req
, res
, ctx
);
202 assert
.deepStrictEqual(ctx
.cookie
, expected
);
204 it('parses cookies', function () {
205 req
.getHeader
.returns('foo=bar; baz="quux"');
210 Dingus
.ingestCookie(req
, res
, ctx
);
211 assert
.deepStrictEqual(ctx
.cookie
, expected
);
213 it('parses nulls', function () {
214 req
.getHeader
.returns('foo=; bar=');
219 Dingus
.ingestCookie(req
, res
, ctx
);
220 assert
.deepStrictEqual(ctx
.cookie
, expected
);
222 it('parses non-uri-encoded', function () {
223 req
.getHeader
.returns('foo%=%qux');
227 Dingus
.ingestCookie(req
, res
, ctx
);
228 assert
.deepStrictEqual(ctx
.cookie
, expected
);
230 it('covers nameless cookie', function () {
231 req
.getHeader
.returns('=bar');
234 Dingus
.ingestCookie(req
, res
, ctx
);
235 assert
.deepStrictEqual(ctx
.cookie
, expected
);
238 it('covers duplicate cookie', function () {
239 req
.getHeader
.returns('foo=bar; foo="quux"');
243 Dingus
.ingestCookie(req
, res
, ctx
);
244 assert
.deepStrictEqual(ctx
.cookie
, expected
);
248 describe('getRequestContentType', function () {
250 beforeEach(function () {
252 getHeader: sinon
.stub(),
253 setHeader: sinon
.stub(),
256 it('handles missing header', function () {
257 const result
= Dingus
.getRequestContentType(req
);
258 assert
.strictEqual(result
, '');
260 it('parses simple type', function () {
261 req
.getHeader
.onCall(0).returns(Enum
.ContentType
.ApplicationJson
);
262 const result
= Dingus
.getRequestContentType(req
);
263 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
265 it('parses complex type', function () {
266 req
.getHeader
.onCall(0).returns('application/json ; charset=UTF-8');
267 const result
= Dingus
.getRequestContentType(req
);
268 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
270 }); // getRequestContentType
272 describe('setResponseContentType', function () {
273 let req
, responseTypes
;
274 beforeEach(function () {
277 setHeader: sinon
.stub(),
278 getHeader: sinon
.stub(),
281 it('handles missing header', function () {
282 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
283 assert
.strictEqual(result
, undefined);
285 it('behaves as expected', function () {
286 responseTypes
.push(Enum
.ContentType
.ApplicationJson
);
287 req
.getHeader
.onCall(0).returns('text, image/png;q=0.5, application/*;q=0.2, audio;q=0.1');
288 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
289 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
291 }); // setResponseContentType
293 describe('on', function () {
295 beforeEach(function () {
296 stubOn
= sinon
.stub(dingus
.router
, 'on');
298 it('covers', function () {
299 dingus
.on('GET', '/', () => {});
300 assert(stubOn
.called
);
304 describe('setEndBodyHandler', function () {
305 let req
, res
, ctx
, handler
, origEnd
, origWrite
;
306 beforeEach(function () {
307 origEnd
= sinon
.stub();
308 origWrite
= sinon
.stub();
315 handler
= sinon
.stub();
317 it('collects body and handles', function () {
318 Dingus
.setEndBodyHandler(req
, res
, ctx
, handler
);
319 res
.write(Buffer
.from('foo'));
323 assert(origWrite
.called
);
324 assert(origEnd
.called
);
325 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
326 assert(handler
.called
);
328 }); // setEndBodyHandler
330 describe('setHeadHandler', function () {
331 let req
, res
, ctx
, origEnd
, origWrite
;
332 beforeEach(function () {
333 origEnd
= sinon
.stub();
334 origWrite
= sinon
.stub();
341 setHeader: sinon
.stub(),
345 it('collects response without writing', function () {
346 Dingus
.setHeadHandler(req
, res
, ctx
);
347 res
.write(Buffer
.from('foo'));
351 assert(!origWrite
.called
);
352 assert(origEnd
.called
);
353 assert
.deepStrictEqual(ctx
.responseBody
, undefined);
355 it('collects response without writing, persists written data', function () {
356 Dingus
.setHeadHandler(req
, res
, ctx
, true);
357 res
.write(Buffer
.from('foo'));
361 assert(!origWrite
.called
);
362 assert(origEnd
.called
);
363 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
365 it('ignores non-head method', function () {
367 Dingus
.setHeadHandler(req
, res
, ctx
);
368 res
.write(Buffer
.from('foo'));
370 assert(origWrite
.called
);
371 assert(origEnd
.called
);
373 }); // setHeadHandler
375 describe('addEncodingHeader', function () {
377 beforeEach(function () {
380 // eslint-disable-next-line security/detect-object-injection
381 getHeader: (h
) => res
._headers
[h
],
382 // eslint-disable-next-line security/detect-object-injection
383 setHeader: (h
, v
) => res
._headers
[h
] = v
,
386 it('adds', function () {
388 Dingus
.addEncodingHeader(res
, encoding
);
389 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip');
391 it('extends', function () {
393 Dingus
.addEncodingHeader(res
, encoding
);
394 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'utf8');
396 Dingus
.addEncodingHeader(res
, encoding
);
397 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip, utf8');
399 }); // addEncodingHeader
401 describe('dispatch', function () {
402 let pathsByLengthOrig
;
406 beforeEach(function () {
410 setHeader: sinon
.stub(),
411 getHeader: sinon
.stub(),
416 setHeader: sinon
.stub(),
417 hasHeader: sinon
.stub(),
418 getHeader: sinon
.stub(),
419 getHeaders: sinon
.stub(),
422 pathsByLengthOrig
= dingus
.pathsByLength
;
423 sinon
.spy(dingus
, 'handlerMethodNotAllowed');
424 sinon
.spy(dingus
, 'handlerNotFound');
425 sinon
.spy(dingus
, 'handlerBadRequest');
426 sinon
.spy(dingus
, 'handlerInternalServerError');
427 sinon
.spy(Dingus
, 'setHeadHandler');
428 stubHandler
= sinon
.stub();
430 afterEach(function () {
431 dingus
.pathsByLength
= pathsByLengthOrig
;
434 it('calls handler', async
function () {
435 const urlPath
= '/:id';
436 const method
= 'GET';
437 dingus
.on(method
, urlPath
, stubHandler
);
441 await dingus
.dispatch(req
, res
, ctx
);
442 assert(stubHandler
.called
);
443 assert(!dingus
.handlerMethodNotAllowed
.called
);
444 assert(!dingus
.handlerNotFound
.called
);
446 it('calls handler without context', async
function () {
447 const urlPath
= '/:id';
448 const method
= 'GET';
449 dingus
.on(method
, urlPath
, stubHandler
);
453 await dingus
.dispatch(req
, res
);
454 assert(stubHandler
.called
);
455 assert(!dingus
.handlerMethodNotAllowed
.called
);
456 assert(!dingus
.handlerNotFound
.called
);
458 it('calls fallback handler', async
function () {
459 const urlPath
= '/abc/:id';
460 const method
= 'GET';
461 dingus
.on('*', urlPath
, stubHandler
);
462 req
.url
= '/abc/def';
465 await dingus
.dispatch(req
, res
, ctx
);
466 assert(stubHandler
.called
);
467 assert(!dingus
.handlerMethodNotAllowed
.called
);
468 assert(!dingus
.handlerNotFound
.called
);
470 it('handles error in handler', async
function () {
471 const urlPath
= '/:id';
472 const method
= 'GET';
473 dingus
.on(method
, urlPath
, stubHandler
);
476 stubHandler
.rejects(new Error('blah'));
478 await dingus
.dispatch(req
, res
, ctx
);
479 assert(stubHandler
.called
);
480 assert(!dingus
.handlerMethodNotAllowed
.called
);
481 assert(!dingus
.handlerNotFound
.called
);
483 it('calls unsupported method', async
function () {
484 const urlPath
= '/:id';
485 const method
= 'POST';
486 dingus
.on('GET', urlPath
, stubHandler
);
490 await dingus
.dispatch(req
, res
, ctx
);
491 assert(!stubHandler
.called
);
492 assert(dingus
.handlerMethodNotAllowed
.called
);
493 assert(!dingus
.handlerNotFound
.called
);
495 it('does not lookup nonexistent path', async
function () {
496 req
.url
= '/foo/bar';
499 await dingus
.dispatch(req
, res
, ctx
);
500 assert(!stubHandler
.called
);
501 assert(!dingus
.handlerMethodNotAllowed
.called
);
502 assert(dingus
.handlerNotFound
.called
);
504 it('covers unhandled dingus exception', async
function () {
505 const expectedException
= new DingusError('blah');
506 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
508 await dingus
.dispatch(req
, res
, ctx
);
509 assert(!stubHandler
.called
);
510 assert(dingus
.handlerInternalServerError
.called
);
512 it('covers other exception', async
function () {
513 const expectedException
= new Error('blah');
514 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
516 await dingus
.dispatch(req
, res
, ctx
);
517 assert(!stubHandler
.called
);
518 assert(dingus
.handlerInternalServerError
.called
);
520 it('covers bad uri', async
function () {
523 await dingus
.dispatch(req
, res
, ctx
);
524 assert(dingus
.handlerBadRequest
.called
);
526 it('calls handler with additional arguments', async
function () {
527 dingus
.on('GET', '/', stubHandler
, 'foo', 'bar');
528 await dingus
.dispatch(req
, res
, ctx
);
529 assert(stubHandler
.called
);
530 assert
.strictEqual(stubHandler
.args
[0][3], 'foo');
531 assert
.strictEqual(stubHandler
.args
[0][4], 'bar');
533 describe('intrinsic HEAD handling', function () {
534 it('covers no intrinsic HEAD handling', async
function () {
535 dingus
.intrinsicHeadMethod
= false;
536 dingus
.on('GET', '/', stubHandler
);
538 await dingus
.dispatch(req
, res
, ctx
);
539 assert(!stubHandler
.called
);
540 assert(dingus
.handlerMethodNotAllowed
.called
);
542 it('calls HEAD setup and GET handler', async
function () {
543 dingus
.on('GET', '/', stubHandler
);
545 await dingus
.dispatch(req
, res
, ctx
);
546 assert(Dingus
.setHeadHandler
.called
);
547 assert(stubHandler
.called
);
549 it('covers no GET handler', async
function () {
550 dingus
.on('POST', '/', stubHandler
);
552 await dingus
.dispatch(req
, res
, ctx
);
553 assert(!stubHandler
.called
);
554 assert(dingus
.handlerMethodNotAllowed
.called
);
556 it('covers unexpected router error', async
function () {
557 sinon
.stub(dingus
.router
, 'lookup')
558 .onFirstCall().throws(new RouterNoMethodError())
559 .onSecondCall().throws(new DingusError())
561 dingus
.on('GET', '/', stubHandler
);
563 await dingus
.dispatch(req
, res
, ctx
);
564 assert(dingus
.handlerInternalServerError
.called
);
569 describe('parseBody', function () {
571 beforeEach(function () {
574 it('does not parse unknown type', function () {
576 dingus
.parseBody('unknown/type', ctx
);
577 assert
.fail(noExpectedException
);
579 assert
.strictEqual(e
.statusCode
, 415);
582 it('parses json', function () {
583 const src
= { foo: 'bar' };
584 const rawBody
= JSON
.stringify(src
);
585 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
, rawBody
);
586 assert
.deepStrictEqual(ctx
.parsedBody
, src
);
588 it('handles unparsable json', function () {
589 const rawBody
= 'not json';
591 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
, rawBody
);
592 assert
.fail(noExpectedException
);
594 assert
.strictEqual(e
.statusCode
, 400);
597 it('parses form', function () {
598 const expected
= Object
.assign(Object
.create(null), {
601 const rawBody
= 'foo=bar';
602 dingus
.parseBody('application/x-www-form-urlencoded', ctx
, rawBody
);
603 assert
.deepStrictEqual(ctx
.parsedBody
, expected
);
608 describe('bodyData', function () {
610 beforeEach(function () {
613 // eslint-disable-next-line security/detect-object-injection
614 on: (ev
, fn
) => resEvents
[ev
] = fn
,
617 it('provides data', async
function () {
618 const p
= dingus
.bodyData(res
);
619 resEvents
['data'](Buffer
.from('foo'));
620 resEvents
['data'](Buffer
.from('bar'));
622 const result
= await p
;
623 assert
.strictEqual(result
, 'foobar');
625 it('handles error', async
function () {
626 const p
= dingus
.bodyData(res
);
627 resEvents
['error']('foo');
630 assert
.fail(noExpectedException
);
632 assert
.strictEqual(e
, 'foo');
635 it('limits size', async
function () {
636 const p
= dingus
.bodyData(res
, 8);
637 resEvents
['data'](Buffer
.from('foobar'));
638 resEvents
['data'](Buffer
.from('bazquux'));
641 assert
.fail(noExpectedException
);
643 assert
.strictEqual(e
.statusCode
, 413);
646 it('provides buffer', async
function () {
647 const p
= dingus
.bodyData(res
, 0, false);
648 const expected
= Buffer
.from('bleat');
649 resEvents
['data'](expected
);
651 const result
= await p
;
652 assert
.deepStrictEqual(result
, expected
);
656 describe('ingestBody', function () {
657 it('ingests json', async
function () {
661 sinon
.stub(dingus
, 'bodyData').resolves('{"foo":"bar"}');
662 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
663 await dingus
.ingestBody(req
, res
, ctx
);
664 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
665 assert
.deepStrictEqual(ctx
.rawBody
, undefined);
667 it('persists rawBody', async
function () {
671 const body
= '{"foo":"bar"}';
672 sinon
.stub(dingus
, 'bodyData').resolves(body
);
673 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
674 await dingus
.ingestBody(req
, res
, ctx
, { persistRawBody: true });
675 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
676 assert
.deepStrictEqual(ctx
.rawBody
, body
);
678 it('skips parsing empty body', async
function () {
683 sinon
.stub(dingus
, 'bodyData').resolves(body
);
684 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
685 sinon
.spy(dingus
, 'parseBody');
686 await dingus
.ingestBody(req
, res
, ctx
, { parseEmptyBody: false });
687 assert
.deepStrictEqual(ctx
.parsedBody
, undefined);
688 assert(dingus
.parseBody
.notCalled
);
692 describe('setResponseType', function () {
694 let _sa
; // Preserve strictAccept
696 _sa
= dingus
.strictAccept
;
699 dingus
.strictAccept
= _sa
;
701 beforeEach(function () {
705 setHeader: sinon
.stub(),
707 sinon
.stub(Dingus
, 'getResponseContentType').returns();
709 it('rejects missing', function () {
710 dingus
.strictAccept
= true;
712 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
713 assert
.fail(noExpectedException
);
715 assert
.strictEqual(e
.statusCode
, 406, 'did not get expected status code');
718 it('accepts missing', function () {
719 dingus
.strictAccept
= false;
720 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
721 assert
.strictEqual(ctx
.responseType
, 'my/type');
724 }); // setResponseType
726 describe('_readFileInfo', function () {
727 let stat
, data
, statRes
, dataRes
, filename
;
728 beforeEach(function () {
729 sinon
.stub(fs
.promises
, 'stat');
730 sinon
.stub(fs
.promises
, 'readFile');
732 mtimeMs:1612553697186,
735 filename
= 'dummy.txt';
737 it('succeeds', async
function () {
738 fs
.promises
.stat
.resolves(statRes
);
739 fs
.promises
.readFile
.resolves('data');
740 [stat
, data
] = await dingus
._readFileInfo(filename
);
741 assert
.deepStrictEqual(stat
, statRes
);
742 assert
.deepStrictEqual(data
, dataRes
);
744 it('returns null for non-existant file', async
function () {
748 fs
.promises
.stat
.rejects(noEnt
);
749 fs
.promises
.readFile
.rejects(noEnt
);
750 [stat
, data
] = await dingus
._readFileInfo(filename
);
751 assert
.strictEqual(stat
, null);
752 assert
.strictEqual(data
, null);
754 it('throws unexpected error', async
function () {
755 const expectedException
= new Error('blah');
756 fs
.promises
.stat
.rejects(expectedException
);
757 await assert
.rejects(async () => {
758 await dingus
._readFileInfo(filename
);
759 }, expectedException
);
763 describe('_serveFileMetaHeaders', function () {
764 let res
, directory
, fileName
;
765 beforeEach(function () {
766 sinon
.stub(dingus
, '_readFileInfo');
768 setHeader: sinon
.stub(),
771 fileName
= 'filename';
773 it('covers no meta file', async
function() {
774 dingus
._readFileInfo
.resolves([null, null]);
775 const result
= await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
776 assert(!res
.setHeader
.called
);
777 assert
.strictEqual(result
, false);
779 it('adds extra headers', async
function () {
780 dingus
._readFileInfo
.resolves([{}, Buffer
.from(`Link: <https://example.com/>; rel="relation"
781 X-Folded-Header: data
784 Content-Type: image/sgi
786 const result
= await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
787 assert(res
.setHeader
.called
);
788 assert
.strictEqual(result
, true);
790 }); // _serveFileMetaHeaders
792 describe('serveFile', function () {
793 const path
= require('path');
794 let ctx
, req
, res
, directory
, fileName
, filestats
;
795 beforeEach(function () {
796 directory
= path
.join(__dirname
, '..', 'test-data');
797 fileName
= 'example.html';
801 [Enum
.Header
.Accept
]: undefined,
802 [Enum
.Header
.IfModifiedSince
]: undefined,
803 [Enum
.Header
.AcceptEncoding
]: undefined,
804 [Enum
.Header
.IfNoneMatch
]: undefined,
806 getHeader: (header
) => {
807 if (header
in req
._headers
) {
808 // eslint-disable-next-line security/detect-object-injection
809 return req
._headers
[header
];
811 assert
.fail(`unexpected getHeader ${header}`);
816 getHeader: sinon
.stub(),
817 getHeaders: sinon
.stub(),
818 hasHeader: sinon
.stub().returns(true),
819 setHeader: sinon
.stub(),
832 atimeMs: 1613253436842.815,
833 mtimeMs: 1603485933192.861,
834 ctimeMs: 1603485933192.861,
836 atime: '2021-02-13T21:57:16.843Z',
837 mtime: '2020-10-23T13:45:33.193Z',
838 ctime: '2020-10-23T13:45:33.193Z',
839 birthtime: '1970-01-01T00:00:00.000Z',
841 sinon
.stub(dingus
, 'handlerNotFound');
842 sinon
.stub(fs
.promises
, 'stat').resolves(filestats
);
843 sinon
.spy(fs
.promises
, 'readFile');
845 it('serves a file', async
function () {
846 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
847 assert(fs
.promises
.readFile
.called
);
848 assert(!dingus
.handlerNotFound
.called
);
850 it('covers no meta headers', async
function () {
851 dingus
.staticMetadata
= false;
852 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
853 assert(fs
.promises
.readFile
.called
);
854 assert(!dingus
.handlerNotFound
.called
);
856 it('does not serve dot-file', async
function () {
857 fileName
= '.example';
858 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
859 assert(!fs
.promises
.readFile
.called
);
860 assert(dingus
.handlerNotFound
.called
);
862 it('does not serve encoded navigation', async
function () {
863 fileName
= '/example.html';
864 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
865 assert(!fs
.promises
.readFile
.called
);
866 assert(dingus
.handlerNotFound
.called
);
868 it('does not serve missing file', async
function () {
869 fileName
= 'no-file.here';
870 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
871 assert(dingus
.handlerNotFound
.called
);
873 it('requires directory be specified', async
function () {
874 await dingus
.serveFile(req
, res
, ctx
, '', fileName
);
875 assert(!fs
.promises
.readFile
.called
);
876 assert(dingus
.handlerNotFound
.called
);
878 it('covers fs error', async
function () {
879 const expectedException
= new Error('blah');
880 fs
.promises
.stat
.restore();
881 sinon
.stub(fs
.promises
, 'stat').rejects(expectedException
);
883 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
884 assert
.fail('should have thrown');
886 assert
.strictEqual(e
, expectedException
);
889 it('caches by modified', async
function () {
890 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 23:11:16 GMT';
891 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
892 assert
.strictEqual(res
.statusCode
, 304);
894 it('does not cache old modified', async
function () {
895 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 01:11:16 GMT';
896 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
897 assert
.notStrictEqual(res
.statusCode
, 304);
898 assert(!dingus
.handlerNotFound
.called
);
900 it('caches ETag match', async
function () {
901 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
902 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
903 assert
.strictEqual(res
.statusCode
, 304);
905 it('does not cache ETag non-match', async
function () {
906 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"foo", "bar"';
907 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
908 assert
.notStrictEqual(res
.statusCode
, 304);
909 assert(!dingus
.handlerNotFound
.called
);
911 it('handles no possible encodings', async
function () {
912 req
._headers
[Enum
.Header
.AcceptEncoding
] = '*;q=0';
913 await assert
.rejects(async () => {
914 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
916 name: 'ResponseError',
919 it('handles a valid encoding', async
function () {
920 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'gzip';
921 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
922 assert(res
.end
.called
);
924 it('handles a valid encoding among others', async
function () {
925 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, br, gzip';
926 fs
.promises
.stat
.restore();
927 sinon
.stub(fs
.promises
, 'stat')
928 .onCall(0).resolves(filestats
) // identity file
929 .onCall(1).resolves(null) // br encoding
930 .onCall(2).resolves(filestats
); // gzip encoding
931 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
932 assert(res
.end
.called
);
934 it('handles misconfigured encoding', async
function () {
935 Enum
.EncodingType
.Flarp
= 'flarp';
936 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, gzip';
937 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
938 delete Enum
.EncodingType
.Flarp
;
939 assert(res
.end
.called
);
943 describe('renderError', function () {
945 beforeEach(function () {
949 details: 'hunkydorey',
952 it('renders unknown type', function () {
953 const contentType
= 'unknown/type';
954 const result
= dingus
.renderError(contentType
, err
);
955 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
957 it('renders text', function () {
958 const contentType
= 'text/plain';
959 const result
= dingus
.renderError(contentType
, err
);
960 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
962 it('renders json', function () {
963 const contentType
= Enum
.ContentType
.ApplicationJson
;
964 const result
= dingus
.renderError(contentType
, err
);
965 assert
.deepStrictEqual(result
, JSON
.stringify(err
));
967 it('renders html without details', function () {
970 errorMessage: 'Created',
972 const contentType
= 'text/html';
973 const result
= dingus
.renderError(contentType
, err
);
974 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
977 <title>${err.statusCode} ${err.errorMessage}</title>
980 <h1>${err.errorMessage}</h1>
984 it('renders html', function () {
985 const contentType
= 'text/html';
986 const result
= dingus
.renderError(contentType
, err
);
987 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
990 <title>${err.statusCode} ${err.errorMessage}</title>
993 <h1>${err.errorMessage}</h1>
994 <p>${err.details}</p>
998 it('renders html, multiple details', function () {
999 const contentType
= 'text/html';
1000 err
.details
= ['one detail', 'two detail'];
1001 const result
= dingus
.renderError(contentType
, err
);
1002 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
1005 <title>${err.statusCode} ${err.errorMessage}</title>
1008 <h1>${err.errorMessage}</h1>
1016 describe('sendErrorResponse', function () {
1018 beforeEach(function () {
1023 getHeader: sinon
.stub(),
1024 getHeaders: sinon
.stub(),
1025 hasHeader: sinon
.stub().returns(true),
1026 setHeader: sinon
.stub(),
1028 sinon
.stub(dingus
, 'renderError');
1030 it('covers', function () {
1034 dingus
.sendErrorResponse(err
, req
, res
, ctx
);
1035 assert(res
.end
.called
);
1037 }); // sendErrorResponse
1039 describe('proxyPrefix', function () {
1040 let req
, res
, ctx
, stubHandler
, pfxDingus
;
1043 beforeEach(function () {
1044 pfxDingus
= new Dingus(console
, { proxyPrefix: pfx
});
1046 setHeader: sinon
.stub(),
1047 getHeader: sinon
.stub(),
1052 setHeader: sinon
.stub(),
1053 getHeader: sinon
.stub(),
1056 sinon
.stub(pfxDingus
, 'handlerMethodNotAllowed');
1057 sinon
.stub(pfxDingus
, 'handlerNotFound');
1058 stubHandler
= sinon
.stub();
1060 afterEach(function () {
1064 it('handles prefixed route', async
function () {
1065 const urlPath
= '/:id';
1066 const method
= 'GET';
1067 pfxDingus
.on(method
, urlPath
, stubHandler
);
1068 req
.url
= pfx
+ '/abc';
1069 req
.method
= method
;
1071 await pfxDingus
.dispatch(req
, res
, ctx
);
1072 assert(stubHandler
.called
);
1073 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1074 assert(!pfxDingus
.handlerNotFound
.called
);
1076 it('does not handle prefixed route', async
function () {
1077 const urlPath
= '/:id';
1078 const method
= 'GET';
1079 pfxDingus
.on(method
, urlPath
, stubHandler
);
1080 req
.url
= '/wrongpfx/abc';
1081 req
.method
= method
;
1083 await pfxDingus
.dispatch(req
, res
, ctx
);
1084 assert(!stubHandler
.called
);
1085 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1086 assert(pfxDingus
.handlerNotFound
.called
);
1090 describe('handlerRedirect', function () {
1092 beforeEach(function () {
1094 getHeader: sinon
.stub(),
1097 setHeader: sinon
.stub(),
1102 it('covers', async
function () {
1103 await dingus
.handlerRedirect(req
, res
, ctx
);
1104 assert(res
.setHeader
.called
);
1105 assert(res
.end
.called
);
1107 it('covers non-defaults', async
function () {
1108 await dingus
.handlerRedirect(req
, res
, ctx
, 308);
1109 assert(res
.setHeader
.called
);
1110 assert(res
.end
.called
);
1112 }); // handlerRedirect
1114 describe('handlerGetStaticFile', function () {
1116 beforeEach(function () {
1118 getHeader: sinon
.stub(),
1121 setHeader: sinon
.stub(),
1128 sinon
.stub(dingus
, 'serveFile');
1130 it('covers', async
function () {
1131 await dingus
.handlerGetStaticFile(req
, res
, ctx
);
1132 assert(dingus
.serveFile
.called
);
1134 it('covers specified file', async
function () {
1135 await dingus
.handlerGetStaticFile(req
, res
, ctx
, 'file.txt');
1136 assert(dingus
.serveFile
.called
);
1138 }); // handlerGetStaticFile