initial commit
[squeep-resource-authentication-module] / test / lib / resource-authenticator.js
diff --git a/test/lib/resource-authenticator.js b/test/lib/resource-authenticator.js
new file mode 100644 (file)
index 0000000..826c484
--- /dev/null
@@ -0,0 +1,228 @@
+/* eslint-env mocha */
+'use strict';
+
+const assert = require('assert');
+const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
+const { ResourceAuthenticator, ResourceAuthenticatorError } = require('../../lib/resource-authenticator');
+const Enum = require('../../lib/enum');
+const { ResponseError } = require('../../lib/errors');
+const StubLogger = require('../stub-logger');
+const StubDb = require('../stub-db');
+
+const noExpectedException = 'did not receive expected exception';
+
+describe('Resource Authenticator', function () {
+  let stubLogger, stubDb;
+  let options, ra;
+  let req, res, ctx;
+  beforeEach(function () {
+    stubLogger = new StubLogger();
+    stubLogger._reset();
+    stubDb = new StubDb();
+    stubDb._reset();
+    options = {};
+    ra = new ResourceAuthenticator(stubLogger, stubDb, options);
+    req = {
+      getHeader: sinon.stub(),
+    };
+    res = {
+      setHeader: sinon.stub(),
+    };
+    ctx = {
+      params: {},
+      parsedBody: {},
+      queryParams: {},
+      session: {},
+    };
+  });
+  afterEach(function () {
+    sinon.restore();
+  });
+
+  it('covers no options', function () {
+    ra = new ResourceAuthenticator(stubLogger, stubDb);
+  });
+
+  describe('required', function () {
+    let resource;
+    const validBearerHeader = 'Bearer bq7ZSLHkEeyakQAlkF9xSg:1648836029:xxxxx:fdUYC8Gqe0nAyX_-SWvRsPsx0UjY-vV-Ff0A52j6Zfw';
+    beforeEach(function () {
+      resource = {
+        secret: 'secrety',
+      };
+    });
+    it('requires auth header', async function () {
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('requires bearer token', async function () {
+      req.getHeader.returns('Basic Zm9vcABiYXJr');
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('requires proper bearer token', async function () {
+      req.getHeader.returns('Bearer Zm9vcABiYXJr');
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('requires identifier to exist', async function () {
+      req.getHeader.returns(validBearerHeader);
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('covers db failure', async function () {
+      const expected = new Error('oh no');
+      ra.db.resourceGet.rejects(expected);
+      req.getHeader.returns(validBearerHeader);
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected, noExpectedException);
+      }
+    });
+    it('requires timestamp within grace', async function () {
+      sinon.stub(ResourceAuthenticator, 'currentEpoch').get(() => 1648838184);
+      ra.db.resourceGet.resolves(resource);
+      req.getHeader.returns(validBearerHeader);
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('requires digest to match', async function () {
+      sinon.stub(ResourceAuthenticator, 'currentEpoch').get(() => 1648836031);
+      ra.db.resourceGet.resolves(resource);
+      req.getHeader.returns('Bearer bq7ZSLHkEeyakQAlkF9xSg:1648836029:xxxxx:invalid1M2j9wtoerc3Pqe6kRzqFrkrkwqdeYXG331Q');
+      try {
+        await ra.required(req, res, ctx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('succeeds', async function () {
+      sinon.stub(ResourceAuthenticator, 'currentEpoch').get(() => 1648836031);
+      ra.db.resourceGet.resolves(resource);
+      req.getHeader.returns(validBearerHeader);
+      await ra.required(req, res, ctx);
+      assert.strictEqual(ra.logger.debug.args[1][1], 'success');
+    });
+  }); // required
+
+  describe('currentEpoch', function () {
+    it('covers', function () {
+      const now = 1648836413503;
+      const expected = Math.ceil(now / 1000);
+      sinon.stub(Date, 'now').returns(now);
+      const result = ResourceAuthenticator.currentEpoch;
+      assert.strictEqual(result, expected);
+    });
+  }); // currentEpoch
+
+  describe('getSalt', function () {
+    it('covers', async function () {
+      const result = await ra.getSalt();
+      assert(result);
+      assert.strictEqual(result.length, 28);
+    });
+  }); // getSalt
+
+  describe('authenticate', function () {
+    it('covers', async function () {
+      const identifier = '6eaed948-b1e4-11ec-9a91-0025905f714a';
+      const secret = 'secrety';
+      sinon.stub(ResourceAuthenticator, 'currentEpoch').get(() => 1648836029);
+      sinon.stub(ra, 'getSalt').resolves('xxxxx');
+      const expected = 'bq7ZSLHkEeyakQAlkF9xSg:1648836029:xxxxx:fdUYC8Gqe0nAyX_-SWvRsPsx0UjY-vV-Ff0A52j6Zfw';
+      const result = await ra.authenticate(identifier, secret);
+      assert.strictEqual(result, expected);
+    });
+  }); // authenticate
+
+  describe('Identifier Compaction', function () {
+    it('reciprocates', function () {
+      const identifier = '6eaed948-b1e4-11ec-9a91-0025905f714a';
+      const smaller = ResourceAuthenticator.ensmallenIdentifier(identifier);
+      const bigger = ResourceAuthenticator.embiggenIdentifier(smaller);
+      assert.strictEqual(bigger, identifier);
+    });
+  }); // Identifier Compaction
+
+  describe('createDigest', function () {
+    let secret;
+    beforeEach(function () {
+      secret = 'secret';
+    });
+    it('creates empty digest', function () {
+      const result = ra.createDigest(secret);
+      const expected = '-eZuF5tnR65UEI-C-K3os8Jddv0wr95sOVgixTAZYWk';
+      assert.strictEqual(result, expected);
+    });
+    it('creates digest', function () {
+      const result = ra.createDigest(secret, 'data');
+      const expected = 'GywWt1vSqHDBFBU8zaW8_KYzFLxyL6Fg1pDeEzzLuds';
+      assert.strictEqual(result, expected);
+    });
+  }); // createDigest
+
+  describe('requestBearer', function () {
+    it('covers default response', function () {
+      try {
+        ResourceAuthenticator.requestBearer(res);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(res.setHeader.called);
+        assert.strictEqual(res.setHeader.args[0][0], 'WWW-Authenticate');
+        assert.strictEqual(res.setHeader.args[0][1], 'Bearer');
+        assert(e instanceof ResponseError, noExpectedException);
+        assert.strictEqual(e.statusCode, 401);
+      }
+    });
+    it('covers other response', function () {
+      try {
+        ResourceAuthenticator.requestBearer(res, Enum.ErrorResponse.Forbidden);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(res.setHeader.called);
+        assert.strictEqual(res.setHeader.args[0][0], 'WWW-Authenticate');
+        assert.strictEqual(res.setHeader.args[0][1], 'Bearer');
+        assert(e instanceof ResponseError);
+        assert.strictEqual(e.statusCode, 403);
+      }
+    });
+  }); // requestBearer
+
+  describe('ResourceAuthenticatorError', function () {
+    it('covers', function () {
+      const e = new ResourceAuthenticatorError();
+      const result = e.name;
+      assert.strictEqual(result, 'ResourceAuthenticatorError');
+    });
+  });
+
+}); // ResourceAuthenticator
\ No newline at end of file