3 const assert
= require('node:assert');
4 const sinon
= require('sinon');
5 const fs
= require('fs');
7 const Dingus
= require('../../lib/dingus');
8 const { DingusError
, RouterNoMethodError
} = require('../../lib/errors');
9 const Enum
= require('../../lib/enum');
11 const noExpectedException
= 'did not get expected exception';
13 const _nop
= () => undefined;
14 const _logFn
= (process
.env
['VERBOSE_TESTS'] && console
.log
) || _nop
;
19 sinon
.spy(noLogger
, 'debug');
20 sinon
.spy(noLogger
, 'error');
22 describe('Dingus', function () {
24 beforeEach(function () {
25 dingus
= new Dingus(noLogger
, {});
27 afterEach(function () {
31 describe('constructor', function () {
32 it('covers', function () {
33 const d
= new Dingus();
38 describe('_normalizePath', function () {
39 it('returns normal path', function () {
41 const r
= dingus
._normalizePath(p
);
42 assert
.strictEqual(r
, p
);
44 it('returns normal path', function () {
45 const p
= '////a///b/./bar/..///c';
46 const expected
= '/a/b/c';
47 const r
= dingus
._normalizePath(p
);
48 assert
.strictEqual(r
, expected
);
52 describe('_splitUrl', function () {
53 const nullObject
= Object
.create(null);
55 it('splits a simple path', function () {
59 queryParams: nullObject
,
61 const r
= dingus
._splitUrl(p
);
62 assert
.deepStrictEqual(r
, expected
);
64 it('splits a path with trailing slash preserved', function () {
68 queryParams: nullObject
,
70 const r
= dingus
._splitUrl(p
);
71 assert
.deepStrictEqual(r
, expected
);
73 it('splits a path with trailing slash ignored', function () {
77 queryParams: nullObject
,
79 dingus
.ignoreTrailingSlash
= true;
80 const r
= dingus
._splitUrl(p
);
81 assert
.deepStrictEqual(r
, expected
);
83 it('splits a path with empty query string', function () {
87 queryParams: nullObject
,
89 const r
= dingus
._splitUrl(p
);
90 assert
.deepStrictEqual(r
, expected
);
92 it('splits a path with query string', function () {
93 const p
= '/a/b/c?x=1&y=2&z';
96 queryParams: Object
.assign(Object
.create(null), {
99 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.
102 const r
= dingus
._splitUrl(p
);
103 assert
.deepStrictEqual(r
, expected
);
107 describe('tagContext', function () {
109 beforeEach(function () {
111 getHeader: sinon
.stub(),
112 setHeader: sinon
.stub(),
115 getHeader: sinon
.stub(),
116 setHeader: sinon
.stub(),
120 it ('sets id in context', function () {
121 const result
= Dingus
.tagContext(req
, res
, ctx
);
122 assert
.strictEqual(ctx
.requestId
, result
);
123 assert(res
.setHeader
.called
);
125 it ('sets provided header', function () {
126 req
.getHeader
.onCall(0).returns('abc'); // X-Request-ID
127 const result
= Dingus
.tagContext(req
, res
, ctx
);
128 assert
.strictEqual(ctx
.requestId
, result
);
129 assert
.strictEqual(res
.setHeader
.getCall(0).args
[0], 'Request-ID');
130 assert
.strictEqual(res
.setHeader
.getCall(1).args
[0], 'X-Request-ID');
131 assert
.strictEqual(res
.setHeader
.getCall(1).args
[1], 'abc');
132 assert
.strictEqual(res
.setHeader
.callCount
, 2);
136 describe('clientAddressContext', function () {
140 _tp
= dingus
.trustProxy
;
143 dingus
.trustProxy
= _tp
;
145 beforeEach(function () {
147 getHeader: sinon
.stub(),
148 setHeader: sinon
.stub(),
152 getHeader: sinon
.stub(),
153 setHeader: sinon
.stub(),
157 it ('covers untrusted proxy', function () {
158 dingus
.trustProxy
= false;
161 clientProtocol: 'http',
163 dingus
.clientAddressContext(req
, res
, ctx
);
164 assert
.deepStrictEqual(ctx
, expected
);
165 assert(!req
.getHeader
.called
);
167 it ('covers missing', function () {
168 dingus
.trustProxy
= true;
170 clientAddress: '::1',
171 clientProtocol: 'https',
173 req
.connection
.remoteAddress
= '::1';
174 req
.connection
.encrypted
= true;
175 dingus
.clientAddressContext(req
, res
, ctx
);
176 assert(req
.getHeader
.called
);
177 assert
.deepStrictEqual(ctx
, expected
);
179 }); // clientAddressContext
181 describe('ingestCookie', function () {
183 beforeEach(function () {
185 getHeader: sinon
.stub(),
189 it('covers no header', function () {
191 Dingus
.ingestCookie(req
, res
, ctx
);
192 assert
.deepStrictEqual(ctx
.cookie
, expected
);
194 it('covers non variable', function () {
195 req
.getHeader
.returns('foo');
199 Dingus
.ingestCookie(req
, res
, ctx
);
200 assert
.deepStrictEqual(ctx
.cookie
, expected
);
202 it('parses cookies', function () {
203 req
.getHeader
.returns('foo=bar; baz="quux"');
208 Dingus
.ingestCookie(req
, res
, ctx
);
209 assert
.deepStrictEqual(ctx
.cookie
, expected
);
211 it('parses nulls', function () {
212 req
.getHeader
.returns('foo=; bar=');
217 Dingus
.ingestCookie(req
, res
, ctx
);
218 assert
.deepStrictEqual(ctx
.cookie
, expected
);
220 it('parses non-uri-encoded', function () {
221 req
.getHeader
.returns('foo%=%qux');
225 Dingus
.ingestCookie(req
, res
, ctx
);
226 assert
.deepStrictEqual(ctx
.cookie
, expected
);
228 it('covers nameless cookie', function () {
229 req
.getHeader
.returns('=bar');
232 Dingus
.ingestCookie(req
, res
, ctx
);
233 assert
.deepStrictEqual(ctx
.cookie
, expected
);
236 it('covers duplicate cookie', function () {
237 req
.getHeader
.returns('foo=bar; foo="quux"');
241 Dingus
.ingestCookie(req
, res
, ctx
);
242 assert
.deepStrictEqual(ctx
.cookie
, expected
);
246 describe('getRequestContentType', function () {
248 beforeEach(function () {
250 getHeader: sinon
.stub(),
251 setHeader: sinon
.stub(),
254 it('handles missing header', function () {
255 const result
= Dingus
.getRequestContentType(req
);
256 assert
.strictEqual(result
, '');
258 it('parses simple type', function () {
259 req
.getHeader
.onCall(0).returns(Enum
.ContentType
.ApplicationJson
);
260 const result
= Dingus
.getRequestContentType(req
);
261 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
263 it('parses complex type', function () {
264 req
.getHeader
.onCall(0).returns('application/json ; charset=UTF-8');
265 const result
= Dingus
.getRequestContentType(req
);
266 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
268 }); // getRequestContentType
270 describe('setResponseContentType', function () {
271 let req
, responseTypes
;
272 beforeEach(function () {
275 setHeader: sinon
.stub(),
276 getHeader: sinon
.stub(),
279 it('handles missing header', function () {
280 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
281 assert
.strictEqual(result
, undefined);
283 it('behaves as expected', function () {
284 responseTypes
.push(Enum
.ContentType
.ApplicationJson
);
285 req
.getHeader
.onCall(0).returns('text, image/png;q=0.5, application/*;q=0.2, audio;q=0.1');
286 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
287 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
289 }); // setResponseContentType
291 describe('on', function () {
293 beforeEach(function () {
294 stubOn
= sinon
.stub(dingus
.router
, 'on');
296 it('covers', function () {
297 dingus
.on('GET', '/', () => {});
298 assert(stubOn
.called
);
302 describe('setEndBodyHandler', function () {
303 let req
, res
, ctx
, handler
, origEnd
, origWrite
;
304 beforeEach(function () {
305 origEnd
= sinon
.stub();
306 origWrite
= sinon
.stub();
313 handler
= sinon
.stub();
315 it('collects body and handles', function () {
316 Dingus
.setEndBodyHandler(req
, res
, ctx
, handler
);
317 res
.write(Buffer
.from('foo'));
321 assert(origWrite
.called
);
322 assert(origEnd
.called
);
323 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
324 assert(handler
.called
);
326 }); // setEndBodyHandler
328 describe('setHeadHandler', function () {
329 let req
, res
, ctx
, origEnd
, origWrite
;
330 beforeEach(function () {
331 origEnd
= sinon
.stub();
332 origWrite
= sinon
.stub();
339 setHeader: sinon
.stub(),
343 it('collects response without writing', function () {
344 Dingus
.setHeadHandler(req
, res
, ctx
);
345 res
.write(Buffer
.from('foo'));
349 assert(!origWrite
.called
);
350 assert(origEnd
.called
);
351 assert
.deepStrictEqual(ctx
.responseBody
, undefined);
353 it('collects response without writing, persists written data', function () {
354 Dingus
.setHeadHandler(req
, res
, ctx
, true);
355 res
.write(Buffer
.from('foo'));
359 assert(!origWrite
.called
);
360 assert(origEnd
.called
);
361 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
363 it('ignores non-head method', function () {
365 Dingus
.setHeadHandler(req
, res
, ctx
);
366 res
.write(Buffer
.from('foo'));
368 assert(origWrite
.called
);
369 assert(origEnd
.called
);
371 }); // setHeadHandler
373 describe('addEncodingHeader', function () {
375 beforeEach(function () {
378 // eslint-disable-next-line security/detect-object-injection
379 getHeader: (h
) => res
._headers
[h
],
380 // eslint-disable-next-line security/detect-object-injection
381 setHeader: (h
, v
) => res
._headers
[h
] = v
,
384 it('adds', function () {
386 Dingus
.addEncodingHeader(res
, encoding
);
387 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip');
389 it('extends', function () {
391 Dingus
.addEncodingHeader(res
, encoding
);
392 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'utf8');
394 Dingus
.addEncodingHeader(res
, encoding
);
395 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip, utf8');
397 }); // addEncodingHeader
399 describe('dispatch', function () {
400 let pathsByLengthOrig
;
404 beforeEach(function () {
408 setHeader: sinon
.stub(),
409 getHeader: sinon
.stub(),
414 setHeader: sinon
.stub(),
415 hasHeader: sinon
.stub(),
416 getHeader: sinon
.stub(),
417 getHeaders: sinon
.stub(),
420 pathsByLengthOrig
= dingus
.pathsByLength
;
421 sinon
.spy(dingus
, 'handlerMethodNotAllowed');
422 sinon
.spy(dingus
, 'handlerNotFound');
423 sinon
.spy(dingus
, 'handlerBadRequest');
424 sinon
.spy(dingus
, 'handlerInternalServerError');
425 sinon
.spy(Dingus
, 'setHeadHandler');
426 stubHandler
= sinon
.stub();
428 afterEach(function () {
429 dingus
.pathsByLength
= pathsByLengthOrig
;
432 it('calls handler', async
function () {
433 const urlPath
= '/:id';
434 const method
= 'GET';
435 dingus
.on(method
, urlPath
, stubHandler
);
439 await dingus
.dispatch(req
, res
, ctx
);
440 assert(stubHandler
.called
);
441 assert(!dingus
.handlerMethodNotAllowed
.called
);
442 assert(!dingus
.handlerNotFound
.called
);
444 it('calls handler without context', async
function () {
445 const urlPath
= '/:id';
446 const method
= 'GET';
447 dingus
.on(method
, urlPath
, stubHandler
);
451 await dingus
.dispatch(req
, res
);
452 assert(stubHandler
.called
);
453 assert(!dingus
.handlerMethodNotAllowed
.called
);
454 assert(!dingus
.handlerNotFound
.called
);
456 it('calls fallback handler', async
function () {
457 const urlPath
= '/abc/:id';
458 const method
= 'GET';
459 dingus
.on('*', urlPath
, stubHandler
);
460 req
.url
= '/abc/def';
463 await dingus
.dispatch(req
, res
, ctx
);
464 assert(stubHandler
.called
);
465 assert(!dingus
.handlerMethodNotAllowed
.called
);
466 assert(!dingus
.handlerNotFound
.called
);
468 it('handles error in handler', async
function () {
469 const urlPath
= '/:id';
470 const method
= 'GET';
471 dingus
.on(method
, urlPath
, stubHandler
);
474 stubHandler
.rejects(new Error('blah'));
476 await dingus
.dispatch(req
, res
, ctx
);
477 assert(stubHandler
.called
);
478 assert(!dingus
.handlerMethodNotAllowed
.called
);
479 assert(!dingus
.handlerNotFound
.called
);
481 it('calls unsupported method', async
function () {
482 const urlPath
= '/:id';
483 const method
= 'POST';
484 dingus
.on('GET', urlPath
, stubHandler
);
488 await dingus
.dispatch(req
, res
, ctx
);
489 assert(!stubHandler
.called
);
490 assert(dingus
.handlerMethodNotAllowed
.called
);
491 assert(!dingus
.handlerNotFound
.called
);
493 it('does not lookup nonexistent path', async
function () {
494 req
.url
= '/foo/bar';
497 await dingus
.dispatch(req
, res
, ctx
);
498 assert(!stubHandler
.called
);
499 assert(!dingus
.handlerMethodNotAllowed
.called
);
500 assert(dingus
.handlerNotFound
.called
);
502 it('covers unhandled dingus exception', async
function () {
503 const expectedException
= new DingusError('blah');
504 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
506 await dingus
.dispatch(req
, res
, ctx
);
507 assert(!stubHandler
.called
);
508 assert(dingus
.handlerInternalServerError
.called
);
510 it('covers other exception', async
function () {
511 const expectedException
= new Error('blah');
512 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
514 await dingus
.dispatch(req
, res
, ctx
);
515 assert(!stubHandler
.called
);
516 assert(dingus
.handlerInternalServerError
.called
);
518 it('covers bad uri', async
function () {
521 await dingus
.dispatch(req
, res
, ctx
);
522 assert(dingus
.handlerBadRequest
.called
);
524 it('calls handler with additional arguments', async
function () {
525 dingus
.on('GET', '/', stubHandler
, 'foo', 'bar');
526 await dingus
.dispatch(req
, res
, ctx
);
527 assert(stubHandler
.called
);
528 assert
.strictEqual(stubHandler
.args
[0][3], 'foo');
529 assert
.strictEqual(stubHandler
.args
[0][4], 'bar');
531 describe('intrinsic HEAD handling', function () {
532 it('covers no intrinsic HEAD handling', async
function () {
533 dingus
.intrinsicHeadMethod
= false;
534 dingus
.on('GET', '/', stubHandler
);
536 await dingus
.dispatch(req
, res
, ctx
);
537 assert(!stubHandler
.called
);
538 assert(dingus
.handlerMethodNotAllowed
.called
);
540 it('calls HEAD setup and GET handler', async
function () {
541 dingus
.on('GET', '/', stubHandler
);
543 await dingus
.dispatch(req
, res
, ctx
);
544 assert(Dingus
.setHeadHandler
.called
);
545 assert(stubHandler
.called
);
547 it('covers no GET handler', async
function () {
548 dingus
.on('POST', '/', stubHandler
);
550 await dingus
.dispatch(req
, res
, ctx
);
551 assert(!stubHandler
.called
);
552 assert(dingus
.handlerMethodNotAllowed
.called
);
554 it('covers unexpected router error', async
function () {
555 sinon
.stub(dingus
.router
, 'lookup')
556 .onFirstCall().throws(new RouterNoMethodError())
557 .onSecondCall().throws(new DingusError())
559 dingus
.on('GET', '/', stubHandler
);
561 await dingus
.dispatch(req
, res
, ctx
);
562 assert(dingus
.handlerInternalServerError
.called
);
567 describe('parseBody', function () {
569 beforeEach(function () {
572 it('does not parse unknown type', function () {
574 dingus
.parseBody('unknown/type', ctx
);
575 assert
.fail(noExpectedException
);
577 assert
.strictEqual(e
.statusCode
, 415);
580 it('parses json', function () {
581 const src
= { foo: 'bar' };
582 const rawBody
= JSON
.stringify(src
);
583 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
, rawBody
);
584 assert
.deepStrictEqual(ctx
.parsedBody
, src
);
586 it('handles unparsable json', function () {
587 const rawBody
= 'not json';
589 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
, rawBody
);
590 assert
.fail(noExpectedException
);
592 assert
.strictEqual(e
.statusCode
, 400);
595 it('parses form', function () {
596 const expected
= Object
.assign(Object
.create(null), {
599 const rawBody
= 'foo=bar';
600 dingus
.parseBody('application/x-www-form-urlencoded', ctx
, rawBody
);
601 assert
.deepStrictEqual(ctx
.parsedBody
, expected
);
606 describe('bodyData', function () {
608 beforeEach(function () {
611 // eslint-disable-next-line security/detect-object-injection
612 on: (ev
, fn
) => resEvents
[ev
] = fn
,
615 it('provides data', async
function () {
616 const p
= dingus
.bodyData(res
);
617 resEvents
['data'](Buffer
.from('foo'));
618 resEvents
['data'](Buffer
.from('bar'));
620 const result
= await p
;
621 assert
.strictEqual(result
, 'foobar');
623 it('handles error', async
function () {
624 const p
= dingus
.bodyData(res
);
625 resEvents
['error']('foo');
628 assert
.fail(noExpectedException
);
630 assert
.strictEqual(e
, 'foo');
633 it('limits size', async
function () {
634 const p
= dingus
.bodyData(res
, 8);
635 resEvents
['data'](Buffer
.from('foobar'));
636 resEvents
['data'](Buffer
.from('bazquux'));
639 assert
.fail(noExpectedException
);
641 assert
.strictEqual(e
.statusCode
, 413);
644 it('provides buffer', async
function () {
645 const p
= dingus
.bodyData(res
, 0, false);
646 const expected
= Buffer
.from('bleat');
647 resEvents
['data'](expected
);
649 const result
= await p
;
650 assert
.deepStrictEqual(result
, expected
);
654 describe('ingestBody', function () {
655 it('ingests json', async
function () {
659 sinon
.stub(dingus
, 'bodyData').resolves('{"foo":"bar"}');
660 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
661 await dingus
.ingestBody(req
, res
, ctx
);
662 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
663 assert
.deepStrictEqual(ctx
.rawBody
, undefined);
665 it('persists rawBody', async
function () {
669 const body
= '{"foo":"bar"}';
670 sinon
.stub(dingus
, 'bodyData').resolves(body
);
671 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
672 await dingus
.ingestBody(req
, res
, ctx
, { persistRawBody: true });
673 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
674 assert
.deepStrictEqual(ctx
.rawBody
, body
);
676 it('skips parsing empty body', async
function () {
681 sinon
.stub(dingus
, 'bodyData').resolves(body
);
682 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
683 sinon
.spy(dingus
, 'parseBody');
684 await dingus
.ingestBody(req
, res
, ctx
, { parseEmptyBody: false });
685 assert
.deepStrictEqual(ctx
.parsedBody
, undefined);
686 assert(dingus
.parseBody
.notCalled
);
690 describe('setResponseType', function () {
692 let _sa
; // Preserve strictAccept
694 _sa
= dingus
.strictAccept
;
697 dingus
.strictAccept
= _sa
;
699 beforeEach(function () {
703 setHeader: sinon
.stub(),
705 sinon
.stub(Dingus
, 'getResponseContentType').returns();
707 it('rejects missing', function () {
708 dingus
.strictAccept
= true;
710 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
711 assert
.fail(noExpectedException
);
713 assert
.strictEqual(e
.statusCode
, 406, 'did not get expected status code');
716 it('accepts missing', function () {
717 dingus
.strictAccept
= false;
718 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
719 assert
.strictEqual(ctx
.responseType
, 'my/type');
722 }); // setResponseType
724 describe('_readFileInfo', function () {
725 let stat
, data
, statRes
, dataRes
, filename
;
726 beforeEach(function () {
727 sinon
.stub(fs
.promises
, 'stat');
728 sinon
.stub(fs
.promises
, 'readFile');
730 mtimeMs:1612553697186,
733 filename
= 'dummy.txt';
735 it('succeeds', async
function () {
736 fs
.promises
.stat
.resolves(statRes
);
737 fs
.promises
.readFile
.resolves('data');
738 [stat
, data
] = await dingus
._readFileInfo(filename
);
739 assert
.deepStrictEqual(stat
, statRes
);
740 assert
.deepStrictEqual(data
, dataRes
);
742 it('returns null for non-existant file', async
function () {
746 fs
.promises
.stat
.rejects(noEnt
);
747 fs
.promises
.readFile
.rejects(noEnt
);
748 [stat
, data
] = await dingus
._readFileInfo(filename
);
749 assert
.strictEqual(stat
, null);
750 assert
.strictEqual(data
, null);
752 it('throws unexpected error', async
function () {
753 const expectedException
= new Error('blah');
754 fs
.promises
.stat
.rejects(expectedException
);
755 await assert
.rejects(async () => {
756 await dingus
._readFileInfo(filename
);
757 }, expectedException
);
761 describe('_serveFileMetaHeaders', function () {
762 let res
, directory
, fileName
;
763 beforeEach(function () {
764 sinon
.stub(dingus
, '_readFileInfo');
766 setHeader: sinon
.stub(),
769 fileName
= 'filename';
771 it('covers no meta file', async
function() {
772 dingus
._readFileInfo
.resolves([null, null]);
773 const result
= await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
774 assert(!res
.setHeader
.called
);
775 assert
.strictEqual(result
, false);
777 it('adds extra headers', async
function () {
778 dingus
._readFileInfo
.resolves([{}, Buffer
.from(`Link: <https://example.com/>; rel="relation"
779 X-Folded-Header: data
782 Content-Type: image/sgi
784 const result
= await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
785 assert(res
.setHeader
.called
);
786 assert
.strictEqual(result
, true);
788 }); // _serveFileMetaHeaders
790 describe('serveFile', function () {
791 const path
= require('path');
792 let ctx
, req
, res
, directory
, fileName
, filestats
;
793 beforeEach(function () {
794 directory
= path
.join(__dirname
, '..', 'test-data');
795 fileName
= 'example.html';
799 [Enum
.Header
.Accept
]: undefined,
800 [Enum
.Header
.IfModifiedSince
]: undefined,
801 [Enum
.Header
.AcceptEncoding
]: undefined,
802 [Enum
.Header
.IfNoneMatch
]: undefined,
804 getHeader: (header
) => {
805 if (header
in req
._headers
) {
806 // eslint-disable-next-line security/detect-object-injection
807 return req
._headers
[header
];
809 assert
.fail(`unexpected getHeader ${header}`);
814 getHeader: sinon
.stub(),
815 getHeaders: sinon
.stub(),
816 hasHeader: sinon
.stub().returns(true),
817 setHeader: sinon
.stub(),
830 atimeMs: 1613253436842.815,
831 mtimeMs: 1603485933192.861,
832 ctimeMs: 1603485933192.861,
834 atime: '2021-02-13T21:57:16.843Z',
835 mtime: '2020-10-23T13:45:33.193Z',
836 ctime: '2020-10-23T13:45:33.193Z',
837 birthtime: '1970-01-01T00:00:00.000Z',
839 sinon
.stub(dingus
, 'handlerNotFound');
840 sinon
.stub(fs
.promises
, 'stat').resolves(filestats
);
841 sinon
.spy(fs
.promises
, 'readFile');
843 it('serves a file', async
function () {
844 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
845 assert(fs
.promises
.readFile
.called
);
846 assert(!dingus
.handlerNotFound
.called
);
848 it('covers no meta headers', async
function () {
849 dingus
.staticMetadata
= false;
850 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
851 assert(fs
.promises
.readFile
.called
);
852 assert(!dingus
.handlerNotFound
.called
);
854 it('does not serve dot-file', async
function () {
855 fileName
= '.example';
856 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
857 assert(!fs
.promises
.readFile
.called
);
858 assert(dingus
.handlerNotFound
.called
);
860 it('does not serve encoded navigation', async
function () {
861 fileName
= '/example.html';
862 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
863 assert(!fs
.promises
.readFile
.called
);
864 assert(dingus
.handlerNotFound
.called
);
866 it('does not serve missing file', async
function () {
867 fileName
= 'no-file.here';
868 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
869 assert(dingus
.handlerNotFound
.called
);
871 it('requires directory be specified', async
function () {
872 await dingus
.serveFile(req
, res
, ctx
, '', fileName
);
873 assert(!fs
.promises
.readFile
.called
);
874 assert(dingus
.handlerNotFound
.called
);
876 it('covers fs error', async
function () {
877 const expectedException
= new Error('blah');
878 fs
.promises
.stat
.restore();
879 sinon
.stub(fs
.promises
, 'stat').rejects(expectedException
);
881 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
882 assert
.fail('should have thrown');
884 assert
.strictEqual(e
, expectedException
);
887 it('caches by modified', async
function () {
888 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 23:11:16 GMT';
889 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
890 assert
.strictEqual(res
.statusCode
, 304);
892 it('does not cache old modified', async
function () {
893 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 01:11:16 GMT';
894 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
895 assert
.notStrictEqual(res
.statusCode
, 304);
896 assert(!dingus
.handlerNotFound
.called
);
898 it('caches ETag match', async
function () {
899 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
900 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
901 assert
.strictEqual(res
.statusCode
, 304);
903 it('does not cache ETag non-match', async
function () {
904 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"foo", "bar"';
905 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
906 assert
.notStrictEqual(res
.statusCode
, 304);
907 assert(!dingus
.handlerNotFound
.called
);
909 it('handles no possible encodings', async
function () {
910 req
._headers
[Enum
.Header
.AcceptEncoding
] = '*;q=0';
911 await assert
.rejects(async () => {
912 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
914 name: 'ResponseError',
917 it('handles a valid encoding', async
function () {
918 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'gzip';
919 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
920 assert(res
.end
.called
);
922 it('handles a valid encoding among others', async
function () {
923 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, br, gzip';
924 fs
.promises
.stat
.restore();
925 sinon
.stub(fs
.promises
, 'stat')
926 .onCall(0).resolves(filestats
) // identity file
927 .onCall(1).resolves(null) // br encoding
928 .onCall(2).resolves(filestats
); // gzip encoding
929 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
930 assert(res
.end
.called
);
932 it('handles misconfigured encoding', async
function () {
933 Enum
.EncodingType
.Flarp
= 'flarp';
934 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, gzip';
935 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
936 delete Enum
.EncodingType
.Flarp
;
937 assert(res
.end
.called
);
941 describe('renderError', function () {
943 beforeEach(function () {
947 details: 'hunkydorey',
950 it('renders unknown type', function () {
951 const contentType
= 'unknown/type';
952 const result
= dingus
.renderError(contentType
, err
);
953 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
955 it('renders text', function () {
956 const contentType
= 'text/plain';
957 const result
= dingus
.renderError(contentType
, err
);
958 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
960 it('renders json', function () {
961 const contentType
= Enum
.ContentType
.ApplicationJson
;
962 const result
= dingus
.renderError(contentType
, err
);
963 assert
.deepStrictEqual(result
, JSON
.stringify(err
));
965 it('renders html without details', function () {
968 errorMessage: 'Created',
970 const contentType
= 'text/html';
971 const result
= dingus
.renderError(contentType
, err
);
972 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
975 <title>${err.statusCode} ${err.errorMessage}</title>
978 <h1>${err.errorMessage}</h1>
982 it('renders html', function () {
983 const contentType
= 'text/html';
984 const result
= dingus
.renderError(contentType
, err
);
985 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
988 <title>${err.statusCode} ${err.errorMessage}</title>
991 <h1>${err.errorMessage}</h1>
992 <p>${err.details}</p>
996 it('renders html, multiple details', function () {
997 const contentType
= 'text/html';
998 err
.details
= ['one detail', 'two detail'];
999 const result
= dingus
.renderError(contentType
, err
);
1000 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
1003 <title>${err.statusCode} ${err.errorMessage}</title>
1006 <h1>${err.errorMessage}</h1>
1014 describe('sendErrorResponse', function () {
1016 beforeEach(function () {
1021 getHeader: sinon
.stub(),
1022 getHeaders: sinon
.stub(),
1023 hasHeader: sinon
.stub().returns(true),
1024 setHeader: sinon
.stub(),
1026 sinon
.stub(dingus
, 'renderError');
1028 it('covers', function () {
1032 dingus
.sendErrorResponse(err
, req
, res
, ctx
);
1033 assert(res
.end
.called
);
1035 }); // sendErrorResponse
1037 describe('proxyPrefix', function () {
1038 let req
, res
, ctx
, stubHandler
, pfxDingus
;
1041 beforeEach(function () {
1042 pfxDingus
= new Dingus(console
, { proxyPrefix: pfx
});
1044 setHeader: sinon
.stub(),
1045 getHeader: sinon
.stub(),
1050 setHeader: sinon
.stub(),
1051 getHeader: sinon
.stub(),
1054 sinon
.stub(pfxDingus
, 'handlerMethodNotAllowed');
1055 sinon
.stub(pfxDingus
, 'handlerNotFound');
1056 stubHandler
= sinon
.stub();
1058 afterEach(function () {
1062 it('handles prefixed route', async
function () {
1063 const urlPath
= '/:id';
1064 const method
= 'GET';
1065 pfxDingus
.on(method
, urlPath
, stubHandler
);
1066 req
.url
= pfx
+ '/abc';
1067 req
.method
= method
;
1069 await pfxDingus
.dispatch(req
, res
, ctx
);
1070 assert(stubHandler
.called
);
1071 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1072 assert(!pfxDingus
.handlerNotFound
.called
);
1074 it('does not handle prefixed route', async
function () {
1075 const urlPath
= '/:id';
1076 const method
= 'GET';
1077 pfxDingus
.on(method
, urlPath
, stubHandler
);
1078 req
.url
= '/wrongpfx/abc';
1079 req
.method
= method
;
1081 await pfxDingus
.dispatch(req
, res
, ctx
);
1082 assert(!stubHandler
.called
);
1083 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1084 assert(pfxDingus
.handlerNotFound
.called
);
1088 describe('handlerRedirect', function () {
1090 beforeEach(function () {
1092 getHeader: sinon
.stub(),
1095 setHeader: sinon
.stub(),
1100 it('covers', async
function () {
1101 await dingus
.handlerRedirect(req
, res
, ctx
);
1102 assert(res
.setHeader
.called
);
1103 assert(res
.end
.called
);
1105 it('covers non-defaults', async
function () {
1106 await dingus
.handlerRedirect(req
, res
, ctx
, 308);
1107 assert(res
.setHeader
.called
);
1108 assert(res
.end
.called
);
1110 }); // handlerRedirect
1112 describe('handlerGetStaticFile', function () {
1114 beforeEach(function () {
1116 getHeader: sinon
.stub(),
1119 setHeader: sinon
.stub(),
1126 sinon
.stub(dingus
, 'serveFile');
1128 it('covers', async
function () {
1129 await dingus
.handlerGetStaticFile(req
, res
, ctx
);
1130 assert(dingus
.serveFile
.called
);
1132 it('covers specified file', async
function () {
1133 await dingus
.handlerGetStaticFile(req
, res
, ctx
, 'file.txt');
1134 assert(dingus
.serveFile
.called
);
1136 }); // handlerGetStaticFile