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
} = require('../../lib/errors');
11 const Enum
= require('../../lib/enum');
13 const noExpectedException
= 'did not get expected exception';
15 describe('Dingus', function () {
17 beforeEach(function () {
18 dingus
= new Dingus();
20 afterEach(function () {
24 describe('constructor', function () {
25 it('covers', function () {
26 const d
= new Dingus({}, {});
31 describe('_normalizePath', function () {
32 it('returns normal path', function () {
34 const r
= dingus
._normalizePath(p
);
35 assert
.strictEqual(r
, p
);
37 it('returns normal path', function () {
38 const p
= '////a///b/./bar/..///c';
39 const expected
= '/a/b/c'
40 const r
= dingus
._normalizePath(p
);
41 assert
.strictEqual(r
, expected
);
45 describe('_splitUrl', function () {
46 const nullObject
= Object
.create(null);
48 it('splits a simple path', function () {
52 queryParams: nullObject
,
54 const r
= dingus
._splitUrl(p
);
55 assert
.deepStrictEqual(r
, expected
);
57 it('splits a path with trailing slash preserved', function () {
61 queryParams: nullObject
,
63 const r
= dingus
._splitUrl(p
);
64 assert
.deepStrictEqual(r
, expected
);
66 it('splits a path with trailing slash ignored', function () {
70 queryParams: nullObject
,
72 dingus
.ignoreTrailingSlash
= true;
73 const r
= dingus
._splitUrl(p
);
74 assert
.deepStrictEqual(r
, expected
);
76 it('splits a path with empty query string', function () {
80 queryParams: nullObject
,
82 const r
= dingus
._splitUrl(p
);
83 assert
.deepStrictEqual(r
, expected
);
85 it('splits a path with query string', function () {
86 const p
= '/a/b/c?x=1&y=2&z';
89 queryParams: Object
.assign(Object
.create(null), {
92 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.
95 const r
= dingus
._splitUrl(p
);
96 assert
.deepStrictEqual(r
, expected
);
100 describe('tagContext', function () {
102 beforeEach(function () {
104 getHeader: sinon
.stub(),
105 setHeader: sinon
.stub(),
108 getHeader: sinon
.stub(),
109 setHeader: sinon
.stub(),
113 it ('sets id in context', function () {
114 const result
= Dingus
.tagContext(req
, res
, ctx
);
115 assert
.strictEqual(ctx
.requestId
, result
);
116 assert(res
.setHeader
.called
);
118 it ('sets provided header', function () {
119 req
.getHeader
.onCall(0).returns('abc'); // X-Request-ID
120 const result
= Dingus
.tagContext(req
, res
, ctx
);
121 assert
.strictEqual(ctx
.requestId
, result
);
122 assert
.strictEqual(res
.setHeader
.getCall(0).args
[0], 'Request-ID');
123 assert
.strictEqual(res
.setHeader
.getCall(1).args
[0], 'X-Request-ID');
124 assert
.strictEqual(res
.setHeader
.getCall(1).args
[1], 'abc');
125 assert
.strictEqual(res
.setHeader
.callCount
, 2);
129 describe('clientAddressContext', function () {
133 _tp
= dingus
.trustProxy
;
136 dingus
.trustProxy
= _tp
;
138 beforeEach(function () {
140 getHeader: sinon
.stub(),
141 setHeader: sinon
.stub(),
145 getHeader: sinon
.stub(),
146 setHeader: sinon
.stub(),
150 it ('covers untrusted proxy', function () {
151 dingus
.trustProxy
= false;
154 clientProtocol: 'http',
156 dingus
.clientAddressContext(req
, res
, ctx
);
157 assert
.deepStrictEqual(ctx
, expected
);
158 assert(!req
.getHeader
.called
);
160 it ('covers missing', function () {
161 dingus
.trustProxy
= true;
163 clientAddress: '::1',
164 clientProtocol: 'https',
166 req
.connection
.remoteAddress
= '::1';
167 req
.connection
.encrypted
= true;
168 dingus
.clientAddressContext(req
, res
, ctx
);
169 assert(req
.getHeader
.called
);
170 assert
.deepStrictEqual(ctx
, expected
);
172 }); // clientAddressContext
174 describe('getRequestContentType', function () {
176 beforeEach(function () {
178 getHeader: sinon
.stub(),
179 setHeader: sinon
.stub(),
182 it('handles missing header', function () {
183 const result
= Dingus
.getRequestContentType(req
);
184 assert
.strictEqual(result
, '');
186 it('parses simple type', function () {
187 req
.getHeader
.onCall(0).returns(Enum
.ContentType
.ApplicationJson
);
188 const result
= Dingus
.getRequestContentType(req
);
189 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
191 it('parses complex type', function () {
192 req
.getHeader
.onCall(0).returns('application/json ; charset=UTF-8');
193 const result
= Dingus
.getRequestContentType(req
);
194 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
196 }); // getRequestContentType
198 describe('setResponseContentType', function () {
199 let req
, responseTypes
;
200 beforeEach(function () {
203 setHeader: sinon
.stub(),
204 getHeader: sinon
.stub(),
207 it('handles missing header', function () {
208 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
209 assert
.strictEqual(result
, undefined);
211 it('behaves as expected', function () {
212 responseTypes
.push(Enum
.ContentType
.ApplicationJson
);
213 req
.getHeader
.onCall(0).returns('text, image/png;q=0.5, application/*;q=0.2, audio;q=0.1');
214 const result
= Dingus
.getResponseContentType(responseTypes
, req
);
215 assert
.strictEqual(result
, Enum
.ContentType
.ApplicationJson
);
217 }); // setResponseContentType
219 describe('on', function () {
221 beforeEach(function () {
222 stubOn
= sinon
.stub(dingus
.router
, 'on');
224 it('covers', function () {
225 dingus
.on('GET', '/', () => {});
226 assert(stubOn
.called
);
230 describe('setEndBodyHandler', function () {
231 let req
, res
, ctx
, handler
, origEnd
, origWrite
;
232 beforeEach(function () {
233 origEnd
= sinon
.stub();
234 origWrite
= sinon
.stub();
241 handler
= sinon
.stub();
243 it('collects body and handles', function () {
244 Dingus
.setEndBodyHandler(req
, res
, ctx
, handler
);
245 res
.write(Buffer
.from('foo'));
249 assert(origWrite
.called
);
250 assert(origEnd
.called
);
251 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
252 assert(handler
.called
);
254 }); // setEndBodyHandler
256 describe('setHeadHandler', function () {
257 let req
, res
, ctx
, origEnd
, origWrite
;
258 beforeEach(function () {
259 origEnd
= sinon
.stub();
260 origWrite
= sinon
.stub();
267 setHeader: sinon
.stub(),
271 it('collects response without writing', function () {
272 Dingus
.setHeadHandler(req
, res
, ctx
);
273 res
.write(Buffer
.from('foo'));
277 assert(!origWrite
.called
);
278 assert(origEnd
.called
);
279 assert
.deepStrictEqual(ctx
.responseBody
, undefined);
281 it('collects response without writing, persists written data', function () {
282 Dingus
.setHeadHandler(req
, res
, ctx
, true);
283 res
.write(Buffer
.from('foo'));
287 assert(!origWrite
.called
);
288 assert(origEnd
.called
);
289 assert
.deepStrictEqual(ctx
.responseBody
, Buffer
.from('foobazquux'));
291 it('ignores non-head method', function () {
293 Dingus
.setHeadHandler(req
, res
, ctx
);
294 res
.write(Buffer
.from('foo'));
296 assert(origWrite
.called
);
297 assert(origEnd
.called
);
299 }); // setHeadHandler
301 describe('addEncodingHeader', function () {
303 beforeEach(function () {
306 // eslint-disable-next-line security/detect-object-injection
307 getHeader: (h
) => res
._headers
[h
],
308 // eslint-disable-next-line security/detect-object-injection
309 setHeader: (h
, v
) => res
._headers
[h
] = v
,
312 it('adds', function () {
314 Dingus
.addEncodingHeader(res
, encoding
);
315 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip');
317 it('extends', function () {
319 Dingus
.addEncodingHeader(res
, encoding
);
320 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'utf8');
322 Dingus
.addEncodingHeader(res
, encoding
);
323 assert
.strictEqual(res
._headers
[Enum
.Header
.ContentEncoding
], 'gzip, utf8');
325 }); // addEncodingHeader
327 describe('dispatch', function () {
328 let pathsByLengthOrig
;
332 beforeEach(function () {
336 setHeader: sinon
.stub(),
337 getHeader: sinon
.stub(),
342 setHeader: sinon
.stub(),
343 hasHeader: sinon
.stub(),
344 getHeader: sinon
.stub(),
345 getHeaders: sinon
.stub(),
348 pathsByLengthOrig
= dingus
.pathsByLength
;
349 sinon
.spy(dingus
, 'handlerMethodNotAllowed');
350 sinon
.spy(dingus
, 'handlerNotFound');
351 sinon
.spy(dingus
, 'handlerBadRequest');
352 sinon
.spy(dingus
, 'handlerInternalServerError');
353 stubHandler
= sinon
.stub();
355 afterEach(function () {
356 dingus
.pathsByLength
= pathsByLengthOrig
;
359 it('calls handler', async
function () {
360 const urlPath
= '/:id';
361 const method
= 'GET';
362 dingus
.on(method
, urlPath
, stubHandler
);
366 await dingus
.dispatch(req
, res
, ctx
);
367 assert(stubHandler
.called
);
368 assert(!dingus
.handlerMethodNotAllowed
.called
);
369 assert(!dingus
.handlerNotFound
.called
);
371 it('calls handler without context', async
function () {
372 const urlPath
= '/:id';
373 const method
= 'GET';
374 dingus
.on(method
, urlPath
, stubHandler
);
378 await dingus
.dispatch(req
, res
);
379 assert(stubHandler
.called
);
380 assert(!dingus
.handlerMethodNotAllowed
.called
);
381 assert(!dingus
.handlerNotFound
.called
);
383 it('calls fallback handler', async
function () {
384 const urlPath
= '/abc/:id';
385 const method
= 'GET';
386 dingus
.on('*', urlPath
, stubHandler
);
387 req
.url
= '/abc/def';
390 await dingus
.dispatch(req
, res
, ctx
);
391 assert(stubHandler
.called
);
392 assert(!dingus
.handlerMethodNotAllowed
.called
);
393 assert(!dingus
.handlerNotFound
.called
);
395 it('handles error in handler', async
function () {
396 const urlPath
= '/:id';
397 const method
= 'GET';
398 dingus
.on(method
, urlPath
, stubHandler
);
401 stubHandler
.rejects(new Error('blah'));
403 await dingus
.dispatch(req
, res
, ctx
);
404 assert(stubHandler
.called
);
405 assert(!dingus
.handlerMethodNotAllowed
.called
);
406 assert(!dingus
.handlerNotFound
.called
);
408 it('calls unsupported method', async
function () {
409 const urlPath
= '/:id';
410 const method
= 'POST';
411 dingus
.on('GET', urlPath
, stubHandler
);
415 await dingus
.dispatch(req
, res
, ctx
);
416 assert(!stubHandler
.called
);
417 assert(dingus
.handlerMethodNotAllowed
.called
);
418 assert(!dingus
.handlerNotFound
.called
);
420 it('does not lookup nonexistent path', async
function () {
421 req
.url
= '/foo/bar';
424 await dingus
.dispatch(req
, res
, ctx
);
425 assert(!stubHandler
.called
);
426 assert(!dingus
.handlerMethodNotAllowed
.called
);
427 assert(dingus
.handlerNotFound
.called
);
429 it('covers unhandled dingus exception', async
function () {
430 const expectedException
= new DingusError('blah');
431 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
433 await dingus
.dispatch(req
, res
, ctx
);
434 assert(!stubHandler
.called
);
435 assert(dingus
.handlerInternalServerError
.called
);
437 it('covers other exception', async
function () {
438 const expectedException
= new Error('blah');
439 sinon
.stub(dingus
.router
, 'lookup').throws(expectedException
);
441 await dingus
.dispatch(req
, res
, ctx
);
442 assert(!stubHandler
.called
);
443 assert(dingus
.handlerInternalServerError
.called
);
445 it('covers bad uri', async
function () {
448 await dingus
.dispatch(req
, res
, ctx
);
449 assert(dingus
.handlerBadRequest
.called
);
451 it('calls handler with additional arguments', async
function () {
452 dingus
.on('GET', '/', stubHandler
, 'foo', 'bar');
453 await dingus
.dispatch(req
, res
, ctx
);
454 assert(stubHandler
.called
);
455 assert
.strictEqual(stubHandler
.args
[0][3], 'foo');
456 assert
.strictEqual(stubHandler
.args
[0][4], 'bar');
460 describe('parseBody', function () {
462 beforeEach(function () {
465 it('does not parse unknown type', function () {
467 dingus
.parseBody('unknown/type', ctx
);
468 assert
.fail(noExpectedException
);
470 assert
.strictEqual(e
.statusCode
, 415);
473 it('parses json', function () {
474 const src
= { foo: 'bar' };
475 ctx
.rawBody
= JSON
.stringify(src
);
476 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
);
477 assert
.deepStrictEqual(ctx
.parsedBody
, src
);
479 it('handles unparsable json', function () {
480 ctx
.rawBody
= 'not json';
482 dingus
.parseBody(Enum
.ContentType
.ApplicationJson
, ctx
);
483 assert
.fail(noExpectedException
);
485 assert
.strictEqual(e
.statusCode
, 400);
488 it('parses form', function () {
489 const expected
= Object
.assign(Object
.create(null), {
492 ctx
.rawBody
= 'foo=bar';
493 dingus
.parseBody('application/x-www-form-urlencoded', ctx
);
494 assert
.deepStrictEqual(ctx
.parsedBody
, expected
);
499 describe('bodyData', function () {
501 beforeEach(function () {
504 // eslint-disable-next-line security/detect-object-injection
505 on: (ev
, fn
) => resEvents
[ev
] = fn
,
508 it('provides data', async
function () {
509 const p
= dingus
.bodyData(res
);
510 resEvents
['data'](Buffer
.from('foo'));
511 resEvents
['data'](Buffer
.from('bar'));
513 const result
= await p
;
514 assert
.strictEqual(result
, 'foobar');
516 it('handles error', async
function () {
517 const p
= dingus
.bodyData(res
);
518 resEvents
['error']('foo');
521 assert
.fail(noExpectedException
);
523 assert
.strictEqual(e
, 'foo');
526 it('limits size', async
function () {
527 const p
= dingus
.bodyData(res
, 8);
528 resEvents
['data'](Buffer
.from('foobar'));
529 resEvents
['data'](Buffer
.from('bazquux'));
532 assert
.fail(noExpectedException
);
534 assert
.strictEqual(e
.statusCode
, 413);
537 it('provides buffer', async
function () {
538 const p
= dingus
.bodyData(res
, 0, false);
539 const expected
= Buffer
.from('bleat');
540 resEvents
['data'](expected
);
542 const result
= await p
;
543 assert
.deepStrictEqual(result
, expected
);
547 describe('ingestBody', function () {
548 it('ingests json', async
function () {
552 sinon
.stub(dingus
, 'bodyData').resolves('{"foo":"bar"}')
553 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
554 await dingus
.ingestBody(req
, res
, ctx
);
555 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
556 assert
.deepStrictEqual(ctx
.rawBody
, undefined);
558 it('persists rawBody', async
function () {
562 const body
= '{"foo":"bar"}';
563 sinon
.stub(dingus
, 'bodyData').resolves(body
);
564 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
565 await dingus
.ingestBody(req
, res
, ctx
, { persistRawBody: true });
566 assert
.deepStrictEqual(ctx
.parsedBody
, { foo: 'bar' });
567 assert
.deepStrictEqual(ctx
.rawBody
, body
);
569 it('skips parsing empty body', async
function () {
574 sinon
.stub(dingus
, 'bodyData').resolves(body
);
575 sinon
.stub(Dingus
, 'getRequestContentType').returns(Enum
.ContentType
.ApplicationJson
);
576 sinon
.spy(dingus
, 'parseBody');
577 await dingus
.ingestBody(req
, res
, ctx
, { parseEmptyBody: false });
578 assert
.deepStrictEqual(ctx
.parsedBody
, undefined);
579 assert(dingus
.parseBody
.notCalled
);
583 describe('setResponseType', function () {
585 let _sa
; // Preserve strictAccept
587 _sa
= dingus
.strictAccept
;
590 dingus
.strictAccept
= _sa
;
592 beforeEach(function () {
596 setHeader: sinon
.stub(),
598 sinon
.stub(Dingus
, 'getResponseContentType').returns();
600 it('rejects missing', function () {
601 dingus
.strictAccept
= true;
603 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
604 assert
.fail(noExpectedException
);
606 assert
.strictEqual(e
.statusCode
, 406, 'did not get expected status code');
609 it('accepts missing', function () {
610 dingus
.strictAccept
= false;
611 dingus
.setResponseType(['my/type'], req
, res
, ctx
);
612 assert
.strictEqual(ctx
.responseType
, 'my/type');
615 }); // setResponseType
617 describe('_readFileInfo', function () {
618 let stat
, data
, statRes
, dataRes
, filename
;
619 beforeEach(function () {
620 sinon
.stub(fs
.promises
, 'stat');
621 sinon
.stub(fs
.promises
, 'readFile');
623 mtimeMs:1612553697186,
626 filename
= 'dummy.txt';
628 it('succeeds', async
function () {
629 fs
.promises
.stat
.resolves(statRes
);
630 fs
.promises
.readFile
.resolves('data');
631 [stat
, data
] = await dingus
._readFileInfo(filename
);
632 assert
.deepStrictEqual(stat
, statRes
);
633 assert
.deepStrictEqual(data
, dataRes
);
635 it('returns null for non-existant file', async
function () {
639 fs
.promises
.stat
.rejects(noEnt
);
640 fs
.promises
.readFile
.rejects(noEnt
);
641 [stat
, data
] = await dingus
._readFileInfo(filename
);
642 assert
.strictEqual(stat
, null);
643 assert
.strictEqual(data
, null);
645 it('throws unexpected error', async
function () {
646 const expectedException
= new Error('blah');
647 fs
.promises
.stat
.rejects(expectedException
);
648 await assert
.rejects(async () => {
649 await dingus
._readFileInfo(filename
);
650 }, expectedException
);
654 describe('_serveFileMetaHeaders', function () {
655 let res
, directory
, fileName
;
656 beforeEach(function () {
657 sinon
.stub(dingus
, '_readFileInfo');
659 setHeader: sinon
.stub(),
662 fileName
= 'filename';
664 it('covers no meta file', async
function() {
665 dingus
._readFileInfo
.resolves([null, null]);
666 await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
667 assert(!res
.setHeader
.called
);
669 it('adds extra headers', async
function () {
670 dingus
._readFileInfo
.resolves([{}, Buffer
.from(`Link: <https://example.com/>; rel="relation"
671 X-Folded-Header: data
674 Content-Type: image/sgi
676 await dingus
._serveFileMetaHeaders(res
, directory
, fileName
);
677 assert(res
.setHeader
.called
);
679 }); // _serveFileMetaHeaders
681 describe('serveFile', function () {
682 const path
= require('path');
683 let ctx
, req
, res
, directory
, fileName
, filestats
;
684 beforeEach(function () {
685 directory
= path
.join(__dirname
, '..', 'test-data');
686 fileName
= 'example.html';
690 [Enum
.Header
.Accept
]: undefined,
691 [Enum
.Header
.IfModifiedSince
]: undefined,
692 [Enum
.Header
.AcceptEncoding
]: undefined,
693 [Enum
.Header
.IfNoneMatch
]: undefined,
695 getHeader: (header
) => {
696 if (header
in req
._headers
) {
697 // eslint-disable-next-line security/detect-object-injection
698 return req
._headers
[header
];
700 assert
.fail(`unexpected getHeader ${header}`);
705 getHeader: sinon
.stub(),
706 getHeaders: sinon
.stub(),
707 hasHeader: sinon
.stub().returns(true),
708 setHeader: sinon
.stub(),
721 atimeMs: 1613253436842.815,
722 mtimeMs: 1603485933192.861,
723 ctimeMs: 1603485933192.861,
725 atime: '2021-02-13T21:57:16.843Z',
726 mtime: '2020-10-23T13:45:33.193Z',
727 ctime: '2020-10-23T13:45:33.193Z',
728 birthtime: '1970-01-01T00:00:00.000Z',
730 sinon
.stub(dingus
, 'handlerNotFound');
731 sinon
.stub(fs
.promises
, 'stat').resolves(filestats
);
732 sinon
.spy(fs
.promises
, 'readFile');
734 it('serves a file', async
function () {
735 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
736 assert(fs
.promises
.readFile
.called
);
737 assert(!dingus
.handlerNotFound
.called
);
739 it('covers no meta headers', async
function () {
740 dingus
.staticMetadata
= false;
741 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
742 assert(fs
.promises
.readFile
.called
);
743 assert(!dingus
.handlerNotFound
.called
);
745 it('does not serve dot-file', async
function () {
746 fileName
= '.example';
747 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
748 assert(!fs
.promises
.readFile
.called
);
749 assert(dingus
.handlerNotFound
.called
);
751 it('does not serve encoded navigation', async
function () {
752 fileName
= '/example.html';
753 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
754 assert(!fs
.promises
.readFile
.called
);
755 assert(dingus
.handlerNotFound
.called
);
757 it('does not serve missing file', async
function () {
758 fileName
= 'no-file.here';
759 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
760 assert(dingus
.handlerNotFound
.called
);
762 it('requires directory be specified', async
function () {
763 await dingus
.serveFile(req
, res
, ctx
, '', fileName
);
764 assert(!fs
.promises
.readFile
.called
);
765 assert(dingus
.handlerNotFound
.called
);
767 it('covers fs error', async
function () {
768 const expectedException
= new Error('blah');
769 fs
.promises
.stat
.restore();
770 sinon
.stub(fs
.promises
, 'stat').rejects(expectedException
);
772 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
773 assert
.fail('should have thrown');
775 assert
.strictEqual(e
, expectedException
);
778 it('caches by modified', async
function () {
779 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 23:11:16 GMT';
780 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
781 assert
.strictEqual(res
.statusCode
, 304);
783 it('does not cache old modified', async
function () {
784 req
._headers
[Enum
.Header
.IfModifiedSince
] = 'Fri, 23 Oct 2020 01:11:16 GMT';
785 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
786 assert
.notStrictEqual(res
.statusCode
, 304);
787 assert(!dingus
.handlerNotFound
.called
);
789 it('caches ETag match', async
function () {
790 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
791 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
792 assert
.strictEqual(res
.statusCode
, 304);
794 it('does not cache ETag non-match', async
function () {
795 req
._headers
[Enum
.Header
.IfNoneMatch
] = '"foo", "bar"';
796 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
797 assert
.notStrictEqual(res
.statusCode
, 304);
798 assert(!dingus
.handlerNotFound
.called
);
800 it('handles no possible encodings', async
function () {
801 req
._headers
[Enum
.Header
.AcceptEncoding
] = '*;q=0';
802 await assert
.rejects(async () => {
803 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
805 name: 'ResponseError',
808 it('handles a valid encoding', async
function () {
809 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'gzip';
810 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
811 assert(res
.end
.called
);
813 it('handles a valid encoding among others', async
function () {
814 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, br, gzip';
815 fs
.promises
.stat
.restore();
816 sinon
.stub(fs
.promises
, 'stat')
817 .onCall(0).resolves(filestats
) // identity file
818 .onCall(1).resolves(null) // br encoding
819 .onCall(2).resolves(filestats
); // gzip encoding
820 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
821 assert(res
.end
.called
);
823 it('handles misconfigured encoding', async
function () {
824 Enum
.EncodingType
.Flarp
= 'flarp';
825 req
._headers
[Enum
.Header
.AcceptEncoding
] = 'flarp, gzip';
826 await dingus
.serveFile(req
, res
, ctx
, directory
, fileName
);
827 delete Enum
.EncodingType
.Flarp
;
828 assert(res
.end
.called
);
832 describe('renderError', function () {
834 beforeEach(function () {
838 details: 'hunkydorey',
841 it('renders unknown type', function () {
842 const contentType
= 'unknown/type';
843 const result
= dingus
.renderError(contentType
, err
);
844 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
846 it('renders text', function () {
847 const contentType
= 'text/plain';
848 const result
= dingus
.renderError(contentType
, err
);
849 assert
.deepStrictEqual(result
, 'OK\r\nhunkydorey');
851 it('renders json', function () {
852 const contentType
= Enum
.ContentType
.ApplicationJson
;
853 const result
= dingus
.renderError(contentType
, err
);
854 assert
.deepStrictEqual(result
, JSON
.stringify(err
));
856 it('renders html without details', function () {
859 errorMessage: 'Created',
861 const contentType
= 'text/html';
862 const result
= dingus
.renderError(contentType
, err
);
863 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
866 <title>${err.statusCode} ${err.errorMessage}</title>
869 <h1>${err.errorMessage}</h1>
873 it('renders html', function () {
874 const contentType
= 'text/html';
875 const result
= dingus
.renderError(contentType
, err
);
876 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
879 <title>${err.statusCode} ${err.errorMessage}</title>
882 <h1>${err.errorMessage}</h1>
883 <p>${err.details}</p>
887 it('renders html, multiple details', function () {
888 const contentType
= 'text/html';
889 err
.details
= ['one detail', 'two detail'];
890 const result
= dingus
.renderError(contentType
, err
);
891 assert
.deepStrictEqual(result
, `<!DOCTYPE html>
894 <title>${err.statusCode} ${err.errorMessage}</title>
897 <h1>${err.errorMessage}</h1>
905 describe('sendErrorResponse', function () {
907 beforeEach(function () {
912 getHeader: sinon
.stub(),
913 getHeaders: sinon
.stub(),
914 hasHeader: sinon
.stub().returns(true),
915 setHeader: sinon
.stub(),
917 sinon
.stub(dingus
, 'renderError');
919 it('covers', function () {
923 dingus
.sendErrorResponse(err
, req
, res
, ctx
);
924 assert(res
.end
.called
);
926 }); // sendErrorResponse
928 describe('proxyPrefix', function () {
929 let req
, res
, ctx
, stubHandler
, pfxDingus
;
932 beforeEach(function () {
933 pfxDingus
= new Dingus(console
, { proxyPrefix: pfx
});
935 setHeader: sinon
.stub(),
936 getHeader: sinon
.stub(),
941 setHeader: sinon
.stub(),
942 getHeader: sinon
.stub(),
945 sinon
.stub(pfxDingus
, 'handlerMethodNotAllowed');
946 sinon
.stub(pfxDingus
, 'handlerNotFound');
947 stubHandler
= sinon
.stub();
949 afterEach(function () {
953 it('handles prefixed route', async
function () {
954 const urlPath
= '/:id';
955 const method
= 'GET';
956 pfxDingus
.on(method
, urlPath
, stubHandler
);
957 req
.url
= pfx
+ '/abc';
960 await pfxDingus
.dispatch(req
, res
, ctx
);
961 assert(stubHandler
.called
);
962 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
963 assert(!pfxDingus
.handlerNotFound
.called
);
965 it('does not handle prefixed route', async
function () {
966 const urlPath
= '/:id';
967 const method
= 'GET';
968 pfxDingus
.on(method
, urlPath
, stubHandler
);
969 req
.url
= '/wrongpfx/abc';
972 await pfxDingus
.dispatch(req
, res
, ctx
);
973 assert(!stubHandler
.called
);
974 assert(!pfxDingus
.handlerMethodNotAllowed
.called
);
975 assert(pfxDingus
.handlerNotFound
.called
);
979 describe('handlerRedirect', function () {
981 beforeEach(function () {
983 getHeader: sinon
.stub(),
986 setHeader: sinon
.stub(),
991 it('covers', async
function () {
992 await dingus
.handlerRedirect(req
, res
, ctx
);
993 assert(res
.setHeader
.called
);
994 assert(res
.end
.called
);
996 it('covers non-defaults', async
function () {
997 await dingus
.handlerRedirect(req
, res
, ctx
, 308);
998 assert(res
.setHeader
.called
);
999 assert(res
.end
.called
);
1001 }); // handlerRedirect
1003 describe('handlerGetStaticFile', function () {
1005 beforeEach(function () {
1007 getHeader: sinon
.stub(),
1010 setHeader: sinon
.stub(),
1017 sinon
.stub(dingus
, 'serveFile');
1019 it('covers', async
function () {
1020 await dingus
.handlerGetStaticFile(req
, res
, ctx
);
1021 assert(dingus
.serveFile
.called
);
1023 it('covers specified file', async
function () {
1024 await dingus
.handlerGetStaticFile(req
, res
, ctx
, 'file.txt');
1025 assert(dingus
.serveFile
.called
);
1027 }); // handlerGetStaticFile