3 const assert
= require('node:assert');
4 const sinon
= require('sinon');
6 const SessionManager
= require('../../lib/session-manager');
7 const Enum
= require('../../lib/enum');
8 const Config
= require('../stub-config');
9 const stubLogger
= require('../stub-logger');
10 const stubDb
= require('../stub-db');
12 describe('SessionManager', function () {
13 let manager
, options
, stubAuthenticator
;
16 beforeEach(function () {
18 options
= new Config('test');
21 setHeader: sinon
.stub(),
22 appendHeader: sinon
.stub(),
33 isValidIdentifierCredential: sinon
.stub(),
34 checkOTP: sinon
.stub(),
35 _validateAuthDataCredential: sinon
.stub(),
36 updateCredential: sinon
.stub(),
39 manager
= new SessionManager(stubLogger
, stubAuthenticator
, options
);
40 sinon
.stub(manager
.indieAuthCommunication
);
43 afterEach(function () {
47 describe('constructor', function () {
48 it('covers options', function () {
49 delete options
.dingus
.proxyPrefix
;
50 delete options
.authenticator
.secureAuthOnly
;
51 options
.authenticator
.sessionCookieSameSite
= 'None';
52 manager
= new SessionManager(stubLogger
, stubAuthenticator
, options
);
56 describe('_sessionCookieSet', function () {
58 beforeEach(function () {
62 it('covers', async
function () {
63 await manager
._sessionCookieSet(res
, session
, maxAge
);
64 assert(res
.appendHeader
.called
);
66 it('covers reset', async
function () {
69 await manager
._sessionCookieSet(res
, session
, maxAge
);
70 assert(res
.appendHeader
.called
);
72 it('covers options', async
function() {
73 options
.authenticator
.secureAuthOnly
= false;
74 await manager
._sessionCookieSet(res
, session
, 'none', '');
75 assert(res
.appendHeader
.called
);
77 }); // _sessionCookieSet
79 describe('_sessionCookieClear', function () {
80 it('covers', async
function () {
81 await manager
._sessionCookieClear(res
);
82 assert(res
.appendHeader
.called
);
84 }); // _sessionCookieClear
86 describe('getAdminLogin', function () {
87 it('covers no session', async
function () {
88 await manager
.getAdminLogin(res
, ctx
);
90 it('covers established session', async
function () {
91 ctx
.authenticationId
= 'identifier';
92 ctx
.queryParams
['r'] = '/admin';
93 await manager
.getAdminLogin(res
, ctx
);
94 assert
.strictEqual(res
.statusCode
, 302);
95 assert(res
.setHeader
.called
);
99 describe('postAdminLogin', function () {
100 beforeEach(function () {
101 sinon
.stub(manager
, '_otpSubmission').resolves(false);
103 it('covers otp submission', async
function () {
104 manager
._otpSubmission
.resolves(true);
105 await manager
.postAdminLogin(res
, ctx
);
106 assert(res
.end
.notCalled
);
108 it('covers valid local', async
function () {
109 ctx
.parsedBody
.identifier
= 'user';
110 ctx
.parsedBody
.credential
= 'password';
111 manager
.authenticator
.isValidIdentifierCredential
.resolves(true);
112 await manager
.postAdminLogin(res
, ctx
);
113 assert
.strictEqual(res
.statusCode
, 302);
115 it('covers invalid local', async
function () {
116 ctx
.parsedBody
.identifier
= 'user';
117 ctx
.parsedBody
.credential
= 'password';
118 manager
.authenticator
.isValidIdentifierCredential
.resolves(false);
119 await manager
.postAdminLogin(res
, ctx
);
120 assert(!res
.setHeader
.called
);
122 it('covers valid profile', async
function () {
123 ctx
.parsedBody
.me
= 'https://example.com/profile';
124 manager
.indieAuthCommunication
.fetchProfile
.resolves({
126 authorizationEndpoint: 'https://example.com/auth',
129 await manager
.postAdminLogin(res
, ctx
);
130 assert
.strictEqual(res
.statusCode
, 302);
132 it('covers invalid profile', async
function () {
133 ctx
.parsedBody
.me
= 'not a profile';
134 manager
.indieAuthCommunication
.fetchProfile
.resolves();
135 await manager
.postAdminLogin(res
, ctx
);
136 assert(!res
.setHeader
.called
);
138 it('covers invalid profile response', async
function () {
139 ctx
.parsedBody
.me
= 'https://example.com/profile';
140 manager
.indieAuthCommunication
.fetchProfile
.resolves();
141 await manager
.postAdminLogin(res
, ctx
);
142 assert(!res
.setHeader
.called
);
144 it('covers invalid profile response endpoint', async
function () {
145 ctx
.parsedBody
.me
= 'https://example.com/profile';
146 manager
.indieAuthCommunication
.fetchProfile
.resolves({
148 authorizationEndpoint: 'not an auth endpoint',
151 await manager
.postAdminLogin(res
, ctx
);
152 assert(!res
.setHeader
.called
);
154 it('covers profile scheme fallback', async
function () {
155 ctx
.parsedBody
.me
= 'https://example.com/profile';
156 ctx
.parsedBody
['me_auto_scheme'] = '1';
157 manager
.indieAuthCommunication
.fetchProfile
158 .onCall(0).resolves()
159 .onCall(1).resolves({
161 issuer: 'https://example.com/',
162 authorizationEndpoint: 'https://example.com/auth',
165 await manager
.postAdminLogin(res
, ctx
);
166 assert
.strictEqual(res
.statusCode
, 302);
169 describe('living-standard-20220212', function () {
170 it('covers valid profile', async
function () {
171 ctx
.parsedBody
.me
= 'https://example.com/profile';
172 manager
.indieAuthCommunication
.fetchProfile
.resolves({
174 issuer: 'https://example.com/',
175 authorizationEndpoint: 'https://example.com/auth',
178 await manager
.postAdminLogin(res
, ctx
);
179 assert
.strictEqual(res
.statusCode
, 302);
181 it('covers bad issuer url', async
function () {
182 ctx
.parsedBody
.me
= 'https://example.com/profile';
183 manager
.indieAuthCommunication
.fetchProfile
.resolves({
185 issuer: 'http://example.com/?bah#foo',
186 authorizationEndpoint: 'https://example.com/auth',
189 await manager
.postAdminLogin(res
, ctx
);
190 assert(!res
.setHeader
.called
);
192 it('covers unparsable issuer url', async
function () {
193 ctx
.parsedBody
.me
= 'https://example.com/profile';
194 manager
.indieAuthCommunication
.fetchProfile
.resolves({
197 authorizationEndpoint: 'https://example.com/auth',
200 await manager
.postAdminLogin(res
, ctx
);
201 assert(!res
.setHeader
.called
);
203 }); // living-standard-20220212
204 }); // postAdminLogin
206 describe('_otpSubmission', function () {
208 beforeEach(function () {
209 sinon
.useFakeTimers({ now: new Date(), toFake: ['Date'] });
211 authenticatedIdentifier: 'identifier',
212 key: '1234567890123456789012',
217 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(otpState
);
218 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.Valid
);
219 ctx
.parsedBody
.state
= 'state_data';
220 ctx
.parsedBody
.otp
= '123456';
222 it('returns false if no otp state', async
function () {
223 delete ctx
.parsedBody
.state
;
224 const result
= await manager
._otpSubmission(res
, ctx
);
225 assert(manager
.mysteryBox
.unpack
.notCalled
);
226 assert
.strictEqual(result
, false);
228 it('returns false when presented with invalid otp state', async
function () {
229 manager
.mysteryBox
.unpack
.rejects();
230 const result
= await manager
._otpSubmission(res
, ctx
);
231 assert(manager
.mysteryBox
.unpack
.called
);
232 assert
.strictEqual(result
, false);
234 it('returns false when otp state missing identifier field', async
function () {
235 delete otpState
.authenticatedIdentifier
;
236 manager
.mysteryBox
.unpack
.resolves(otpState
);
237 const result
= await manager
._otpSubmission(res
, ctx
);
238 assert(manager
.mysteryBox
.unpack
.called
);
239 assert
.strictEqual(result
, false);
241 it('returns false when otp state missing key field', async
function () {
243 manager
.mysteryBox
.unpack
.resolves(otpState
);
244 const result
= await manager
._otpSubmission(res
, ctx
);
245 assert(manager
.mysteryBox
.unpack
.called
);
246 assert
.strictEqual(result
, false);
248 it('returns false when otp state missing attempt field', async
function () {
249 delete otpState
.attempt
;
250 manager
.mysteryBox
.unpack
.resolves(otpState
);
251 const result
= await manager
._otpSubmission(res
, ctx
);
252 assert(manager
.mysteryBox
.unpack
.called
);
253 assert
.strictEqual(result
, false);
255 it('returns false when otp state missing epoch field', async
function () {
256 delete otpState
.epochMs
;
257 manager
.mysteryBox
.unpack
.resolves(otpState
);
258 const result
= await manager
._otpSubmission(res
, ctx
);
259 assert(manager
.mysteryBox
.unpack
.called
);
260 assert
.strictEqual(result
, false);
262 it('returns true when submitted otp is invalid, but allowed to retry', async
function () {
263 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidSoftFail
);
264 const result
= await manager
._otpSubmission(res
, ctx
);
265 assert(manager
.mysteryBox
.unpack
.called
);
266 assert
.strictEqual(result
, true);
267 assert(res
.end
.called
);
269 it('returns false when submitted otp is invalid and too many attempts', async
function () {
270 otpState
.attempt
= 10;
271 manager
.mysteryBox
.unpack
.resolves(otpState
);
272 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidHardFail
);
273 const result
= await manager
._otpSubmission(res
, ctx
);
274 assert(manager
.mysteryBox
.unpack
.called
);
275 assert
.strictEqual(result
, false);
277 it('returns false when submitted otp is invalid and too much time has passed', async
function () {
278 otpState
.epochMs
= Date
.now() - 99999999;
279 manager
.mysteryBox
.unpack
.resolves(otpState
);
280 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidHardFail
);
281 const result
= await manager
._otpSubmission(res
, ctx
);
282 assert(manager
.mysteryBox
.unpack
.called
);
283 assert
.strictEqual(result
, false);
285 it('returns true when no otp submitted', async
function () {
286 ctx
.parsedBody
.otp
= '';
287 const result
= await manager
._otpSubmission(res
, ctx
);
288 assert(manager
.mysteryBox
.unpack
.called
);
289 assert
.strictEqual(result
, true);
290 assert(res
.end
.called
);
292 it('returns true when submitted otp is valid', async
function () {
293 const result
= await manager
._otpSubmission(res
, ctx
);
294 assert(res
.end
.called
);
295 assert
.strictEqual(result
, true);
297 it('covers unexpected otp response', async
function () {
298 manager
.authenticator
.checkOTP
.resolves('wrong');
299 assert
.rejects(() => manager
._otpSubmission(res
, ctx
), RangeError
);
301 }); // _otpSubmission
303 describe('_validateOTPState', function () {
305 it('covers valid', function () {
307 authenticatedIdentifier: 'identifier',
308 key: '1234567890123456789012',
313 SessionManager
._validateOTPState(otpState
);
315 it('covers missing identifier', function () {
317 authenticatedIdentifier: '',
318 key: '1234567890123456789012',
323 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
325 it('covers missing key', function () {
327 authenticatedIdentifier: 'identifier',
333 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
335 it('covers missing attempt', function () {
337 authenticatedIdentifier: 'identifier',
338 key: '1234567890123456789012',
342 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
344 it('covers missing epoch', function () {
346 authenticatedIdentifier: 'identifier',
347 key: '1234567890123456789012',
351 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
353 it('covers missing redirect', function () {
355 authenticatedIdentifier: 'identifier',
356 key: '1234567890123456789012',
360 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
362 }); // _validateOTPState
364 describe('_localUserAuth', function () {
365 beforeEach(function () {
366 ctx
.parsedBody
.identifier
= 'identifier';
367 ctx
.parsedBody
.credential
= 'credential';
368 manager
.authenticator
.isValidIdentifierCredential
.resolves(true);
369 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('box');
371 it('returns false if indieauth available', async
function () {
372 ctx
.parsedBody
.me
= 'https://example.com/';
373 const result
= await manager
._localUserAuth(res
, ctx
);
374 assert
.strictEqual(result
, false);
376 it('returns true if identifier is invalid', async
function () {
377 manager
.authenticator
.isValidIdentifierCredential
.resolves(false);
378 const result
= await manager
._localUserAuth(res
, ctx
);
379 assert
.strictEqual(result
, true);
380 assert(manager
.authenticator
.isValidIdentifierCredential
.called
);
381 assert(res
.end
.called
);
383 it('returns true if valid identifier', async
function () {
384 const result
= await manager
._localUserAuth(res
, ctx
);
385 assert
.strictEqual(result
, true);
386 assert(res
.end
.called
);
388 it('returns true if valid identifier requires otp entry', async
function () {
389 ctx
.otpKey
= '1234567890123456789012';
390 const result
= await manager
._localUserAuth(res
, ctx
);
391 assert
.strictEqual(result
, true);
392 assert(manager
.mysteryBox
.pack
.called
);
393 assert(res
.end
.called
);
395 }); // _localUserAuth
397 describe('getAdminLogout', function () {
398 it('covers', async
function () {
399 await manager
.getAdminLogout(res
, ctx
);
401 }); // getAdminLogout
403 describe('getAdminIA', function () {
404 let state
, me
, authorizationEndpoint
;
405 beforeEach(function () {
406 state
= '4ea7e936-3427-11ec-9f4b-0025905f714a';
407 me
= 'https://example.com/profile';
408 authorizationEndpoint
= 'https://example.com/auth';
410 squeepSession: 'sessionCookie',
412 manager
.indieAuthCommunication
.redeemProfileCode
.resolves({
415 manager
.indieAuthCommunication
.fetchProfile
.resolves({
417 authorizationEndpoint
,
420 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
421 authorizationEndpoint
,
426 it('covers valid', async
function () {
427 ctx
.queryParams
['state'] = state
;
428 ctx
.queryParams
['code'] = 'codeCodeCode';
430 await manager
.getAdminIA(res
, ctx
);
432 assert
.strictEqual(res
.statusCode
, 302);
434 it('covers missing cookie', async
function () {
437 await manager
.getAdminIA(res
, ctx
);
439 assert(ctx
.errors
.length
);
441 it('covers invalid cookie', async
function () {
442 manager
.mysteryBox
.unpack
.restore();
443 sinon
.stub(manager
.mysteryBox
, 'unpack').rejects();
445 await manager
.getAdminIA(res
, ctx
);
447 assert(ctx
.errors
.length
);
449 it('covers mis-matched state', async
function () {
450 ctx
.queryParams
['state'] = 'incorrect-state';
451 ctx
.queryParams
['code'] = 'codeCodeCode';
453 await manager
.getAdminIA(res
, ctx
);
455 assert(ctx
.errors
.length
);
457 it('relays auth endpoint errors', async
function () {
458 ctx
.queryParams
['state'] = state
;
459 ctx
.queryParams
['code'] = 'codeCodeCode';
460 ctx
.queryParams
['error'] = 'error_code';
461 ctx
.queryParams
['error_description'] = 'something went wrong';
463 await manager
.getAdminIA(res
, ctx
);
465 assert(ctx
.errors
.length
);
467 it('covers empty error_description', async
function () {
468 ctx
.queryParams
['state'] = state
;
469 ctx
.queryParams
['code'] = 'codeCodeCode';
470 ctx
.queryParams
['error'] = 'error_code';
472 await manager
.getAdminIA(res
, ctx
);
474 assert(ctx
.errors
.length
);
476 it('covers invalid restored session', async
function () {
477 manager
.mysteryBox
.unpack
.restore();
478 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
479 authorizationEndpoint: 'not a url',
483 ctx
.queryParams
['state'] = state
;
484 ctx
.queryParams
['code'] = 'codeCodeCode';
486 await manager
.getAdminIA(res
, ctx
);
488 assert(ctx
.errors
.length
);
490 it('covers empty profile redemption response', async
function () {
491 ctx
.queryParams
['state'] = state
;
492 ctx
.queryParams
['code'] = 'codeCodeCode';
493 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
494 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves();
496 await manager
.getAdminIA(res
, ctx
);
498 assert(ctx
.errors
.length
);
500 it('covers missing profile in redemption response', async
function () {
501 ctx
.queryParams
['state'] = state
;
502 ctx
.queryParams
['code'] = 'codeCodeCode';
503 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
504 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
507 await manager
.getAdminIA(res
, ctx
);
509 assert(ctx
.errors
.length
);
511 it('covers different canonical profile response', async
function () {
512 ctx
.queryParams
['state'] = state
;
513 ctx
.queryParams
['code'] = 'codeCodeCode';
514 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
515 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
516 me: 'https://different.example.com/profile',
519 await manager
.getAdminIA(res
, ctx
);
521 assert
.strictEqual(res
.statusCode
, 302);
523 it('covers different canonical profile response mis-matched endpoint', async
function () {
524 ctx
.queryParams
['state'] = state
;
525 ctx
.queryParams
['code'] = 'codeCodeCode';
526 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
527 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
528 me: 'https://different.example.com/profile',
530 manager
.indieAuthCommunication
.fetchProfile
.restore();
531 sinon
.stub(manager
.indieAuthCommunication
, 'fetchProfile').resolves({
533 authorizationEndpoint: 'https://elsewhere.example.com/auth',
537 await manager
.getAdminIA(res
, ctx
);
539 assert(ctx
.errors
.length
);
541 describe('living-standard-20220212', function () {
542 beforeEach(function () {
543 manager
.indieAuthCommunication
.fetchProfile
.resolves({
545 authorizationEndpoint
,
546 issuer: 'https://example.com/',
549 manager
.mysteryBox
.unpack
.resolves({
550 authorizationEndpoint
,
551 issuer: 'https://example.com/',
556 it('covers valid', async
function () {
557 ctx
.queryParams
['state'] = state
;
558 ctx
.queryParams
['code'] = 'codeCodeCode';
559 ctx
.queryParams
['iss'] = 'https://example.com/';
561 await manager
.getAdminIA(res
, ctx
);
563 assert
.strictEqual(res
.statusCode
, 302);
565 it('covers mis-matched issuer', async
function () {
566 ctx
.queryParams
['state'] = state
;
567 ctx
.queryParams
['code'] = 'codeCodeCode';
569 await manager
.getAdminIA(res
, ctx
);
571 assert(ctx
.errors
.length
);
573 }); // living-standard-20220212
576 describe('getAdminSettings', function () {
577 it('covers success', async
function () {
578 manager
.db
.authenticationGet
.resolves({});
579 await manager
.getAdminSettings(res
, ctx
);
580 assert(!ctx
.errors
.length
);
582 it('covers no user', async
function () {
583 manager
.db
.authenticationGet
.resolves();
584 await manager
.getAdminSettings(res
, ctx
);
585 assert(ctx
.errors
.length
);
587 it('covers db failure', async
function () {
588 manager
.db
.authenticationGet
.throws();
589 await manager
.getAdminSettings(res
, ctx
);
590 assert(ctx
.errors
.length
);
592 }); // getAdminSettings
594 describe('postAdminSettings', function () {
596 beforeEach(function () {
599 credential: 'password',
600 otpKey: '12345678901234567890123456789012',
602 manager
.db
.authenticationGet
.resolves(authData
);
603 sinon
.stub(manager
, '_credentialUpdate');
604 sinon
.stub(manager
, '_otpEnable');
605 sinon
.stub(manager
, '_otpConfirm');
606 sinon
.stub(manager
, '_otpDisable');
608 it('covers no action', async
function () {
609 await manager
.postAdminSettings(res
, ctx
);
610 assert(!ctx
.errors
.length
);
612 it('covers db empty', async
function () {
613 manager
.db
.authenticationGet
.resolves();
614 await manager
.postAdminSettings(res
, ctx
);
615 assert(ctx
.errors
.length
);
617 it('covers db error', async
function () {
618 manager
.db
.authenticationGet
.throws();
619 await manager
.postAdminSettings(res
, ctx
);
620 assert(ctx
.errors
.length
);
622 it('covers credential update', async
function () {
623 ctx
.parsedBody
.credential
= 'update';
624 await manager
.postAdminSettings(res
, ctx
);
625 assert(manager
._credentialUpdate
.called
);
627 it('covers otp enabling', async
function () {
628 ctx
.parsedBody
.otp
= 'enable';
629 await manager
.postAdminSettings(res
, ctx
);
630 assert(manager
._otpEnable
.called
);
632 it('covers otp confirmation', async
function () {
633 ctx
.parsedBody
.otp
= 'confirm';
634 await manager
.postAdminSettings(res
, ctx
);
635 assert(manager
._otpConfirm
.called
);
637 it('covers otp disabling', async
function () {
638 ctx
.parsedBody
.otp
= 'disable';
639 await manager
.postAdminSettings(res
, ctx
);
640 assert(manager
._otpDisable
.called
);
642 }); // postAdminSettings
644 describe('_otpDisable', function () {
646 beforeEach(function () {
647 ctx
.otpKey
= '12345678901234567890123456789012';
650 otpKey: '12345678901234567890123456789012',
653 it('covers success', async
function () {
654 await manager
._otpDisable(dbCtx
, ctx
, authData
);
656 assert(!authData
.otpKey
);
657 assert(manager
.db
.authenticationUpdateOTPKey
.called
);
658 assert(ctx
.notifications
.length
);
659 assert(!ctx
.errors
.length
);
661 it('covers db failure', async
function () {
662 manager
.db
.authenticationUpdateOTPKey
.throws();
663 await manager
._otpDisable(dbCtx
, ctx
, authData
);
664 assert(!ctx
.notifications
.length
);
665 assert(ctx
.errors
.length
);
669 describe('_otpEnsable', function () {
670 it('covers success', async
function () {
671 await manager
._otpEnable(ctx
);
672 assert('otpConfirmKey' in ctx
);
673 assert('otpConfirmBox' in ctx
);
674 assert(!ctx
.errors
.length
);
676 it('covers failure', async
function () {
677 sinon
.stub(manager
.mysteryBox
, 'pack').throws();
678 await manager
._otpEnable(ctx
);
679 assert(!('otpConfirmKey' in ctx
));
680 assert(!('otpConfirmBox' in ctx
));
681 assert(ctx
.errors
.length
);
685 describe('_otpConfirm', function () {
687 beforeEach(function () {
688 sinon
.stub(Date
, 'now').returns(1710435655000);
691 'otp-box': 'xxxBoxedStatexxx',
692 'otp-token': '350876',
695 otpKey: 'CDBGB3U3B2ILECQORMINGGSZN7LXY565',
697 otpInitiatedMs: 1710434052084,
699 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(otpState
);
701 it('covers success', async
function () {
702 await manager
._otpConfirm(dbCtx
, ctx
);
703 assert(manager
.db
.authenticationUpdateOTPKey
.called
);
704 assert(ctx
.notifications
.length
);
705 assert(!ctx
.errors
.length
);
707 it('covers bad state', async
function () {
708 manager
.mysteryBox
.unpack
.throws();
709 await manager
._otpConfirm(dbCtx
, ctx
);
710 assert(ctx
.errors
.length
);
711 assert(manager
.db
.authenticationUpdateOTPKey
.notCalled
);
713 it('covers no token entered', async
function () {
714 ctx
.parsedBody
['otp-token'] = '';
715 await manager
._otpConfirm(dbCtx
, ctx
);
716 assert(!ctx
.errors
.length
);
717 assert(manager
.db
.authenticationUpdateOTPKey
.notCalled
);
719 it('covers bad token entered', async
function () {
720 ctx
.parsedBody
['otp-token'] = '123456';
721 await manager
._otpConfirm(dbCtx
, ctx
);
722 assert(ctx
.errors
.length
);
723 assert(manager
.db
.authenticationUpdateOTPKey
.notCalled
);
725 it('covers db error', async
function () {
726 manager
.db
.authenticationUpdateOTPKey
.throws();
727 await manager
._otpConfirm(dbCtx
, ctx
);
728 assert(ctx
.errors
.length
);
732 describe('_credentialUpdate', function () {
734 beforeEach(function () {
736 'credential-new': 'abc',
737 'credential-new-2': 'abc',
738 'credential-current': '123',
741 manager
.authenticator
._validateAuthDataCredential
.resolves(true);
743 it('covers success', async
function () {
744 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
745 assert(ctx
.notifications
.length
);
746 assert(!ctx
.errors
.length
);
748 it('covers invalid current password', async
function () {
749 manager
.authenticator
._validateAuthDataCredential
.resolves(false);
750 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
751 assert(!ctx
.notifications
.length
);
752 assert(ctx
.errors
.length
);
754 it('covers empty new password', async
function () {
755 delete ctx
.parsedBody
['credential-new'];
756 manager
.authenticator
._validateAuthDataCredential
.resolves(false);
757 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
758 assert(!ctx
.notifications
.length
);
759 assert(ctx
.errors
.length
);
761 it('covers mismatched new password', async
function () {
762 ctx
.parsedBody
['credential-new'] = 'cde';
763 manager
.authenticator
._validateAuthDataCredential
.resolves(false);
764 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
765 assert(!ctx
.notifications
.length
);
766 assert(ctx
.errors
.length
);
768 it('covers db failure', async
function () {
769 manager
.authenticator
.updateCredential
.throws();
770 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
771 assert(!ctx
.notifications
.length
);
772 assert(ctx
.errors
.length
);
773 assert(manager
.logger
.error
.called
);
775 }); // _credentialUpdate
777 }); // SessionManager