2 /* eslint-disable sonarjs/no-duplicate-string */
6 const assert
= require('assert');
7 const sinon
= require('sinon');
9 const SessionManager
= require('../../lib/session-manager');
10 const Enum
= require('../../lib/enum');
11 const Config
= require('../stub-config');
12 const stubLogger
= require('../stub-logger');
13 const stubDb
= require('../stub-db');
15 describe('SessionManager', function () {
16 let manager
, options
, stubAuthenticator
;
19 beforeEach(function () {
21 options
= new Config('test');
24 setHeader: sinon
.stub(),
25 appendHeader: sinon
.stub(),
36 isValidIdentifierCredential: sinon
.stub(),
37 checkOTP: sinon
.stub(),
38 _validateAuthDataCredential: sinon
.stub(),
39 updateCredential: sinon
.stub(),
42 manager
= new SessionManager(stubLogger
, stubAuthenticator
, options
);
43 sinon
.stub(manager
.indieAuthCommunication
);
46 afterEach(function () {
50 describe('constructor', function () {
51 it('covers options', function () {
52 delete options
.dingus
.proxyPrefix
;
53 manager
= new SessionManager(stubLogger
, stubAuthenticator
, options
);
57 describe('_sessionCookieSet', function () {
59 beforeEach(function () {
63 it('covers', async
function () {
64 await manager
._sessionCookieSet(res
, session
, maxAge
);
65 assert(res
.appendHeader
.called
);
67 it('covers reset', async
function () {
70 await manager
._sessionCookieSet(res
, session
, maxAge
);
71 assert(res
.appendHeader
.called
);
73 it('covers options', async
function() {
74 options
.authenticator
.secureAuthOnly
= false;
75 await manager
._sessionCookieSet(res
, session
, 'none', '');
76 assert(res
.appendHeader
.called
);
78 }); // _sessionCookieSet
80 describe('_sessionCookieClear', function () {
81 it('covers', async
function () {
82 await manager
._sessionCookieClear(res
);
83 assert(res
.appendHeader
.called
);
85 }); // _sessionCookieClear
87 describe('getAdminLogin', function () {
88 it('covers no session', async
function () {
89 await manager
.getAdminLogin(res
, ctx
);
91 it('covers established session', async
function () {
92 ctx
.authenticationId
= 'identifier';
93 ctx
.queryParams
['r'] = '/admin';
94 await manager
.getAdminLogin(res
, ctx
);
95 assert
.strictEqual(res
.statusCode
, 302);
96 assert(res
.setHeader
.called
);
100 describe('postAdminLogin', function () {
101 beforeEach(function () {
102 sinon
.stub(manager
, '_otpSubmission').resolves(false);
104 it('covers otp submission', async
function () {
105 manager
._otpSubmission
.resolves(true);
106 await manager
.postAdminLogin(res
, ctx
);
107 assert(res
.end
.notCalled
);
109 it('covers valid local', async
function () {
110 ctx
.parsedBody
.identifier
= 'user';
111 ctx
.parsedBody
.credential
= 'password';
112 manager
.authenticator
.isValidIdentifierCredential
.resolves(true);
113 await manager
.postAdminLogin(res
, ctx
);
114 assert
.strictEqual(res
.statusCode
, 302);
116 it('covers invalid local', async
function () {
117 ctx
.parsedBody
.identifier
= 'user';
118 ctx
.parsedBody
.credential
= 'password';
119 manager
.authenticator
.isValidIdentifierCredential
.resolves(false);
120 await manager
.postAdminLogin(res
, ctx
);
121 assert(!res
.setHeader
.called
);
123 it('covers valid profile', async
function () {
124 ctx
.parsedBody
.me
= 'https://example.com/profile';
125 manager
.indieAuthCommunication
.fetchProfile
.resolves({
127 authorizationEndpoint: 'https://example.com/auth',
130 await manager
.postAdminLogin(res
, ctx
);
131 assert
.strictEqual(res
.statusCode
, 302);
133 it('covers invalid profile', async
function () {
134 ctx
.parsedBody
.me
= 'not a profile';
135 manager
.indieAuthCommunication
.fetchProfile
.resolves();
136 await manager
.postAdminLogin(res
, ctx
);
137 assert(!res
.setHeader
.called
);
139 it('covers invalid profile response', async
function () {
140 ctx
.parsedBody
.me
= 'https://example.com/profile';
141 manager
.indieAuthCommunication
.fetchProfile
.resolves();
142 await manager
.postAdminLogin(res
, ctx
);
143 assert(!res
.setHeader
.called
);
145 it('covers invalid profile response endpoint', async
function () {
146 ctx
.parsedBody
.me
= 'https://example.com/profile';
147 manager
.indieAuthCommunication
.fetchProfile
.resolves({
149 authorizationEndpoint: 'not an auth endpoint',
152 await manager
.postAdminLogin(res
, ctx
);
153 assert(!res
.setHeader
.called
);
155 it('covers profile scheme fallback', async
function () {
156 ctx
.parsedBody
.me
= 'https://example.com/profile';
157 ctx
.parsedBody
['me_auto_scheme'] = '1';
158 manager
.indieAuthCommunication
.fetchProfile
159 .onCall(0).resolves()
160 .onCall(1).resolves({
162 issuer: 'https://example.com/',
163 authorizationEndpoint: 'https://example.com/auth',
166 await manager
.postAdminLogin(res
, ctx
);
167 assert
.strictEqual(res
.statusCode
, 302);
170 describe('living-standard-20220212', function () {
171 it('covers valid profile', async
function () {
172 ctx
.parsedBody
.me
= 'https://example.com/profile';
173 manager
.indieAuthCommunication
.fetchProfile
.resolves({
175 issuer: 'https://example.com/',
176 authorizationEndpoint: 'https://example.com/auth',
179 await manager
.postAdminLogin(res
, ctx
);
180 assert
.strictEqual(res
.statusCode
, 302);
182 it('covers bad issuer url', async
function () {
183 ctx
.parsedBody
.me
= 'https://example.com/profile';
184 manager
.indieAuthCommunication
.fetchProfile
.resolves({
186 issuer: 'http://example.com/?bah#foo',
187 authorizationEndpoint: 'https://example.com/auth',
190 await manager
.postAdminLogin(res
, ctx
);
191 assert(!res
.setHeader
.called
);
193 it('covers unparsable issuer url', async
function () {
194 ctx
.parsedBody
.me
= 'https://example.com/profile';
195 manager
.indieAuthCommunication
.fetchProfile
.resolves({
198 authorizationEndpoint: 'https://example.com/auth',
201 await manager
.postAdminLogin(res
, ctx
);
202 assert(!res
.setHeader
.called
);
204 }); // living-standard-20220212
205 }); // postAdminLogin
207 describe('_otpSubmission', function () {
209 beforeEach(function () {
210 sinon
.useFakeTimers(new Date());
212 authenticatedIdentifier: 'identifier',
213 key: '1234567890123456789012',
218 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(otpState
);
219 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.Valid
);
220 ctx
.parsedBody
.state
= 'state_data';
221 ctx
.parsedBody
.otp
= '123456';
223 it('returns false if no otp state', async
function () {
224 delete ctx
.parsedBody
.state
;
225 const result
= await manager
._otpSubmission(res
, ctx
);
226 assert(manager
.mysteryBox
.unpack
.notCalled
);
227 assert
.strictEqual(result
, false);
229 it('returns false when presented with invalid otp state', async
function () {
230 manager
.mysteryBox
.unpack
.rejects();
231 const result
= await manager
._otpSubmission(res
, ctx
);
232 assert(manager
.mysteryBox
.unpack
.called
);
233 assert
.strictEqual(result
, false);
235 it('returns false when otp state missing identifier field', async
function () {
236 delete otpState
.authenticatedIdentifier
;
237 manager
.mysteryBox
.unpack
.resolves(otpState
);
238 const result
= await manager
._otpSubmission(res
, ctx
);
239 assert(manager
.mysteryBox
.unpack
.called
);
240 assert
.strictEqual(result
, false);
242 it('returns false when otp state missing key field', async
function () {
244 manager
.mysteryBox
.unpack
.resolves(otpState
);
245 const result
= await manager
._otpSubmission(res
, ctx
);
246 assert(manager
.mysteryBox
.unpack
.called
);
247 assert
.strictEqual(result
, false);
249 it('returns false when otp state missing attempt field', async
function () {
250 delete otpState
.attempt
;
251 manager
.mysteryBox
.unpack
.resolves(otpState
);
252 const result
= await manager
._otpSubmission(res
, ctx
);
253 assert(manager
.mysteryBox
.unpack
.called
);
254 assert
.strictEqual(result
, false);
256 it('returns false when otp state missing epoch field', async
function () {
257 delete otpState
.epochMs
;
258 manager
.mysteryBox
.unpack
.resolves(otpState
);
259 const result
= await manager
._otpSubmission(res
, ctx
);
260 assert(manager
.mysteryBox
.unpack
.called
);
261 assert
.strictEqual(result
, false);
263 it('returns true when submitted otp is invalid, but allowed to retry', async
function () {
264 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidSoftFail
);
265 const result
= await manager
._otpSubmission(res
, ctx
);
266 assert(manager
.mysteryBox
.unpack
.called
);
267 assert
.strictEqual(result
, true);
268 assert(res
.end
.called
);
270 it('returns false when submitted otp is invalid and too many attempts', async
function () {
271 otpState
.attempt
= 10;
272 manager
.mysteryBox
.unpack
.resolves(otpState
);
273 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidHardFail
);
274 const result
= await manager
._otpSubmission(res
, ctx
);
275 assert(manager
.mysteryBox
.unpack
.called
);
276 assert
.strictEqual(result
, false);
278 it('returns false when submitted otp is invalid and too much time has passed', async
function () {
279 otpState
.epochMs
= Date
.now() - 99999999;
280 manager
.mysteryBox
.unpack
.resolves(otpState
);
281 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidHardFail
);
282 const result
= await manager
._otpSubmission(res
, ctx
);
283 assert(manager
.mysteryBox
.unpack
.called
);
284 assert
.strictEqual(result
, false);
286 it('returns true when no otp submitted', async
function () {
287 ctx
.parsedBody
.otp
= '';
288 const result
= await manager
._otpSubmission(res
, ctx
);
289 assert(manager
.mysteryBox
.unpack
.called
);
290 assert
.strictEqual(result
, true);
291 assert(res
.end
.called
);
293 it('returns true when submitted otp is valid', async
function () {
294 const result
= await manager
._otpSubmission(res
, ctx
);
295 assert(res
.end
.called
);
296 assert
.strictEqual(result
, true);
298 it('covers unexpected otp response', async
function () {
299 manager
.authenticator
.checkOTP
.resolves('wrong');
300 assert
.rejects(() => manager
._otpSubmission(res
, ctx
), RangeError
);
302 }); // _otpSubmission
304 describe('_validateOTPState', function () {
306 it('covers valid', function () {
308 authenticatedIdentifier: 'identifier',
309 key: '1234567890123456789012',
314 SessionManager
._validateOTPState(otpState
);
316 it('covers missing identifier', function () {
318 authenticatedIdentifier: '',
319 key: '1234567890123456789012',
324 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
326 it('covers missing key', function () {
328 authenticatedIdentifier: 'identifier',
334 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
336 it('covers missing attempt', function () {
338 authenticatedIdentifier: 'identifier',
339 key: '1234567890123456789012',
343 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
345 it('covers missing epoch', function () {
347 authenticatedIdentifier: 'identifier',
348 key: '1234567890123456789012',
352 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
354 it('covers missing redirect', function () {
356 authenticatedIdentifier: 'identifier',
357 key: '1234567890123456789012',
361 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
363 }); // _validateOTPState
365 describe('_localUserAuth', function () {
366 beforeEach(function () {
367 ctx
.parsedBody
.identifier
= 'identifier';
368 ctx
.parsedBody
.credential
= 'credential';
369 manager
.authenticator
.isValidIdentifierCredential
.resolves(true);
370 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('box');
372 it('returns false if indieauth available', async
function () {
373 ctx
.parsedBody
.me
= 'https://example.com/';
374 const result
= await manager
._localUserAuth(res
, ctx
);
375 assert
.strictEqual(result
, false);
377 it('returns true if identifier is invalid', async
function () {
378 manager
.authenticator
.isValidIdentifierCredential
.resolves(false);
379 const result
= await manager
._localUserAuth(res
, ctx
);
380 assert
.strictEqual(result
, true);
381 assert(manager
.authenticator
.isValidIdentifierCredential
.called
);
382 assert(res
.end
.called
);
384 it('returns true if valid identifier', async
function () {
385 const result
= await manager
._localUserAuth(res
, ctx
);
386 assert
.strictEqual(result
, true);
387 assert(res
.end
.called
);
389 it('returns true if valid identifier requires otp entry', async
function () {
390 ctx
.otpKey
= '1234567890123456789012';
391 const result
= await manager
._localUserAuth(res
, ctx
);
392 assert
.strictEqual(result
, true);
393 assert(manager
.mysteryBox
.pack
.called
);
394 assert(res
.end
.called
);
396 }); // _localUserAuth
398 describe('getAdminLogout', function () {
399 it('covers', async
function () {
400 await manager
.getAdminLogout(res
, ctx
);
402 }); // getAdminLogout
404 describe('getAdminIA', function () {
405 let state
, me
, authorizationEndpoint
;
406 beforeEach(function () {
407 state
= '4ea7e936-3427-11ec-9f4b-0025905f714a';
408 me
= 'https://example.com/profile';
409 authorizationEndpoint
= 'https://example.com/auth';
411 squeepSession: 'sessionCookie',
413 manager
.indieAuthCommunication
.redeemProfileCode
.resolves({
416 manager
.indieAuthCommunication
.fetchProfile
.resolves({
418 authorizationEndpoint
,
421 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
422 authorizationEndpoint
,
427 it('covers valid', async
function () {
428 ctx
.queryParams
['state'] = state
;
429 ctx
.queryParams
['code'] = 'codeCodeCode';
431 await manager
.getAdminIA(res
, ctx
);
433 assert
.strictEqual(res
.statusCode
, 302);
435 it('covers missing cookie', async
function () {
438 await manager
.getAdminIA(res
, ctx
);
440 assert(ctx
.errors
.length
);
442 it('covers invalid cookie', async
function () {
443 manager
.mysteryBox
.unpack
.restore();
444 sinon
.stub(manager
.mysteryBox
, 'unpack').rejects();
446 await manager
.getAdminIA(res
, ctx
);
448 assert(ctx
.errors
.length
);
450 it('covers mis-matched state', async
function () {
451 ctx
.queryParams
['state'] = 'incorrect-state';
452 ctx
.queryParams
['code'] = 'codeCodeCode';
454 await manager
.getAdminIA(res
, ctx
);
456 assert(ctx
.errors
.length
);
458 it('relays auth endpoint errors', async
function () {
459 ctx
.queryParams
['state'] = state
;
460 ctx
.queryParams
['code'] = 'codeCodeCode';
461 ctx
.queryParams
['error'] = 'error_code';
462 ctx
.queryParams
['error_description'] = 'something went wrong';
464 await manager
.getAdminIA(res
, ctx
);
466 assert(ctx
.errors
.length
);
468 it('covers empty error_description', async
function () {
469 ctx
.queryParams
['state'] = state
;
470 ctx
.queryParams
['code'] = 'codeCodeCode';
471 ctx
.queryParams
['error'] = 'error_code';
473 await manager
.getAdminIA(res
, ctx
);
475 assert(ctx
.errors
.length
);
477 it('covers invalid restored session', async
function () {
478 manager
.mysteryBox
.unpack
.restore();
479 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
480 authorizationEndpoint: 'not a url',
484 ctx
.queryParams
['state'] = state
;
485 ctx
.queryParams
['code'] = 'codeCodeCode';
487 await manager
.getAdminIA(res
, ctx
);
489 assert(ctx
.errors
.length
);
491 it('covers empty profile redemption response', async
function () {
492 ctx
.queryParams
['state'] = state
;
493 ctx
.queryParams
['code'] = 'codeCodeCode';
494 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
495 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves();
497 await manager
.getAdminIA(res
, ctx
);
499 assert(ctx
.errors
.length
);
501 it('covers missing profile in redemption response', async
function () {
502 ctx
.queryParams
['state'] = state
;
503 ctx
.queryParams
['code'] = 'codeCodeCode';
504 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
505 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
508 await manager
.getAdminIA(res
, ctx
);
510 assert(ctx
.errors
.length
);
512 it('covers different canonical profile response', async
function () {
513 ctx
.queryParams
['state'] = state
;
514 ctx
.queryParams
['code'] = 'codeCodeCode';
515 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
516 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
517 me: 'https://different.example.com/profile',
520 await manager
.getAdminIA(res
, ctx
);
522 assert
.strictEqual(res
.statusCode
, 302);
524 it('covers different canonical profile response mis-matched endpoint', async
function () {
525 ctx
.queryParams
['state'] = state
;
526 ctx
.queryParams
['code'] = 'codeCodeCode';
527 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
528 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
529 me: 'https://different.example.com/profile',
531 manager
.indieAuthCommunication
.fetchProfile
.restore();
532 sinon
.stub(manager
.indieAuthCommunication
, 'fetchProfile').resolves({
534 authorizationEndpoint: 'https://elsewhere.example.com/auth',
538 await manager
.getAdminIA(res
, ctx
);
540 assert(ctx
.errors
.length
);
542 describe('living-standard-20220212', function () {
543 beforeEach(function () {
544 manager
.indieAuthCommunication
.fetchProfile
.resolves({
546 authorizationEndpoint
,
547 issuer: 'https://example.com/',
550 manager
.mysteryBox
.unpack
.resolves({
551 authorizationEndpoint
,
552 issuer: 'https://example.com/',
557 it('covers valid', async
function () {
558 ctx
.queryParams
['state'] = state
;
559 ctx
.queryParams
['code'] = 'codeCodeCode';
560 ctx
.queryParams
['iss'] = 'https://example.com/';
562 await manager
.getAdminIA(res
, ctx
);
564 assert
.strictEqual(res
.statusCode
, 302);
566 it('covers mis-matched issuer', async
function () {
567 ctx
.queryParams
['state'] = state
;
568 ctx
.queryParams
['code'] = 'codeCodeCode';
570 await manager
.getAdminIA(res
, ctx
);
572 assert(ctx
.errors
.length
);
574 }); // living-standard-20220212
577 describe('getAdminSettings', function () {
578 it('covers success', async
function () {
579 manager
.db
.authenticationGet
.resolves({});
580 await manager
.getAdminSettings(res
, ctx
);
581 assert(!ctx
.errors
.length
);
583 it('covers no user', async
function () {
584 manager
.db
.authenticationGet
.resolves();
585 await manager
.getAdminSettings(res
, ctx
);
586 assert(ctx
.errors
.length
);
588 it('covers db failure', async
function () {
589 manager
.db
.authenticationGet
.throws();
590 await manager
.getAdminSettings(res
, ctx
);
591 assert(ctx
.errors
.length
);
593 }); // getAdminSettings
595 describe('postAdminSettings', function () {
597 beforeEach(function () {
600 credential: 'password',
601 otpKey: '12345678901234567890123456789012',
603 manager
.db
.authenticationGet
.resolves(authData
);
604 sinon
.stub(manager
, '_credentialUpdate');
605 sinon
.stub(manager
, '_otpEnable');
606 sinon
.stub(manager
, '_otpConfirm');
607 sinon
.stub(manager
, '_otpDisable');
609 it('covers no action', async
function () {
610 await manager
.postAdminSettings(res
, ctx
);
611 assert(!ctx
.errors
.length
);
613 it('covers db empty', async
function () {
614 manager
.db
.authenticationGet
.resolves();
615 await manager
.postAdminSettings(res
, ctx
);
616 assert(ctx
.errors
.length
);
618 it('covers db error', async
function () {
619 manager
.db
.authenticationGet
.throws();
620 await manager
.postAdminSettings(res
, ctx
);
621 assert(ctx
.errors
.length
);
623 it('covers credential update', async
function () {
624 ctx
.parsedBody
.credential
= 'update';
625 await manager
.postAdminSettings(res
, ctx
);
626 assert(manager
._credentialUpdate
.called
);
628 it('covers otp enabling', async
function () {
629 ctx
.parsedBody
.otp
= 'enable';
630 await manager
.postAdminSettings(res
, ctx
);
631 assert(manager
._otpEnable
.called
);
633 it('covers otp confirmation', async
function () {
634 ctx
.parsedBody
.otp
= 'confirm';
635 await manager
.postAdminSettings(res
, ctx
);
636 assert(manager
._otpConfirm
.called
);
638 it('covers otp disabling', async
function () {
639 ctx
.parsedBody
.otp
= 'disable';
640 await manager
.postAdminSettings(res
, ctx
);
641 assert(manager
._otpDisable
.called
);
643 }); // postAdminSettings
645 describe('_otpDisable', function () {
647 beforeEach(function () {
648 ctx
.otpKey
= '12345678901234567890123456789012';
651 otpKey: '12345678901234567890123456789012',
654 it('covers success', async
function () {
655 await manager
._otpDisable(dbCtx
, ctx
, authData
);
657 assert(!authData
.otpKey
);
658 assert(manager
.db
.authenticationUpdateOTPKey
.called
);
659 assert(ctx
.notifications
.length
);
660 assert(!ctx
.errors
.length
);
662 it('covers db failure', async
function () {
663 manager
.db
.authenticationUpdateOTPKey
.throws();
664 await manager
._otpDisable(dbCtx
, ctx
, authData
);
665 assert(!ctx
.notifications
.length
);
666 assert(ctx
.errors
.length
);
670 describe('_otpEnsable', function () {
671 it('covers success', async
function () {
672 await manager
._otpEnable(ctx
);
673 assert('otpConfirmKey' in ctx
);
674 assert('otpConfirmBox' in ctx
);
675 assert(!ctx
.errors
.length
);
677 it('covers failure', async
function () {
678 sinon
.stub(manager
.mysteryBox
, 'pack').throws();
679 await manager
._otpEnable(ctx
);
680 assert(!('otpConfirmKey' in ctx
));
681 assert(!('otpConfirmBox' in ctx
));
682 assert(ctx
.errors
.length
);
686 describe('_otpConfirm', function () {
688 beforeEach(function () {
689 sinon
.stub(Date
, 'now').returns(1710435655000);
692 'otp-box': 'xxxBoxedStatexxx',
693 'otp-token': '350876',
696 otpKey: 'CDBGB3U3B2ILECQORMINGGSZN7LXY565',
698 otpInitiatedMs: 1710434052084,
700 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(otpState
);
702 it('covers success', async
function () {
703 await manager
._otpConfirm(dbCtx
, ctx
);
704 assert(manager
.db
.authenticationUpdateOTPKey
.called
);
705 assert(ctx
.notifications
.length
);
706 assert(!ctx
.errors
.length
);
708 it('covers bad state', async
function () {
709 manager
.mysteryBox
.unpack
.throws();
710 await manager
._otpConfirm(dbCtx
, ctx
);
711 assert(ctx
.errors
.length
);
712 assert(manager
.db
.authenticationUpdateOTPKey
.notCalled
);
714 it('covers no token entered', async
function () {
715 ctx
.parsedBody
['otp-token'] = '';
716 await manager
._otpConfirm(dbCtx
, ctx
);
717 assert(!ctx
.errors
.length
);
718 assert(manager
.db
.authenticationUpdateOTPKey
.notCalled
);
720 it('covers bad token entered', async
function () {
721 ctx
.parsedBody
['otp-token'] = '123456';
722 await manager
._otpConfirm(dbCtx
, ctx
);
723 assert(ctx
.errors
.length
);
724 assert(manager
.db
.authenticationUpdateOTPKey
.notCalled
);
726 it('covers db error', async
function () {
727 manager
.db
.authenticationUpdateOTPKey
.throws();
728 await manager
._otpConfirm(dbCtx
, ctx
);
729 assert(ctx
.errors
.length
);
733 describe('_credentialUpdate', function () {
735 beforeEach(function () {
737 'credential-new': 'abc',
738 'credential-new-2': 'abc',
739 'credential-current': '123',
742 manager
.authenticator
._validateAuthDataCredential
.resolves(true);
744 it('covers success', async
function () {
745 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
746 assert(ctx
.notifications
.length
);
747 assert(!ctx
.errors
.length
);
749 it('covers invalid current password', async
function () {
750 manager
.authenticator
._validateAuthDataCredential
.resolves(false);
751 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
752 assert(!ctx
.notifications
.length
);
753 assert(ctx
.errors
.length
);
755 it('covers empty new password', async
function () {
756 delete ctx
.parsedBody
['credential-new'];
757 manager
.authenticator
._validateAuthDataCredential
.resolves(false);
758 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
759 assert(!ctx
.notifications
.length
);
760 assert(ctx
.errors
.length
);
762 it('covers mismatched new password', async
function () {
763 ctx
.parsedBody
['credential-new'] = 'cde';
764 manager
.authenticator
._validateAuthDataCredential
.resolves(false);
765 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
766 assert(!ctx
.notifications
.length
);
767 assert(ctx
.errors
.length
);
769 it('covers db failure', async
function () {
770 manager
.authenticator
.updateCredential
.throws();
771 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
772 assert(!ctx
.notifications
.length
);
773 assert(ctx
.errors
.length
);
774 assert(manager
.logger
.error
.called
);
776 }); // _credentialUpdate
778 }); // SessionManager