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