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