ab61256b3a89162c17bfb002a0569f7975786efc
[squeep-api-dingus] / test / lib / dingus.js
1 /* eslint-disable capitalized-comments */
2 /* eslint-env mocha */
3 'use strict';
4
5 const assert = require('assert');
6 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
7 const fs = require('fs');
8
9 const Dingus = require('../../lib/dingus');
10 const { DingusError } = require('../../lib/errors');
11 const Enum = require('../../lib/enum');
12
13 const noExpectedException = 'did not get expected exception';
14
15 describe('Dingus', function () {
16 let dingus;
17 beforeEach(function () {
18 dingus = new Dingus();
19 });
20 afterEach(function () {
21 sinon.restore();
22 });
23
24 describe('constructor', function () {
25 it('covers', function () {
26 const d = new Dingus({}, {});
27 assert(d);
28 assert('log' in d.logger);
29 });
30 }); // constructor
31
32 describe('_normalizePath', function () {
33 it('returns normal path', function () {
34 const p = '/a/b/c';
35 const r = dingus._normalizePath(p);
36 assert.strictEqual(r, p);
37 });
38 it('returns normal path', function () {
39 const p = '////a///b/./bar/..///c';
40 const expected = '/a/b/c'
41 const r = dingus._normalizePath(p);
42 assert.strictEqual(r, expected);
43 });
44 }); // _normalizePath
45
46 describe('_splitUrl', function () {
47 const nullObject = Object.create(null);
48
49 it('splits a simple path', function () {
50 const p = '/a/b/c';
51 const expected = {
52 pathPart: p,
53 queryParams: nullObject,
54 };
55 const r = dingus._splitUrl(p);
56 assert.deepStrictEqual(r, expected);
57 });
58 it('splits a path with trailing slash preserved', function () {
59 const p = '/a/b/c/';
60 const expected = {
61 pathPart: p,
62 queryParams: nullObject,
63 };
64 const r = dingus._splitUrl(p);
65 assert.deepStrictEqual(r, expected);
66 });
67 it('splits a path with trailing slash ignored', function () {
68 const p = '/a/b/c/';
69 const expected = {
70 pathPart: p,
71 queryParams: nullObject,
72 };
73 dingus.ignoreTrailingSlash = true;
74 const r = dingus._splitUrl(p);
75 assert.deepStrictEqual(r, expected);
76 });
77 it('splits a path with empty query string', function () {
78 const p = '/a/b/c?';
79 const expected = {
80 pathPart: '/a/b/c',
81 queryParams: nullObject,
82 };
83 const r = dingus._splitUrl(p);
84 assert.deepStrictEqual(r, expected);
85 });
86 it('splits a path with query string', function () {
87 const p = '/a/b/c?x=1&y=2&z';
88 const expected = {
89 pathPart: '/a/b/c',
90 queryParams: Object.assign(Object.create(null), {
91 x: '1',
92 y: '2',
93 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.
94 }),
95 };
96 const r = dingus._splitUrl(p);
97 assert.deepStrictEqual(r, expected);
98 });
99 }); // _splitUrl
100
101 describe('tagContext', function () {
102 let req, res, ctx;
103 beforeEach(function () {
104 req = {
105 getHeader: sinon.stub(),
106 setHeader: sinon.stub(),
107 };
108 res = {
109 getHeader: sinon.stub(),
110 setHeader: sinon.stub(),
111 };
112 ctx = {};
113 });
114 it ('sets id in context', function () {
115 const result = Dingus.tagContext(req, res, ctx);
116 assert.strictEqual(ctx.requestId, result);
117 assert(res.setHeader.called);
118 });
119 it ('sets provided header', function () {
120 req.getHeader.onCall(0).returns('abc'); // X-Request-ID
121 const result = Dingus.tagContext(req, res, ctx);
122 assert.strictEqual(ctx.requestId, result);
123 assert.strictEqual(res.setHeader.getCall(0).args[0], 'Request-ID');
124 assert.strictEqual(res.setHeader.getCall(1).args[0], 'X-Request-ID');
125 assert.strictEqual(res.setHeader.getCall(1).args[1], 'abc');
126 assert.strictEqual(res.setHeader.callCount, 2);
127 });
128 }); // tagContext
129
130 describe('clientAddressContext', function () {
131 let req, res, ctx;
132 let _tp;
133 before(function () {
134 _tp = dingus.trustProxy;
135 });
136 after(function () {
137 dingus.trustProxy = _tp;
138 });
139 beforeEach(function () {
140 req = {
141 getHeader: sinon.stub(),
142 setHeader: sinon.stub(),
143 connection: {},
144 };
145 res = {
146 getHeader: sinon.stub(),
147 setHeader: sinon.stub(),
148 };
149 ctx = {};
150 });
151 it ('covers untrusted proxy', function () {
152 dingus.trustProxy = false;
153 const expected = {
154 clientAddress: '',
155 clientProtocol: 'http',
156 }
157 dingus.clientAddressContext(req, res, ctx);
158 assert.deepStrictEqual(ctx, expected);
159 assert(!req.getHeader.called);
160 });
161 it ('covers missing', function () {
162 dingus.trustProxy = true;
163 const expected = {
164 clientAddress: '::1',
165 clientProtocol: 'https',
166 }
167 req.connection.remoteAddress = '::1';
168 req.connection.encrypted = true;
169 dingus.clientAddressContext(req, res, ctx);
170 assert(req.getHeader.called);
171 assert.deepStrictEqual(ctx, expected);
172 });
173 }); // clientAddressContext
174
175 describe('getRequestContentType', function () {
176 let req;
177 beforeEach(function () {
178 req = {
179 getHeader: sinon.stub(),
180 setHeader: sinon.stub(),
181 };
182 });
183 it('handles missing header', function () {
184 const result = Dingus.getRequestContentType(req);
185 assert.strictEqual(result, '');
186 });
187 it('parses simple type', function () {
188 req.getHeader.onCall(0).returns(Enum.ContentType.ApplicationJson);
189 const result = Dingus.getRequestContentType(req);
190 assert.strictEqual(result, Enum.ContentType.ApplicationJson);
191 });
192 it('parses complex type', function () {
193 req.getHeader.onCall(0).returns('application/json ; charset=UTF-8');
194 const result = Dingus.getRequestContentType(req);
195 assert.strictEqual(result, Enum.ContentType.ApplicationJson);
196 });
197 }); // getRequestContentType
198
199 describe('setResponseContentType', function () {
200 let req, responseTypes;
201 beforeEach(function () {
202 responseTypes = [];
203 req = {
204 setHeader: sinon.stub(),
205 getHeader: sinon.stub(),
206 };
207 });
208 it('handles missing header', function () {
209 const result = Dingus.getResponseContentType(responseTypes, req);
210 assert.strictEqual(result, undefined);
211 });
212 it('behaves as expected', function () {
213 responseTypes.push(Enum.ContentType.ApplicationJson);
214 req.getHeader.onCall(0).returns('text, image/png;q=0.5, application/*;q=0.2, audio;q=0.1');
215 const result = Dingus.getResponseContentType(responseTypes, req);
216 assert.strictEqual(result, Enum.ContentType.ApplicationJson);
217 });
218 }); // setResponseContentType
219
220 describe('on', function () {
221 let stubOn;
222 beforeEach(function () {
223 stubOn = sinon.stub(dingus.router, 'on');
224 });
225 it('covers', function () {
226 dingus.on('GET', '/', () => {});
227 assert(stubOn.called);
228 });
229 }); // on
230
231 describe('setEndBodyHandler', function () {
232 let req, res, ctx, handler, origEnd, origWrite;
233 beforeEach(function () {
234 origEnd = sinon.stub();
235 origWrite = sinon.stub();
236 req = {};
237 res = {
238 write: origWrite,
239 end: origEnd,
240 };
241 ctx = {};
242 handler = sinon.stub();
243 });
244 it('collects body and handles', function () {
245 Dingus.setEndBodyHandler(req, res, ctx, handler);
246 res.write(Buffer.from('foo'));
247 res.write('baz');
248 res.write();
249 res.end('quux');
250 assert(origWrite.called);
251 assert(origEnd.called);
252 assert.deepStrictEqual(ctx.responseBody, Buffer.from('foobazquux'));
253 assert(handler.called);
254 });
255 }); // setEndBodyHandler
256
257 describe('setHeadHandler', function () {
258 let req, res, ctx, origEnd, origWrite;
259 beforeEach(function () {
260 origEnd = sinon.stub();
261 origWrite = sinon.stub();
262 req = {
263 method: 'HEAD',
264 };
265 res = {
266 end: origEnd,
267 write: origWrite,
268 setHeader: sinon.stub(),
269 };
270 ctx = {};
271 });
272 it('collects response without writing', function () {
273 Dingus.setHeadHandler(req, res, ctx);
274 res.write(Buffer.from('foo'));
275 res.write('baz');
276 res.write();
277 res.end('quux');
278 assert(!origWrite.called);
279 assert(origEnd.called);
280 assert.deepStrictEqual(ctx.responseBody, undefined);
281 });
282 it('collects response without writing, persists written data', function () {
283 Dingus.setHeadHandler(req, res, ctx, true);
284 res.write(Buffer.from('foo'));
285 res.write('baz');
286 res.write();
287 res.end('quux');
288 assert(!origWrite.called);
289 assert(origEnd.called);
290 assert.deepStrictEqual(ctx.responseBody, Buffer.from('foobazquux'));
291 });
292 it('ignores non-head method', function () {
293 req.method = 'GET';
294 Dingus.setHeadHandler(req, res, ctx);
295 res.write(Buffer.from('foo'));
296 res.end('bar');
297 assert(origWrite.called);
298 assert(origEnd.called);
299 });
300 }); // setHeadHandler
301
302 describe('addEncodingHeader', function () {
303 let res, encoding;
304 beforeEach(function () {
305 res = {
306 _headers: {},
307 // eslint-disable-next-line security/detect-object-injection
308 getHeader: (h) => res._headers[h],
309 // eslint-disable-next-line security/detect-object-injection
310 setHeader: (h, v) => res._headers[h] = v,
311 };
312 });
313 it('adds', function () {
314 encoding = 'gzip';
315 Dingus.addEncodingHeader(res, encoding);
316 assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'gzip');
317 });
318 it('extends', function () {
319 encoding = 'utf8';
320 Dingus.addEncodingHeader(res, encoding);
321 assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'utf8');
322 encoding = 'gzip';
323 Dingus.addEncodingHeader(res, encoding);
324 assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'gzip, utf8');
325 });
326 }); // addEncodingHeader
327
328 describe('dispatch', function () {
329 let pathsByLengthOrig;
330 let req, res, ctx;
331 let stubHandler;
332
333 beforeEach(function () {
334 req = {
335 url: '/',
336 method: 'GET',
337 setHeader: sinon.stub(),
338 getHeader: sinon.stub(),
339 };
340 res = {
341 statusCode: 200,
342 end: sinon.stub(),
343 setHeader: sinon.stub(),
344 hasHeader: sinon.stub(),
345 getHeader: sinon.stub(),
346 getHeaders: sinon.stub(),
347 };
348 ctx = {};
349 pathsByLengthOrig = dingus.pathsByLength;
350 sinon.spy(dingus, 'handlerMethodNotAllowed');
351 sinon.spy(dingus, 'handlerNotFound');
352 sinon.spy(dingus, 'handlerBadRequest');
353 sinon.spy(dingus, 'handlerInternalServerError');
354 stubHandler = sinon.stub();
355 });
356 afterEach(function () {
357 dingus.pathsByLength = pathsByLengthOrig;
358 });
359
360 it('calls handler', async function () {
361 const urlPath = '/:id';
362 const method = 'GET';
363 dingus.on(method, urlPath, stubHandler);
364 req.url = '/abc';
365 req.method = method;
366
367 await dingus.dispatch(req, res, ctx);
368 assert(stubHandler.called);
369 assert(!dingus.handlerMethodNotAllowed.called);
370 assert(!dingus.handlerNotFound.called);
371 });
372 it('calls handler without context', async function () {
373 const urlPath = '/:id';
374 const method = 'GET';
375 dingus.on(method, urlPath, stubHandler);
376 req.url = '/abc';
377 req.method = method;
378
379 await dingus.dispatch(req, res);
380 assert(stubHandler.called);
381 assert(!dingus.handlerMethodNotAllowed.called);
382 assert(!dingus.handlerNotFound.called);
383 });
384 it('calls fallback handler', async function () {
385 const urlPath = '/abc/:id';
386 const method = 'GET';
387 dingus.on('*', urlPath, stubHandler);
388 req.url = '/abc/def';
389 req.method = method;
390
391 await dingus.dispatch(req, res, ctx);
392 assert(stubHandler.called);
393 assert(!dingus.handlerMethodNotAllowed.called);
394 assert(!dingus.handlerNotFound.called);
395 });
396 it('handles error in handler', async function () {
397 const urlPath = '/:id';
398 const method = 'GET';
399 dingus.on(method, urlPath, stubHandler);
400 req.url = '/abc';
401 req.method = method;
402 stubHandler.rejects(new Error('blah'));
403
404 await dingus.dispatch(req, res, ctx);
405 assert(stubHandler.called);
406 assert(!dingus.handlerMethodNotAllowed.called);
407 assert(!dingus.handlerNotFound.called);
408 });
409 it('calls unsupported method', async function () {
410 const urlPath = '/:id';
411 const method = 'POST';
412 dingus.on('GET', urlPath, stubHandler);
413 req.url = '/abc';
414 req.method = method;
415
416 await dingus.dispatch(req, res, ctx);
417 assert(!stubHandler.called);
418 assert(dingus.handlerMethodNotAllowed.called);
419 assert(!dingus.handlerNotFound.called);
420 });
421 it('does not lookup nonexistent path', async function () {
422 req.url = '/foo/bar';
423 req.method = 'GET';
424
425 await dingus.dispatch(req, res, ctx);
426 assert(!stubHandler.called);
427 assert(!dingus.handlerMethodNotAllowed.called);
428 assert(dingus.handlerNotFound.called);
429 });
430 it('covers unhandled dingus exception', async function () {
431 const expectedException = new DingusError('blah');
432 sinon.stub(dingus.router, 'lookup').throws(expectedException);
433
434 await dingus.dispatch(req, res, ctx);
435 assert(!stubHandler.called);
436 assert(dingus.handlerInternalServerError.called);
437 });
438 it('covers other exception', async function () {
439 const expectedException = new Error('blah');
440 sinon.stub(dingus.router, 'lookup').throws(expectedException);
441
442 await dingus.dispatch(req, res, ctx);
443 assert(!stubHandler.called);
444 assert(dingus.handlerInternalServerError.called);
445 });
446 it('covers bad uri', async function () {
447 req.url = '/%f';
448
449 await dingus.dispatch(req, res, ctx);
450 assert(dingus.handlerBadRequest.called);
451 });
452 it('calls handler with additional arguments', async function () {
453 dingus.on('GET', '/', stubHandler, 'foo', 'bar');
454 await dingus.dispatch(req, res, ctx);
455 assert(stubHandler.called);
456 assert.strictEqual(stubHandler.args[0][3], 'foo');
457 assert.strictEqual(stubHandler.args[0][4], 'bar');
458 });
459 }); // dispatch
460
461 describe('parseBody', function () {
462 let ctx;
463 beforeEach(function () {
464 ctx = {};
465 });
466 it('does not parse unknown type', function () {
467 try {
468 dingus.parseBody('unknown/type', ctx);
469 assert.fail(noExpectedException);
470 } catch (e) {
471 assert.strictEqual(e.statusCode, 415);
472 }
473 });
474 it('parses json', function () {
475 const src = { foo: 'bar' };
476 ctx.rawBody = JSON.stringify(src);
477 dingus.parseBody(Enum.ContentType.ApplicationJson, ctx);
478 assert.deepStrictEqual(ctx.parsedBody, src);
479 });
480 it('handles unparsable json', function () {
481 ctx.rawBody = 'not json';
482 try {
483 dingus.parseBody(Enum.ContentType.ApplicationJson, ctx);
484 assert.fail(noExpectedException);
485 } catch (e) {
486 assert.strictEqual(e.statusCode, 400);
487 }
488 });
489 it('parses form', function () {
490 const expected = Object.assign(Object.create(null), {
491 foo: 'bar',
492 });
493 ctx.rawBody = 'foo=bar';
494 dingus.parseBody('application/x-www-form-urlencoded', ctx);
495 assert.deepStrictEqual(ctx.parsedBody, expected);
496 });
497
498 }); // parseBody
499
500 describe('bodyData', function () {
501 let res, resEvents;
502 beforeEach(function () {
503 resEvents = {};
504 res = {
505 // eslint-disable-next-line security/detect-object-injection
506 on: (ev, fn) => resEvents[ev] = fn,
507 };
508 });
509 it('provides data', async function () {
510 const p = dingus.bodyData(res);
511 resEvents['data'](Buffer.from('foo'));
512 resEvents['data'](Buffer.from('bar'));
513 resEvents['end']();
514 const result = await p;
515 assert.strictEqual(result, 'foobar');
516 });
517 it('handles error', async function () {
518 const p = dingus.bodyData(res);
519 resEvents['error']('foo');
520 try {
521 await p;
522 assert.fail(noExpectedException);
523 } catch (e) {
524 assert.strictEqual(e, 'foo');
525 }
526 });
527 it('limits size', async function () {
528 const p = dingus.bodyData(res, 8);
529 resEvents['data'](Buffer.from('foobar'));
530 resEvents['data'](Buffer.from('bazquux'));
531 try {
532 await p;
533 assert.fail(noExpectedException);
534 } catch (e) {
535 assert.strictEqual(e.statusCode, 413);
536 }
537 });
538 it('provides buffer', async function () {
539 const p = dingus.bodyData(res, 0, false);
540 const expected = Buffer.from('bleat');
541 resEvents['data'](expected);
542 resEvents['end']();
543 const result = await p;
544 assert.deepStrictEqual(result, expected);
545 });
546 }); // bodyData
547
548 describe('ingestBody', function () {
549 it('ingests json', async function () {
550 const req = {};
551 const res = {};
552 const ctx = {};
553 sinon.stub(dingus, 'bodyData').resolves('{"foo":"bar"}')
554 sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
555 await dingus.ingestBody(req, res, ctx);
556 assert.deepStrictEqual(ctx.parsedBody, { foo: 'bar' });
557 assert.deepStrictEqual(ctx.rawBody, undefined);
558 });
559 it('persists rawBody', async function () {
560 const req = {};
561 const res = {};
562 const ctx = {};
563 const body = '{"foo":"bar"}';
564 sinon.stub(dingus, 'bodyData').resolves(body);
565 sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
566 await dingus.ingestBody(req, res, ctx, { persistRawBody: true });
567 assert.deepStrictEqual(ctx.parsedBody, { foo: 'bar' });
568 assert.deepStrictEqual(ctx.rawBody, body);
569 });
570 it('skips parsing empty body', async function () {
571 const req = {};
572 const res = {};
573 const ctx = {};
574 const body = '';
575 sinon.stub(dingus, 'bodyData').resolves(body);
576 sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
577 sinon.spy(dingus, 'parseBody');
578 await dingus.ingestBody(req, res, ctx, { parseEmptyBody: false });
579 assert.deepStrictEqual(ctx.parsedBody, undefined);
580 assert(dingus.parseBody.notCalled);
581 });
582 }); // ingestBody
583
584 describe('setResponseType', function () {
585 let req, res, ctx;
586 let _sa; // Preserve strictAccept
587 before(function () {
588 _sa = dingus.strictAccept;
589 });
590 after(function () {
591 dingus.strictAccept = _sa;
592 });
593 beforeEach(function () {
594 ctx = {};
595 req = {};
596 res = {
597 setHeader: sinon.stub(),
598 };
599 sinon.stub(Dingus, 'getResponseContentType').returns();
600 });
601 it('rejects missing', function () {
602 dingus.strictAccept = true;
603 try {
604 dingus.setResponseType(['my/type'], req, res, ctx);
605 assert.fail(noExpectedException);
606 } catch (e) {
607 assert.strictEqual(e.statusCode, 406, 'did not get expected status code');
608 }
609 });
610 it('accepts missing', function () {
611 dingus.strictAccept = false;
612 dingus.setResponseType(['my/type'], req, res, ctx);
613 assert.strictEqual(ctx.responseType, 'my/type');
614 });
615
616 }); // setResponseType
617
618 describe('_readFileInfo', function () {
619 let stat, data, statRes, dataRes, filename;
620 beforeEach(function () {
621 sinon.stub(fs.promises, 'stat');
622 sinon.stub(fs.promises, 'readFile');
623 statRes = {
624 mtimeMs:1612553697186,
625 };
626 dataRes = 'data';
627 filename = 'dummy.txt';
628 });
629 it('succeeds', async function () {
630 fs.promises.stat.resolves(statRes);
631 fs.promises.readFile.resolves('data');
632 [stat, data] = await dingus._readFileInfo(filename);
633 assert.deepStrictEqual(stat, statRes);
634 assert.deepStrictEqual(data, dataRes);
635 });
636 it('returns null for non-existant file', async function () {
637 const noEnt = {
638 code: 'ENOENT',
639 };
640 fs.promises.stat.rejects(noEnt);
641 fs.promises.readFile.rejects(noEnt);
642 [stat, data] = await dingus._readFileInfo(filename);
643 assert.strictEqual(stat, null);
644 assert.strictEqual(data, null);
645 });
646 it('throws unexpected error', async function () {
647 const expectedException = new Error('blah');
648 fs.promises.stat.rejects(expectedException);
649 await assert.rejects(async () => {
650 await dingus._readFileInfo(filename);
651 }, expectedException);
652 });
653 }); // _readFileInfo
654
655 describe('_serveFileMetaHeaders', function () {
656 let res, directory, fileName;
657 beforeEach(function () {
658 sinon.stub(dingus, '_readFileInfo');
659 res = {
660 setHeader: sinon.stub(),
661 };
662 directory = '/path';
663 fileName = 'filename';
664 });
665 it('covers no meta file', async function() {
666 dingus._readFileInfo.resolves([null, null]);
667 await dingus._serveFileMetaHeaders(res, directory, fileName);
668 assert(!res.setHeader.called);
669 });
670 it('adds extra headers', async function () {
671 dingus._readFileInfo.resolves([{}, Buffer.from(`Link: <https://example.com/>; rel="relation"
672 X-Folded-Header: data
673 data under
674 the fold
675 Content-Type: image/sgi
676 `)]);
677 await dingus._serveFileMetaHeaders(res, directory, fileName);
678 assert(res.setHeader.called);
679 });
680 }); // _serveFileMetaHeaders
681
682 describe('serveFile', function () {
683 const path = require('path');
684 let ctx, req, res, directory, fileName, filestats;
685 beforeEach(function () {
686 directory = path.join(__dirname, '..', 'test-data');
687 fileName = 'example.html';
688 ctx = {};
689 req = {
690 _headers: {
691 [Enum.Header.Accept]: undefined,
692 [Enum.Header.IfModifiedSince]: undefined,
693 [Enum.Header.AcceptEncoding]: undefined,
694 [Enum.Header.IfNoneMatch]: undefined,
695 },
696 getHeader: (header) => {
697 if (header in req._headers) {
698 // eslint-disable-next-line security/detect-object-injection
699 return req._headers[header];
700 }
701 assert.fail(`unexpected getHeader ${header}`);
702 },
703 };
704 res = {
705 end: sinon.stub(),
706 getHeader: sinon.stub(),
707 getHeaders: sinon.stub(),
708 hasHeader: sinon.stub().returns(true),
709 setHeader: sinon.stub(),
710 };
711 filestats = {
712 dev: 39,
713 mode: 33188,
714 nlink: 1,
715 uid: 1002,
716 gid: 1002,
717 rdev: 0,
718 blksize: 512,
719 ino: 897653,
720 size: 8,
721 blocks: 17,
722 atimeMs: 1613253436842.815,
723 mtimeMs: 1603485933192.861,
724 ctimeMs: 1603485933192.861,
725 birthtimeMs: 0,
726 atime: '2021-02-13T21:57:16.843Z',
727 mtime: '2020-10-23T13:45:33.193Z',
728 ctime: '2020-10-23T13:45:33.193Z',
729 birthtime: '1970-01-01T00:00:00.000Z',
730 };
731 sinon.stub(dingus, 'handlerNotFound');
732 sinon.stub(fs.promises, 'stat').resolves(filestats);
733 sinon.spy(fs.promises, 'readFile');
734 });
735 it('serves a file', async function () {
736 await dingus.serveFile(req, res, ctx, directory, fileName);
737 assert(fs.promises.readFile.called);
738 assert(!dingus.handlerNotFound.called);
739 });
740 it('covers no meta headers', async function () {
741 dingus.staticMetadata = false;
742 await dingus.serveFile(req, res, ctx, directory, fileName);
743 assert(fs.promises.readFile.called);
744 assert(!dingus.handlerNotFound.called);
745 });
746 it('does not serve dot-file', async function () {
747 fileName = '.example';
748 await dingus.serveFile(req, res, ctx, directory, fileName);
749 assert(!fs.promises.readFile.called);
750 assert(dingus.handlerNotFound.called);
751 });
752 it('does not serve encoded navigation', async function () {
753 fileName = '/example.html';
754 await dingus.serveFile(req, res, ctx, directory, fileName);
755 assert(!fs.promises.readFile.called);
756 assert(dingus.handlerNotFound.called);
757 });
758 it('does not serve missing file', async function () {
759 fileName = 'no-file.here';
760 await dingus.serveFile(req, res, ctx, directory, fileName);
761 assert(dingus.handlerNotFound.called);
762 });
763 it('requires directory be specified', async function () {
764 await dingus.serveFile(req, res, ctx, '', fileName);
765 assert(!fs.promises.readFile.called);
766 assert(dingus.handlerNotFound.called);
767 });
768 it('covers fs error', async function () {
769 const expectedException = new Error('blah');
770 fs.promises.stat.restore();
771 sinon.stub(fs.promises, 'stat').rejects(expectedException);
772 try {
773 await dingus.serveFile(req, res, ctx, directory, fileName);
774 assert.fail('should have thrown');
775 } catch (e) {
776 assert.strictEqual(e, expectedException);
777 }
778 });
779 it('caches by modified', async function () {
780 req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 23:11:16 GMT';
781 await dingus.serveFile(req, res, ctx, directory, fileName);
782 assert.strictEqual(res.statusCode, 304);
783 });
784 it('does not cache old modified', async function () {
785 req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 01:11:16 GMT';
786 await dingus.serveFile(req, res, ctx, directory, fileName);
787 assert.notStrictEqual(res.statusCode, 304);
788 assert(!dingus.handlerNotFound.called);
789 });
790 it('caches ETag match', async function () {
791 req._headers[Enum.Header.IfNoneMatch] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
792 await dingus.serveFile(req, res, ctx, directory, fileName);
793 assert.strictEqual(res.statusCode, 304);
794 });
795 it('does not cache ETag non-match', async function () {
796 req._headers[Enum.Header.IfNoneMatch] = '"foo", "bar"';
797 await dingus.serveFile(req, res, ctx, directory, fileName);
798 assert.notStrictEqual(res.statusCode, 304);
799 assert(!dingus.handlerNotFound.called);
800 });
801 it('handles no possible encodings', async function () {
802 req._headers[Enum.Header.AcceptEncoding] = '*;q=0';
803 await assert.rejects(async () => {
804 await dingus.serveFile(req, res, ctx, directory, fileName);
805 }, {
806 name: 'ResponseError',
807 });
808 });
809 it('handles a valid encoding', async function () {
810 req._headers[Enum.Header.AcceptEncoding] = 'gzip';
811 await dingus.serveFile(req, res, ctx, directory, fileName);
812 assert(res.end.called);
813 });
814 it('handles a valid encoding among others', async function () {
815 req._headers[Enum.Header.AcceptEncoding] = 'flarp, br, gzip';
816 fs.promises.stat.restore();
817 sinon.stub(fs.promises, 'stat')
818 .onCall(0).resolves(filestats) // identity file
819 .onCall(1).resolves(null) // br encoding
820 .onCall(2).resolves(filestats); // gzip encoding
821 await dingus.serveFile(req, res, ctx, directory, fileName);
822 assert(res.end.called);
823 });
824 it('handles misconfigured encoding', async function () {
825 Enum.EncodingType.Flarp = 'flarp';
826 req._headers[Enum.Header.AcceptEncoding] = 'flarp, gzip';
827 await dingus.serveFile(req, res, ctx, directory, fileName);
828 delete Enum.EncodingType.Flarp;
829 assert(res.end.called);
830 });
831 }); // serveFile
832
833 describe('renderError', function () {
834 let err;
835 beforeEach(function () {
836 err = {
837 statusCode: '200',
838 errorMessage: 'OK',
839 details: 'hunkydorey',
840 };
841 });
842 it('renders unknown type', function () {
843 const contentType = 'unknown/type';
844 const result = dingus.renderError(contentType, err);
845 assert.deepStrictEqual(result, 'OK\r\nhunkydorey');
846 });
847 it('renders text', function () {
848 const contentType = 'text/plain';
849 const result = dingus.renderError(contentType, err);
850 assert.deepStrictEqual(result, 'OK\r\nhunkydorey');
851 });
852 it('renders json', function () {
853 const contentType = Enum.ContentType.ApplicationJson;
854 const result = dingus.renderError(contentType, err);
855 assert.deepStrictEqual(result, JSON.stringify(err));
856 });
857 it('renders html without details', function () {
858 err = {
859 statusCode: '201',
860 errorMessage: 'Created',
861 };
862 const contentType = 'text/html';
863 const result = dingus.renderError(contentType, err);
864 assert.deepStrictEqual(result, `<!DOCTYPE html>
865 <html lang="en">
866 <head>
867 <title>${err.statusCode} ${err.errorMessage}</title>
868 </head>
869 <body>
870 <h1>${err.errorMessage}</h1>
871 </body>
872 </html>`);
873 });
874 it('renders html', function () {
875 const contentType = 'text/html';
876 const result = dingus.renderError(contentType, err);
877 assert.deepStrictEqual(result, `<!DOCTYPE html>
878 <html lang="en">
879 <head>
880 <title>${err.statusCode} ${err.errorMessage}</title>
881 </head>
882 <body>
883 <h1>${err.errorMessage}</h1>
884 <p>${err.details}</p>
885 </body>
886 </html>`);
887 });
888 it('renders html, multiple details', function () {
889 const contentType = 'text/html';
890 err.details = ['one detail', 'two detail'];
891 const result = dingus.renderError(contentType, err);
892 assert.deepStrictEqual(result, `<!DOCTYPE html>
893 <html lang="en">
894 <head>
895 <title>${err.statusCode} ${err.errorMessage}</title>
896 </head>
897 <body>
898 <h1>${err.errorMessage}</h1>
899 <p>one detail</p>
900 <p>two detail</p>
901 </body>
902 </html>`);
903 });
904 }); // renderError
905
906 describe('sendErrorResponse', function () {
907 let ctx, req, res;
908 beforeEach(function () {
909 ctx = {};
910 req = {};
911 res = {
912 end: sinon.stub(),
913 getHeader: sinon.stub(),
914 getHeaders: sinon.stub(),
915 hasHeader: sinon.stub().returns(true),
916 setHeader: sinon.stub(),
917 };
918 sinon.stub(dingus, 'renderError');
919 });
920 it('covers', function () {
921 const err = {
922 statusCode: 444,
923 };
924 dingus.sendErrorResponse(err, req, res, ctx);
925 assert(res.end.called);
926 });
927 }); // sendErrorResponse
928
929 describe('proxyPrefix', function () {
930 let req, res, ctx, stubHandler, pfxDingus;
931 const pfx = '/pfx';
932
933 beforeEach(function () {
934 pfxDingus = new Dingus(console, { proxyPrefix: pfx });
935 req = {
936 setHeader: sinon.stub(),
937 getHeader: sinon.stub(),
938 };
939 res = {
940 statusCode: 200,
941 end: sinon.stub(),
942 setHeader: sinon.stub(),
943 getHeader: sinon.stub(),
944 };
945 ctx = {};
946 sinon.stub(pfxDingus, 'handlerMethodNotAllowed');
947 sinon.stub(pfxDingus, 'handlerNotFound');
948 stubHandler = sinon.stub();
949 });
950 afterEach(function () {
951 sinon.restore();
952 });
953
954 it('handles prefixed route', async function () {
955 const urlPath = '/:id';
956 const method = 'GET';
957 pfxDingus.on(method, urlPath, stubHandler);
958 req.url = pfx + '/abc';
959 req.method = method;
960
961 await pfxDingus.dispatch(req, res, ctx);
962 assert(stubHandler.called);
963 assert(!pfxDingus.handlerMethodNotAllowed.called);
964 assert(!pfxDingus.handlerNotFound.called);
965 });
966 it('does not handle prefixed route', async function () {
967 const urlPath = '/:id';
968 const method = 'GET';
969 pfxDingus.on(method, urlPath, stubHandler);
970 req.url = '/wrongpfx/abc';
971 req.method = method;
972
973 await pfxDingus.dispatch(req, res, ctx);
974 assert(!stubHandler.called);
975 assert(!pfxDingus.handlerMethodNotAllowed.called);
976 assert(pfxDingus.handlerNotFound.called);
977 });
978 }); // proxyPrefix
979
980 describe('handlerRedirect', function () {
981 let req, res, ctx;
982 beforeEach(function () {
983 req = {
984 getHeader: sinon.stub(),
985 };
986 res = {
987 setHeader: sinon.stub(),
988 end: sinon.stub(),
989 };
990 ctx = {};
991 });
992 it('covers', async function () {
993 await dingus.handlerRedirect(req, res, ctx);
994 assert(res.setHeader.called);
995 assert(res.end.called);
996 });
997 it('covers non-defaults', async function () {
998 await dingus.handlerRedirect(req, res, ctx, 308);
999 assert(res.setHeader.called);
1000 assert(res.end.called);
1001 });
1002 }); // handlerRedirect
1003
1004 describe('handlerGetStaticFile', function () {
1005 let req, res, ctx;
1006 beforeEach(function () {
1007 req = {
1008 getHeader: sinon.stub(),
1009 };
1010 res = {
1011 setHeader: sinon.stub(),
1012 };
1013 ctx = {
1014 params: {
1015 file: '',
1016 },
1017 };
1018 sinon.stub(dingus, 'serveFile');
1019 });
1020 it('covers', async function () {
1021 await dingus.handlerGetStaticFile(req, res, ctx);
1022 assert(dingus.serveFile.called);
1023 });
1024 it('covers specified file', async function () {
1025 await dingus.handlerGetStaticFile(req, res, ctx, 'file.txt');
1026 assert(dingus.serveFile.called);
1027 });
1028 }); // handlerGetStaticFile
1029 });