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 const result
= await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
711 assert(!res
.setHeader
.called
);
712 assert
.strictEqual(result
, false);
714 it('adds extra headers', async
function () {
715 dingus
._readFileInfo
.resolves([{}, Buffer
.from(`Link: <https://example.com/>; rel="relation"
716 X-Folded-Header: data
719 Content-Type: image/sgi
721 const result
= await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
722 assert(res
.setHeader
.called
);
723 assert
.strictEqual(result
, true);
725 }); // _serveFileMetaHeaders
727 describe('serveFile', function () {
728 const path
= require('path');
729 let ctx
, req
, res
, directory
, fileName
, filestats
;
730 beforeEach(function () {
731 directory
= path
.join(__dirname
, '..', 'test-data');
732 fileName
= 'example.html';
736 [Enum
.Header
.Accept
]: undefined,
737 [Enum
.Header
.IfModifiedSince
]: undefined,
738 [Enum
.Header
.AcceptEncoding
]: undefined,
739 [Enum
.Header
.IfNoneMatch
]: undefined,
741 getHeader: (header
) => {
742 if (header
in req
._headers
) {
743 // eslint-disable-next-line security/detect-object-injection
744 return req
._headers
[header
];
746 assert
.fail(`unexpected getHeader ${header}`);
751 getHeader: sinon
.stub(),
752 getHeaders: sinon
.stub(),
753 hasHeader: sinon
.stub().returns(true),
754 setHeader: sinon
.stub(),
767 atimeMs: 1613253436842.815,
768 mtimeMs: 1603485933192.861,
769 ctimeMs: 1603485933192.861,
771 atime: '2021-02-13T21:57:16.843Z',
772 mtime: '2020-10-23T13:45:33.193Z',
773 ctime: '2020-10-23T13:45:33.193Z',
774 birthtime: '1970-01-01T00:00:00.000Z',
776 sinon
.stub(dingus
, 'handlerNotFound');
777 sinon
.stub(fs
.promises
, 'stat').resolves(filestats
);
778 sinon
.spy(fs
.promises
, 'readFile');
780 it('serves a file', async
function () {
781 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
782 assert(fs
.promises
.readFile
.called
);
783 assert(!dingus
.handlerNotFound
.called
);
785 it('covers no meta headers', async
function () {
786 dingus
.staticMetadata
= false;
787 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
788 assert(fs
.promises
.readFile
.called
);
789 assert(!dingus
.handlerNotFound
.called
);
791 it('does not serve dot-file', async
function () {
792 fileName
= '.example';
793 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
794 assert(!fs
.promises
.readFile
.called
);
795 assert(dingus
.handlerNotFound
.called
);
797 it('does not serve encoded navigation', async
function () {
798 fileName
= '/example.html';
799 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
800 assert(!fs
.promises
.readFile
.called
);
801 assert(dingus
.handlerNotFound
.called
);
803 it('does not serve missing file', async
function () {
804 fileName
= 'no-file.here';
805 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
806 assert(dingus
.handlerNotFound
.called
);
808 it('requires directory be specified', async
function () {
809 await dingus
.serveFile(req
, res
, ctx
, '', fileName
);
810 assert(!fs
.promises
.readFile
.called
);
811 assert(dingus
.handlerNotFound
.called
);
813 it('covers fs error', async
function () {
814 const expectedException
= new Error('blah');
815 fs
.promises
.stat
.restore();
816 sinon
.stub(fs
.promises
, 'stat').rejects(expectedException
);
818 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
819 assert
.fail('should have thrown');
821 assert
.strictEqual(e
, expectedException
);
824 it('caches by modified', async
function () {
825 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 23:11:16 GMT';
826 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
827 assert
.strictEqual(res
.statusCode
, 304);
829 it('does not cache old modified', async
function () {
830 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 01:11:16 GMT';
831 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
832 assert
.notStrictEqual(res
.statusCode
, 304);
833 assert(!dingus
.handlerNotFound
.called
);
835 it('caches ETag match', async
function () {
836 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
837 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
838 assert
.strictEqual(res
.statusCode
, 304);
840 it('does not cache ETag non-match', async
function () {
841 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"foo", "bar"';
842 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
843 assert
.notStrictEqual(res
.statusCode
, 304);
844 assert(!dingus
.handlerNotFound
.called
);
846 it('handles no possible encodings', async
function () {
847 req
._headers
[Enum
.Header
.AcceptEncoding
] = '*;q=0';
848 await assert
.rejects(async () => {
849 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
851 name: 'ResponseError',
854 it('handles a valid encoding', async
function () {
855 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'gzip';
856 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
857 assert(res
.end
.called
);
859 it('handles a valid encoding among others', async
function () {
860 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, br, gzip';
861 fs
.promises
.stat
.restore();
862 sinon
.stub(fs
.promises
, 'stat')
863 .onCall(0).resolves(filestats
) // identity file
864 .onCall(1).resolves(null) // br encoding
865 .onCall(2).resolves(filestats
); // gzip encoding
866 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
867 assert(res
.end
.called
);
869 it('handles misconfigured encoding', async
function () {
870 Enum
.EncodingType
.Flarp
= 'flarp';
871 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, gzip';
872 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
873 delete Enum
.EncodingType
.Flarp
;
874 assert(res
.end
.called
);
878 describe('renderError', function () {
880 beforeEach(function () {
884 details: 'hunkydorey',
887 it('renders unknown type', function () {
888 const contentType
= 'unknown/type';
889 const result
= dingus
.renderError(contentType
, err
);
890 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
892 it('renders text', function () {
893 const contentType
= 'text/plain';
894 const result
= dingus
.renderError(contentType
, err
);
895 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
897 it('renders json', function () {
898 const contentType
= Enum
.ContentType
.ApplicationJson
;
899 const result
= dingus
.renderError(contentType
, err
);
900 assert
.deepStrictEqual(result
, JSON
.stringify(err
));
902 it('renders html without details', function () {
905 errorMessage: 'Created',
907 const contentType
= 'text/html';
908 const result
= dingus
.renderError(contentType
, err
);
909 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
912 <title>${err.statusCode} ${err.errorMessage}</title>
915 <h1>${err.errorMessage}</h1>
919 it('renders html', function () {
920 const contentType
= 'text/html';
921 const result
= dingus
.renderError(contentType
, err
);
922 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
925 <title>${err.statusCode} ${err.errorMessage}</title>
928 <h1>${err.errorMessage}</h1>
929 <p>${err.details}</p>
933 it('renders html, multiple details', function () {
934 const contentType
= 'text/html';
935 err
.details
= ['one detail', 'two detail'];
936 const result
= dingus
.renderError(contentType
, err
);
937 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
940 <title>${err.statusCode} ${err.errorMessage}</title>
943 <h1>${err.errorMessage}</h1>
951 describe('sendErrorResponse', function () {
953 beforeEach(function () {
958 getHeader: sinon
.stub(),
959 getHeaders: sinon
.stub(),
960 hasHeader: sinon
.stub().returns(true),
961 setHeader: sinon
.stub(),
963 sinon
.stub(dingus
, 'renderError');
965 it('covers', function () {
969 dingus
.sendErrorResponse(err
, req
, res
, ctx
);
970 assert(res
.end
.called
);
972 }); // sendErrorResponse
974 describe('proxyPrefix', function () {
975 let req
, res
, ctx
, stubHandler
, pfxDingus
;
978 beforeEach(function () {
979 pfxDingus
= new Dingus(console
, { proxyPrefix: pfx
});
981 setHeader: sinon
.stub(),
982 getHeader: sinon
.stub(),
987 setHeader: sinon
.stub(),
988 getHeader: sinon
.stub(),
991 sinon
.stub(pfxDingus
, 'handlerMethodNotAllowed');
992 sinon
.stub(pfxDingus
, 'handlerNotFound');
993 stubHandler
= sinon
.stub();
995 afterEach(function () {
999 it('handles prefixed route', async
function () {
1000 const urlPath
= '/:id';
1001 const method
= 'GET';
1002 pfxDingus
.on(method
, urlPath
, stubHandler
);
1003 req
.url
= pfx
+ '/abc';
1004 req
.method
= method
;
1006 await pfxDingus
.dispatch(req
, res
, ctx
);
1007 assert(stubHandler
.called
);
1008 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1009 assert(!pfxDingus
.handlerNotFound
.called
);
1011 it('does not handle prefixed route', async
function () {
1012 const urlPath
= '/:id';
1013 const method
= 'GET';
1014 pfxDingus
.on(method
, urlPath
, stubHandler
);
1015 req
.url
= '/wrongpfx/abc';
1016 req
.method
= method
;
1018 await pfxDingus
.dispatch(req
, res
, ctx
);
1019 assert(!stubHandler
.called
);
1020 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1021 assert(pfxDingus
.handlerNotFound
.called
);
1025 describe('handlerRedirect', function () {
1027 beforeEach(function () {
1029 getHeader: sinon
.stub(),
1032 setHeader: sinon
.stub(),
1037 it('covers', async
function () {
1038 await dingus
.handlerRedirect(req
, res
, ctx
);
1039 assert(res
.setHeader
.called
);
1040 assert(res
.end
.called
);
1042 it('covers non-defaults', async
function () {
1043 await dingus
.handlerRedirect(req
, res
, ctx
, 308);
1044 assert(res
.setHeader
.called
);
1045 assert(res
.end
.called
);
1047 }); // handlerRedirect
1049 describe('handlerGetStaticFile', function () {
1051 beforeEach(function () {
1053 getHeader: sinon
.stub(),
1056 setHeader: sinon
.stub(),
1063 sinon
.stub(dingus
, 'serveFile');
1065 it('covers', async
function () {
1066 await dingus
.handlerGetStaticFile(req
, res
, ctx
);
1067 assert(dingus
.serveFile
.called
);
1069 it('covers specified file', async
function () {
1070 await dingus
.handlerGetStaticFile(req
, res
, ctx
, 'file.txt');
1071 assert(dingus
.serveFile
.called
);
1073 }); // handlerGetStaticFile