--- /dev/null
+/* 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