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';
20 describe('Dingus', function () {
22 beforeEach(function () {
23 dingus
= new Dingus(noLogger
, {});
25 afterEach(function () {
29 describe('constructor', function () {
30 it('covers', function () {
31 const d
= new Dingus();
36 describe('_normalizePath', function () {
37 it('returns normal path', function () {
39 const r
= dingus
._normalizePath(p
);
40 assert
.strictEqual(r
, p
);
42 it('returns normal path', function () {
43 const p
= '////a///b/./bar/..///c';
44 const expected
= '/a/b/c';
45 const r
= dingus
._normalizePath(p
);
46 assert
.strictEqual(r
, expected
);
50 describe('_splitUrl', function () {
51 const nullObject
= Object
.create(null);
53 it('splits a simple path', function () {
57 queryParams: nullObject
,
59 const r
= dingus
._splitUrl(p
);
60 assert
.deepStrictEqual(r
, expected
);
62 it('splits a path with trailing slash preserved', function () {
66 queryParams: nullObject
,
68 const r
= dingus
._splitUrl(p
);
69 assert
.deepStrictEqual(r
, expected
);
71 it('splits a path with trailing slash ignored', function () {
75 queryParams: nullObject
,
77 dingus
.ignoreTrailingSlash
= true;
78 const r
= dingus
._splitUrl(p
);
79 assert
.deepStrictEqual(r
, expected
);
81 it('splits a path with empty query string', function () {
85 queryParams: nullObject
,
87 const r
= dingus
._splitUrl(p
);
88 assert
.deepStrictEqual(r
, expected
);
90 it('splits a path with query string', function () {
91 const p
= '/a/b/c?x=1&y=2&z';
94 queryParams: Object
.assign(Object
.create(null), {
97 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.
100 const r
= dingus
._splitUrl(p
);
101 assert
.deepStrictEqual(r
, expected
);
105 describe('tagContext', function () {
107 beforeEach(function () {
109 getHeader: sinon
.stub(),
110 setHeader: sinon
.stub(),
113 getHeader: sinon
.stub(),
114 setHeader: sinon
.stub(),
118 it ('sets id in context', function () {
119 const result
= Dingus
.tagContext(req
, res
, ctx
);
120 assert
.strictEqual(ctx
.requestId
, result
);
121 assert(res
.setHeader
.called
);
123 it ('sets provided header', function () {
124 req
.getHeader
.onCall(0).returns('abc'); // X-Request-ID
125 const result
= Dingus
.tagContext(req
, res
, ctx
);
126 assert
.strictEqual(ctx
.requestId
, result
);
127 assert
.strictEqual(res
.setHeader
.getCall(0).args
[0], 'Request-ID');
128 assert
.strictEqual(res
.setHeader
.getCall(1).args
[0], 'X-Request-ID');
129 assert
.strictEqual(res
.setHeader
.getCall(1).args
[1], 'abc');
130 assert
.strictEqual(res
.setHeader
.callCount
, 2);
134 describe('clientAddressContext', function () {
138 _tp
= dingus
.trustProxy
;
141 dingus
.trustProxy
= _tp
;
143 beforeEach(function () {
145 getHeader: sinon
.stub(),
146 setHeader: sinon
.stub(),
150 getHeader: sinon
.stub(),
151 setHeader: sinon
.stub(),
155 it ('covers untrusted proxy', function () {
156 dingus
.trustProxy
= false;
159 clientProtocol: 'http',
161 dingus
.clientAddressContext(req
, res
, ctx
);
162 assert
.deepStrictEqual(ctx
, expected
);
163 assert(!req
.getHeader
.called
);
165 it ('covers missing', function () {
166 dingus
.trustProxy
= true;
168 clientAddress: '::1',
169 clientProtocol: 'https',
171 req
.connection
.remoteAddress
= '::1';
172 req
.connection
.encrypted
= true;
173 dingus
.clientAddressContext(req
, res
, ctx
);
174 assert(req
.getHeader
.called
);
175 assert
.deepStrictEqual(ctx
, expected
);
177 }); // clientAddressContext
179 describe('getRequestContentType', function () {
181 beforeEach(function () {
183 getHeader: sinon
.stub(),
184 setHeader: sinon
.stub(),
187 it('handles missing header', function () {
188 const result
= Dingus
.getRequestContentType(req
);
189 assert
.strictEqual(result
, '');
191 it('parses simple type', function () {
192 req
.getHeader
.onCall(0).returns(Enum
.ContentType
.ApplicationJson
);
193 const result
= Dingus
.getRequestContentType(req
);
194 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
196 it('parses complex type', function () {
197 req
.getHeader
.onCall(0).returns('application/json ; charset=UTF-8');
198 const result
= Dingus
.getRequestContentType(req
);
199 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
201 }); // getRequestContentType
203 describe('setResponseContentType', function () {
204 let req
, responseTypes
;
205 beforeEach(function () {
208 setHeader: sinon
.stub(),
209 getHeader: sinon
.stub(),
212 it('handles missing header', function () {
213 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
214 assert
.strictEqual(result
, undefined);
216 it('behaves as expected', function () {
217 responseTypes
.push(Enum
.ContentType
.ApplicationJson
);
218 req
.getHeader
.onCall(0).returns('text, image/png;q=0.5, application/*;q=0.2, audio;q=0.1');
219 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
220 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
222 }); // setResponseContentType
224 describe('on', function () {
226 beforeEach(function () {
227 stubOn
= sinon
.stub(dingus
.router
, 'on');
229 it('covers', function () {
230 dingus
.on('GET', '/', () => {});
231 assert(stubOn
.called
);
235 describe('setEndBodyHandler', function () {
236 let req
, res
, ctx
, handler
, origEnd
, origWrite
;
237 beforeEach(function () {
238 origEnd
= sinon
.stub();
239 origWrite
= sinon
.stub();
246 handler
= sinon
.stub();
248 it('collects body and handles', function () {
249 Dingus
.setEndBodyHandler(req
, res
, ctx
, handler
);
250 res
.write(Buffer
.from('foo'));
254 assert(origWrite
.called
);
255 assert(origEnd
.called
);
256 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
257 assert(handler
.called
);
259 }); // setEndBodyHandler
261 describe('setHeadHandler', function () {
262 let req
, res
, ctx
, origEnd
, origWrite
;
263 beforeEach(function () {
264 origEnd
= sinon
.stub();
265 origWrite
= sinon
.stub();
272 setHeader: sinon
.stub(),
276 it('collects response without writing', function () {
277 Dingus
.setHeadHandler(req
, res
, ctx
);
278 res
.write(Buffer
.from('foo'));
282 assert(!origWrite
.called
);
283 assert(origEnd
.called
);
284 assert
.deepStrictEqual(ctx
.responseBody
, undefined);
286 it('collects response without writing, persists written data', function () {
287 Dingus
.setHeadHandler(req
, res
, ctx
, true);
288 res
.write(Buffer
.from('foo'));
292 assert(!origWrite
.called
);
293 assert(origEnd
.called
);
294 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
296 it('ignores non-head method', function () {
298 Dingus
.setHeadHandler(req
, res
, ctx
);
299 res
.write(Buffer
.from('foo'));
301 assert(origWrite
.called
);
302 assert(origEnd
.called
);
304 }); // setHeadHandler
306 describe('addEncodingHeader', function () {
308 beforeEach(function () {
311 // eslint-disable-next-line security/detect-object-injection
312 getHeader: (h
) => res
._headers
[h
],
313 // eslint-disable-next-line security/detect-object-injection
314 setHeader: (h
, v
) => res
._headers
[h
] = v
,
317 it('adds', function () {
319 Dingus
.addEncodingHeader(res
, encoding
);
320 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip');
322 it('extends', function () {
324 Dingus
.addEncodingHeader(res
, encoding
);
325 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'utf8');
327 Dingus
.addEncodingHeader(res
, encoding
);
328 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip, utf8');
330 }); // addEncodingHeader
332 describe('dispatch', function () {
333 let pathsByLengthOrig
;
337 beforeEach(function () {
341 setHeader: sinon
.stub(),
342 getHeader: sinon
.stub(),
347 setHeader: sinon
.stub(),
348 hasHeader: sinon
.stub(),
349 getHeader: sinon
.stub(),
350 getHeaders: sinon
.stub(),
353 pathsByLengthOrig
= dingus
.pathsByLength
;
354 sinon
.spy(dingus
, 'handlerMethodNotAllowed');
355 sinon
.spy(dingus
, 'handlerNotFound');
356 sinon
.spy(dingus
, 'handlerBadRequest');
357 sinon
.spy(dingus
, 'handlerInternalServerError');
358 sinon
.spy(Dingus
, 'setHeadHandler');
359 stubHandler
= sinon
.stub();
361 afterEach(function () {
362 dingus
.pathsByLength
= pathsByLengthOrig
;
365 it('calls handler', async
function () {
366 const urlPath
= '/:id';
367 const method
= 'GET';
368 dingus
.on(method
, urlPath
, stubHandler
);
372 await dingus
.dispatch(req
, res
, ctx
);
373 assert(stubHandler
.called
);
374 assert(!dingus
.handlerMethodNotAllowed
.called
);
375 assert(!dingus
.handlerNotFound
.called
);
377 it('calls handler without context', async
function () {
378 const urlPath
= '/:id';
379 const method
= 'GET';
380 dingus
.on(method
, urlPath
, stubHandler
);
384 await dingus
.dispatch(req
, res
);
385 assert(stubHandler
.called
);
386 assert(!dingus
.handlerMethodNotAllowed
.called
);
387 assert(!dingus
.handlerNotFound
.called
);
389 it('calls fallback handler', async
function () {
390 const urlPath
= '/abc/:id';
391 const method
= 'GET';
392 dingus
.on('*', urlPath
, stubHandler
);
393 req
.url
= '/abc/def';
396 await dingus
.dispatch(req
, res
, ctx
);
397 assert(stubHandler
.called
);
398 assert(!dingus
.handlerMethodNotAllowed
.called
);
399 assert(!dingus
.handlerNotFound
.called
);
401 it('handles error in handler', async
function () {
402 const urlPath
= '/:id';
403 const method
= 'GET';
404 dingus
.on(method
, urlPath
, stubHandler
);
407 stubHandler
.rejects(new Error('blah'));
409 await dingus
.dispatch(req
, res
, ctx
);
410 assert(stubHandler
.called
);
411 assert(!dingus
.handlerMethodNotAllowed
.called
);
412 assert(!dingus
.handlerNotFound
.called
);
414 it('calls unsupported method', async
function () {
415 const urlPath
= '/:id';
416 const method
= 'POST';
417 dingus
.on('GET', urlPath
, stubHandler
);
421 await dingus
.dispatch(req
, res
, ctx
);
422 assert(!stubHandler
.called
);
423 assert(dingus
.handlerMethodNotAllowed
.called
);
424 assert(!dingus
.handlerNotFound
.called
);
426 it('does not lookup nonexistent path', async
function () {
427 req
.url
= '/foo/bar';
430 await dingus
.dispatch(req
, res
, ctx
);
431 assert(!stubHandler
.called
);
432 assert(!dingus
.handlerMethodNotAllowed
.called
);
433 assert(dingus
.handlerNotFound
.called
);
435 it('covers unhandled dingus exception', async
function () {
436 const expectedException
= new DingusError('blah');
437 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
439 await dingus
.dispatch(req
, res
, ctx
);
440 assert(!stubHandler
.called
);
441 assert(dingus
.handlerInternalServerError
.called
);
443 it('covers other exception', async
function () {
444 const expectedException
= new Error('blah');
445 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
447 await dingus
.dispatch(req
, res
, ctx
);
448 assert(!stubHandler
.called
);
449 assert(dingus
.handlerInternalServerError
.called
);
451 it('covers bad uri', async
function () {
454 await dingus
.dispatch(req
, res
, ctx
);
455 assert(dingus
.handlerBadRequest
.called
);
457 it('calls handler with additional arguments', async
function () {
458 dingus
.on('GET', '/', stubHandler
, 'foo', 'bar');
459 await dingus
.dispatch(req
, res
, ctx
);
460 assert(stubHandler
.called
);
461 assert
.strictEqual(stubHandler
.args
[0][3], 'foo');
462 assert
.strictEqual(stubHandler
.args
[0][4], 'bar');
464 describe('intrinsic HEAD handling', function () {
465 it('covers no intrinsic HEAD handling', async
function () {
466 dingus
.intrinsicHeadMethod
= false;
467 dingus
.on('GET', '/', stubHandler
);
469 await dingus
.dispatch(req
, res
, ctx
);
470 assert(!stubHandler
.called
);
471 assert(dingus
.handlerMethodNotAllowed
.called
);
473 it('calls HEAD setup and GET handler', async
function () {
474 dingus
.on('GET', '/', stubHandler
);
476 await dingus
.dispatch(req
, res
, ctx
);
477 assert(Dingus
.setHeadHandler
.called
);
478 assert(stubHandler
.called
);
480 it('covers no GET handler', async
function () {
481 dingus
.on('POST', '/', stubHandler
);
483 await dingus
.dispatch(req
, res
, ctx
);
484 assert(!stubHandler
.called
);
485 assert(dingus
.handlerMethodNotAllowed
.called
);
487 it('covers unexpected router error', async
function () {
488 sinon
.stub(dingus
.router
, 'lookup')
489 .onFirstCall().throws(new RouterNoMethodError())
490 .onSecondCall().throws(new DingusError())
492 dingus
.on('GET', '/', stubHandler
);
494 await dingus
.dispatch(req
, res
, ctx
);
495 assert(dingus
.handlerInternalServerError
.called
);
500 describe('parseBody', function () {
502 beforeEach(function () {
505 it('does not parse unknown type', function () {
507 dingus
.parseBody('unknown/type', ctx
);
508 assert
.fail(noExpectedException
);
510 assert
.strictEqual(e
.statusCode
, 415);
513 it('parses json', function () {
514 const src
= { foo: 'bar' };
515 const rawBody
= JSON
.stringify(src
);
516 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
, rawBody
);
517 assert
.deepStrictEqual(ctx
.parsedBody
, src
);
519 it('handles unparsable json', function () {
520 const rawBody
= 'not json';
522 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
, rawBody
);
523 assert
.fail(noExpectedException
);
525 assert
.strictEqual(e
.statusCode
, 400);
528 it('parses form', function () {
529 const expected
= Object
.assign(Object
.create(null), {
532 const rawBody
= 'foo=bar';
533 dingus
.parseBody('application/x-www-form-urlencoded', ctx
, rawBody
);
534 assert
.deepStrictEqual(ctx
.parsedBody
, expected
);
539 describe('bodyData', function () {
541 beforeEach(function () {
544 // eslint-disable-next-line security/detect-object-injection
545 on: (ev
, fn
) => resEvents
[ev
] = fn
,
548 it('provides data', async
function () {
549 const p
= dingus
.bodyData(res
);
550 resEvents
['data'](Buffer
.from('foo'));
551 resEvents
['data'](Buffer
.from('bar'));
553 const result
= await p
;
554 assert
.strictEqual(result
, 'foobar');
556 it('handles error', async
function () {
557 const p
= dingus
.bodyData(res
);
558 resEvents
['error']('foo');
561 assert
.fail(noExpectedException
);
563 assert
.strictEqual(e
, 'foo');
566 it('limits size', async
function () {
567 const p
= dingus
.bodyData(res
, 8);
568 resEvents
['data'](Buffer
.from('foobar'));
569 resEvents
['data'](Buffer
.from('bazquux'));
572 assert
.fail(noExpectedException
);
574 assert
.strictEqual(e
.statusCode
, 413);
577 it('provides buffer', async
function () {
578 const p
= dingus
.bodyData(res
, 0, false);
579 const expected
= Buffer
.from('bleat');
580 resEvents
['data'](expected
);
582 const result
= await p
;
583 assert
.deepStrictEqual(result
, expected
);
587 describe('ingestBody', function () {
588 it('ingests json', async
function () {
592 sinon
.stub(dingus
, 'bodyData').resolves('{"foo":"bar"}');
593 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
594 await dingus
.ingestBody(req
, res
, ctx
);
595 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
596 assert
.deepStrictEqual(ctx
.rawBody
, undefined);
598 it('persists rawBody', async
function () {
602 const body
= '{"foo":"bar"}';
603 sinon
.stub(dingus
, 'bodyData').resolves(body
);
604 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
605 await dingus
.ingestBody(req
, res
, ctx
, { persistRawBody: true });
606 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
607 assert
.deepStrictEqual(ctx
.rawBody
, body
);
609 it('skips parsing empty body', async
function () {
614 sinon
.stub(dingus
, 'bodyData').resolves(body
);
615 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
616 sinon
.spy(dingus
, 'parseBody');
617 await dingus
.ingestBody(req
, res
, ctx
, { parseEmptyBody: false });
618 assert
.deepStrictEqual(ctx
.parsedBody
, undefined);
619 assert(dingus
.parseBody
.notCalled
);
623 describe('setResponseType', function () {
625 let _sa
; // Preserve strictAccept
627 _sa
= dingus
.strictAccept
;
630 dingus
.strictAccept
= _sa
;
632 beforeEach(function () {
636 setHeader: sinon
.stub(),
638 sinon
.stub(Dingus
, 'getResponseContentType').returns();
640 it('rejects missing', function () {
641 dingus
.strictAccept
= true;
643 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
644 assert
.fail(noExpectedException
);
646 assert
.strictEqual(e
.statusCode
, 406, 'did not get expected status code');
649 it('accepts missing', function () {
650 dingus
.strictAccept
= false;
651 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
652 assert
.strictEqual(ctx
.responseType
, 'my/type');
655 }); // setResponseType
657 describe('_readFileInfo', function () {
658 let stat
, data
, statRes
, dataRes
, filename
;
659 beforeEach(function () {
660 sinon
.stub(fs
.promises
, 'stat');
661 sinon
.stub(fs
.promises
, 'readFile');
663 mtimeMs:1612553697186,
666 filename
= 'dummy.txt';
668 it('succeeds', async
function () {
669 fs
.promises
.stat
.resolves(statRes
);
670 fs
.promises
.readFile
.resolves('data');
671 [stat
, data
] = await dingus
._readFileInfo(filename
);
672 assert
.deepStrictEqual(stat
, statRes
);
673 assert
.deepStrictEqual(data
, dataRes
);
675 it('returns null for non-existant file', async
function () {
679 fs
.promises
.stat
.rejects(noEnt
);
680 fs
.promises
.readFile
.rejects(noEnt
);
681 [stat
, data
] = await dingus
._readFileInfo(filename
);
682 assert
.strictEqual(stat
, null);
683 assert
.strictEqual(data
, null);
685 it('throws unexpected error', async
function () {
686 const expectedException
= new Error('blah');
687 fs
.promises
.stat
.rejects(expectedException
);
688 await assert
.rejects(async () => {
689 await dingus
._readFileInfo(filename
);
690 }, expectedException
);
694 describe('_serveFileMetaHeaders', function () {
695 let res
, directory
, fileName
;
696 beforeEach(function () {
697 sinon
.stub(dingus
, '_readFileInfo');
699 setHeader: sinon
.stub(),
702 fileName
= 'filename';
704 it('covers no meta file', async
function() {
705 dingus
._readFileInfo
.resolves([null, null]);
706 await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
707 assert(!res
.setHeader
.called
);
709 it('adds extra headers', async
function () {
710 dingus
._readFileInfo
.resolves([{}, Buffer
.from(`Link: <https://example.com/>; rel="relation"
711 X-Folded-Header: data
714 Content-Type: image/sgi
716 await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
717 assert(res
.setHeader
.called
);
719 }); // _serveFileMetaHeaders
721 describe('serveFile', function () {
722 const path
= require('path');
723 let ctx
, req
, res
, directory
, fileName
, filestats
;
724 beforeEach(function () {
725 directory
= path
.join(__dirname
, '..', 'test-data');
726 fileName
= 'example.html';
730 [Enum
.Header
.Accept
]: undefined,
731 [Enum
.Header
.IfModifiedSince
]: undefined,
732 [Enum
.Header
.AcceptEncoding
]: undefined,
733 [Enum
.Header
.IfNoneMatch
]: undefined,
735 getHeader: (header
) => {
736 if (header
in req
._headers
) {
737 // eslint-disable-next-line security/detect-object-injection
738 return req
._headers
[header
];
740 assert
.fail(`unexpected getHeader ${header}`);
745 getHeader: sinon
.stub(),
746 getHeaders: sinon
.stub(),
747 hasHeader: sinon
.stub().returns(true),
748 setHeader: sinon
.stub(),
761 atimeMs: 1613253436842.815,
762 mtimeMs: 1603485933192.861,
763 ctimeMs: 1603485933192.861,
765 atime: '2021-02-13T21:57:16.843Z',
766 mtime: '2020-10-23T13:45:33.193Z',
767 ctime: '2020-10-23T13:45:33.193Z',
768 birthtime: '1970-01-01T00:00:00.000Z',
770 sinon
.stub(dingus
, 'handlerNotFound');
771 sinon
.stub(fs
.promises
, 'stat').resolves(filestats
);
772 sinon
.spy(fs
.promises
, 'readFile');
774 it('serves a file', async
function () {
775 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
776 assert(fs
.promises
.readFile
.called
);
777 assert(!dingus
.handlerNotFound
.called
);
779 it('covers no meta headers', async
function () {
780 dingus
.staticMetadata
= false;
781 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
782 assert(fs
.promises
.readFile
.called
);
783 assert(!dingus
.handlerNotFound
.called
);
785 it('does not serve dot-file', async
function () {
786 fileName
= '.example';
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 encoded navigation', async
function () {
792 fileName
= '/example.html';
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 missing file', async
function () {
798 fileName
= 'no-file.here';
799 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
800 assert(dingus
.handlerNotFound
.called
);
802 it('requires directory be specified', async
function () {
803 await dingus
.serveFile(req
, res
, ctx
, '', fileName
);
804 assert(!fs
.promises
.readFile
.called
);
805 assert(dingus
.handlerNotFound
.called
);
807 it('covers fs error', async
function () {
808 const expectedException
= new Error('blah');
809 fs
.promises
.stat
.restore();
810 sinon
.stub(fs
.promises
, 'stat').rejects(expectedException
);
812 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
813 assert
.fail('should have thrown');
815 assert
.strictEqual(e
, expectedException
);
818 it('caches by modified', async
function () {
819 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 23:11:16 GMT';
820 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
821 assert
.strictEqual(res
.statusCode
, 304);
823 it('does not cache old modified', async
function () {
824 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 01:11:16 GMT';
825 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
826 assert
.notStrictEqual(res
.statusCode
, 304);
827 assert(!dingus
.handlerNotFound
.called
);
829 it('caches ETag match', async
function () {
830 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
831 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
832 assert
.strictEqual(res
.statusCode
, 304);
834 it('does not cache ETag non-match', async
function () {
835 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"foo", "bar"';
836 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
837 assert
.notStrictEqual(res
.statusCode
, 304);
838 assert(!dingus
.handlerNotFound
.called
);
840 it('handles no possible encodings', async
function () {
841 req
._headers
[Enum
.Header
.AcceptEncoding
] = '*;q=0';
842 await assert
.rejects(async () => {
843 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
845 name: 'ResponseError',
848 it('handles a valid encoding', async
function () {
849 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'gzip';
850 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
851 assert(res
.end
.called
);
853 it('handles a valid encoding among others', async
function () {
854 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, br, gzip';
855 fs
.promises
.stat
.restore();
856 sinon
.stub(fs
.promises
, 'stat')
857 .onCall(0).resolves(filestats
) // identity file
858 .onCall(1).resolves(null) // br encoding
859 .onCall(2).resolves(filestats
); // gzip encoding
860 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
861 assert(res
.end
.called
);
863 it('handles misconfigured encoding', async
function () {
864 Enum
.EncodingType
.Flarp
= 'flarp';
865 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, gzip';
866 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
867 delete Enum
.EncodingType
.Flarp
;
868 assert(res
.end
.called
);
872 describe('renderError', function () {
874 beforeEach(function () {
878 details: 'hunkydorey',
881 it('renders unknown type', function () {
882 const contentType
= 'unknown/type';
883 const result
= dingus
.renderError(contentType
, err
);
884 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
886 it('renders text', function () {
887 const contentType
= 'text/plain';
888 const result
= dingus
.renderError(contentType
, err
);
889 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
891 it('renders json', function () {
892 const contentType
= Enum
.ContentType
.ApplicationJson
;
893 const result
= dingus
.renderError(contentType
, err
);
894 assert
.deepStrictEqual(result
, JSON
.stringify(err
));
896 it('renders html without details', function () {
899 errorMessage: 'Created',
901 const contentType
= 'text/html';
902 const result
= dingus
.renderError(contentType
, err
);
903 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
906 <title>${err.statusCode} ${err.errorMessage}</title>
909 <h1>${err.errorMessage}</h1>
913 it('renders html', function () {
914 const contentType
= 'text/html';
915 const result
= dingus
.renderError(contentType
, err
);
916 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
919 <title>${err.statusCode} ${err.errorMessage}</title>
922 <h1>${err.errorMessage}</h1>
923 <p>${err.details}</p>
927 it('renders html, multiple details', function () {
928 const contentType
= 'text/html';
929 err
.details
= ['one detail', 'two detail'];
930 const result
= dingus
.renderError(contentType
, err
);
931 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
934 <title>${err.statusCode} ${err.errorMessage}</title>
937 <h1>${err.errorMessage}</h1>
945 describe('sendErrorResponse', function () {
947 beforeEach(function () {
952 getHeader: sinon
.stub(),
953 getHeaders: sinon
.stub(),
954 hasHeader: sinon
.stub().returns(true),
955 setHeader: sinon
.stub(),
957 sinon
.stub(dingus
, 'renderError');
959 it('covers', function () {
963 dingus
.sendErrorResponse(err
, req
, res
, ctx
);
964 assert(res
.end
.called
);
966 }); // sendErrorResponse
968 describe('proxyPrefix', function () {
969 let req
, res
, ctx
, stubHandler
, pfxDingus
;
972 beforeEach(function () {
973 pfxDingus
= new Dingus(console
, { proxyPrefix: pfx
});
975 setHeader: sinon
.stub(),
976 getHeader: sinon
.stub(),
981 setHeader: sinon
.stub(),
982 getHeader: sinon
.stub(),
985 sinon
.stub(pfxDingus
, 'handlerMethodNotAllowed');
986 sinon
.stub(pfxDingus
, 'handlerNotFound');
987 stubHandler
= sinon
.stub();
989 afterEach(function () {
993 it('handles prefixed route', async
function () {
994 const urlPath
= '/:id';
995 const method
= 'GET';
996 pfxDingus
.on(method
, urlPath
, stubHandler
);
997 req
.url
= pfx
+ '/abc';
1000 await pfxDingus
.dispatch(req
, res
, ctx
);
1001 assert(stubHandler
.called
);
1002 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1003 assert(!pfxDingus
.handlerNotFound
.called
);
1005 it('does not handle prefixed route', async
function () {
1006 const urlPath
= '/:id';
1007 const method
= 'GET';
1008 pfxDingus
.on(method
, urlPath
, stubHandler
);
1009 req
.url
= '/wrongpfx/abc';
1010 req
.method
= method
;
1012 await pfxDingus
.dispatch(req
, res
, ctx
);
1013 assert(!stubHandler
.called
);
1014 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
1015 assert(pfxDingus
.handlerNotFound
.called
);
1019 describe('handlerRedirect', function () {
1021 beforeEach(function () {
1023 getHeader: sinon
.stub(),
1026 setHeader: sinon
.stub(),
1031 it('covers', async
function () {
1032 await dingus
.handlerRedirect(req
, res
, ctx
);
1033 assert(res
.setHeader
.called
);
1034 assert(res
.end
.called
);
1036 it('covers non-defaults', async
function () {
1037 await dingus
.handlerRedirect(req
, res
, ctx
, 308);
1038 assert(res
.setHeader
.called
);
1039 assert(res
.end
.called
);
1041 }); // handlerRedirect
1043 describe('handlerGetStaticFile', function () {
1045 beforeEach(function () {
1047 getHeader: sinon
.stub(),
1050 setHeader: sinon
.stub(),
1057 sinon
.stub(dingus
, 'serveFile');
1059 it('covers', async
function () {
1060 await dingus
.handlerGetStaticFile(req
, res
, ctx
);
1061 assert(dingus
.serveFile
.called
);
1063 it('covers specified file', async
function () {
1064 await dingus
.handlerGetStaticFile(req
, res
, ctx
, 'file.txt');
1065 assert(dingus
.serveFile
.called
);
1067 }); // handlerGetStaticFile