update changelog
[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 });
29 }); // constructor
30
31 describe('_normalizePath', function () {
32 it('returns normal path', function () {
33 const p = '/a/b/c';
34 const r = dingus._normalizePath(p);
35 assert.strictEqual(r, p);
36 });
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);
42 });
43 }); // _normalizePath
44
45 describe('_splitUrl', function () {
46 const nullObject = Object.create(null);
47
48 it('splits a simple path', function () {
49 const p = '/a/b/c';
50 const expected = {
51 pathPart: p,
52 queryParams: nullObject,
53 };
54 const r = dingus._splitUrl(p);
55 assert.deepStrictEqual(r, expected);
56 });
57 it('splits a path with trailing slash preserved', function () {
58 const p = '/a/b/c/';
59 const expected = {
60 pathPart: p,
61 queryParams: nullObject,
62 };
63 const r = dingus._splitUrl(p);
64 assert.deepStrictEqual(r, expected);
65 });
66 it('splits a path with trailing slash ignored', function () {
67 const p = '/a/b/c/';
68 const expected = {
69 pathPart: p,
70 queryParams: nullObject,
71 };
72 dingus.ignoreTrailingSlash = true;
73 const r = dingus._splitUrl(p);
74 assert.deepStrictEqual(r, expected);
75 });
76 it('splits a path with empty query string', function () {
77 const p = '/a/b/c?';
78 const expected = {
79 pathPart: '/a/b/c',
80 queryParams: nullObject,
81 };
82 const r = dingus._splitUrl(p);
83 assert.deepStrictEqual(r, expected);
84 });
85 it('splits a path with query string', function () {
86 const p = '/a/b/c?x=1&y=2&z';
87 const expected = {
88 pathPart: '/a/b/c',
89 queryParams: Object.assign(Object.create(null), {
90 x: '1',
91 y: '2',
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.
93 }),
94 };
95 const r = dingus._splitUrl(p);
96 assert.deepStrictEqual(r, expected);
97 });
98 }); // _splitUrl
99
100 describe('tagContext', function () {
101 let req, res, ctx;
102 beforeEach(function () {
103 req = {
104 getHeader: sinon.stub(),
105 setHeader: sinon.stub(),
106 };
107 res = {
108 getHeader: sinon.stub(),
109 setHeader: sinon.stub(),
110 };
111 ctx = {};
112 });
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);
117 });
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);
126 });
127 }); // tagContext
128
129 describe('clientAddressContext', function () {
130 let req, res, ctx;
131 let _tp;
132 before(function () {
133 _tp = dingus.trustProxy;
134 });
135 after(function () {
136 dingus.trustProxy = _tp;
137 });
138 beforeEach(function () {
139 req = {
140 getHeader: sinon.stub(),
141 setHeader: sinon.stub(),
142 connection: {},
143 };
144 res = {
145 getHeader: sinon.stub(),
146 setHeader: sinon.stub(),
147 };
148 ctx = {};
149 });
150 it ('covers untrusted proxy', function () {
151 dingus.trustProxy = false;
152 const expected = {
153 clientAddress: '',
154 clientProtocol: 'http',
155 }
156 dingus.clientAddressContext(req, res, ctx);
157 assert.deepStrictEqual(ctx, expected);
158 assert(!req.getHeader.called);
159 });
160 it ('covers missing', function () {
161 dingus.trustProxy = true;
162 const expected = {
163 clientAddress: '::1',
164 clientProtocol: 'https',
165 }
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);
171 });
172 }); // clientAddressContext
173
174 describe('getRequestContentType', function () {
175 let req;
176 beforeEach(function () {
177 req = {
178 getHeader: sinon.stub(),
179 setHeader: sinon.stub(),
180 };
181 });
182 it('handles missing header', function () {
183 const result = Dingus.getRequestContentType(req);
184 assert.strictEqual(result, '');
185 });
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);
190 });
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);
195 });
196 }); // getRequestContentType
197
198 describe('setResponseContentType', function () {
199 let req, responseTypes;
200 beforeEach(function () {
201 responseTypes = [];
202 req = {
203 setHeader: sinon.stub(),
204 getHeader: sinon.stub(),
205 };
206 });
207 it('handles missing header', function () {
208 const result = Dingus.getResponseContentType(responseTypes, req);
209 assert.strictEqual(result, undefined);
210 });
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);
216 });
217 }); // setResponseContentType
218
219 describe('on', function () {
220 let stubOn;
221 beforeEach(function () {
222 stubOn = sinon.stub(dingus.router, 'on');
223 });
224 it('covers', function () {
225 dingus.on('GET', '/', () => {});
226 assert(stubOn.called);
227 });
228 }); // on
229
230 describe('setEndBodyHandler', function () {
231 let req, res, ctx, handler, origEnd, origWrite;
232 beforeEach(function () {
233 origEnd = sinon.stub();
234 origWrite = sinon.stub();
235 req = {};
236 res = {
237 write: origWrite,
238 end: origEnd,
239 };
240 ctx = {};
241 handler = sinon.stub();
242 });
243 it('collects body and handles', function () {
244 Dingus.setEndBodyHandler(req, res, ctx, handler);
245 res.write(Buffer.from('foo'));
246 res.write('baz');
247 res.write();
248 res.end('quux');
249 assert(origWrite.called);
250 assert(origEnd.called);
251 assert.deepStrictEqual(ctx.responseBody, Buffer.from('foobazquux'));
252 assert(handler.called);
253 });
254 }); // setEndBodyHandler
255
256 describe('setHeadHandler', function () {
257 let req, res, ctx, origEnd, origWrite;
258 beforeEach(function () {
259 origEnd = sinon.stub();
260 origWrite = sinon.stub();
261 req = {
262 method: 'HEAD',
263 };
264 res = {
265 end: origEnd,
266 write: origWrite,
267 setHeader: sinon.stub(),
268 };
269 ctx = {};
270 });
271 it('collects response without writing', function () {
272 Dingus.setHeadHandler(req, res, ctx);
273 res.write(Buffer.from('foo'));
274 res.write('baz');
275 res.write();
276 res.end('quux');
277 assert(!origWrite.called);
278 assert(origEnd.called);
279 assert.deepStrictEqual(ctx.responseBody, undefined);
280 });
281 it('collects response without writing, persists written data', function () {
282 Dingus.setHeadHandler(req, res, ctx, true);
283 res.write(Buffer.from('foo'));
284 res.write('baz');
285 res.write();
286 res.end('quux');
287 assert(!origWrite.called);
288 assert(origEnd.called);
289 assert.deepStrictEqual(ctx.responseBody, Buffer.from('foobazquux'));
290 });
291 it('ignores non-head method', function () {
292 req.method = 'GET';
293 Dingus.setHeadHandler(req, res, ctx);
294 res.write(Buffer.from('foo'));
295 res.end('bar');
296 assert(origWrite.called);
297 assert(origEnd.called);
298 });
299 }); // setHeadHandler
300
301 describe('addEncodingHeader', function () {
302 let res, encoding;
303 beforeEach(function () {
304 res = {
305 _headers: {},
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,
310 };
311 });
312 it('adds', function () {
313 encoding = 'gzip';
314 Dingus.addEncodingHeader(res, encoding);
315 assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'gzip');
316 });
317 it('extends', function () {
318 encoding = 'utf8';
319 Dingus.addEncodingHeader(res, encoding);
320 assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'utf8');
321 encoding = 'gzip';
322 Dingus.addEncodingHeader(res, encoding);
323 assert.strictEqual(res._headers[Enum.Header.ContentEncoding], 'gzip, utf8');
324 });
325 }); // addEncodingHeader
326
327 describe('dispatch', function () {
328 let pathsByLengthOrig;
329 let req, res, ctx;
330 let stubHandler;
331
332 beforeEach(function () {
333 req = {
334 url: '/',
335 method: 'GET',
336 setHeader: sinon.stub(),
337 getHeader: sinon.stub(),
338 };
339 res = {
340 statusCode: 200,
341 end: sinon.stub(),
342 setHeader: sinon.stub(),
343 hasHeader: sinon.stub(),
344 getHeader: sinon.stub(),
345 getHeaders: sinon.stub(),
346 };
347 ctx = {};
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();
354 });
355 afterEach(function () {
356 dingus.pathsByLength = pathsByLengthOrig;
357 });
358
359 it('calls handler', async function () {
360 const urlPath = '/:id';
361 const method = 'GET';
362 dingus.on(method, urlPath, stubHandler);
363 req.url = '/abc';
364 req.method = method;
365
366 await dingus.dispatch(req, res, ctx);
367 assert(stubHandler.called);
368 assert(!dingus.handlerMethodNotAllowed.called);
369 assert(!dingus.handlerNotFound.called);
370 });
371 it('calls handler without context', async function () {
372 const urlPath = '/:id';
373 const method = 'GET';
374 dingus.on(method, urlPath, stubHandler);
375 req.url = '/abc';
376 req.method = method;
377
378 await dingus.dispatch(req, res);
379 assert(stubHandler.called);
380 assert(!dingus.handlerMethodNotAllowed.called);
381 assert(!dingus.handlerNotFound.called);
382 });
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';
388 req.method = method;
389
390 await dingus.dispatch(req, res, ctx);
391 assert(stubHandler.called);
392 assert(!dingus.handlerMethodNotAllowed.called);
393 assert(!dingus.handlerNotFound.called);
394 });
395 it('handles error in handler', async function () {
396 const urlPath = '/:id';
397 const method = 'GET';
398 dingus.on(method, urlPath, stubHandler);
399 req.url = '/abc';
400 req.method = method;
401 stubHandler.rejects(new Error('blah'));
402
403 await dingus.dispatch(req, res, ctx);
404 assert(stubHandler.called);
405 assert(!dingus.handlerMethodNotAllowed.called);
406 assert(!dingus.handlerNotFound.called);
407 });
408 it('calls unsupported method', async function () {
409 const urlPath = '/:id';
410 const method = 'POST';
411 dingus.on('GET', urlPath, stubHandler);
412 req.url = '/abc';
413 req.method = method;
414
415 await dingus.dispatch(req, res, ctx);
416 assert(!stubHandler.called);
417 assert(dingus.handlerMethodNotAllowed.called);
418 assert(!dingus.handlerNotFound.called);
419 });
420 it('does not lookup nonexistent path', async function () {
421 req.url = '/foo/bar';
422 req.method = 'GET';
423
424 await dingus.dispatch(req, res, ctx);
425 assert(!stubHandler.called);
426 assert(!dingus.handlerMethodNotAllowed.called);
427 assert(dingus.handlerNotFound.called);
428 });
429 it('covers unhandled dingus exception', async function () {
430 const expectedException = new DingusError('blah');
431 sinon.stub(dingus.router, 'lookup').throws(expectedException);
432
433 await dingus.dispatch(req, res, ctx);
434 assert(!stubHandler.called);
435 assert(dingus.handlerInternalServerError.called);
436 });
437 it('covers other exception', async function () {
438 const expectedException = new Error('blah');
439 sinon.stub(dingus.router, 'lookup').throws(expectedException);
440
441 await dingus.dispatch(req, res, ctx);
442 assert(!stubHandler.called);
443 assert(dingus.handlerInternalServerError.called);
444 });
445 it('covers bad uri', async function () {
446 req.url = '/%f';
447
448 await dingus.dispatch(req, res, ctx);
449 assert(dingus.handlerBadRequest.called);
450 });
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');
457 });
458 }); // dispatch
459
460 describe('parseBody', function () {
461 let ctx;
462 beforeEach(function () {
463 ctx = {};
464 });
465 it('does not parse unknown type', function () {
466 try {
467 dingus.parseBody('unknown/type', ctx);
468 assert.fail(noExpectedException);
469 } catch (e) {
470 assert.strictEqual(e.statusCode, 415);
471 }
472 });
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);
478 });
479 it('handles unparsable json', function () {
480 ctx.rawBody = 'not json';
481 try {
482 dingus.parseBody(Enum.ContentType.ApplicationJson, ctx);
483 assert.fail(noExpectedException);
484 } catch (e) {
485 assert.strictEqual(e.statusCode, 400);
486 }
487 });
488 it('parses form', function () {
489 const expected = Object.assign(Object.create(null), {
490 foo: 'bar',
491 });
492 ctx.rawBody = 'foo=bar';
493 dingus.parseBody('application/x-www-form-urlencoded', ctx);
494 assert.deepStrictEqual(ctx.parsedBody, expected);
495 });
496
497 }); // parseBody
498
499 describe('bodyData', function () {
500 let res, resEvents;
501 beforeEach(function () {
502 resEvents = {};
503 res = {
504 // eslint-disable-next-line security/detect-object-injection
505 on: (ev, fn) => resEvents[ev] = fn,
506 };
507 });
508 it('provides data', async function () {
509 const p = dingus.bodyData(res);
510 resEvents['data'](Buffer.from('foo'));
511 resEvents['data'](Buffer.from('bar'));
512 resEvents['end']();
513 const result = await p;
514 assert.strictEqual(result, 'foobar');
515 });
516 it('handles error', async function () {
517 const p = dingus.bodyData(res);
518 resEvents['error']('foo');
519 try {
520 await p;
521 assert.fail(noExpectedException);
522 } catch (e) {
523 assert.strictEqual(e, 'foo');
524 }
525 });
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'));
530 try {
531 await p;
532 assert.fail(noExpectedException);
533 } catch (e) {
534 assert.strictEqual(e.statusCode, 413);
535 }
536 });
537 it('provides buffer', async function () {
538 const p = dingus.bodyData(res, 0, false);
539 const expected = Buffer.from('bleat');
540 resEvents['data'](expected);
541 resEvents['end']();
542 const result = await p;
543 assert.deepStrictEqual(result, expected);
544 });
545 }); // bodyData
546
547 describe('ingestBody', function () {
548 it('ingests json', async function () {
549 const req = {};
550 const res = {};
551 const ctx = {};
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);
557 });
558 it('persists rawBody', async function () {
559 const req = {};
560 const res = {};
561 const ctx = {};
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);
568 });
569 it('skips parsing empty body', async function () {
570 const req = {};
571 const res = {};
572 const ctx = {};
573 const body = '';
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);
580 });
581 }); // ingestBody
582
583 describe('setResponseType', function () {
584 let req, res, ctx;
585 let _sa; // Preserve strictAccept
586 before(function () {
587 _sa = dingus.strictAccept;
588 });
589 after(function () {
590 dingus.strictAccept = _sa;
591 });
592 beforeEach(function () {
593 ctx = {};
594 req = {};
595 res = {
596 setHeader: sinon.stub(),
597 };
598 sinon.stub(Dingus, 'getResponseContentType').returns();
599 });
600 it('rejects missing', function () {
601 dingus.strictAccept = true;
602 try {
603 dingus.setResponseType(['my/type'], req, res, ctx);
604 assert.fail(noExpectedException);
605 } catch (e) {
606 assert.strictEqual(e.statusCode, 406, 'did not get expected status code');
607 }
608 });
609 it('accepts missing', function () {
610 dingus.strictAccept = false;
611 dingus.setResponseType(['my/type'], req, res, ctx);
612 assert.strictEqual(ctx.responseType, 'my/type');
613 });
614
615 }); // setResponseType
616
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');
622 statRes = {
623 mtimeMs:1612553697186,
624 };
625 dataRes = 'data';
626 filename = 'dummy.txt';
627 });
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);
634 });
635 it('returns null for non-existant file', async function () {
636 const noEnt = {
637 code: 'ENOENT',
638 };
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);
644 });
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);
651 });
652 }); // _readFileInfo
653
654 describe('_serveFileMetaHeaders', function () {
655 let res, directory, fileName;
656 beforeEach(function () {
657 sinon.stub(dingus, '_readFileInfo');
658 res = {
659 setHeader: sinon.stub(),
660 };
661 directory = '/path';
662 fileName = 'filename';
663 });
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);
668 });
669 it('adds extra headers', async function () {
670 dingus._readFileInfo.resolves([{}, Buffer.from(`Link: <https://example.com/>; rel="relation"
671 X-Folded-Header: data
672 data under
673 the fold
674 Content-Type: image/sgi
675 `)]);
676 await dingus._serveFileMetaHeaders(res, directory, fileName);
677 assert(res.setHeader.called);
678 });
679 }); // _serveFileMetaHeaders
680
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';
687 ctx = {};
688 req = {
689 _headers: {
690 [Enum.Header.Accept]: undefined,
691 [Enum.Header.IfModifiedSince]: undefined,
692 [Enum.Header.AcceptEncoding]: undefined,
693 [Enum.Header.IfNoneMatch]: undefined,
694 },
695 getHeader: (header) => {
696 if (header in req._headers) {
697 // eslint-disable-next-line security/detect-object-injection
698 return req._headers[header];
699 }
700 assert.fail(`unexpected getHeader ${header}`);
701 },
702 };
703 res = {
704 end: sinon.stub(),
705 getHeader: sinon.stub(),
706 getHeaders: sinon.stub(),
707 hasHeader: sinon.stub().returns(true),
708 setHeader: sinon.stub(),
709 };
710 filestats = {
711 dev: 39,
712 mode: 33188,
713 nlink: 1,
714 uid: 1002,
715 gid: 1002,
716 rdev: 0,
717 blksize: 512,
718 ino: 897653,
719 size: 8,
720 blocks: 17,
721 atimeMs: 1613253436842.815,
722 mtimeMs: 1603485933192.861,
723 ctimeMs: 1603485933192.861,
724 birthtimeMs: 0,
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',
729 };
730 sinon.stub(dingus, 'handlerNotFound');
731 sinon.stub(fs.promises, 'stat').resolves(filestats);
732 sinon.spy(fs.promises, 'readFile');
733 });
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);
738 });
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);
744 });
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);
750 });
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);
756 });
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);
761 });
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);
766 });
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);
771 try {
772 await dingus.serveFile(req, res, ctx, directory, fileName);
773 assert.fail('should have thrown');
774 } catch (e) {
775 assert.strictEqual(e, expectedException);
776 }
777 });
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);
782 });
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);
788 });
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);
793 });
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);
799 });
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);
804 }, {
805 name: 'ResponseError',
806 });
807 });
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);
812 });
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);
822 });
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);
829 });
830 }); // serveFile
831
832 describe('renderError', function () {
833 let err;
834 beforeEach(function () {
835 err = {
836 statusCode: '200',
837 errorMessage: 'OK',
838 details: 'hunkydorey',
839 };
840 });
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');
845 });
846 it('renders text', function () {
847 const contentType = 'text/plain';
848 const result = dingus.renderError(contentType, err);
849 assert.deepStrictEqual(result, 'OK\r\nhunkydorey');
850 });
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));
855 });
856 it('renders html without details', function () {
857 err = {
858 statusCode: '201',
859 errorMessage: 'Created',
860 };
861 const contentType = 'text/html';
862 const result = dingus.renderError(contentType, err);
863 assert.deepStrictEqual(result, `<!DOCTYPE html>
864 <html lang="en">
865 <head>
866 <title>${err.statusCode} ${err.errorMessage}</title>
867 </head>
868 <body>
869 <h1>${err.errorMessage}</h1>
870 </body>
871 </html>`);
872 });
873 it('renders html', function () {
874 const contentType = 'text/html';
875 const result = dingus.renderError(contentType, err);
876 assert.deepStrictEqual(result, `<!DOCTYPE html>
877 <html lang="en">
878 <head>
879 <title>${err.statusCode} ${err.errorMessage}</title>
880 </head>
881 <body>
882 <h1>${err.errorMessage}</h1>
883 <p>${err.details}</p>
884 </body>
885 </html>`);
886 });
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>
892 <html lang="en">
893 <head>
894 <title>${err.statusCode} ${err.errorMessage}</title>
895 </head>
896 <body>
897 <h1>${err.errorMessage}</h1>
898 <p>one detail</p>
899 <p>two detail</p>
900 </body>
901 </html>`);
902 });
903 }); // renderError
904
905 describe('sendErrorResponse', function () {
906 let ctx, req, res;
907 beforeEach(function () {
908 ctx = {};
909 req = {};
910 res = {
911 end: sinon.stub(),
912 getHeader: sinon.stub(),
913 getHeaders: sinon.stub(),
914 hasHeader: sinon.stub().returns(true),
915 setHeader: sinon.stub(),
916 };
917 sinon.stub(dingus, 'renderError');
918 });
919 it('covers', function () {
920 const err = {
921 statusCode: 444,
922 };
923 dingus.sendErrorResponse(err, req, res, ctx);
924 assert(res.end.called);
925 });
926 }); // sendErrorResponse
927
928 describe('proxyPrefix', function () {
929 let req, res, ctx, stubHandler, pfxDingus;
930 const pfx = '/pfx';
931
932 beforeEach(function () {
933 pfxDingus = new Dingus(console, { proxyPrefix: pfx });
934 req = {
935 setHeader: sinon.stub(),
936 getHeader: sinon.stub(),
937 };
938 res = {
939 statusCode: 200,
940 end: sinon.stub(),
941 setHeader: sinon.stub(),
942 getHeader: sinon.stub(),
943 };
944 ctx = {};
945 sinon.stub(pfxDingus, 'handlerMethodNotAllowed');
946 sinon.stub(pfxDingus, 'handlerNotFound');
947 stubHandler = sinon.stub();
948 });
949 afterEach(function () {
950 sinon.restore();
951 });
952
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';
958 req.method = method;
959
960 await pfxDingus.dispatch(req, res, ctx);
961 assert(stubHandler.called);
962 assert(!pfxDingus.handlerMethodNotAllowed.called);
963 assert(!pfxDingus.handlerNotFound.called);
964 });
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';
970 req.method = method;
971
972 await pfxDingus.dispatch(req, res, ctx);
973 assert(!stubHandler.called);
974 assert(!pfxDingus.handlerMethodNotAllowed.called);
975 assert(pfxDingus.handlerNotFound.called);
976 });
977 }); // proxyPrefix
978
979 describe('handlerRedirect', function () {
980 let req, res, ctx;
981 beforeEach(function () {
982 req = {
983 getHeader: sinon.stub(),
984 };
985 res = {
986 setHeader: sinon.stub(),
987 end: sinon.stub(),
988 };
989 ctx = {};
990 });
991 it('covers', async function () {
992 await dingus.handlerRedirect(req, res, ctx);
993 assert(res.setHeader.called);
994 assert(res.end.called);
995 });
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);
1000 });
1001 }); // handlerRedirect
1002
1003 describe('handlerGetStaticFile', function () {
1004 let req, res, ctx;
1005 beforeEach(function () {
1006 req = {
1007 getHeader: sinon.stub(),
1008 };
1009 res = {
1010 setHeader: sinon.stub(),
1011 };
1012 ctx = {
1013 params: {
1014 file: '',
1015 },
1016 };
1017 sinon.stub(dingus, 'serveFile');
1018 });
1019 it('covers', async function () {
1020 await dingus.handlerGetStaticFile(req, res, ctx);
1021 assert(dingus.serveFile.called);
1022 });
1023 it('covers specified file', async function () {
1024 await dingus.handlerGetStaticFile(req, res, ctx, 'file.txt');
1025 assert(dingus.serveFile.called);
1026 });
1027 }); // handlerGetStaticFile
1028 });