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