add account settings page, rest of otp support, stdio credential helper, other misc
[squeep-authentication-module] / test / lib / session-manager.js
1 /* eslint-env mocha */
2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string, sonarjs/no-identical-functions */
3
4 'use strict';
5
6 const assert = require('assert');
7 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
8
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');
14
15 describe('SessionManager', function () {
16 let manager, options, stubAuthenticator;
17 let res, ctx;
18
19 beforeEach(function () {
20 stubDb._reset();
21 options = new Config('test');
22 res = {
23 end: sinon.stub(),
24 setHeader: sinon.stub(),
25 appendHeader: sinon.stub(),
26 };
27 ctx = {
28 cookie: '',
29 params: {},
30 queryParams: {},
31 parsedBody: {},
32 errors: [],
33 notifications: [],
34 };
35 stubAuthenticator = {
36 isValidIdentifierCredential: sinon.stub(),
37 checkOTP: sinon.stub(),
38 _validateAuthDataCredential: sinon.stub(),
39 updateCredential: sinon.stub(),
40 db: stubDb,
41 };
42 manager = new SessionManager(stubLogger, stubAuthenticator, options);
43 sinon.stub(manager.indieAuthCommunication);
44 stubLogger._reset();
45 });
46 afterEach(function () {
47 sinon.restore();
48 });
49
50 describe('constructor', function () {
51 it('covers options', function () {
52 delete options.dingus.proxyPrefix;
53 manager = new SessionManager(stubLogger, stubAuthenticator, options);
54 });
55 }); // constructor
56
57 describe('_sessionCookieSet', function () {
58 let session, maxAge;
59 beforeEach(function () {
60 session = {};
61 maxAge = 86400;
62 });
63 it('covers', async function () {
64 await manager._sessionCookieSet(res, session, maxAge);
65 assert(res.appendHeader.called);
66 });
67 it('covers reset', async function () {
68 session = undefined;
69 maxAge = 0;
70 await manager._sessionCookieSet(res, session, maxAge);
71 assert(res.appendHeader.called);
72 });
73 it('covers options', async function() {
74 options.authenticator.secureAuthOnly = false;
75 await manager._sessionCookieSet(res, session, 'none', '');
76 assert(res.appendHeader.called);
77 });
78 }); // _sessionCookieSet
79
80 describe('_sessionCookieClear', function () {
81 it('covers', async function () {
82 await manager._sessionCookieClear(res);
83 assert(res.appendHeader.called);
84 })
85 }); // _sessionCookieClear
86
87 describe('getAdminLogin', function () {
88 it('covers no session', async function () {
89 await manager.getAdminLogin(res, ctx);
90 });
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);
97 });
98 }); // getAdminLogin
99
100 describe('postAdminLogin', function () {
101 beforeEach(function () {
102 sinon.stub(manager, '_otpSubmission').resolves(false);
103 });
104 it('covers otp submission', async function () {
105 manager._otpSubmission.resolves(true);
106 await manager.postAdminLogin(res, ctx);
107 assert(res.end.notCalled);
108 });
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);
115 });
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);
122 });
123 it('covers valid profile', async function () {
124 ctx.parsedBody.me = 'https://example.com/profile';
125 manager.indieAuthCommunication.fetchProfile.resolves({
126 metadata: {
127 authorizationEndpoint: 'https://example.com/auth',
128 },
129 });
130 await manager.postAdminLogin(res, ctx);
131 assert.strictEqual(res.statusCode, 302);
132 });
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);
138 });
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);
144 });
145 it('covers invalid profile response endpoint', async function () {
146 ctx.parsedBody.me = 'https://example.com/profile';
147 manager.indieAuthCommunication.fetchProfile.resolves({
148 metadata: {
149 authorizationEndpoint: 'not an auth endpoint',
150 },
151 });
152 await manager.postAdminLogin(res, ctx);
153 assert(!res.setHeader.called);
154 });
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({
161 metadata: {
162 issuer: 'https://example.com/',
163 authorizationEndpoint: 'https://example.com/auth',
164 },
165 });
166 await manager.postAdminLogin(res, ctx);
167 assert.strictEqual(res.statusCode, 302);
168
169 });
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({
174 metadata: {
175 issuer: 'https://example.com/',
176 authorizationEndpoint: 'https://example.com/auth',
177 },
178 });
179 await manager.postAdminLogin(res, ctx);
180 assert.strictEqual(res.statusCode, 302);
181 });
182 it('covers bad issuer url', async function () {
183 ctx.parsedBody.me = 'https://example.com/profile';
184 manager.indieAuthCommunication.fetchProfile.resolves({
185 metadata: {
186 issuer: 'http://example.com/?bah#foo',
187 authorizationEndpoint: 'https://example.com/auth',
188 },
189 });
190 await manager.postAdminLogin(res, ctx);
191 assert(!res.setHeader.called);
192 });
193 it('covers unparsable issuer url', async function () {
194 ctx.parsedBody.me = 'https://example.com/profile';
195 manager.indieAuthCommunication.fetchProfile.resolves({
196 metadata: {
197 issuer: 'not a url',
198 authorizationEndpoint: 'https://example.com/auth',
199 },
200 });
201 await manager.postAdminLogin(res, ctx);
202 assert(!res.setHeader.called);
203 });
204 }); // living-standard-20220212
205 }); // postAdminLogin
206
207 describe('_otpSubmission', function () {
208 let otpState;
209 beforeEach(function () {
210 sinon.useFakeTimers(new Date());
211 otpState = {
212 authenticatedIdentifier: 'identifier',
213 key: '1234567890123456789012',
214 attempt: 0,
215 epochMs: Date.now(),
216 redirect: '/',
217 };
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';
222 });
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);
228 });
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);
234 });
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);
241 });
242 it('returns false when otp state missing key field', async function () {
243 delete otpState.key;
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);
248 });
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);
255 });
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);
262 });
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);
269 });
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);
277 });
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);
285 });
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);
292 });
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);
297 });
298 it('covers unexpected otp response', async function () {
299 manager.authenticator.checkOTP.resolves('wrong');
300 assert.rejects(() => manager._otpSubmission(res, ctx), RangeError);
301 });
302 }); // _otpSubmission
303
304 describe('_validateOTPState', function () {
305 let otpState;
306 it('covers valid', function () {
307 otpState = {
308 authenticatedIdentifier: 'identifier',
309 key: '1234567890123456789012',
310 attempt: 0,
311 epochMs: Date.now(),
312 redirect: '/',
313 };
314 SessionManager._validateOTPState(otpState);
315 });
316 it('covers missing identifier', function () {
317 otpState = {
318 authenticatedIdentifier: '',
319 key: '1234567890123456789012',
320 attempt: 0,
321 epochMs: Date.now(),
322 redirect: '/',
323 };
324 assert.throws(() => SessionManager._validateOTPState(otpState));
325 });
326 it('covers missing key', function () {
327 otpState = {
328 authenticatedIdentifier: 'identifier',
329 key: '',
330 attempt: 0,
331 epochMs: Date.now(),
332 redirect: '/',
333 };
334 assert.throws(() => SessionManager._validateOTPState(otpState));
335 });
336 it('covers missing attempt', function () {
337 otpState = {
338 authenticatedIdentifier: 'identifier',
339 key: '1234567890123456789012',
340 epochMs: Date.now(),
341 redirect: '/',
342 };
343 assert.throws(() => SessionManager._validateOTPState(otpState));
344 });
345 it('covers missing epoch', function () {
346 otpState = {
347 authenticatedIdentifier: 'identifier',
348 key: '1234567890123456789012',
349 attempt: 0,
350 redirect: '/',
351 };
352 assert.throws(() => SessionManager._validateOTPState(otpState));
353 });
354 it('covers missing redirect', function () {
355 otpState = {
356 authenticatedIdentifier: 'identifier',
357 key: '1234567890123456789012',
358 attempt: 0,
359 epochMs: Date.now(),
360 };
361 assert.throws(() => SessionManager._validateOTPState(otpState));
362 });
363 }); // _validateOTPState
364
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');
371 });
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);
376 });
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);
383 });
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);
388 });
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);
395 });
396 }); // _localUserAuth
397
398 describe('getAdminLogout', function () {
399 it('covers', async function () {
400 await manager.getAdminLogout(res, ctx);
401 });
402 }); // getAdminLogout
403
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'
410 ctx.cookie = {
411 squeepSession: 'sessionCookie',
412 };
413 manager.indieAuthCommunication.redeemProfileCode.resolves({
414 me,
415 });
416 manager.indieAuthCommunication.fetchProfile.resolves({
417 metadata: {
418 authorizationEndpoint,
419 },
420 });
421 sinon.stub(manager.mysteryBox, 'unpack').resolves({
422 authorizationEndpoint,
423 state,
424 me,
425 });
426 });
427 it('covers valid', async function () {
428 ctx.queryParams['state'] = state;
429 ctx.queryParams['code'] = 'codeCodeCode';
430
431 await manager.getAdminIA(res, ctx);
432
433 assert.strictEqual(res.statusCode, 302);
434 });
435 it('covers missing cookie', async function () {
436 delete ctx.cookie;
437
438 await manager.getAdminIA(res, ctx);
439
440 assert(ctx.errors.length);
441 });
442 it('covers invalid cookie', async function () {
443 manager.mysteryBox.unpack.restore();
444 sinon.stub(manager.mysteryBox, 'unpack').rejects();
445
446 await manager.getAdminIA(res, ctx);
447
448 assert(ctx.errors.length);
449 });
450 it('covers mis-matched state', async function () {
451 ctx.queryParams['state'] = 'incorrect-state';
452 ctx.queryParams['code'] = 'codeCodeCode';
453
454 await manager.getAdminIA(res, ctx);
455
456 assert(ctx.errors.length);
457 });
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';
463
464 await manager.getAdminIA(res, ctx);
465
466 assert(ctx.errors.length);
467 });
468 it('covers empty error_description', async function () {
469 ctx.queryParams['state'] = state;
470 ctx.queryParams['code'] = 'codeCodeCode';
471 ctx.queryParams['error'] = 'error_code';
472
473 await manager.getAdminIA(res, ctx);
474
475 assert(ctx.errors.length);
476 });
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',
481 state,
482 me,
483 });
484 ctx.queryParams['state'] = state;
485 ctx.queryParams['code'] = 'codeCodeCode';
486
487 await manager.getAdminIA(res, ctx);
488
489 assert(ctx.errors.length);
490 });
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();
496
497 await manager.getAdminIA(res, ctx);
498
499 assert(ctx.errors.length);
500 });
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({
506 });
507
508 await manager.getAdminIA(res, ctx);
509
510 assert(ctx.errors.length);
511 });
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',
518 });
519
520 await manager.getAdminIA(res, ctx);
521
522 assert.strictEqual(res.statusCode, 302);
523 });
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',
530 });
531 manager.indieAuthCommunication.fetchProfile.restore();
532 sinon.stub(manager.indieAuthCommunication, 'fetchProfile').resolves({
533 metadata: {
534 authorizationEndpoint: 'https://elsewhere.example.com/auth',
535 },
536 });
537
538 await manager.getAdminIA(res, ctx);
539
540 assert(ctx.errors.length);
541 });
542 describe('living-standard-20220212', function () {
543 beforeEach(function () {
544 manager.indieAuthCommunication.fetchProfile.resolves({
545 metadata: {
546 authorizationEndpoint,
547 issuer: 'https://example.com/',
548 },
549 });
550 manager.mysteryBox.unpack.resolves({
551 authorizationEndpoint,
552 issuer: 'https://example.com/',
553 state,
554 me,
555 });
556 });
557 it('covers valid', async function () {
558 ctx.queryParams['state'] = state;
559 ctx.queryParams['code'] = 'codeCodeCode';
560 ctx.queryParams['iss'] = 'https://example.com/';
561
562 await manager.getAdminIA(res, ctx);
563
564 assert.strictEqual(res.statusCode, 302);
565 });
566 it('covers mis-matched issuer', async function () {
567 ctx.queryParams['state'] = state;
568 ctx.queryParams['code'] = 'codeCodeCode';
569
570 await manager.getAdminIA(res, ctx);
571
572 assert(ctx.errors.length);
573 });
574 }); // living-standard-20220212
575 }); // getAdminIA
576
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);
582 });
583 it('covers no user', async function () {
584 manager.db.authenticationGet.resolves();
585 await manager.getAdminSettings(res, ctx);
586 assert(ctx.errors.length);
587 });
588 it('covers db failure', async function () {
589 manager.db.authenticationGet.throws();
590 await manager.getAdminSettings(res, ctx);
591 assert(ctx.errors.length);
592 });
593 }); // getAdminSettings
594
595 describe('postAdminSettings', function () {
596 let authData;
597 beforeEach(function () {
598 authData = {
599 identifier: 'user',
600 credential: 'password',
601 otpKey: '12345678901234567890123456789012',
602 };
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');
608 });
609 it('covers no action', async function () {
610 await manager.postAdminSettings(res, ctx);
611 assert(!ctx.errors.length);
612 });
613 it('covers db empty', async function () {
614 manager.db.authenticationGet.resolves();
615 await manager.postAdminSettings(res, ctx);
616 assert(ctx.errors.length);
617 });
618 it('covers db error', async function () {
619 manager.db.authenticationGet.throws();
620 await manager.postAdminSettings(res, ctx);
621 assert(ctx.errors.length);
622 });
623 it('covers credential update', async function () {
624 ctx.parsedBody.credential = 'update';
625 await manager.postAdminSettings(res, ctx);
626 assert(manager._credentialUpdate.called);
627 });
628 it('covers otp enabling', async function () {
629 ctx.parsedBody.otp = 'enable';
630 await manager.postAdminSettings(res, ctx);
631 assert(manager._otpEnable.called);
632 });
633 it('covers otp confirmation', async function () {
634 ctx.parsedBody.otp = 'confirm';
635 await manager.postAdminSettings(res, ctx);
636 assert(manager._otpConfirm.called);
637 });
638 it('covers otp disabling', async function () {
639 ctx.parsedBody.otp = 'disable';
640 await manager.postAdminSettings(res, ctx);
641 assert(manager._otpDisable.called);
642 });
643 }); // postAdminSettings
644
645 describe('_otpDisable', function () {
646 let dbCtx, authData;
647 beforeEach(function () {
648 ctx.otpKey = '12345678901234567890123456789012';
649 dbCtx = {};
650 authData = {
651 otpKey: '12345678901234567890123456789012',
652 };
653 });
654 it('covers success', async function () {
655 await manager._otpDisable(dbCtx, ctx, authData);
656 assert(!ctx.otpKey);
657 assert(!authData.otpKey);
658 assert(manager.db.authenticationUpdateOTPKey.called);
659 assert(ctx.notifications.length);
660 assert(!ctx.errors.length);
661 });
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);
667 });
668 }); // _otpDisable
669
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);
676 });
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);
683 });
684 }); // _otpEnsable
685
686 describe('_otpConfirm', function () {
687 let dbCtx, otpState;
688 beforeEach(function () {
689 sinon.stub(Date, 'now').returns(1710435655000);
690 dbCtx = {};
691 ctx.parsedBody = {
692 'otp-box': 'xxxBoxedStatexxx',
693 'otp-token': '350876',
694 };
695 otpState = {
696 otpKey: 'CDBGB3U3B2ILECQORMINGGSZN7LXY565',
697 otpAttempt: 0,
698 otpInitiatedMs: 1710434052084,
699 };
700 sinon.stub(manager.mysteryBox, 'unpack').resolves(otpState);
701 });
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);
707 });
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);
713 });
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);
719 });
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);
725 });
726 it('covers db error', async function () {
727 manager.db.authenticationUpdateOTPKey.throws();
728 await manager._otpConfirm(dbCtx, ctx);
729 assert(ctx.errors.length);
730 });
731 }); // _otpConfirm
732
733 describe('_credentialUpdate', function () {
734 let dbCtx, authData;
735 beforeEach(function () {
736 ctx.parsedBody = {
737 'credential-new': 'abc',
738 'credential-new-2': 'abc',
739 'credential-current': '123',
740 };
741 authData = {};
742 manager.authenticator._validateAuthDataCredential.resolves(true);
743 });
744 it('covers success', async function () {
745 await manager._credentialUpdate(dbCtx, ctx, authData);
746 assert(ctx.notifications.length);
747 assert(!ctx.errors.length);
748 });
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);
754 });
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);
761 });
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);
768 });
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);
775 });
776 }); // _credentialUpdate
777
778 }); // SessionManager