2 /* eslint-disable sonarjs/no-duplicate-string */
6 const assert
= require('node: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 delete options
.authenticator
.secureAuthOnly
;
54 options
.authenticator
.sessionCookieSameSite
= 'None';
55 manager
= new SessionManager(stubLogger
, stubAuthenticator
, options
);
59 describe('_sessionCookieSet', function () {
61 beforeEach(function () {
65 it('covers', async
function () {
66 await manager
._sessionCookieSet(res
, session
, maxAge
);
67 assert(res
.appendHeader
.called
);
69 it('covers reset', async
function () {
72 await manager
._sessionCookieSet(res
, session
, maxAge
);
73 assert(res
.appendHeader
.called
);
75 it('covers options', async
function() {
76 options
.authenticator
.secureAuthOnly
= false;
77 await manager
._sessionCookieSet(res
, session
, 'none', '');
78 assert(res
.appendHeader
.called
);
80 }); // _sessionCookieSet
82 describe('_sessionCookieClear', function () {
83 it('covers', async
function () {
84 await manager
._sessionCookieClear(res
);
85 assert(res
.appendHeader
.called
);
87 }); // _sessionCookieClear
89 describe('getAdminLogin', function () {
90 it('covers no session', async
function () {
91 await manager
.getAdminLogin(res
, ctx
);
93 it('covers established session', async
function () {
94 ctx
.authenticationId
= 'identifier';
95 ctx
.queryParams
['r'] = '/admin';
96 await manager
.getAdminLogin(res
, ctx
);
97 assert
.strictEqual(res
.statusCode
, 302);
98 assert(res
.setHeader
.called
);
102 describe('postAdminLogin', function () {
103 beforeEach(function () {
104 sinon
.stub(manager
, '_otpSubmission').resolves(false);
106 it('covers otp submission', async
function () {
107 manager
._otpSubmission
.resolves(true);
108 await manager
.postAdminLogin(res
, ctx
);
109 assert(res
.end
.notCalled
);
111 it('covers valid local', async
function () {
112 ctx
.parsedBody
.identifier
= 'user';
113 ctx
.parsedBody
.credential
= 'password';
114 manager
.authenticator
.isValidIdentifierCredential
.resolves(true);
115 await manager
.postAdminLogin(res
, ctx
);
116 assert
.strictEqual(res
.statusCode
, 302);
118 it('covers invalid local', async
function () {
119 ctx
.parsedBody
.identifier
= 'user';
120 ctx
.parsedBody
.credential
= 'password';
121 manager
.authenticator
.isValidIdentifierCredential
.resolves(false);
122 await manager
.postAdminLogin(res
, ctx
);
123 assert(!res
.setHeader
.called
);
125 it('covers valid profile', async
function () {
126 ctx
.parsedBody
.me
= 'https://example.com/profile';
127 manager
.indieAuthCommunication
.fetchProfile
.resolves({
129 authorizationEndpoint: 'https://example.com/auth',
132 await manager
.postAdminLogin(res
, ctx
);
133 assert
.strictEqual(res
.statusCode
, 302);
135 it('covers invalid profile', async
function () {
136 ctx
.parsedBody
.me
= 'not a profile';
137 manager
.indieAuthCommunication
.fetchProfile
.resolves();
138 await manager
.postAdminLogin(res
, ctx
);
139 assert(!res
.setHeader
.called
);
141 it('covers invalid profile response', async
function () {
142 ctx
.parsedBody
.me
= 'https://example.com/profile';
143 manager
.indieAuthCommunication
.fetchProfile
.resolves();
144 await manager
.postAdminLogin(res
, ctx
);
145 assert(!res
.setHeader
.called
);
147 it('covers invalid profile response endpoint', async
function () {
148 ctx
.parsedBody
.me
= 'https://example.com/profile';
149 manager
.indieAuthCommunication
.fetchProfile
.resolves({
151 authorizationEndpoint: 'not an auth endpoint',
154 await manager
.postAdminLogin(res
, ctx
);
155 assert(!res
.setHeader
.called
);
157 it('covers profile scheme fallback', async
function () {
158 ctx
.parsedBody
.me
= 'https://example.com/profile';
159 ctx
.parsedBody
['me_auto_scheme'] = '1';
160 manager
.indieAuthCommunication
.fetchProfile
161 .onCall(0).resolves()
162 .onCall(1).resolves({
164 issuer: 'https://example.com/',
165 authorizationEndpoint: 'https://example.com/auth',
168 await manager
.postAdminLogin(res
, ctx
);
169 assert
.strictEqual(res
.statusCode
, 302);
172 describe('living-standard-20220212', function () {
173 it('covers valid profile', async
function () {
174 ctx
.parsedBody
.me
= 'https://example.com/profile';
175 manager
.indieAuthCommunication
.fetchProfile
.resolves({
177 issuer: 'https://example.com/',
178 authorizationEndpoint: 'https://example.com/auth',
181 await manager
.postAdminLogin(res
, ctx
);
182 assert
.strictEqual(res
.statusCode
, 302);
184 it('covers bad issuer url', async
function () {
185 ctx
.parsedBody
.me
= 'https://example.com/profile';
186 manager
.indieAuthCommunication
.fetchProfile
.resolves({
188 issuer: 'http://example.com/?bah#foo',
189 authorizationEndpoint: 'https://example.com/auth',
192 await manager
.postAdminLogin(res
, ctx
);
193 assert(!res
.setHeader
.called
);
195 it('covers unparsable issuer url', async
function () {
196 ctx
.parsedBody
.me
= 'https://example.com/profile';
197 manager
.indieAuthCommunication
.fetchProfile
.resolves({
200 authorizationEndpoint: 'https://example.com/auth',
203 await manager
.postAdminLogin(res
, ctx
);
204 assert(!res
.setHeader
.called
);
206 }); // living-standard-20220212
207 }); // postAdminLogin
209 describe('_otpSubmission', function () {
211 beforeEach(function () {
212 sinon
.useFakeTimers(new Date());
214 authenticatedIdentifier: 'identifier',
215 key: '1234567890123456789012',
220 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(otpState
);
221 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.Valid
);
222 ctx
.parsedBody
.state
= 'state_data';
223 ctx
.parsedBody
.otp
= '123456';
225 it('returns false if no otp state', async
function () {
226 delete ctx
.parsedBody
.state
;
227 const result
= await manager
._otpSubmission(res
, ctx
);
228 assert(manager
.mysteryBox
.unpack
.notCalled
);
229 assert
.strictEqual(result
, false);
231 it('returns false when presented with invalid otp state', async
function () {
232 manager
.mysteryBox
.unpack
.rejects();
233 const result
= await manager
._otpSubmission(res
, ctx
);
234 assert(manager
.mysteryBox
.unpack
.called
);
235 assert
.strictEqual(result
, false);
237 it('returns false when otp state missing identifier field', async
function () {
238 delete otpState
.authenticatedIdentifier
;
239 manager
.mysteryBox
.unpack
.resolves(otpState
);
240 const result
= await manager
._otpSubmission(res
, ctx
);
241 assert(manager
.mysteryBox
.unpack
.called
);
242 assert
.strictEqual(result
, false);
244 it('returns false when otp state missing key field', async
function () {
246 manager
.mysteryBox
.unpack
.resolves(otpState
);
247 const result
= await manager
._otpSubmission(res
, ctx
);
248 assert(manager
.mysteryBox
.unpack
.called
);
249 assert
.strictEqual(result
, false);
251 it('returns false when otp state missing attempt field', async
function () {
252 delete otpState
.attempt
;
253 manager
.mysteryBox
.unpack
.resolves(otpState
);
254 const result
= await manager
._otpSubmission(res
, ctx
);
255 assert(manager
.mysteryBox
.unpack
.called
);
256 assert
.strictEqual(result
, false);
258 it('returns false when otp state missing epoch field', async
function () {
259 delete otpState
.epochMs
;
260 manager
.mysteryBox
.unpack
.resolves(otpState
);
261 const result
= await manager
._otpSubmission(res
, ctx
);
262 assert(manager
.mysteryBox
.unpack
.called
);
263 assert
.strictEqual(result
, false);
265 it('returns true when submitted otp is invalid, but allowed to retry', async
function () {
266 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidSoftFail
);
267 const result
= await manager
._otpSubmission(res
, ctx
);
268 assert(manager
.mysteryBox
.unpack
.called
);
269 assert
.strictEqual(result
, true);
270 assert(res
.end
.called
);
272 it('returns false when submitted otp is invalid and too many attempts', async
function () {
273 otpState
.attempt
= 10;
274 manager
.mysteryBox
.unpack
.resolves(otpState
);
275 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidHardFail
);
276 const result
= await manager
._otpSubmission(res
, ctx
);
277 assert(manager
.mysteryBox
.unpack
.called
);
278 assert
.strictEqual(result
, false);
280 it('returns false when submitted otp is invalid and too much time has passed', async
function () {
281 otpState
.epochMs
= Date
.now() - 99999999;
282 manager
.mysteryBox
.unpack
.resolves(otpState
);
283 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidHardFail
);
284 const result
= await manager
._otpSubmission(res
, ctx
);
285 assert(manager
.mysteryBox
.unpack
.called
);
286 assert
.strictEqual(result
, false);
288 it('returns true when no otp submitted', async
function () {
289 ctx
.parsedBody
.otp
= '';
290 const result
= await manager
._otpSubmission(res
, ctx
);
291 assert(manager
.mysteryBox
.unpack
.called
);
292 assert
.strictEqual(result
, true);
293 assert(res
.end
.called
);
295 it('returns true when submitted otp is valid', async
function () {
296 const result
= await manager
._otpSubmission(res
, ctx
);
297 assert(res
.end
.called
);
298 assert
.strictEqual(result
, true);
300 it('covers unexpected otp response', async
function () {
301 manager
.authenticator
.checkOTP
.resolves('wrong');
302 assert
.rejects(() => manager
._otpSubmission(res
, ctx
), RangeError
);
304 }); // _otpSubmission
306 describe('_validateOTPState', function () {
308 it('covers valid', function () {
310 authenticatedIdentifier: 'identifier',
311 key: '1234567890123456789012',
316 SessionManager
._validateOTPState(otpState
);
318 it('covers missing identifier', function () {
320 authenticatedIdentifier: '',
321 key: '1234567890123456789012',
326 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
328 it('covers missing key', function () {
330 authenticatedIdentifier: 'identifier',
336 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
338 it('covers missing attempt', function () {
340 authenticatedIdentifier: 'identifier',
341 key: '1234567890123456789012',
345 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
347 it('covers missing epoch', function () {
349 authenticatedIdentifier: 'identifier',
350 key: '1234567890123456789012',
354 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
356 it('covers missing redirect', function () {
358 authenticatedIdentifier: 'identifier',
359 key: '1234567890123456789012',
363 assert
.throws(() => SessionManager
._validateOTPState(otpState
));
365 }); // _validateOTPState
367 describe('_localUserAuth', function () {
368 beforeEach(function () {
369 ctx
.parsedBody
.identifier
= 'identifier';
370 ctx
.parsedBody
.credential
= 'credential';
371 manager
.authenticator
.isValidIdentifierCredential
.resolves(true);
372 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('box');
374 it('returns false if indieauth available', async
function () {
375 ctx
.parsedBody
.me
= 'https://example.com/';
376 const result
= await manager
._localUserAuth(res
, ctx
);
377 assert
.strictEqual(result
, false);
379 it('returns true if identifier is invalid', async
function () {
380 manager
.authenticator
.isValidIdentifierCredential
.resolves(false);
381 const result
= await manager
._localUserAuth(res
, ctx
);
382 assert
.strictEqual(result
, true);
383 assert(manager
.authenticator
.isValidIdentifierCredential
.called
);
384 assert(res
.end
.called
);
386 it('returns true if valid identifier', async
function () {
387 const result
= await manager
._localUserAuth(res
, ctx
);
388 assert
.strictEqual(result
, true);
389 assert(res
.end
.called
);
391 it('returns true if valid identifier requires otp entry', async
function () {
392 ctx
.otpKey
= '1234567890123456789012';
393 const result
= await manager
._localUserAuth(res
, ctx
);
394 assert
.strictEqual(result
, true);
395 assert(manager
.mysteryBox
.pack
.called
);
396 assert(res
.end
.called
);
398 }); // _localUserAuth
400 describe('getAdminLogout', function () {
401 it('covers', async
function () {
402 await manager
.getAdminLogout(res
, ctx
);
404 }); // getAdminLogout
406 describe('getAdminIA', function () {
407 let state
, me
, authorizationEndpoint
;
408 beforeEach(function () {
409 state
= '4ea7e936-3427-11ec-9f4b-0025905f714a';
410 me
= 'https://example.com/profile';
411 authorizationEndpoint
= 'https://example.com/auth';
413 squeepSession: 'sessionCookie',
415 manager
.indieAuthCommunication
.redeemProfileCode
.resolves({
418 manager
.indieAuthCommunication
.fetchProfile
.resolves({
420 authorizationEndpoint
,
423 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
424 authorizationEndpoint
,
429 it('covers valid', async
function () {
430 ctx
.queryParams
['state'] = state
;
431 ctx
.queryParams
['code'] = 'codeCodeCode';
433 await manager
.getAdminIA(res
, ctx
);
435 assert
.strictEqual(res
.statusCode
, 302);
437 it('covers missing cookie', async
function () {
440 await manager
.getAdminIA(res
, ctx
);
442 assert(ctx
.errors
.length
);
444 it('covers invalid cookie', async
function () {
445 manager
.mysteryBox
.unpack
.restore();
446 sinon
.stub(manager
.mysteryBox
, 'unpack').rejects();
448 await manager
.getAdminIA(res
, ctx
);
450 assert(ctx
.errors
.length
);
452 it('covers mis-matched state', async
function () {
453 ctx
.queryParams
['state'] = 'incorrect-state';
454 ctx
.queryParams
['code'] = 'codeCodeCode';
456 await manager
.getAdminIA(res
, ctx
);
458 assert(ctx
.errors
.length
);
460 it('relays auth endpoint errors', async
function () {
461 ctx
.queryParams
['state'] = state
;
462 ctx
.queryParams
['code'] = 'codeCodeCode';
463 ctx
.queryParams
['error'] = 'error_code';
464 ctx
.queryParams
['error_description'] = 'something went wrong';
466 await manager
.getAdminIA(res
, ctx
);
468 assert(ctx
.errors
.length
);
470 it('covers empty error_description', async
function () {
471 ctx
.queryParams
['state'] = state
;
472 ctx
.queryParams
['code'] = 'codeCodeCode';
473 ctx
.queryParams
['error'] = 'error_code';
475 await manager
.getAdminIA(res
, ctx
);
477 assert(ctx
.errors
.length
);
479 it('covers invalid restored session', async
function () {
480 manager
.mysteryBox
.unpack
.restore();
481 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
482 authorizationEndpoint: 'not a url',
486 ctx
.queryParams
['state'] = state
;
487 ctx
.queryParams
['code'] = 'codeCodeCode';
489 await manager
.getAdminIA(res
, ctx
);
491 assert(ctx
.errors
.length
);
493 it('covers empty profile redemption response', async
function () {
494 ctx
.queryParams
['state'] = state
;
495 ctx
.queryParams
['code'] = 'codeCodeCode';
496 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
497 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves();
499 await manager
.getAdminIA(res
, ctx
);
501 assert(ctx
.errors
.length
);
503 it('covers missing profile in redemption response', async
function () {
504 ctx
.queryParams
['state'] = state
;
505 ctx
.queryParams
['code'] = 'codeCodeCode';
506 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
507 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
510 await manager
.getAdminIA(res
, ctx
);
512 assert(ctx
.errors
.length
);
514 it('covers different canonical profile response', async
function () {
515 ctx
.queryParams
['state'] = state
;
516 ctx
.queryParams
['code'] = 'codeCodeCode';
517 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
518 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
519 me: 'https://different.example.com/profile',
522 await manager
.getAdminIA(res
, ctx
);
524 assert
.strictEqual(res
.statusCode
, 302);
526 it('covers different canonical profile response mis-matched endpoint', async
function () {
527 ctx
.queryParams
['state'] = state
;
528 ctx
.queryParams
['code'] = 'codeCodeCode';
529 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
530 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
531 me: 'https://different.example.com/profile',
533 manager
.indieAuthCommunication
.fetchProfile
.restore();
534 sinon
.stub(manager
.indieAuthCommunication
, 'fetchProfile').resolves({
536 authorizationEndpoint: 'https://elsewhere.example.com/auth',
540 await manager
.getAdminIA(res
, ctx
);
542 assert(ctx
.errors
.length
);
544 describe('living-standard-20220212', function () {
545 beforeEach(function () {
546 manager
.indieAuthCommunication
.fetchProfile
.resolves({
548 authorizationEndpoint
,
549 issuer: 'https://example.com/',
552 manager
.mysteryBox
.unpack
.resolves({
553 authorizationEndpoint
,
554 issuer: 'https://example.com/',
559 it('covers valid', async
function () {
560 ctx
.queryParams
['state'] = state
;
561 ctx
.queryParams
['code'] = 'codeCodeCode';
562 ctx
.queryParams
['iss'] = 'https://example.com/';
564 await manager
.getAdminIA(res
, ctx
);
566 assert
.strictEqual(res
.statusCode
, 302);
568 it('covers mis-matched issuer', async
function () {
569 ctx
.queryParams
['state'] = state
;
570 ctx
.queryParams
['code'] = 'codeCodeCode';
572 await manager
.getAdminIA(res
, ctx
);
574 assert(ctx
.errors
.length
);
576 }); // living-standard-20220212
579 describe('getAdminSettings', function () {
580 it('covers success', async
function () {
581 manager
.db
.authenticationGet
.resolves({});
582 await manager
.getAdminSettings(res
, ctx
);
583 assert(!ctx
.errors
.length
);
585 it('covers no user', async
function () {
586 manager
.db
.authenticationGet
.resolves();
587 await manager
.getAdminSettings(res
, ctx
);
588 assert(ctx
.errors
.length
);
590 it('covers db failure', async
function () {
591 manager
.db
.authenticationGet
.throws();
592 await manager
.getAdminSettings(res
, ctx
);
593 assert(ctx
.errors
.length
);
595 }); // getAdminSettings
597 describe('postAdminSettings', function () {
599 beforeEach(function () {
602 credential: 'password',
603 otpKey: '12345678901234567890123456789012',
605 manager
.db
.authenticationGet
.resolves(authData
);
606 sinon
.stub(manager
, '_credentialUpdate');
607 sinon
.stub(manager
, '_otpEnable');
608 sinon
.stub(manager
, '_otpConfirm');
609 sinon
.stub(manager
, '_otpDisable');
611 it('covers no action', async
function () {
612 await manager
.postAdminSettings(res
, ctx
);
613 assert(!ctx
.errors
.length
);
615 it('covers db empty', async
function () {
616 manager
.db
.authenticationGet
.resolves();
617 await manager
.postAdminSettings(res
, ctx
);
618 assert(ctx
.errors
.length
);
620 it('covers db error', async
function () {
621 manager
.db
.authenticationGet
.throws();
622 await manager
.postAdminSettings(res
, ctx
);
623 assert(ctx
.errors
.length
);
625 it('covers credential update', async
function () {
626 ctx
.parsedBody
.credential
= 'update';
627 await manager
.postAdminSettings(res
, ctx
);
628 assert(manager
._credentialUpdate
.called
);
630 it('covers otp enabling', async
function () {
631 ctx
.parsedBody
.otp
= 'enable';
632 await manager
.postAdminSettings(res
, ctx
);
633 assert(manager
._otpEnable
.called
);
635 it('covers otp confirmation', async
function () {
636 ctx
.parsedBody
.otp
= 'confirm';
637 await manager
.postAdminSettings(res
, ctx
);
638 assert(manager
._otpConfirm
.called
);
640 it('covers otp disabling', async
function () {
641 ctx
.parsedBody
.otp
= 'disable';
642 await manager
.postAdminSettings(res
, ctx
);
643 assert(manager
._otpDisable
.called
);
645 }); // postAdminSettings
647 describe('_otpDisable', function () {
649 beforeEach(function () {
650 ctx
.otpKey
= '12345678901234567890123456789012';
653 otpKey: '12345678901234567890123456789012',
656 it('covers success', async
function () {
657 await manager
._otpDisable(dbCtx
, ctx
, authData
);
659 assert(!authData
.otpKey
);
660 assert(manager
.db
.authenticationUpdateOTPKey
.called
);
661 assert(ctx
.notifications
.length
);
662 assert(!ctx
.errors
.length
);
664 it('covers db failure', async
function () {
665 manager
.db
.authenticationUpdateOTPKey
.throws();
666 await manager
._otpDisable(dbCtx
, ctx
, authData
);
667 assert(!ctx
.notifications
.length
);
668 assert(ctx
.errors
.length
);
672 describe('_otpEnsable', function () {
673 it('covers success', async
function () {
674 await manager
._otpEnable(ctx
);
675 assert('otpConfirmKey' in ctx
);
676 assert('otpConfirmBox' in ctx
);
677 assert(!ctx
.errors
.length
);
679 it('covers failure', async
function () {
680 sinon
.stub(manager
.mysteryBox
, 'pack').throws();
681 await manager
._otpEnable(ctx
);
682 assert(!('otpConfirmKey' in ctx
));
683 assert(!('otpConfirmBox' in ctx
));
684 assert(ctx
.errors
.length
);
688 describe('_otpConfirm', function () {
690 beforeEach(function () {
691 sinon
.stub(Date
, 'now').returns(1710435655000);
694 'otp-box': 'xxxBoxedStatexxx',
695 'otp-token': '350876',
698 otpKey: 'CDBGB3U3B2ILECQORMINGGSZN7LXY565',
700 otpInitiatedMs: 1710434052084,
702 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(otpState
);
704 it('covers success', async
function () {
705 await manager
._otpConfirm(dbCtx
, ctx
);
706 assert(manager
.db
.authenticationUpdateOTPKey
.called
);
707 assert(ctx
.notifications
.length
);
708 assert(!ctx
.errors
.length
);
710 it('covers bad state', async
function () {
711 manager
.mysteryBox
.unpack
.throws();
712 await manager
._otpConfirm(dbCtx
, ctx
);
713 assert(ctx
.errors
.length
);
714 assert(manager
.db
.authenticationUpdateOTPKey
.notCalled
);
716 it('covers no token entered', async
function () {
717 ctx
.parsedBody
['otp-token'] = '';
718 await manager
._otpConfirm(dbCtx
, ctx
);
719 assert(!ctx
.errors
.length
);
720 assert(manager
.db
.authenticationUpdateOTPKey
.notCalled
);
722 it('covers bad token entered', async
function () {
723 ctx
.parsedBody
['otp-token'] = '123456';
724 await manager
._otpConfirm(dbCtx
, ctx
);
725 assert(ctx
.errors
.length
);
726 assert(manager
.db
.authenticationUpdateOTPKey
.notCalled
);
728 it('covers db error', async
function () {
729 manager
.db
.authenticationUpdateOTPKey
.throws();
730 await manager
._otpConfirm(dbCtx
, ctx
);
731 assert(ctx
.errors
.length
);
735 describe('_credentialUpdate', function () {
737 beforeEach(function () {
739 'credential-new': 'abc',
740 'credential-new-2': 'abc',
741 'credential-current': '123',
744 manager
.authenticator
._validateAuthDataCredential
.resolves(true);
746 it('covers success', async
function () {
747 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
748 assert(ctx
.notifications
.length
);
749 assert(!ctx
.errors
.length
);
751 it('covers invalid current password', async
function () {
752 manager
.authenticator
._validateAuthDataCredential
.resolves(false);
753 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
754 assert(!ctx
.notifications
.length
);
755 assert(ctx
.errors
.length
);
757 it('covers empty new password', async
function () {
758 delete ctx
.parsedBody
['credential-new'];
759 manager
.authenticator
._validateAuthDataCredential
.resolves(false);
760 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
761 assert(!ctx
.notifications
.length
);
762 assert(ctx
.errors
.length
);
764 it('covers mismatched new password', async
function () {
765 ctx
.parsedBody
['credential-new'] = 'cde';
766 manager
.authenticator
._validateAuthDataCredential
.resolves(false);
767 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
768 assert(!ctx
.notifications
.length
);
769 assert(ctx
.errors
.length
);
771 it('covers db failure', async
function () {
772 manager
.authenticator
.updateCredential
.throws();
773 await manager
._credentialUpdate(dbCtx
, ctx
, authData
);
774 assert(!ctx
.notifications
.length
);
775 assert(ctx
.errors
.length
);
776 assert(manager
.logger
.error
.called
);
778 }); // _credentialUpdate
780 }); // SessionManager