allow config of session cookie sameSite value
[squeep-authentication-module] / test / lib / session-manager.js
1 /* eslint-env mocha */
2 /* eslint-disable sonarjs/no-duplicate-string */
3
4 'use strict';
5
6 const assert = require('node:assert');
7 const sinon = require('sinon');
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 delete options.authenticator.secureAuthOnly;
54 options.authenticator.sessionCookieSameSite = 'None';
55 manager = new SessionManager(stubLogger, stubAuthenticator, options);
56 });
57 }); // constructor
58
59 describe('_sessionCookieSet', function () {
60 let session, maxAge;
61 beforeEach(function () {
62 session = {};
63 maxAge = 86400;
64 });
65 it('covers', async function () {
66 await manager._sessionCookieSet(res, session, maxAge);
67 assert(res.appendHeader.called);
68 });
69 it('covers reset', async function () {
70 session = undefined;
71 maxAge = 0;
72 await manager._sessionCookieSet(res, session, maxAge);
73 assert(res.appendHeader.called);
74 });
75 it('covers options', async function() {
76 options.authenticator.secureAuthOnly = false;
77 await manager._sessionCookieSet(res, session, 'none', '');
78 assert(res.appendHeader.called);
79 });
80 }); // _sessionCookieSet
81
82 describe('_sessionCookieClear', function () {
83 it('covers', async function () {
84 await manager._sessionCookieClear(res);
85 assert(res.appendHeader.called);
86 });
87 }); // _sessionCookieClear
88
89 describe('getAdminLogin', function () {
90 it('covers no session', async function () {
91 await manager.getAdminLogin(res, ctx);
92 });
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);
99 });
100 }); // getAdminLogin
101
102 describe('postAdminLogin', function () {
103 beforeEach(function () {
104 sinon.stub(manager, '_otpSubmission').resolves(false);
105 });
106 it('covers otp submission', async function () {
107 manager._otpSubmission.resolves(true);
108 await manager.postAdminLogin(res, ctx);
109 assert(res.end.notCalled);
110 });
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);
117 });
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);
124 });
125 it('covers valid profile', async function () {
126 ctx.parsedBody.me = 'https://example.com/profile';
127 manager.indieAuthCommunication.fetchProfile.resolves({
128 metadata: {
129 authorizationEndpoint: 'https://example.com/auth',
130 },
131 });
132 await manager.postAdminLogin(res, ctx);
133 assert.strictEqual(res.statusCode, 302);
134 });
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);
140 });
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);
146 });
147 it('covers invalid profile response endpoint', async function () {
148 ctx.parsedBody.me = 'https://example.com/profile';
149 manager.indieAuthCommunication.fetchProfile.resolves({
150 metadata: {
151 authorizationEndpoint: 'not an auth endpoint',
152 },
153 });
154 await manager.postAdminLogin(res, ctx);
155 assert(!res.setHeader.called);
156 });
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({
163 metadata: {
164 issuer: 'https://example.com/',
165 authorizationEndpoint: 'https://example.com/auth',
166 },
167 });
168 await manager.postAdminLogin(res, ctx);
169 assert.strictEqual(res.statusCode, 302);
170
171 });
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({
176 metadata: {
177 issuer: 'https://example.com/',
178 authorizationEndpoint: 'https://example.com/auth',
179 },
180 });
181 await manager.postAdminLogin(res, ctx);
182 assert.strictEqual(res.statusCode, 302);
183 });
184 it('covers bad issuer url', async function () {
185 ctx.parsedBody.me = 'https://example.com/profile';
186 manager.indieAuthCommunication.fetchProfile.resolves({
187 metadata: {
188 issuer: 'http://example.com/?bah#foo',
189 authorizationEndpoint: 'https://example.com/auth',
190 },
191 });
192 await manager.postAdminLogin(res, ctx);
193 assert(!res.setHeader.called);
194 });
195 it('covers unparsable issuer url', async function () {
196 ctx.parsedBody.me = 'https://example.com/profile';
197 manager.indieAuthCommunication.fetchProfile.resolves({
198 metadata: {
199 issuer: 'not a url',
200 authorizationEndpoint: 'https://example.com/auth',
201 },
202 });
203 await manager.postAdminLogin(res, ctx);
204 assert(!res.setHeader.called);
205 });
206 }); // living-standard-20220212
207 }); // postAdminLogin
208
209 describe('_otpSubmission', function () {
210 let otpState;
211 beforeEach(function () {
212 sinon.useFakeTimers(new Date());
213 otpState = {
214 authenticatedIdentifier: 'identifier',
215 key: '1234567890123456789012',
216 attempt: 0,
217 epochMs: Date.now(),
218 redirect: '/',
219 };
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';
224 });
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);
230 });
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);
236 });
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);
243 });
244 it('returns false when otp state missing key field', async function () {
245 delete otpState.key;
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);
250 });
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);
257 });
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);
264 });
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);
271 });
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);
279 });
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);
287 });
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);
294 });
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);
299 });
300 it('covers unexpected otp response', async function () {
301 manager.authenticator.checkOTP.resolves('wrong');
302 assert.rejects(() => manager._otpSubmission(res, ctx), RangeError);
303 });
304 }); // _otpSubmission
305
306 describe('_validateOTPState', function () {
307 let otpState;
308 it('covers valid', function () {
309 otpState = {
310 authenticatedIdentifier: 'identifier',
311 key: '1234567890123456789012',
312 attempt: 0,
313 epochMs: Date.now(),
314 redirect: '/',
315 };
316 SessionManager._validateOTPState(otpState);
317 });
318 it('covers missing identifier', function () {
319 otpState = {
320 authenticatedIdentifier: '',
321 key: '1234567890123456789012',
322 attempt: 0,
323 epochMs: Date.now(),
324 redirect: '/',
325 };
326 assert.throws(() => SessionManager._validateOTPState(otpState));
327 });
328 it('covers missing key', function () {
329 otpState = {
330 authenticatedIdentifier: 'identifier',
331 key: '',
332 attempt: 0,
333 epochMs: Date.now(),
334 redirect: '/',
335 };
336 assert.throws(() => SessionManager._validateOTPState(otpState));
337 });
338 it('covers missing attempt', function () {
339 otpState = {
340 authenticatedIdentifier: 'identifier',
341 key: '1234567890123456789012',
342 epochMs: Date.now(),
343 redirect: '/',
344 };
345 assert.throws(() => SessionManager._validateOTPState(otpState));
346 });
347 it('covers missing epoch', function () {
348 otpState = {
349 authenticatedIdentifier: 'identifier',
350 key: '1234567890123456789012',
351 attempt: 0,
352 redirect: '/',
353 };
354 assert.throws(() => SessionManager._validateOTPState(otpState));
355 });
356 it('covers missing redirect', function () {
357 otpState = {
358 authenticatedIdentifier: 'identifier',
359 key: '1234567890123456789012',
360 attempt: 0,
361 epochMs: Date.now(),
362 };
363 assert.throws(() => SessionManager._validateOTPState(otpState));
364 });
365 }); // _validateOTPState
366
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');
373 });
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);
378 });
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);
385 });
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);
390 });
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);
397 });
398 }); // _localUserAuth
399
400 describe('getAdminLogout', function () {
401 it('covers', async function () {
402 await manager.getAdminLogout(res, ctx);
403 });
404 }); // getAdminLogout
405
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';
412 ctx.cookie = {
413 squeepSession: 'sessionCookie',
414 };
415 manager.indieAuthCommunication.redeemProfileCode.resolves({
416 me,
417 });
418 manager.indieAuthCommunication.fetchProfile.resolves({
419 metadata: {
420 authorizationEndpoint,
421 },
422 });
423 sinon.stub(manager.mysteryBox, 'unpack').resolves({
424 authorizationEndpoint,
425 state,
426 me,
427 });
428 });
429 it('covers valid', async function () {
430 ctx.queryParams['state'] = state;
431 ctx.queryParams['code'] = 'codeCodeCode';
432
433 await manager.getAdminIA(res, ctx);
434
435 assert.strictEqual(res.statusCode, 302);
436 });
437 it('covers missing cookie', async function () {
438 delete ctx.cookie;
439
440 await manager.getAdminIA(res, ctx);
441
442 assert(ctx.errors.length);
443 });
444 it('covers invalid cookie', async function () {
445 manager.mysteryBox.unpack.restore();
446 sinon.stub(manager.mysteryBox, 'unpack').rejects();
447
448 await manager.getAdminIA(res, ctx);
449
450 assert(ctx.errors.length);
451 });
452 it('covers mis-matched state', async function () {
453 ctx.queryParams['state'] = 'incorrect-state';
454 ctx.queryParams['code'] = 'codeCodeCode';
455
456 await manager.getAdminIA(res, ctx);
457
458 assert(ctx.errors.length);
459 });
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';
465
466 await manager.getAdminIA(res, ctx);
467
468 assert(ctx.errors.length);
469 });
470 it('covers empty error_description', async function () {
471 ctx.queryParams['state'] = state;
472 ctx.queryParams['code'] = 'codeCodeCode';
473 ctx.queryParams['error'] = 'error_code';
474
475 await manager.getAdminIA(res, ctx);
476
477 assert(ctx.errors.length);
478 });
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',
483 state,
484 me,
485 });
486 ctx.queryParams['state'] = state;
487 ctx.queryParams['code'] = 'codeCodeCode';
488
489 await manager.getAdminIA(res, ctx);
490
491 assert(ctx.errors.length);
492 });
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();
498
499 await manager.getAdminIA(res, ctx);
500
501 assert(ctx.errors.length);
502 });
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({
508 });
509
510 await manager.getAdminIA(res, ctx);
511
512 assert(ctx.errors.length);
513 });
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',
520 });
521
522 await manager.getAdminIA(res, ctx);
523
524 assert.strictEqual(res.statusCode, 302);
525 });
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',
532 });
533 manager.indieAuthCommunication.fetchProfile.restore();
534 sinon.stub(manager.indieAuthCommunication, 'fetchProfile').resolves({
535 metadata: {
536 authorizationEndpoint: 'https://elsewhere.example.com/auth',
537 },
538 });
539
540 await manager.getAdminIA(res, ctx);
541
542 assert(ctx.errors.length);
543 });
544 describe('living-standard-20220212', function () {
545 beforeEach(function () {
546 manager.indieAuthCommunication.fetchProfile.resolves({
547 metadata: {
548 authorizationEndpoint,
549 issuer: 'https://example.com/',
550 },
551 });
552 manager.mysteryBox.unpack.resolves({
553 authorizationEndpoint,
554 issuer: 'https://example.com/',
555 state,
556 me,
557 });
558 });
559 it('covers valid', async function () {
560 ctx.queryParams['state'] = state;
561 ctx.queryParams['code'] = 'codeCodeCode';
562 ctx.queryParams['iss'] = 'https://example.com/';
563
564 await manager.getAdminIA(res, ctx);
565
566 assert.strictEqual(res.statusCode, 302);
567 });
568 it('covers mis-matched issuer', async function () {
569 ctx.queryParams['state'] = state;
570 ctx.queryParams['code'] = 'codeCodeCode';
571
572 await manager.getAdminIA(res, ctx);
573
574 assert(ctx.errors.length);
575 });
576 }); // living-standard-20220212
577 }); // getAdminIA
578
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);
584 });
585 it('covers no user', async function () {
586 manager.db.authenticationGet.resolves();
587 await manager.getAdminSettings(res, ctx);
588 assert(ctx.errors.length);
589 });
590 it('covers db failure', async function () {
591 manager.db.authenticationGet.throws();
592 await manager.getAdminSettings(res, ctx);
593 assert(ctx.errors.length);
594 });
595 }); // getAdminSettings
596
597 describe('postAdminSettings', function () {
598 let authData;
599 beforeEach(function () {
600 authData = {
601 identifier: 'user',
602 credential: 'password',
603 otpKey: '12345678901234567890123456789012',
604 };
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');
610 });
611 it('covers no action', async function () {
612 await manager.postAdminSettings(res, ctx);
613 assert(!ctx.errors.length);
614 });
615 it('covers db empty', async function () {
616 manager.db.authenticationGet.resolves();
617 await manager.postAdminSettings(res, ctx);
618 assert(ctx.errors.length);
619 });
620 it('covers db error', async function () {
621 manager.db.authenticationGet.throws();
622 await manager.postAdminSettings(res, ctx);
623 assert(ctx.errors.length);
624 });
625 it('covers credential update', async function () {
626 ctx.parsedBody.credential = 'update';
627 await manager.postAdminSettings(res, ctx);
628 assert(manager._credentialUpdate.called);
629 });
630 it('covers otp enabling', async function () {
631 ctx.parsedBody.otp = 'enable';
632 await manager.postAdminSettings(res, ctx);
633 assert(manager._otpEnable.called);
634 });
635 it('covers otp confirmation', async function () {
636 ctx.parsedBody.otp = 'confirm';
637 await manager.postAdminSettings(res, ctx);
638 assert(manager._otpConfirm.called);
639 });
640 it('covers otp disabling', async function () {
641 ctx.parsedBody.otp = 'disable';
642 await manager.postAdminSettings(res, ctx);
643 assert(manager._otpDisable.called);
644 });
645 }); // postAdminSettings
646
647 describe('_otpDisable', function () {
648 let dbCtx, authData;
649 beforeEach(function () {
650 ctx.otpKey = '12345678901234567890123456789012';
651 dbCtx = {};
652 authData = {
653 otpKey: '12345678901234567890123456789012',
654 };
655 });
656 it('covers success', async function () {
657 await manager._otpDisable(dbCtx, ctx, authData);
658 assert(!ctx.otpKey);
659 assert(!authData.otpKey);
660 assert(manager.db.authenticationUpdateOTPKey.called);
661 assert(ctx.notifications.length);
662 assert(!ctx.errors.length);
663 });
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);
669 });
670 }); // _otpDisable
671
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);
678 });
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);
685 });
686 }); // _otpEnsable
687
688 describe('_otpConfirm', function () {
689 let dbCtx, otpState;
690 beforeEach(function () {
691 sinon.stub(Date, 'now').returns(1710435655000);
692 dbCtx = {};
693 ctx.parsedBody = {
694 'otp-box': 'xxxBoxedStatexxx',
695 'otp-token': '350876',
696 };
697 otpState = {
698 otpKey: 'CDBGB3U3B2ILECQORMINGGSZN7LXY565',
699 otpAttempt: 0,
700 otpInitiatedMs: 1710434052084,
701 };
702 sinon.stub(manager.mysteryBox, 'unpack').resolves(otpState);
703 });
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);
709 });
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);
715 });
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);
721 });
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);
727 });
728 it('covers db error', async function () {
729 manager.db.authenticationUpdateOTPKey.throws();
730 await manager._otpConfirm(dbCtx, ctx);
731 assert(ctx.errors.length);
732 });
733 }); // _otpConfirm
734
735 describe('_credentialUpdate', function () {
736 let dbCtx, authData;
737 beforeEach(function () {
738 ctx.parsedBody = {
739 'credential-new': 'abc',
740 'credential-new-2': 'abc',
741 'credential-current': '123',
742 };
743 authData = {};
744 manager.authenticator._validateAuthDataCredential.resolves(true);
745 });
746 it('covers success', async function () {
747 await manager._credentialUpdate(dbCtx, ctx, authData);
748 assert(ctx.notifications.length);
749 assert(!ctx.errors.length);
750 });
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);
756 });
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);
763 });
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);
770 });
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);
777 });
778 }); // _credentialUpdate
779
780 }); // SessionManager