9d471229d3a26ebde87ebfae9d53efbde46c9aaa
[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 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 sinon.spy(Dingus, 'setHeadHandler');
359 stubHandler = sinon.stub();
360 });
361 afterEach(function () {
362 dingus.pathsByLength = pathsByLengthOrig;
363 });
364
365 it('calls handler', async function () {
366 const urlPath = '/:id';
367 const method = 'GET';
368 dingus.on(method, urlPath, stubHandler);
369 req.url = '/abc';
370 req.method = method;
371
372 await dingus.dispatch(req, res, ctx);
373 assert(stubHandler.called);
374 assert(!dingus.handlerMethodNotAllowed.called);
375 assert(!dingus.handlerNotFound.called);
376 });
377 it('calls handler without context', async function () {
378 const urlPath = '/:id';
379 const method = 'GET';
380 dingus.on(method, urlPath, stubHandler);
381 req.url = '/abc';
382 req.method = method;
383
384 await dingus.dispatch(req, res);
385 assert(stubHandler.called);
386 assert(!dingus.handlerMethodNotAllowed.called);
387 assert(!dingus.handlerNotFound.called);
388 });
389 it('calls fallback handler', async function () {
390 const urlPath = '/abc/:id';
391 const method = 'GET';
392 dingus.on('*', urlPath, stubHandler);
393 req.url = '/abc/def';
394 req.method = method;
395
396 await dingus.dispatch(req, res, ctx);
397 assert(stubHandler.called);
398 assert(!dingus.handlerMethodNotAllowed.called);
399 assert(!dingus.handlerNotFound.called);
400 });
401 it('handles error in handler', async function () {
402 const urlPath = '/:id';
403 const method = 'GET';
404 dingus.on(method, urlPath, stubHandler);
405 req.url = '/abc';
406 req.method = method;
407 stubHandler.rejects(new Error('blah'));
408
409 await dingus.dispatch(req, res, ctx);
410 assert(stubHandler.called);
411 assert(!dingus.handlerMethodNotAllowed.called);
412 assert(!dingus.handlerNotFound.called);
413 });
414 it('calls unsupported method', async function () {
415 const urlPath = '/:id';
416 const method = 'POST';
417 dingus.on('GET', urlPath, stubHandler);
418 req.url = '/abc';
419 req.method = method;
420
421 await dingus.dispatch(req, res, ctx);
422 assert(!stubHandler.called);
423 assert(dingus.handlerMethodNotAllowed.called);
424 assert(!dingus.handlerNotFound.called);
425 });
426 it('does not lookup nonexistent path', async function () {
427 req.url = '/foo/bar';
428 req.method = 'GET';
429
430 await dingus.dispatch(req, res, ctx);
431 assert(!stubHandler.called);
432 assert(!dingus.handlerMethodNotAllowed.called);
433 assert(dingus.handlerNotFound.called);
434 });
435 it('covers unhandled dingus exception', async function () {
436 const expectedException = new DingusError('blah');
437 sinon.stub(dingus.router, 'lookup').throws(expectedException);
438
439 await dingus.dispatch(req, res, ctx);
440 assert(!stubHandler.called);
441 assert(dingus.handlerInternalServerError.called);
442 });
443 it('covers other exception', async function () {
444 const expectedException = new Error('blah');
445 sinon.stub(dingus.router, 'lookup').throws(expectedException);
446
447 await dingus.dispatch(req, res, ctx);
448 assert(!stubHandler.called);
449 assert(dingus.handlerInternalServerError.called);
450 });
451 it('covers bad uri', async function () {
452 req.url = '/%f';
453
454 await dingus.dispatch(req, res, ctx);
455 assert(dingus.handlerBadRequest.called);
456 });
457 it('calls handler with additional arguments', async function () {
458 dingus.on('GET', '/', stubHandler, 'foo', 'bar');
459 await dingus.dispatch(req, res, ctx);
460 assert(stubHandler.called);
461 assert.strictEqual(stubHandler.args[0][3], 'foo');
462 assert.strictEqual(stubHandler.args[0][4], 'bar');
463 });
464 describe('intrinsic HEAD handling', function () {
465 it('covers no intrinsic HEAD handling', async function () {
466 dingus.intrinsicHeadMethod = false;
467 dingus.on('GET', '/', stubHandler);
468 req.method = 'HEAD';
469 await dingus.dispatch(req, res, ctx);
470 assert(!stubHandler.called);
471 assert(dingus.handlerMethodNotAllowed.called);
472 });
473 it('calls HEAD setup and GET handler', async function () {
474 dingus.on('GET', '/', stubHandler);
475 req.method = 'HEAD';
476 await dingus.dispatch(req, res, ctx);
477 assert(Dingus.setHeadHandler.called);
478 assert(stubHandler.called);
479 });
480 it('covers no GET handler', async function () {
481 dingus.on('POST', '/', stubHandler);
482 req.method = 'HEAD';
483 await dingus.dispatch(req, res, ctx);
484 assert(!stubHandler.called);
485 assert(dingus.handlerMethodNotAllowed.called);
486 });
487 it('covers unexpected router error', async function () {
488 sinon.stub(dingus.router, 'lookup')
489 .onFirstCall().throws(new RouterNoMethodError())
490 .onSecondCall().throws(new DingusError())
491 ;
492 dingus.on('GET', '/', stubHandler);
493 req.method = 'HEAD';
494 await dingus.dispatch(req, res, ctx);
495 assert(dingus.handlerInternalServerError.called);
496 });
497 });
498 }); // dispatch
499
500 describe('parseBody', function () {
501 let ctx;
502 beforeEach(function () {
503 ctx = {};
504 });
505 it('does not parse unknown type', function () {
506 try {
507 dingus.parseBody('unknown/type', ctx);
508 assert.fail(noExpectedException);
509 } catch (e) {
510 assert.strictEqual(e.statusCode, 415);
511 }
512 });
513 it('parses json', function () {
514 const src = { foo: 'bar' };
515 const rawBody = JSON.stringify(src);
516 dingus.parseBody(Enum.ContentType.ApplicationJson, ctx, rawBody);
517 assert.deepStrictEqual(ctx.parsedBody, src);
518 });
519 it('handles unparsable json', function () {
520 const rawBody = 'not json';
521 try {
522 dingus.parseBody(Enum.ContentType.ApplicationJson, ctx, rawBody);
523 assert.fail(noExpectedException);
524 } catch (e) {
525 assert.strictEqual(e.statusCode, 400);
526 }
527 });
528 it('parses form', function () {
529 const expected = Object.assign(Object.create(null), {
530 foo: 'bar',
531 });
532 const rawBody = 'foo=bar';
533 dingus.parseBody('application/x-www-form-urlencoded', ctx, rawBody);
534 assert.deepStrictEqual(ctx.parsedBody, expected);
535 });
536
537 }); // parseBody
538
539 describe('bodyData', function () {
540 let res, resEvents;
541 beforeEach(function () {
542 resEvents = {};
543 res = {
544 // eslint-disable-next-line security/detect-object-injection
545 on: (ev, fn) => resEvents[ev] = fn,
546 };
547 });
548 it('provides data', async function () {
549 const p = dingus.bodyData(res);
550 resEvents['data'](Buffer.from('foo'));
551 resEvents['data'](Buffer.from('bar'));
552 resEvents['end']();
553 const result = await p;
554 assert.strictEqual(result, 'foobar');
555 });
556 it('handles error', async function () {
557 const p = dingus.bodyData(res);
558 resEvents['error']('foo');
559 try {
560 await p;
561 assert.fail(noExpectedException);
562 } catch (e) {
563 assert.strictEqual(e, 'foo');
564 }
565 });
566 it('limits size', async function () {
567 const p = dingus.bodyData(res, 8);
568 resEvents['data'](Buffer.from('foobar'));
569 resEvents['data'](Buffer.from('bazquux'));
570 try {
571 await p;
572 assert.fail(noExpectedException);
573 } catch (e) {
574 assert.strictEqual(e.statusCode, 413);
575 }
576 });
577 it('provides buffer', async function () {
578 const p = dingus.bodyData(res, 0, false);
579 const expected = Buffer.from('bleat');
580 resEvents['data'](expected);
581 resEvents['end']();
582 const result = await p;
583 assert.deepStrictEqual(result, expected);
584 });
585 }); // bodyData
586
587 describe('ingestBody', function () {
588 it('ingests json', async function () {
589 const req = {};
590 const res = {};
591 const ctx = {};
592 sinon.stub(dingus, 'bodyData').resolves('{"foo":"bar"}');
593 sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
594 await dingus.ingestBody(req, res, ctx);
595 assert.deepStrictEqual(ctx.parsedBody, { foo: 'bar' });
596 assert.deepStrictEqual(ctx.rawBody, undefined);
597 });
598 it('persists rawBody', async function () {
599 const req = {};
600 const res = {};
601 const ctx = {};
602 const body = '{"foo":"bar"}';
603 sinon.stub(dingus, 'bodyData').resolves(body);
604 sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
605 await dingus.ingestBody(req, res, ctx, { persistRawBody: true });
606 assert.deepStrictEqual(ctx.parsedBody, { foo: 'bar' });
607 assert.deepStrictEqual(ctx.rawBody, body);
608 });
609 it('skips parsing empty body', async function () {
610 const req = {};
611 const res = {};
612 const ctx = {};
613 const body = '';
614 sinon.stub(dingus, 'bodyData').resolves(body);
615 sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
616 sinon.spy(dingus, 'parseBody');
617 await dingus.ingestBody(req, res, ctx, { parseEmptyBody: false });
618 assert.deepStrictEqual(ctx.parsedBody, undefined);
619 assert(dingus.parseBody.notCalled);
620 });
621 }); // ingestBody
622
623 describe('setResponseType', function () {
624 let req, res, ctx;
625 let _sa; // Preserve strictAccept
626 before(function () {
627 _sa = dingus.strictAccept;
628 });
629 after(function () {
630 dingus.strictAccept = _sa;
631 });
632 beforeEach(function () {
633 ctx = {};
634 req = {};
635 res = {
636 setHeader: sinon.stub(),
637 };
638 sinon.stub(Dingus, 'getResponseContentType').returns();
639 });
640 it('rejects missing', function () {
641 dingus.strictAccept = true;
642 try {
643 dingus.setResponseType(['my/type'], req, res, ctx);
644 assert.fail(noExpectedException);
645 } catch (e) {
646 assert.strictEqual(e.statusCode, 406, 'did not get expected status code');
647 }
648 });
649 it('accepts missing', function () {
650 dingus.strictAccept = false;
651 dingus.setResponseType(['my/type'], req, res, ctx);
652 assert.strictEqual(ctx.responseType, 'my/type');
653 });
654
655 }); // setResponseType
656
657 describe('_readFileInfo', function () {
658 let stat, data, statRes, dataRes, filename;
659 beforeEach(function () {
660 sinon.stub(fs.promises, 'stat');
661 sinon.stub(fs.promises, 'readFile');
662 statRes = {
663 mtimeMs:1612553697186,
664 };
665 dataRes = 'data';
666 filename = 'dummy.txt';
667 });
668 it('succeeds', async function () {
669 fs.promises.stat.resolves(statRes);
670 fs.promises.readFile.resolves('data');
671 [stat, data] = await dingus._readFileInfo(filename);
672 assert.deepStrictEqual(stat, statRes);
673 assert.deepStrictEqual(data, dataRes);
674 });
675 it('returns null for non-existant file', async function () {
676 const noEnt = {
677 code: 'ENOENT',
678 };
679 fs.promises.stat.rejects(noEnt);
680 fs.promises.readFile.rejects(noEnt);
681 [stat, data] = await dingus._readFileInfo(filename);
682 assert.strictEqual(stat, null);
683 assert.strictEqual(data, null);
684 });
685 it('throws unexpected error', async function () {
686 const expectedException = new Error('blah');
687 fs.promises.stat.rejects(expectedException);
688 await assert.rejects(async () => {
689 await dingus._readFileInfo(filename);
690 }, expectedException);
691 });
692 }); // _readFileInfo
693
694 describe('_serveFileMetaHeaders', function () {
695 let res, directory, fileName;
696 beforeEach(function () {
697 sinon.stub(dingus, '_readFileInfo');
698 res = {
699 setHeader: sinon.stub(),
700 };
701 directory = '/path';
702 fileName = 'filename';
703 });
704 it('covers no meta file', async function() {
705 dingus._readFileInfo.resolves([null, null]);
706 await dingus._serveFileMetaHeaders(res, directory, fileName);
707 assert(!res.setHeader.called);
708 });
709 it('adds extra headers', async function () {
710 dingus._readFileInfo.resolves([{}, Buffer.from(`Link: <https://example.com/>; rel="relation"
711 X-Folded-Header: data
712 data under
713 the fold
714 Content-Type: image/sgi
715 `)]);
716 await dingus._serveFileMetaHeaders(res, directory, fileName);
717 assert(res.setHeader.called);
718 });
719 }); // _serveFileMetaHeaders
720
721 describe('serveFile', function () {
722 const path = require('path');
723 let ctx, req, res, directory, fileName, filestats;
724 beforeEach(function () {
725 directory = path.join(__dirname, '..', 'test-data');
726 fileName = 'example.html';
727 ctx = {};
728 req = {
729 _headers: {
730 [Enum.Header.Accept]: undefined,
731 [Enum.Header.IfModifiedSince]: undefined,
732 [Enum.Header.AcceptEncoding]: undefined,
733 [Enum.Header.IfNoneMatch]: undefined,
734 },
735 getHeader: (header) => {
736 if (header in req._headers) {
737 // eslint-disable-next-line security/detect-object-injection
738 return req._headers[header];
739 }
740 assert.fail(`unexpected getHeader ${header}`);
741 },
742 };
743 res = {
744 end: sinon.stub(),
745 getHeader: sinon.stub(),
746 getHeaders: sinon.stub(),
747 hasHeader: sinon.stub().returns(true),
748 setHeader: sinon.stub(),
749 };
750 filestats = {
751 dev: 39,
752 mode: 33188,
753 nlink: 1,
754 uid: 1002,
755 gid: 1002,
756 rdev: 0,
757 blksize: 512,
758 ino: 897653,
759 size: 8,
760 blocks: 17,
761 atimeMs: 1613253436842.815,
762 mtimeMs: 1603485933192.861,
763 ctimeMs: 1603485933192.861,
764 birthtimeMs: 0,
765 atime: '2021-02-13T21:57:16.843Z',
766 mtime: '2020-10-23T13:45:33.193Z',
767 ctime: '2020-10-23T13:45:33.193Z',
768 birthtime: '1970-01-01T00:00:00.000Z',
769 };
770 sinon.stub(dingus, 'handlerNotFound');
771 sinon.stub(fs.promises, 'stat').resolves(filestats);
772 sinon.spy(fs.promises, 'readFile');
773 });
774 it('serves a file', async function () {
775 await dingus.serveFile(req, res, ctx, directory, fileName);
776 assert(fs.promises.readFile.called);
777 assert(!dingus.handlerNotFound.called);
778 });
779 it('covers no meta headers', async function () {
780 dingus.staticMetadata = false;
781 await dingus.serveFile(req, res, ctx, directory, fileName);
782 assert(fs.promises.readFile.called);
783 assert(!dingus.handlerNotFound.called);
784 });
785 it('does not serve dot-file', async function () {
786 fileName = '.example';
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 encoded navigation', async function () {
792 fileName = '/example.html';
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 missing file', async function () {
798 fileName = 'no-file.here';
799 await dingus.serveFile(req, res, ctx, directory, fileName);
800 assert(dingus.handlerNotFound.called);
801 });
802 it('requires directory be specified', async function () {
803 await dingus.serveFile(req, res, ctx, '', fileName);
804 assert(!fs.promises.readFile.called);
805 assert(dingus.handlerNotFound.called);
806 });
807 it('covers fs error', async function () {
808 const expectedException = new Error('blah');
809 fs.promises.stat.restore();
810 sinon.stub(fs.promises, 'stat').rejects(expectedException);
811 try {
812 await dingus.serveFile(req, res, ctx, directory, fileName);
813 assert.fail('should have thrown');
814 } catch (e) {
815 assert.strictEqual(e, expectedException);
816 }
817 });
818 it('caches by modified', async function () {
819 req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 23:11:16 GMT';
820 await dingus.serveFile(req, res, ctx, directory, fileName);
821 assert.strictEqual(res.statusCode, 304);
822 });
823 it('does not cache old modified', async function () {
824 req._headers[Enum.Header.IfModifiedSince] = 'Fri, 23 Oct 2020 01:11:16 GMT';
825 await dingus.serveFile(req, res, ctx, directory, fileName);
826 assert.notStrictEqual(res.statusCode, 304);
827 assert(!dingus.handlerNotFound.called);
828 });
829 it('caches ETag match', async function () {
830 req._headers[Enum.Header.IfNoneMatch] = '"zPPQVfXV36sgXq4fRLdsm+7rRMb8IUfb/eJ6N6mnwWs"';
831 await dingus.serveFile(req, res, ctx, directory, fileName);
832 assert.strictEqual(res.statusCode, 304);
833 });
834 it('does not cache ETag non-match', async function () {
835 req._headers[Enum.Header.IfNoneMatch] = '"foo", "bar"';
836 await dingus.serveFile(req, res, ctx, directory, fileName);
837 assert.notStrictEqual(res.statusCode, 304);
838 assert(!dingus.handlerNotFound.called);
839 });
840 it('handles no possible encodings', async function () {
841 req._headers[Enum.Header.AcceptEncoding] = '*;q=0';
842 await assert.rejects(async () => {
843 await dingus.serveFile(req, res, ctx, directory, fileName);
844 }, {
845 name: 'ResponseError',
846 });
847 });
848 it('handles a valid encoding', async function () {
849 req._headers[Enum.Header.AcceptEncoding] = 'gzip';
850 await dingus.serveFile(req, res, ctx, directory, fileName);
851 assert(res.end.called);
852 });
853 it('handles a valid encoding among others', async function () {
854 req._headers[Enum.Header.AcceptEncoding] = 'flarp, br, gzip';
855 fs.promises.stat.restore();
856 sinon.stub(fs.promises, 'stat')
857 .onCall(0).resolves(filestats) // identity file
858 .onCall(1).resolves(null) // br encoding
859 .onCall(2).resolves(filestats); // gzip encoding
860 await dingus.serveFile(req, res, ctx, directory, fileName);
861 assert(res.end.called);
862 });
863 it('handles misconfigured encoding', async function () {
864 Enum.EncodingType.Flarp = 'flarp';
865 req._headers[Enum.Header.AcceptEncoding] = 'flarp, gzip';
866 await dingus.serveFile(req, res, ctx, directory, fileName);
867 delete Enum.EncodingType.Flarp;
868 assert(res.end.called);
869 });
870 }); // serveFile
871
872 describe('renderError', function () {
873 let err;
874 beforeEach(function () {
875 err = {
876 statusCode: '200',
877 errorMessage: 'OK',
878 details: 'hunkydorey',
879 };
880 });
881 it('renders unknown type', function () {
882 const contentType = 'unknown/type';
883 const result = dingus.renderError(contentType, err);
884 assert.deepStrictEqual(result, 'OK\r\nhunkydorey');
885 });
886 it('renders text', function () {
887 const contentType = 'text/plain';
888 const result = dingus.renderError(contentType, err);
889 assert.deepStrictEqual(result, 'OK\r\nhunkydorey');
890 });
891 it('renders json', function () {
892 const contentType = Enum.ContentType.ApplicationJson;
893 const result = dingus.renderError(contentType, err);
894 assert.deepStrictEqual(result, JSON.stringify(err));
895 });
896 it('renders html without details', function () {
897 err = {
898 statusCode: '201',
899 errorMessage: 'Created',
900 };
901 const contentType = 'text/html';
902 const result = dingus.renderError(contentType, err);
903 assert.deepStrictEqual(result, `<!DOCTYPE html>
904 <html lang="en">
905 <head>
906 <title>${err.statusCode} ${err.errorMessage}</title>
907 </head>
908 <body>
909 <h1>${err.errorMessage}</h1>
910 </body>
911 </html>`);
912 });
913 it('renders html', function () {
914 const contentType = 'text/html';
915 const result = dingus.renderError(contentType, err);
916 assert.deepStrictEqual(result, `<!DOCTYPE html>
917 <html lang="en">
918 <head>
919 <title>${err.statusCode} ${err.errorMessage}</title>
920 </head>
921 <body>
922 <h1>${err.errorMessage}</h1>
923 <p>${err.details}</p>
924 </body>
925 </html>`);
926 });
927 it('renders html, multiple details', function () {
928 const contentType = 'text/html';
929 err.details = ['one detail', 'two detail'];
930 const result = dingus.renderError(contentType, err);
931 assert.deepStrictEqual(result, `<!DOCTYPE html>
932 <html lang="en">
933 <head>
934 <title>${err.statusCode} ${err.errorMessage}</title>
935 </head>
936 <body>
937 <h1>${err.errorMessage}</h1>
938 <p>one detail</p>
939 <p>two detail</p>
940 </body>
941 </html>`);
942 });
943 }); // renderError
944
945 describe('sendErrorResponse', function () {
946 let ctx, req, res;
947 beforeEach(function () {
948 ctx = {};
949 req = {};
950 res = {
951 end: sinon.stub(),
952 getHeader: sinon.stub(),
953 getHeaders: sinon.stub(),
954 hasHeader: sinon.stub().returns(true),
955 setHeader: sinon.stub(),
956 };
957 sinon.stub(dingus, 'renderError');
958 });
959 it('covers', function () {
960 const err = {
961 statusCode: 444,
962 };
963 dingus.sendErrorResponse(err, req, res, ctx);
964 assert(res.end.called);
965 });
966 }); // sendErrorResponse
967
968 describe('proxyPrefix', function () {
969 let req, res, ctx, stubHandler, pfxDingus;
970 const pfx = '/pfx';
971
972 beforeEach(function () {
973 pfxDingus = new Dingus(console, { proxyPrefix: pfx });
974 req = {
975 setHeader: sinon.stub(),
976 getHeader: sinon.stub(),
977 };
978 res = {
979 statusCode: 200,
980 end: sinon.stub(),
981 setHeader: sinon.stub(),
982 getHeader: sinon.stub(),
983 };
984 ctx = {};
985 sinon.stub(pfxDingus, 'handlerMethodNotAllowed');
986 sinon.stub(pfxDingus, 'handlerNotFound');
987 stubHandler = sinon.stub();
988 });
989 afterEach(function () {
990 sinon.restore();
991 });
992
993 it('handles prefixed route', async function () {
994 const urlPath = '/:id';
995 const method = 'GET';
996 pfxDingus.on(method, urlPath, stubHandler);
997 req.url = pfx + '/abc';
998 req.method = method;
999
1000 await pfxDingus.dispatch(req, res, ctx);
1001 assert(stubHandler.called);
1002 assert(!pfxDingus.handlerMethodNotAllowed.called);
1003 assert(!pfxDingus.handlerNotFound.called);
1004 });
1005 it('does not handle prefixed route', async function () {
1006 const urlPath = '/:id';
1007 const method = 'GET';
1008 pfxDingus.on(method, urlPath, stubHandler);
1009 req.url = '/wrongpfx/abc';
1010 req.method = method;
1011
1012 await pfxDingus.dispatch(req, res, ctx);
1013 assert(!stubHandler.called);
1014 assert(!pfxDingus.handlerMethodNotAllowed.called);
1015 assert(pfxDingus.handlerNotFound.called);
1016 });
1017 }); // proxyPrefix
1018
1019 describe('handlerRedirect', function () {
1020 let req, res, ctx;
1021 beforeEach(function () {
1022 req = {
1023 getHeader: sinon.stub(),
1024 };
1025 res = {
1026 setHeader: sinon.stub(),
1027 end: sinon.stub(),
1028 };
1029 ctx = {};
1030 });
1031 it('covers', async function () {
1032 await dingus.handlerRedirect(req, res, ctx);
1033 assert(res.setHeader.called);
1034 assert(res.end.called);
1035 });
1036 it('covers non-defaults', async function () {
1037 await dingus.handlerRedirect(req, res, ctx, 308);
1038 assert(res.setHeader.called);
1039 assert(res.end.called);
1040 });
1041 }); // handlerRedirect
1042
1043 describe('handlerGetStaticFile', function () {
1044 let req, res, ctx;
1045 beforeEach(function () {
1046 req = {
1047 getHeader: sinon.stub(),
1048 };
1049 res = {
1050 setHeader: sinon.stub(),
1051 };
1052 ctx = {
1053 params: {
1054 file: '',
1055 },
1056 };
1057 sinon.stub(dingus, 'serveFile');
1058 });
1059 it('covers', async function () {
1060 await dingus.handlerGetStaticFile(req, res, ctx);
1061 assert(dingus.serveFile.called);
1062 });
1063 it('covers specified file', async function () {
1064 await dingus.handlerGetStaticFile(req, res, ctx, 'file.txt');
1065 assert(dingus.serveFile.called);
1066 });
1067 }); // handlerGetStaticFile
1068 });