X-Git-Url: http://git.squeep.com/?p=squeep-authentication-module;a=blobdiff_plain;f=test%2Flib%2Fsession-manager.js;h=f0aca6d8e339ad1329d125c340ae47dd4425dea6;hp=79abbf007e8805ab05018aa77a5aae541489c62c;hb=53ef948ea83106e82d55e60d6695a15e94bf725e;hpb=842a3da269de1ab82e9a2a12aae8ed5677f064d8 diff --git a/test/lib/session-manager.js b/test/lib/session-manager.js index 79abbf0..f0aca6d 100644 --- a/test/lib/session-manager.js +++ b/test/lib/session-manager.js @@ -10,16 +10,19 @@ const SessionManager = require('../../lib/session-manager'); 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: '', @@ -27,10 +30,14 @@ describe('SessionManager', function () { 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); @@ -40,6 +47,13 @@ describe('SessionManager', function () { 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 () { @@ -48,21 +62,28 @@ describe('SessionManager', 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); @@ -184,13 +205,17 @@ describe('SessionManager', function () { }); // 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'; @@ -207,6 +232,34 @@ describe('SessionManager', function () { 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); @@ -215,27 +268,28 @@ describe('SessionManager', function () { 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); @@ -247,6 +301,67 @@ describe('SessionManager', function () { }); }); // _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'; @@ -272,7 +387,7 @@ describe('SessionManager', function () { 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); @@ -292,7 +407,9 @@ describe('SessionManager', function () { 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, }); @@ -457,4 +574,205 @@ describe('SessionManager', function () { }); // 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