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