cookies are now parsed and populated on ctx.cookie by deault
[squeep-api-dingus] / test / lib / dingus.js
index 60b0dad59d68ea21bfba45c759df89b5a7ccebe9..01094a7c8e94bc9647411b422d76f8339783a0cd 100644 (file)
@@ -7,15 +7,24 @@ const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-requi
 const fs = require('fs');
 
 const Dingus = require('../../lib/dingus');
-const { DingusError } = require('../../lib/errors');
+const { DingusError, RouterNoMethodError } = require('../../lib/errors');
 const Enum = require('../../lib/enum');
 
 const noExpectedException = 'did not get expected exception';
 
+const _nop = () => undefined;
+const _logFn = (process.env['VERBOSE_TESTS'] && console.log) || _nop;
+const noLogger = {
+  debug: _logFn,
+  error: _logFn,
+};
+sinon.spy(noLogger, 'debug');
+sinon.spy(noLogger, 'error');
+
 describe('Dingus', function () {
   let dingus;
   beforeEach(function () {
-    dingus = new Dingus();
+    dingus = new Dingus(noLogger, {});
   });
   afterEach(function () {
     sinon.restore();
@@ -23,9 +32,8 @@ describe('Dingus', function () {
 
   describe('constructor', function () {
     it('covers', function () {
-      const d = new Dingus({}, {});
+      const d = new Dingus();
       assert(d);
-      assert('log' in d.logger);
     });
   }); // constructor
 
@@ -37,7 +45,7 @@ describe('Dingus', function () {
     });
     it('returns normal path', function () {
       const p = '////a///b/./bar/..///c';
-      const expected = '/a/b/c'
+      const expected = '/a/b/c';
       const r = dingus._normalizePath(p);
       assert.strictEqual(r, expected);
     });
@@ -153,7 +161,7 @@ describe('Dingus', function () {
       const expected = {
         clientAddress: '',
         clientProtocol: 'http',
-      }
+      };
       dingus.clientAddressContext(req, res, ctx);
       assert.deepStrictEqual(ctx, expected);
       assert(!req.getHeader.called);
@@ -163,7 +171,7 @@ describe('Dingus', function () {
       const expected = {
         clientAddress: '::1',
         clientProtocol: 'https',
-      }
+      };
       req.connection.remoteAddress = '::1';
       req.connection.encrypted = true;
       dingus.clientAddressContext(req, res, ctx);
@@ -172,6 +180,71 @@ describe('Dingus', function () {
     });
   }); // clientAddressContext
 
+  describe('ingestCookie', function () {
+    let req, res, ctx;
+    beforeEach(function () {
+      req = {
+        getHeader: sinon.stub(),
+      };
+      ctx = {};
+    });
+    it('covers no header', function () {
+      const expected = {};
+      Dingus.ingestCookie(req, res, ctx);
+      assert.deepStrictEqual(ctx.cookie, expected);
+    });
+    it('covers non variable', function () {
+      req.getHeader.returns('foo');
+      const expected = {
+        foo: null,
+      };
+      Dingus.ingestCookie(req, res, ctx);
+      assert.deepStrictEqual(ctx.cookie, expected);
+    });
+    it('parses cookies', function () {
+      req.getHeader.returns('foo=bar; baz="quux"');
+      const expected = {
+        foo: 'bar',
+        baz: 'quux',
+      };
+      Dingus.ingestCookie(req, res, ctx);
+      assert.deepStrictEqual(ctx.cookie, expected);
+    });
+    it('parses nulls', function () {
+      req.getHeader.returns('foo=; bar=');
+      const expected = {
+        foo: '',
+        bar: '',
+      };
+      Dingus.ingestCookie(req, res, ctx);
+      assert.deepStrictEqual(ctx.cookie, expected);
+    });
+    it('parses non-uri-encoded', function () {
+      req.getHeader.returns('foo%=%qux');
+      const expected = {
+        'foo%': '%qux',
+      };
+      Dingus.ingestCookie(req, res, ctx);
+      assert.deepStrictEqual(ctx.cookie, expected);
+    });
+    it('covers nameless cookie', function () {
+      req.getHeader.returns('=bar');
+      const expected = {
+      };
+      Dingus.ingestCookie(req, res, ctx);
+      assert.deepStrictEqual(ctx.cookie, expected);
+
+    });
+    it('covers duplicate cookie', function () {
+      req.getHeader.returns('foo=bar; foo="quux"');
+      const expected = {
+        foo: 'bar',
+      };
+      Dingus.ingestCookie(req, res, ctx);
+      assert.deepStrictEqual(ctx.cookie, expected);
+    });
+  }); // ingestCookie
+
   describe('getRequestContentType', function () {
     let req;
     beforeEach(function () {
@@ -269,7 +342,7 @@ describe('Dingus', function () {
       };
       ctx = {};
     });
-    it('collects body without writing', function () {
+    it('collects response without writing', function () {
       Dingus.setHeadHandler(req, res, ctx);
       res.write(Buffer.from('foo'));
       res.write('baz');
@@ -277,6 +350,16 @@ describe('Dingus', function () {
       res.end('quux');
       assert(!origWrite.called);
       assert(origEnd.called);
+      assert.deepStrictEqual(ctx.responseBody, undefined);
+    });
+    it('collects response without writing, persists written data', function () {
+      Dingus.setHeadHandler(req, res, ctx, true);
+      res.write(Buffer.from('foo'));
+      res.write('baz');
+      res.write();
+      res.end('quux');
+      assert(!origWrite.called);
+      assert(origEnd.called);
       assert.deepStrictEqual(ctx.responseBody, Buffer.from('foobazquux'));
     });
     it('ignores non-head method', function () {
@@ -341,6 +424,7 @@ describe('Dingus', function () {
       sinon.spy(dingus, 'handlerNotFound');
       sinon.spy(dingus, 'handlerBadRequest');
       sinon.spy(dingus, 'handlerInternalServerError');
+      sinon.spy(Dingus, 'setHeadHandler');
       stubHandler = sinon.stub();
     });
     afterEach(function () {
@@ -439,7 +523,47 @@ describe('Dingus', function () {
       await dingus.dispatch(req, res, ctx);
       assert(dingus.handlerBadRequest.called);
     });
-
+    it('calls handler with additional arguments', async function () {
+      dingus.on('GET', '/', stubHandler, 'foo', 'bar');
+      await dingus.dispatch(req, res, ctx);
+      assert(stubHandler.called);
+      assert.strictEqual(stubHandler.args[0][3], 'foo');
+      assert.strictEqual(stubHandler.args[0][4], 'bar');
+    });
+    describe('intrinsic HEAD handling', function () {
+      it('covers no intrinsic HEAD handling', async function () {
+        dingus.intrinsicHeadMethod = false;
+        dingus.on('GET', '/', stubHandler);
+        req.method = 'HEAD';
+        await dingus.dispatch(req, res, ctx);
+        assert(!stubHandler.called);
+        assert(dingus.handlerMethodNotAllowed.called);
+      });
+      it('calls HEAD setup and GET handler', async function () {
+        dingus.on('GET', '/', stubHandler);
+        req.method = 'HEAD';
+        await dingus.dispatch(req, res, ctx);
+        assert(Dingus.setHeadHandler.called);
+        assert(stubHandler.called);
+      });
+      it('covers no GET handler', async function () {
+        dingus.on('POST', '/', stubHandler);
+        req.method = 'HEAD';
+        await dingus.dispatch(req, res, ctx);
+        assert(!stubHandler.called);
+        assert(dingus.handlerMethodNotAllowed.called);
+      });
+      it('covers unexpected router error', async function () {
+        sinon.stub(dingus.router, 'lookup')
+          .onFirstCall().throws(new RouterNoMethodError())
+          .onSecondCall().throws(new DingusError())
+        ;
+        dingus.on('GET', '/', stubHandler);
+        req.method = 'HEAD';
+        await dingus.dispatch(req, res, ctx);
+        assert(dingus.handlerInternalServerError.called);
+      });
+    });
   }); // dispatch
 
   describe('parseBody', function () {
@@ -457,14 +581,14 @@ describe('Dingus', function () {
     });
     it('parses json', function () {
       const src = { foo: 'bar' };
-      ctx.rawBody = JSON.stringify(src);
-      dingus.parseBody(Enum.ContentType.ApplicationJson, ctx);
+      const rawBody = JSON.stringify(src);
+      dingus.parseBody(Enum.ContentType.ApplicationJson, ctx, rawBody);
       assert.deepStrictEqual(ctx.parsedBody, src);
     });
     it('handles unparsable json', function () {
-      ctx.rawBody = 'not json';
+      const rawBody = 'not json';
       try {
-        dingus.parseBody(Enum.ContentType.ApplicationJson, ctx);
+        dingus.parseBody(Enum.ContentType.ApplicationJson, ctx, rawBody);
         assert.fail(noExpectedException);
       } catch (e) {
         assert.strictEqual(e.statusCode, 400);
@@ -474,8 +598,8 @@ describe('Dingus', function () {
       const expected = Object.assign(Object.create(null), {
         foo: 'bar',
       });
-      ctx.rawBody = 'foo=bar';
-      dingus.parseBody('application/x-www-form-urlencoded', ctx);
+      const rawBody = 'foo=bar';
+      dingus.parseBody('application/x-www-form-urlencoded', ctx, rawBody);
       assert.deepStrictEqual(ctx.parsedBody, expected);
     });
 
@@ -508,17 +632,60 @@ describe('Dingus', function () {
         assert.strictEqual(e, 'foo');
       }
     });
+    it('limits size', async function () {
+      const p = dingus.bodyData(res, 8);
+      resEvents['data'](Buffer.from('foobar'));
+      resEvents['data'](Buffer.from('bazquux'));
+      try {
+        await p;
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.strictEqual(e.statusCode, 413);
+      }
+    });
+    it('provides buffer', async function () {
+      const p = dingus.bodyData(res, 0, false);
+      const expected = Buffer.from('bleat');
+      resEvents['data'](expected);
+      resEvents['end']();
+      const result = await p;
+      assert.deepStrictEqual(result, expected);
+    });
   }); // bodyData
 
   describe('ingestBody', function () {
-    it('covers', async function () {
+    it('ingests json', async function () {
       const req = {};
       const res = {};
       const ctx = {};
-      sinon.stub(dingus, 'bodyData').resolves('{"foo":"bar"}')
+      sinon.stub(dingus, 'bodyData').resolves('{"foo":"bar"}');
       sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
       await dingus.ingestBody(req, res, ctx);
       assert.deepStrictEqual(ctx.parsedBody, { foo: 'bar' });
+      assert.deepStrictEqual(ctx.rawBody, undefined);
+    });
+    it('persists rawBody', async function () {
+      const req = {};
+      const res = {};
+      const ctx = {};
+      const body = '{"foo":"bar"}';
+      sinon.stub(dingus, 'bodyData').resolves(body);
+      sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
+      await dingus.ingestBody(req, res, ctx, { persistRawBody: true });
+      assert.deepStrictEqual(ctx.parsedBody, { foo: 'bar' });
+      assert.deepStrictEqual(ctx.rawBody, body);
+    });
+    it('skips parsing empty body', async function () {
+      const req = {};
+      const res = {};
+      const ctx = {};
+      const body = '';
+      sinon.stub(dingus, 'bodyData').resolves(body);
+      sinon.stub(Dingus, 'getRequestContentType').returns(Enum.ContentType.ApplicationJson);
+      sinon.spy(dingus, 'parseBody');
+      await dingus.ingestBody(req, res, ctx, { parseEmptyBody: false });
+      assert.deepStrictEqual(ctx.parsedBody, undefined);
+      assert(dingus.parseBody.notCalled);
     });
   }); // ingestBody
 
@@ -605,8 +772,9 @@ describe('Dingus', function () {
     });
     it('covers no meta file', async function() {
       dingus._readFileInfo.resolves([null, null]);
-      await dingus._serveFileMetaHeaders(res, directory, fileName);
+      const result = await dingus._serveFileMetaHeaders(res, directory, fileName);
       assert(!res.setHeader.called);
+      assert.strictEqual(result, false);
     });
     it('adds extra headers', async function () {
       dingus._readFileInfo.resolves([{}, Buffer.from(`Link: <https://example.com/>; rel="relation"
@@ -615,8 +783,9 @@ X-Folded-Header: data
   the fold
 Content-Type: image/sgi
 `)]);
-      await dingus._serveFileMetaHeaders(res, directory, fileName);
+      const result = await dingus._serveFileMetaHeaders(res, directory, fileName);
       assert(res.setHeader.called);
+      assert.strictEqual(result, true);
     });
   }); // _serveFileMetaHeaders
 
@@ -661,8 +830,8 @@ Content-Type: image/sgi
         size: 8,
         blocks: 17,
         atimeMs: 1613253436842.815,
-        mtimeMs: 1603485933192.8610,
-        ctimeMs: 1603485933192.8610,
+        mtimeMs: 1603485933192.861,
+        ctimeMs: 1603485933192.861,
         birthtimeMs: 0,
         atime: '2021-02-13T21:57:16.843Z',
         mtime: '2020-10-23T13:45:33.193Z',
@@ -701,6 +870,11 @@ Content-Type: image/sgi
       await dingus.serveFile(req, res, ctx, directory, fileName);
       assert(dingus.handlerNotFound.called);
     });
+    it('requires directory be specified', async function () {
+      await dingus.serveFile(req, res, ctx, '', fileName);
+      assert(!fs.promises.readFile.called);
+      assert(dingus.handlerNotFound.called);
+    });
     it('covers fs error', async function () {
       const expectedException = new Error('blah');
       fs.promises.stat.restore();
@@ -757,6 +931,13 @@ Content-Type: image/sgi
       await dingus.serveFile(req, res, ctx, directory, fileName);
       assert(res.end.called);
     });
+    it('handles misconfigured encoding', async function () {
+      Enum.EncodingType.Flarp = 'flarp';
+      req._headers[Enum.Header.AcceptEncoding] = 'flarp, gzip';
+      await dingus.serveFile(req, res, ctx, directory, fileName);
+      delete Enum.EncodingType.Flarp;
+      assert(res.end.called);
+    });
   }); // serveFile
 
   describe('renderError', function () {
@@ -905,4 +1086,54 @@ Content-Type: image/sgi
       assert(pfxDingus.handlerNotFound.called);
     });
   }); // proxyPrefix
-});
\ No newline at end of file
+
+  describe('handlerRedirect', function () {
+    let req, res, ctx;
+    beforeEach(function () {
+      req = {
+        getHeader: sinon.stub(),
+      };
+      res = {
+        setHeader: sinon.stub(),
+        end: sinon.stub(),
+      };
+      ctx = {};
+    });
+    it('covers', async function () {
+      await dingus.handlerRedirect(req, res, ctx);
+      assert(res.setHeader.called);
+      assert(res.end.called);
+    });
+    it('covers non-defaults', async function () {
+      await dingus.handlerRedirect(req, res, ctx, 308);
+      assert(res.setHeader.called);
+      assert(res.end.called);
+    });
+  }); // handlerRedirect
+
+  describe('handlerGetStaticFile', function () {
+    let req, res, ctx;
+    beforeEach(function () {
+      req = {
+        getHeader: sinon.stub(),
+      };
+      res = {
+        setHeader: sinon.stub(),
+      };
+      ctx = {
+        params: {
+          file: '',
+        },
+      };
+      sinon.stub(dingus, 'serveFile');
+    });
+    it('covers', async function () {
+      await dingus.handlerGetStaticFile(req, res, ctx);
+      assert(dingus.serveFile.called);
+    });
+    it('covers specified file', async function () {
+      await dingus.handlerGetStaticFile(req, res, ctx, 'file.txt');
+      assert(dingus.serveFile.called);
+    });
+  }); // handlerGetStaticFile
+});