log a little less about static meta headers
[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 const result = await dingus._serveFileMetaHeaders(res, directory, fileName);
711 assert(!res.setHeader.called);
712 assert.strictEqual(result, false);
713 });
714 it('adds extra headers', async function () {
715 dingus._readFileInfo.resolves([{}, Buffer.from(`Link: <https://example.com/>; rel="relation"
716 X-Folded-Header: data
717 data under
718 the fold
719 Content-Type: image/sgi
720 `)]);
721 const result = await dingus._serveFileMetaHeaders(res, directory, fileName);
722 assert(res.setHeader.called);
723 assert.strictEqual(result, true);
724 });
725 }); // _serveFileMetaHeaders
726
727 describe('serveFile', function () {
728 const path = require('path');
729 let ctx, req, res, directory, fileName, filestats;
730 beforeEach(function () {
731 directory = path.join(__dirname, '..', 'test-data');
732 fileName = 'example.html';
733 ctx = {};
734 req = {
735 _headers: {
736 [Enum.Header.Accept]: undefined,
737 [Enum.Header.IfModifiedSince]: undefined,
738 [Enum.Header.AcceptEncoding]: undefined,
739 [Enum.Header.IfNoneMatch]: undefined,
740 },
741 getHeader: (header) => {
742 if (header in req._headers) {
743 // eslint-disable-next-line security/detect-object-injection
744 return req._headers[header];
745 }
746 assert.fail(`unexpected getHeader ${header}`);
747 },
748 };
749 res = {
750 end: sinon.stub(),
751 getHeader: sinon.stub(),
752 getHeaders: sinon.stub(),
753 hasHeader: sinon.stub().returns(true),
754 setHeader: sinon.stub(),
755 };
756 filestats = {
757 dev: 39,
758 mode: 33188,
759 nlink: 1,
760 uid: 1002,
761 gid: 1002,
762 rdev: 0,
763 blksize: 512,
764 ino: 897653,
765 size: 8,
766 blocks: 17,
767 atimeMs: 1613253436842.815,
768 mtimeMs: 1603485933192.861,
769 ctimeMs: 1603485933192.861,
770 birthtimeMs: 0,
771 atime: '2021-02-13T21:57:16.843Z',
772 mtime: '2020-10-23T13:45:33.193Z',
773 ctime: '2020-10-23T13:45:33.193Z',
774 birthtime: '1970-01-01T00:00:00.000Z',
775 };
776 sinon.stub(dingus, 'handlerNotFound');
777 sinon.stub(fs.promises, 'stat').resolves(filestats);
778 sinon.spy(fs.promises, 'readFile');
779 });
780 it('serves a file', async function () {
781 await dingus.serveFile(req, res, ctx, directory, fileName);
782 assert(fs.promises.readFile.called);
783 assert(!dingus.handlerNotFound.called);
784 });
785 it('covers no meta headers', async function () {
786 dingus.staticMetadata = false;
787 await dingus.serveFile(req, res, ctx, directory, fileName);
788 assert(fs.promises.readFile.called);
789 assert(!dingus.handlerNotFound.called);
790 });
791 it('does not serve dot-file', async function () {
792 fileName = '.example';
793 await dingus.serveFile(req, res, ctx, directory, fileName);
794 assert(!fs.promises.readFile.called);
795 assert(dingus.handlerNotFound.called);
796 });
797 it('does not serve encoded navigation', async function () {
798 fileName = '/example.html';
799 await dingus.serveFile(req, res, ctx, directory, fileName);
800 assert(!fs.promises.readFile.called);
801 assert(dingus.handlerNotFound.called);
802 });
803 it('does not serve missing file', async function () {
804 fileName = 'no-file.here';
805 await dingus.serveFile(req, res, ctx, directory, fileName);
806 assert(dingus.handlerNotFound.called);
807 });
808 it('requires directory be specified', async function () {
809 await dingus.serveFile(req, res, ctx, '', fileName);
810 assert(!fs.promises.readFile.called);
811 assert(dingus.handlerNotFound.called);
812 });
813 it('covers fs error', async function () {
814 const expectedException = new Error('blah');
815 fs.promises.stat.restore();
816 sinon.stub(fs.promises, 'stat').rejects(expectedException);
817 try {
818 await dingus.serveFile(req, res, ctx, directory, fileName);
819 assert.fail('should have thrown');
820 } catch (e) {
821 assert.strictEqual(e, expectedException);
822 }
823 });
824 it('caches by modified', async function () {
825 req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 23:11:16 GMT';
826 await dingus.serveFile(req, res, ctx, directory, fileName);
827 assert.strictEqual(res.statusCode, 304);
828 });
829 it('does not cache old modified', async function () {
830 req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 01:11:16 GMT';
831 await dingus.serveFile(req, res, ctx, directory, fileName);
832 assert.notStrictEqual(res.statusCode, 304);
833 assert(!dingus.handlerNotFound.called);
834 });
835 it('caches ETag match', async function () {
836 req._headers[Enum.Header.IfNoneMatch] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
837 await dingus.serveFile(req, res, ctx, directory, fileName);
838 assert.strictEqual(res.statusCode, 304);
839 });
840 it('does not cache ETag non-match', async function () {
841 req._headers[Enum.Header.IfNoneMatch] = '"foo", "bar"';
842 await dingus.serveFile(req, res, ctx, directory, fileName);
843 assert.notStrictEqual(res.statusCode, 304);
844 assert(!dingus.handlerNotFound.called);
845 });
846 it('handles no possible encodings', async function () {
847 req._headers[Enum.Header.AcceptEncoding] = '*;q=0';
848 await assert.rejects(async () => {
849 await dingus.serveFile(req, res, ctx, directory, fileName);
850 }, {
851 name: 'ResponseError',
852 });
853 });
854 it('handles a valid encoding', async function () {
855 req._headers[Enum.Header.AcceptEncoding] = 'gzip';
856 await dingus.serveFile(req, res, ctx, directory, fileName);
857 assert(res.end.called);
858 });
859 it('handles a valid encoding among others', async function () {
860 req._headers[Enum.Header.AcceptEncoding] = 'flarp, br, gzip';
861 fs.promises.stat.restore();
862 sinon.stub(fs.promises, 'stat')
863 .onCall(0).resolves(filestats) // identity file
864 .onCall(1).resolves(null) // br encoding
865 .onCall(2).resolves(filestats); // gzip encoding
866 await dingus.serveFile(req, res, ctx, directory, fileName);
867 assert(res.end.called);
868 });
869 it('handles misconfigured encoding', async function () {
870 Enum.EncodingType.Flarp = 'flarp';
871 req._headers[Enum.Header.AcceptEncoding] = 'flarp, gzip';
872 await dingus.serveFile(req, res, ctx, directory, fileName);
873 delete Enum.EncodingType.Flarp;
874 assert(res.end.called);
875 });
876 }); // serveFile
877
878 describe('renderError', function () {
879 let err;
880 beforeEach(function () {
881 err = {
882 statusCode: '200',
883 errorMessage: 'OK',
884 details: 'hunkydorey',
885 };
886 });
887 it('renders unknown type', function () {
888 const contentType = 'unknown/type';
889 const result = dingus.renderError(contentType, err);
890 assert.deepStrictEqual(result, 'OK\r\nhunkydorey');
891 });
892 it('renders text', function () {
893 const contentType = 'text/plain';
894 const result = dingus.renderError(contentType, err);
895 assert.deepStrictEqual(result, 'OK\r\nhunkydorey');
896 });
897 it('renders json', function () {
898 const contentType = Enum.ContentType.ApplicationJson;
899 const result = dingus.renderError(contentType, err);
900 assert.deepStrictEqual(result, JSON.stringify(err));
901 });
902 it('renders html without details', function () {
903 err = {
904 statusCode: '201',
905 errorMessage: 'Created',
906 };
907 const contentType = 'text/html';
908 const result = dingus.renderError(contentType, err);
909 assert.deepStrictEqual(result, `<!DOCTYPE html>
910 <html lang="en">
911 <head>
912 <title>${err.statusCode} ${err.errorMessage}</title>
913 </head>
914 <body>
915 <h1>${err.errorMessage}</h1>
916 </body>
917 </html>`);
918 });
919 it('renders html', function () {
920 const contentType = 'text/html';
921 const result = dingus.renderError(contentType, err);
922 assert.deepStrictEqual(result, `<!DOCTYPE html>
923 <html lang="en">
924 <head>
925 <title>${err.statusCode} ${err.errorMessage}</title>
926 </head>
927 <body>
928 <h1>${err.errorMessage}</h1>
929 <p>${err.details}</p>
930 </body>
931 </html>`);
932 });
933 it('renders html, multiple details', function () {
934 const contentType = 'text/html';
935 err.details = ['one detail', 'two detail'];
936 const result = dingus.renderError(contentType, err);
937 assert.deepStrictEqual(result, `<!DOCTYPE html>
938 <html lang="en">
939 <head>
940 <title>${err.statusCode} ${err.errorMessage}</title>
941 </head>
942 <body>
943 <h1>${err.errorMessage}</h1>
944 <p>one detail</p>
945 <p>two detail</p>
946 </body>
947 </html>`);
948 });
949 }); // renderError
950
951 describe('sendErrorResponse', function () {
952 let ctx, req, res;
953 beforeEach(function () {
954 ctx = {};
955 req = {};
956 res = {
957 end: sinon.stub(),
958 getHeader: sinon.stub(),
959 getHeaders: sinon.stub(),
960 hasHeader: sinon.stub().returns(true),
961 setHeader: sinon.stub(),
962 };
963 sinon.stub(dingus, 'renderError');
964 });
965 it('covers', function () {
966 const err = {
967 statusCode: 444,
968 };
969 dingus.sendErrorResponse(err, req, res, ctx);
970 assert(res.end.called);
971 });
972 }); // sendErrorResponse
973
974 describe('proxyPrefix', function () {
975 let req, res, ctx, stubHandler, pfxDingus;
976 const pfx = '/pfx';
977
978 beforeEach(function () {
979 pfxDingus = new Dingus(console, { proxyPrefix: pfx });
980 req = {
981 setHeader: sinon.stub(),
982 getHeader: sinon.stub(),
983 };
984 res = {
985 statusCode: 200,
986 end: sinon.stub(),
987 setHeader: sinon.stub(),
988 getHeader: sinon.stub(),
989 };
990 ctx = {};
991 sinon.stub(pfxDingus, 'handlerMethodNotAllowed');
992 sinon.stub(pfxDingus, 'handlerNotFound');
993 stubHandler = sinon.stub();
994 });
995 afterEach(function () {
996 sinon.restore();
997 });
998
999 it('handles prefixed route', async function () {
1000 const urlPath = '/:id';
1001 const method = 'GET';
1002 pfxDingus.on(method, urlPath, stubHandler);
1003 req.url = pfx + '/abc';
1004 req.method = method;
1005
1006 await pfxDingus.dispatch(req, res, ctx);
1007 assert(stubHandler.called);
1008 assert(!pfxDingus.handlerMethodNotAllowed.called);
1009 assert(!pfxDingus.handlerNotFound.called);
1010 });
1011 it('does not handle prefixed route', async function () {
1012 const urlPath = '/:id';
1013 const method = 'GET';
1014 pfxDingus.on(method, urlPath, stubHandler);
1015 req.url = '/wrongpfx/abc';
1016 req.method = method;
1017
1018 await pfxDingus.dispatch(req, res, ctx);
1019 assert(!stubHandler.called);
1020 assert(!pfxDingus.handlerMethodNotAllowed.called);
1021 assert(pfxDingus.handlerNotFound.called);
1022 });
1023 }); // proxyPrefix
1024
1025 describe('handlerRedirect', function () {
1026 let req, res, ctx;
1027 beforeEach(function () {
1028 req = {
1029 getHeader: sinon.stub(),
1030 };
1031 res = {
1032 setHeader: sinon.stub(),
1033 end: sinon.stub(),
1034 };
1035 ctx = {};
1036 });
1037 it('covers', async function () {
1038 await dingus.handlerRedirect(req, res, ctx);
1039 assert(res.setHeader.called);
1040 assert(res.end.called);
1041 });
1042 it('covers non-defaults', async function () {
1043 await dingus.handlerRedirect(req, res, ctx, 308);
1044 assert(res.setHeader.called);
1045 assert(res.end.called);
1046 });
1047 }); // handlerRedirect
1048
1049 describe('handlerGetStaticFile', function () {
1050 let req, res, ctx;
1051 beforeEach(function () {
1052 req = {
1053 getHeader: sinon.stub(),
1054 };
1055 res = {
1056 setHeader: sinon.stub(),
1057 };
1058 ctx = {
1059 params: {
1060 file: '',
1061 },
1062 };
1063 sinon.stub(dingus, 'serveFile');
1064 });
1065 it('covers', async function () {
1066 await dingus.handlerGetStaticFile(req, res, ctx);
1067 assert(dingus.serveFile.called);
1068 });
1069 it('covers specified file', async function () {
1070 await dingus.handlerGetStaticFile(req, res, ctx, 'file.txt');
1071 assert(dingus.serveFile.called);
1072 });
1073 }); // handlerGetStaticFile
1074 });