const Enum = require('../../lib/enum');
const Config = require('../stub-config');
const stubLogger = require('../stub-logger');
+const stubDb = require('../stub-db');
describe('SessionManager', function () {
let manager, options, stubAuthenticator;
let res, ctx;
beforeEach(function () {
+ stubDb._reset();
options = new Config('test');
res = {
end: sinon.stub(),
setHeader: sinon.stub(),
+ appendHeader: sinon.stub(),
};
ctx = {
cookie: '',
queryParams: {},
parsedBody: {},
errors: [],
+ notifications: [],
};
stubAuthenticator = {
isValidIdentifierCredential: sinon.stub(),
checkOTP: sinon.stub(),
+ _validateAuthDataCredential: sinon.stub(),
+ updateCredential: sinon.stub(),
+ db: stubDb,
};
manager = new SessionManager(stubLogger, stubAuthenticator, options);
sinon.stub(manager.indieAuthCommunication);
sinon.restore();
});
+ describe('constructor', function () {
+ it('covers options', function () {
+ delete options.dingus.proxyPrefix;
+ manager = new SessionManager(stubLogger, stubAuthenticator, options);
+ });
+ }); // constructor
+
describe('_sessionCookieSet', function () {
let session, maxAge;
beforeEach(function () {
});
it('covers', async function () {
await manager._sessionCookieSet(res, session, maxAge);
- assert(res.setHeader.called);
+ assert(res.appendHeader.called);
});
it('covers reset', async function () {
session = undefined;
maxAge = 0;
await manager._sessionCookieSet(res, session, maxAge);
- assert(res.setHeader.called);
+ assert(res.appendHeader.called);
});
it('covers options', async function() {
options.authenticator.secureAuthOnly = false;
- await manager._sessionCookieSet(res, session, undefined, '');
- assert(res.setHeader.called);
+ await manager._sessionCookieSet(res, session, 'none', '');
+ assert(res.appendHeader.called);
});
}); // _sessionCookieSet
+ describe('_sessionCookieClear', function () {
+ it('covers', async function () {
+ await manager._sessionCookieClear(res);
+ assert(res.appendHeader.called);
+ })
+ }); // _sessionCookieClear
+
describe('getAdminLogin', function () {
it('covers no session', async function () {
await manager.getAdminLogin(res, ctx);
}); // postAdminLogin
describe('_otpSubmission', function () {
+ let otpState;
beforeEach(function () {
sinon.useFakeTimers(new Date());
- sinon.stub(manager.mysteryBox, 'unpack').resolves({
+ otpState = {
authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
attempt: 0,
epochMs: Date.now(),
- });
+ redirect: '/',
+ };
+ sinon.stub(manager.mysteryBox, 'unpack').resolves(otpState);
manager.authenticator.checkOTP.resolves(Enum.OTPResult.Valid);
ctx.parsedBody.state = 'state_data';
ctx.parsedBody.otp = '123456';
assert(manager.mysteryBox.unpack.called);
assert.strictEqual(result, false);
});
+ it('returns false when otp state missing identifier field', async function () {
+ delete otpState.authenticatedIdentifier;
+ manager.mysteryBox.unpack.resolves(otpState);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns false when otp state missing key field', async function () {
+ delete otpState.key;
+ manager.mysteryBox.unpack.resolves(otpState);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns false when otp state missing attempt field', async function () {
+ delete otpState.attempt;
+ manager.mysteryBox.unpack.resolves(otpState);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
+ it('returns false when otp state missing epoch field', async function () {
+ delete otpState.epochMs;
+ manager.mysteryBox.unpack.resolves(otpState);
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, false);
+ });
it('returns true when submitted otp is invalid, but allowed to retry', async function () {
manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidSoftFail);
const result = await manager._otpSubmission(res, ctx);
assert(res.end.called);
});
it('returns false when submitted otp is invalid and too many attempts', async function () {
- manager.mysteryBox.unpack.resolves({
- authenticatedIdentifier: 'identifier',
- attempt: 10,
- epochMs: Date.now(),
- });
+ otpState.attempt = 10;
+ manager.mysteryBox.unpack.resolves(otpState);
manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidHardFail);
const result = await manager._otpSubmission(res, ctx);
assert(manager.mysteryBox.unpack.called);
assert.strictEqual(result, false);
});
it('returns false when submitted otp is invalid and too much time has passed', async function () {
- manager.mysteryBox.unpack.resolves({
- authenticatedIdentifier: 'identifier',
- attempt: 0,
- epochMs: Date.now() - 99999999,
- });
+ otpState.epochMs = Date.now() - 99999999;
+ manager.mysteryBox.unpack.resolves(otpState);
manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidHardFail);
const result = await manager._otpSubmission(res, ctx);
assert(manager.mysteryBox.unpack.called);
assert.strictEqual(result, false);
});
+ it('returns true when no otp submitted', async function () {
+ ctx.parsedBody.otp = '';
+ const result = await manager._otpSubmission(res, ctx);
+ assert(manager.mysteryBox.unpack.called);
+ assert.strictEqual(result, true);
+ assert(res.end.called);
+ });
it('returns true when submitted otp is valid', async function () {
const result = await manager._otpSubmission(res, ctx);
assert(res.end.called);
});
}); // _otpSubmission
+ describe('_validateOTPState', function () {
+ let otpState;
+ it('covers valid', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
+ attempt: 0,
+ epochMs: Date.now(),
+ redirect: '/',
+ };
+ SessionManager._validateOTPState(otpState);
+ });
+ it('covers missing identifier', function () {
+ otpState = {
+ authenticatedIdentifier: '',
+ key: '1234567890123456789012',
+ attempt: 0,
+ epochMs: Date.now(),
+ redirect: '/',
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ it('covers missing key', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '',
+ attempt: 0,
+ epochMs: Date.now(),
+ redirect: '/',
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ it('covers missing attempt', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
+ epochMs: Date.now(),
+ redirect: '/',
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ it('covers missing epoch', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
+ attempt: 0,
+ redirect: '/',
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ it('covers missing redirect', function () {
+ otpState = {
+ authenticatedIdentifier: 'identifier',
+ key: '1234567890123456789012',
+ attempt: 0,
+ epochMs: Date.now(),
+ };
+ assert.throws(() => SessionManager._validateOTPState(otpState));
+ });
+ }); // _validateOTPState
+
describe('_localUserAuth', function () {
beforeEach(function () {
ctx.parsedBody.identifier = 'identifier';
assert(res.end.called);
});
it('returns true if valid identifier requires otp entry', async function () {
- ctx.otpNeeded = true;
+ ctx.otpKey = '1234567890123456789012';
const result = await manager._localUserAuth(res, ctx);
assert.strictEqual(result, true);
assert(manager.mysteryBox.pack.called);
state = '4ea7e936-3427-11ec-9f4b-0025905f714a';
me = 'https://example.com/profile';
authorizationEndpoint = 'https://example.com/auth'
- ctx.cookie = 'squeepSession=sessionCookie';
+ ctx.cookie = {
+ squeepSession: 'sessionCookie',
+ };
manager.indieAuthCommunication.redeemProfileCode.resolves({
me,
});
}); // living-standard-20220212
}); // getAdminIA
-}); // SessionManager
\ No newline at end of file
+ describe('getAdminSettings', function () {
+ it('covers success', async function () {
+ manager.db.authenticationGet.resolves({});
+ await manager.getAdminSettings(res, ctx);
+ assert(!ctx.errors.length);
+ });
+ it('covers no user', async function () {
+ manager.db.authenticationGet.resolves();
+ await manager.getAdminSettings(res, ctx);
+ assert(ctx.errors.length);
+ });
+ it('covers db failure', async function () {
+ manager.db.authenticationGet.throws();
+ await manager.getAdminSettings(res, ctx);
+ assert(ctx.errors.length);
+ });
+ }); // getAdminSettings
+
+ describe('postAdminSettings', function () {
+ let authData;
+ beforeEach(function () {
+ authData = {
+ identifier: 'user',
+ credential: 'password',
+ otpKey: '12345678901234567890123456789012',
+ };
+ manager.db.authenticationGet.resolves(authData);
+ sinon.stub(manager, '_credentialUpdate');
+ sinon.stub(manager, '_otpEnable');
+ sinon.stub(manager, '_otpConfirm');
+ sinon.stub(manager, '_otpDisable');
+ });
+ it('covers no action', async function () {
+ await manager.postAdminSettings(res, ctx);
+ assert(!ctx.errors.length);
+ });
+ it('covers db empty', async function () {
+ manager.db.authenticationGet.resolves();
+ await manager.postAdminSettings(res, ctx);
+ assert(ctx.errors.length);
+ });
+ it('covers db error', async function () {
+ manager.db.authenticationGet.throws();
+ await manager.postAdminSettings(res, ctx);
+ assert(ctx.errors.length);
+ });
+ it('covers credential update', async function () {
+ ctx.parsedBody.credential = 'update';
+ await manager.postAdminSettings(res, ctx);
+ assert(manager._credentialUpdate.called);
+ });
+ it('covers otp enabling', async function () {
+ ctx.parsedBody.otp = 'enable';
+ await manager.postAdminSettings(res, ctx);
+ assert(manager._otpEnable.called);
+ });
+ it('covers otp confirmation', async function () {
+ ctx.parsedBody.otp = 'confirm';
+ await manager.postAdminSettings(res, ctx);
+ assert(manager._otpConfirm.called);
+ });
+ it('covers otp disabling', async function () {
+ ctx.parsedBody.otp = 'disable';
+ await manager.postAdminSettings(res, ctx);
+ assert(manager._otpDisable.called);
+ });
+ }); // postAdminSettings
+
+ describe('_otpDisable', function () {
+ let dbCtx, authData;
+ beforeEach(function () {
+ ctx.otpKey = '12345678901234567890123456789012';
+ dbCtx = {};
+ authData = {
+ otpKey: '12345678901234567890123456789012',
+ };
+ });
+ it('covers success', async function () {
+ await manager._otpDisable(dbCtx, ctx, authData);
+ assert(!ctx.otpKey);
+ assert(!authData.otpKey);
+ assert(manager.db.authenticationUpdateOTPKey.called);
+ assert(ctx.notifications.length);
+ assert(!ctx.errors.length);
+ });
+ it('covers db failure', async function () {
+ manager.db.authenticationUpdateOTPKey.throws();
+ await manager._otpDisable(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ });
+ }); // _otpDisable
+
+ describe('_otpEnsable', function () {
+ it('covers success', async function () {
+ await manager._otpEnable(ctx);
+ assert('otpConfirmKey' in ctx);
+ assert('otpConfirmBox' in ctx);
+ assert(!ctx.errors.length);
+ });
+ it('covers failure', async function () {
+ sinon.stub(manager.mysteryBox, 'pack').throws();
+ await manager._otpEnable(ctx);
+ assert(!('otpConfirmKey' in ctx));
+ assert(!('otpConfirmBox' in ctx));
+ assert(ctx.errors.length);
+ });
+ }); // _otpEnsable
+
+ describe('_otpConfirm', function () {
+ let dbCtx, otpState;
+ beforeEach(function () {
+ sinon.stub(Date, 'now').returns(1710435655000);
+ dbCtx = {};
+ ctx.parsedBody = {
+ 'otp-box': 'xxxBoxedStatexxx',
+ 'otp-token': '350876',
+ };
+ otpState = {
+ otpKey: 'CDBGB3U3B2ILECQORMINGGSZN7LXY565',
+ otpAttempt: 0,
+ otpInitiatedMs: 1710434052084,
+ };
+ sinon.stub(manager.mysteryBox, 'unpack').resolves(otpState);
+ });
+ it('covers success', async function () {
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(manager.db.authenticationUpdateOTPKey.called);
+ assert(ctx.notifications.length);
+ assert(!ctx.errors.length);
+ });
+ it('covers bad state', async function () {
+ manager.mysteryBox.unpack.throws();
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(ctx.errors.length);
+ assert(manager.db.authenticationUpdateOTPKey.notCalled);
+ });
+ it('covers no token entered', async function () {
+ ctx.parsedBody['otp-token'] = '';
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(!ctx.errors.length);
+ assert(manager.db.authenticationUpdateOTPKey.notCalled);
+ });
+ it('covers bad token entered', async function () {
+ ctx.parsedBody['otp-token'] = '123456';
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(ctx.errors.length);
+ assert(manager.db.authenticationUpdateOTPKey.notCalled);
+ });
+ it('covers db error', async function () {
+ manager.db.authenticationUpdateOTPKey.throws();
+ await manager._otpConfirm(dbCtx, ctx);
+ assert(ctx.errors.length);
+ });
+ }); // _otpConfirm
+
+ describe('_credentialUpdate', function () {
+ let dbCtx, authData;
+ beforeEach(function () {
+ ctx.parsedBody = {
+ 'credential-new': 'abc',
+ 'credential-new-2': 'abc',
+ 'credential-current': '123',
+ };
+ authData = {};
+ manager.authenticator._validateAuthDataCredential.resolves(true);
+ });
+ it('covers success', async function () {
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(ctx.notifications.length);
+ assert(!ctx.errors.length);
+ });
+ it('covers invalid current password', async function () {
+ manager.authenticator._validateAuthDataCredential.resolves(false);
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ });
+ it('covers empty new password', async function () {
+ delete ctx.parsedBody['credential-new'];
+ manager.authenticator._validateAuthDataCredential.resolves(false);
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ });
+ it('covers mismatched new password', async function () {
+ ctx.parsedBody['credential-new'] = 'cde';
+ manager.authenticator._validateAuthDataCredential.resolves(false);
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ });
+ it('covers db failure', async function () {
+ manager.authenticator.updateCredential.throws();
+ await manager._credentialUpdate(dbCtx, ctx, authData);
+ assert(!ctx.notifications.length);
+ assert(ctx.errors.length);
+ assert(manager.logger.error.called);
+ });
+ }); // _credentialUpdate
+
+}); // SessionManager