update dependencies and devDependencies, fix test for latest sinon
[squeep-authentication-module] / test / lib / session-manager.js
1 'use strict';
2
3 const assert = require('node:assert');
4 const sinon = require('sinon');
5
6 const SessionManager = require('../../lib/session-manager');
7 const Enum = require('../../lib/enum');
8 const Config = require('../stub-config');
9 const stubLogger = require('../stub-logger');
10 const stubDb = require('../stub-db');
11
12 describe('SessionManager', function () {
13 let manager, options, stubAuthenticator;
14 let res, ctx;
15
16 beforeEach(function () {
17 stubDb._reset();
18 options = new Config('test');
19 res = {
20 end: sinon.stub(),
21 setHeader: sinon.stub(),
22 appendHeader: sinon.stub(),
23 };
24 ctx = {
25 cookie: {},
26 params: {},
27 queryParams: {},
28 parsedBody: {},
29 errors: [],
30 notifications: [],
31 };
32 stubAuthenticator = {
33 isValidIdentifierCredential: sinon.stub(),
34 checkOTP: sinon.stub(),
35 _validateAuthDataCredential: sinon.stub(),
36 updateCredential: sinon.stub(),
37 db: stubDb,
38 };
39 manager = new SessionManager(stubLogger, stubAuthenticator, options);
40 sinon.stub(manager.indieAuthCommunication);
41 stubLogger._reset();
42 });
43 afterEach(function () {
44 sinon.restore();
45 });
46
47 describe('constructor', function () {
48 it('covers options', function () {
49 delete options.dingus.proxyPrefix;
50 delete options.authenticator.secureAuthOnly;
51 options.authenticator.sessionCookieSameSite = 'None';
52 manager = new SessionManager(stubLogger, stubAuthenticator, options);
53 });
54 }); // constructor
55
56 describe('_sessionCookieSet', function () {
57 let session, maxAge;
58 beforeEach(function () {
59 session = {};
60 maxAge = 86400;
61 });
62 it('covers', async function () {
63 await manager._sessionCookieSet(res, session, maxAge);
64 assert(res.appendHeader.called);
65 });
66 it('covers reset', async function () {
67 session = undefined;
68 maxAge = 0;
69 await manager._sessionCookieSet(res, session, maxAge);
70 assert(res.appendHeader.called);
71 });
72 it('covers options', async function() {
73 options.authenticator.secureAuthOnly = false;
74 await manager._sessionCookieSet(res, session, 'none', '');
75 assert(res.appendHeader.called);
76 });
77 }); // _sessionCookieSet
78
79 describe('_sessionCookieClear', function () {
80 it('covers', async function () {
81 await manager._sessionCookieClear(res);
82 assert(res.appendHeader.called);
83 });
84 }); // _sessionCookieClear
85
86 describe('getAdminLogin', function () {
87 it('covers no session', async function () {
88 await manager.getAdminLogin(res, ctx);
89 });
90 it('covers established session', async function () {
91 ctx.authenticationId = 'identifier';
92 ctx.queryParams['r'] = '/admin';
93 await manager.getAdminLogin(res, ctx);
94 assert.strictEqual(res.statusCode, 302);
95 assert(res.setHeader.called);
96 });
97 }); // getAdminLogin
98
99 describe('postAdminLogin', function () {
100 beforeEach(function () {
101 sinon.stub(manager, '_otpSubmission').resolves(false);
102 });
103 it('covers otp submission', async function () {
104 manager._otpSubmission.resolves(true);
105 await manager.postAdminLogin(res, ctx);
106 assert(res.end.notCalled);
107 });
108 it('covers valid local', async function () {
109 ctx.parsedBody.identifier = 'user';
110 ctx.parsedBody.credential = 'password';
111 manager.authenticator.isValidIdentifierCredential.resolves(true);
112 await manager.postAdminLogin(res, ctx);
113 assert.strictEqual(res.statusCode, 302);
114 });
115 it('covers invalid local', async function () {
116 ctx.parsedBody.identifier = 'user';
117 ctx.parsedBody.credential = 'password';
118 manager.authenticator.isValidIdentifierCredential.resolves(false);
119 await manager.postAdminLogin(res, ctx);
120 assert(!res.setHeader.called);
121 });
122 it('covers valid profile', async function () {
123 ctx.parsedBody.me = 'https://example.com/profile';
124 manager.indieAuthCommunication.fetchProfile.resolves({
125 metadata: {
126 authorizationEndpoint: 'https://example.com/auth',
127 },
128 });
129 await manager.postAdminLogin(res, ctx);
130 assert.strictEqual(res.statusCode, 302);
131 });
132 it('covers invalid profile', async function () {
133 ctx.parsedBody.me = 'not a profile';
134 manager.indieAuthCommunication.fetchProfile.resolves();
135 await manager.postAdminLogin(res, ctx);
136 assert(!res.setHeader.called);
137 });
138 it('covers invalid profile response', async function () {
139 ctx.parsedBody.me = 'https://example.com/profile';
140 manager.indieAuthCommunication.fetchProfile.resolves();
141 await manager.postAdminLogin(res, ctx);
142 assert(!res.setHeader.called);
143 });
144 it('covers invalid profile response endpoint', async function () {
145 ctx.parsedBody.me = 'https://example.com/profile';
146 manager.indieAuthCommunication.fetchProfile.resolves({
147 metadata: {
148 authorizationEndpoint: 'not an auth endpoint',
149 },
150 });
151 await manager.postAdminLogin(res, ctx);
152 assert(!res.setHeader.called);
153 });
154 it('covers profile scheme fallback', async function () {
155 ctx.parsedBody.me = 'https://example.com/profile';
156 ctx.parsedBody['me_auto_scheme'] = '1';
157 manager.indieAuthCommunication.fetchProfile
158 .onCall(0).resolves()
159 .onCall(1).resolves({
160 metadata: {
161 issuer: 'https://example.com/',
162 authorizationEndpoint: 'https://example.com/auth',
163 },
164 });
165 await manager.postAdminLogin(res, ctx);
166 assert.strictEqual(res.statusCode, 302);
167
168 });
169 describe('living-standard-20220212', function () {
170 it('covers valid profile', async function () {
171 ctx.parsedBody.me = 'https://example.com/profile';
172 manager.indieAuthCommunication.fetchProfile.resolves({
173 metadata: {
174 issuer: 'https://example.com/',
175 authorizationEndpoint: 'https://example.com/auth',
176 },
177 });
178 await manager.postAdminLogin(res, ctx);
179 assert.strictEqual(res.statusCode, 302);
180 });
181 it('covers bad issuer url', async function () {
182 ctx.parsedBody.me = 'https://example.com/profile';
183 manager.indieAuthCommunication.fetchProfile.resolves({
184 metadata: {
185 issuer: 'http://example.com/?bah#foo',
186 authorizationEndpoint: 'https://example.com/auth',
187 },
188 });
189 await manager.postAdminLogin(res, ctx);
190 assert(!res.setHeader.called);
191 });
192 it('covers unparsable issuer url', async function () {
193 ctx.parsedBody.me = 'https://example.com/profile';
194 manager.indieAuthCommunication.fetchProfile.resolves({
195 metadata: {
196 issuer: 'not a url',
197 authorizationEndpoint: 'https://example.com/auth',
198 },
199 });
200 await manager.postAdminLogin(res, ctx);
201 assert(!res.setHeader.called);
202 });
203 }); // living-standard-20220212
204 }); // postAdminLogin
205
206 describe('_otpSubmission', function () {
207 let otpState;
208 beforeEach(function () {
209 sinon.useFakeTimers({ now: new Date(), toFake: ['Date'] });
210 otpState = {
211 authenticatedIdentifier: 'identifier',
212 key: '1234567890123456789012',
213 attempt: 0,
214 epochMs: Date.now(),
215 redirect: '/',
216 };
217 sinon.stub(manager.mysteryBox, 'unpack').resolves(otpState);
218 manager.authenticator.checkOTP.resolves(Enum.OTPResult.Valid);
219 ctx.parsedBody.state = 'state_data';
220 ctx.parsedBody.otp = '123456';
221 });
222 it('returns false if no otp state', async function () {
223 delete ctx.parsedBody.state;
224 const result = await manager._otpSubmission(res, ctx);
225 assert(manager.mysteryBox.unpack.notCalled);
226 assert.strictEqual(result, false);
227 });
228 it('returns false when presented with invalid otp state', async function () {
229 manager.mysteryBox.unpack.rejects();
230 const result = await manager._otpSubmission(res, ctx);
231 assert(manager.mysteryBox.unpack.called);
232 assert.strictEqual(result, false);
233 });
234 it('returns false when otp state missing identifier field', async function () {
235 delete otpState.authenticatedIdentifier;
236 manager.mysteryBox.unpack.resolves(otpState);
237 const result = await manager._otpSubmission(res, ctx);
238 assert(manager.mysteryBox.unpack.called);
239 assert.strictEqual(result, false);
240 });
241 it('returns false when otp state missing key field', async function () {
242 delete otpState.key;
243 manager.mysteryBox.unpack.resolves(otpState);
244 const result = await manager._otpSubmission(res, ctx);
245 assert(manager.mysteryBox.unpack.called);
246 assert.strictEqual(result, false);
247 });
248 it('returns false when otp state missing attempt field', async function () {
249 delete otpState.attempt;
250 manager.mysteryBox.unpack.resolves(otpState);
251 const result = await manager._otpSubmission(res, ctx);
252 assert(manager.mysteryBox.unpack.called);
253 assert.strictEqual(result, false);
254 });
255 it('returns false when otp state missing epoch field', async function () {
256 delete otpState.epochMs;
257 manager.mysteryBox.unpack.resolves(otpState);
258 const result = await manager._otpSubmission(res, ctx);
259 assert(manager.mysteryBox.unpack.called);
260 assert.strictEqual(result, false);
261 });
262 it('returns true when submitted otp is invalid, but allowed to retry', async function () {
263 manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidSoftFail);
264 const result = await manager._otpSubmission(res, ctx);
265 assert(manager.mysteryBox.unpack.called);
266 assert.strictEqual(result, true);
267 assert(res.end.called);
268 });
269 it('returns false when submitted otp is invalid and too many attempts', async function () {
270 otpState.attempt = 10;
271 manager.mysteryBox.unpack.resolves(otpState);
272 manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidHardFail);
273 const result = await manager._otpSubmission(res, ctx);
274 assert(manager.mysteryBox.unpack.called);
275 assert.strictEqual(result, false);
276 });
277 it('returns false when submitted otp is invalid and too much time has passed', async function () {
278 otpState.epochMs = Date.now() - 99999999;
279 manager.mysteryBox.unpack.resolves(otpState);
280 manager.authenticator.checkOTP.resolves(Enum.OTPResult.InvalidHardFail);
281 const result = await manager._otpSubmission(res, ctx);
282 assert(manager.mysteryBox.unpack.called);
283 assert.strictEqual(result, false);
284 });
285 it('returns true when no otp submitted', async function () {
286 ctx.parsedBody.otp = '';
287 const result = await manager._otpSubmission(res, ctx);
288 assert(manager.mysteryBox.unpack.called);
289 assert.strictEqual(result, true);
290 assert(res.end.called);
291 });
292 it('returns true when submitted otp is valid', async function () {
293 const result = await manager._otpSubmission(res, ctx);
294 assert(res.end.called);
295 assert.strictEqual(result, true);
296 });
297 it('covers unexpected otp response', async function () {
298 manager.authenticator.checkOTP.resolves('wrong');
299 assert.rejects(() => manager._otpSubmission(res, ctx), RangeError);
300 });
301 }); // _otpSubmission
302
303 describe('_validateOTPState', function () {
304 let otpState;
305 it('covers valid', function () {
306 otpState = {
307 authenticatedIdentifier: 'identifier',
308 key: '1234567890123456789012',
309 attempt: 0,
310 epochMs: Date.now(),
311 redirect: '/',
312 };
313 SessionManager._validateOTPState(otpState);
314 });
315 it('covers missing identifier', function () {
316 otpState = {
317 authenticatedIdentifier: '',
318 key: '1234567890123456789012',
319 attempt: 0,
320 epochMs: Date.now(),
321 redirect: '/',
322 };
323 assert.throws(() => SessionManager._validateOTPState(otpState));
324 });
325 it('covers missing key', function () {
326 otpState = {
327 authenticatedIdentifier: 'identifier',
328 key: '',
329 attempt: 0,
330 epochMs: Date.now(),
331 redirect: '/',
332 };
333 assert.throws(() => SessionManager._validateOTPState(otpState));
334 });
335 it('covers missing attempt', function () {
336 otpState = {
337 authenticatedIdentifier: 'identifier',
338 key: '1234567890123456789012',
339 epochMs: Date.now(),
340 redirect: '/',
341 };
342 assert.throws(() => SessionManager._validateOTPState(otpState));
343 });
344 it('covers missing epoch', function () {
345 otpState = {
346 authenticatedIdentifier: 'identifier',
347 key: '1234567890123456789012',
348 attempt: 0,
349 redirect: '/',
350 };
351 assert.throws(() => SessionManager._validateOTPState(otpState));
352 });
353 it('covers missing redirect', function () {
354 otpState = {
355 authenticatedIdentifier: 'identifier',
356 key: '1234567890123456789012',
357 attempt: 0,
358 epochMs: Date.now(),
359 };
360 assert.throws(() => SessionManager._validateOTPState(otpState));
361 });
362 }); // _validateOTPState
363
364 describe('_localUserAuth', function () {
365 beforeEach(function () {
366 ctx.parsedBody.identifier = 'identifier';
367 ctx.parsedBody.credential = 'credential';
368 manager.authenticator.isValidIdentifierCredential.resolves(true);
369 sinon.stub(manager.mysteryBox, 'pack').resolves('box');
370 });
371 it('returns false if indieauth available', async function () {
372 ctx.parsedBody.me = 'https://example.com/';
373 const result = await manager._localUserAuth(res, ctx);
374 assert.strictEqual(result, false);
375 });
376 it('returns true if identifier is invalid', async function () {
377 manager.authenticator.isValidIdentifierCredential.resolves(false);
378 const result = await manager._localUserAuth(res, ctx);
379 assert.strictEqual(result, true);
380 assert(manager.authenticator.isValidIdentifierCredential.called);
381 assert(res.end.called);
382 });
383 it('returns true if valid identifier', async function () {
384 const result = await manager._localUserAuth(res, ctx);
385 assert.strictEqual(result, true);
386 assert(res.end.called);
387 });
388 it('returns true if valid identifier requires otp entry', async function () {
389 ctx.otpKey = '1234567890123456789012';
390 const result = await manager._localUserAuth(res, ctx);
391 assert.strictEqual(result, true);
392 assert(manager.mysteryBox.pack.called);
393 assert(res.end.called);
394 });
395 }); // _localUserAuth
396
397 describe('getAdminLogout', function () {
398 it('covers', async function () {
399 await manager.getAdminLogout(res, ctx);
400 });
401 }); // getAdminLogout
402
403 describe('getAdminIA', function () {
404 let state, me, authorizationEndpoint;
405 beforeEach(function () {
406 state = '4ea7e936-3427-11ec-9f4b-0025905f714a';
407 me = 'https://example.com/profile';
408 authorizationEndpoint = 'https://example.com/auth';
409 ctx.cookie = {
410 squeepSession: 'sessionCookie',
411 };
412 manager.indieAuthCommunication.redeemProfileCode.resolves({
413 me,
414 });
415 manager.indieAuthCommunication.fetchProfile.resolves({
416 metadata: {
417 authorizationEndpoint,
418 },
419 });
420 sinon.stub(manager.mysteryBox, 'unpack').resolves({
421 authorizationEndpoint,
422 state,
423 me,
424 });
425 });
426 it('covers valid', async function () {
427 ctx.queryParams['state'] = state;
428 ctx.queryParams['code'] = 'codeCodeCode';
429
430 await manager.getAdminIA(res, ctx);
431
432 assert.strictEqual(res.statusCode, 302);
433 });
434 it('covers missing cookie', async function () {
435 delete ctx.cookie;
436
437 await manager.getAdminIA(res, ctx);
438
439 assert(ctx.errors.length);
440 });
441 it('covers invalid cookie', async function () {
442 manager.mysteryBox.unpack.restore();
443 sinon.stub(manager.mysteryBox, 'unpack').rejects();
444
445 await manager.getAdminIA(res, ctx);
446
447 assert(ctx.errors.length);
448 });
449 it('covers mis-matched state', async function () {
450 ctx.queryParams['state'] = 'incorrect-state';
451 ctx.queryParams['code'] = 'codeCodeCode';
452
453 await manager.getAdminIA(res, ctx);
454
455 assert(ctx.errors.length);
456 });
457 it('relays auth endpoint errors', async function () {
458 ctx.queryParams['state'] = state;
459 ctx.queryParams['code'] = 'codeCodeCode';
460 ctx.queryParams['error'] = 'error_code';
461 ctx.queryParams['error_description'] = 'something went wrong';
462
463 await manager.getAdminIA(res, ctx);
464
465 assert(ctx.errors.length);
466 });
467 it('covers empty error_description', async function () {
468 ctx.queryParams['state'] = state;
469 ctx.queryParams['code'] = 'codeCodeCode';
470 ctx.queryParams['error'] = 'error_code';
471
472 await manager.getAdminIA(res, ctx);
473
474 assert(ctx.errors.length);
475 });
476 it('covers invalid restored session', async function () {
477 manager.mysteryBox.unpack.restore();
478 sinon.stub(manager.mysteryBox, 'unpack').resolves({
479 authorizationEndpoint: 'not a url',
480 state,
481 me,
482 });
483 ctx.queryParams['state'] = state;
484 ctx.queryParams['code'] = 'codeCodeCode';
485
486 await manager.getAdminIA(res, ctx);
487
488 assert(ctx.errors.length);
489 });
490 it('covers empty profile redemption response', async function () {
491 ctx.queryParams['state'] = state;
492 ctx.queryParams['code'] = 'codeCodeCode';
493 manager.indieAuthCommunication.redeemProfileCode.restore();
494 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves();
495
496 await manager.getAdminIA(res, ctx);
497
498 assert(ctx.errors.length);
499 });
500 it('covers missing profile in redemption response', async function () {
501 ctx.queryParams['state'] = state;
502 ctx.queryParams['code'] = 'codeCodeCode';
503 manager.indieAuthCommunication.redeemProfileCode.restore();
504 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
505 });
506
507 await manager.getAdminIA(res, ctx);
508
509 assert(ctx.errors.length);
510 });
511 it('covers different canonical profile response', async function () {
512 ctx.queryParams['state'] = state;
513 ctx.queryParams['code'] = 'codeCodeCode';
514 manager.indieAuthCommunication.redeemProfileCode.restore();
515 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
516 me: 'https://different.example.com/profile',
517 });
518
519 await manager.getAdminIA(res, ctx);
520
521 assert.strictEqual(res.statusCode, 302);
522 });
523 it('covers different canonical profile response mis-matched endpoint', async function () {
524 ctx.queryParams['state'] = state;
525 ctx.queryParams['code'] = 'codeCodeCode';
526 manager.indieAuthCommunication.redeemProfileCode.restore();
527 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
528 me: 'https://different.example.com/profile',
529 });
530 manager.indieAuthCommunication.fetchProfile.restore();
531 sinon.stub(manager.indieAuthCommunication, 'fetchProfile').resolves({
532 metadata: {
533 authorizationEndpoint: 'https://elsewhere.example.com/auth',
534 },
535 });
536
537 await manager.getAdminIA(res, ctx);
538
539 assert(ctx.errors.length);
540 });
541 describe('living-standard-20220212', function () {
542 beforeEach(function () {
543 manager.indieAuthCommunication.fetchProfile.resolves({
544 metadata: {
545 authorizationEndpoint,
546 issuer: 'https://example.com/',
547 },
548 });
549 manager.mysteryBox.unpack.resolves({
550 authorizationEndpoint,
551 issuer: 'https://example.com/',
552 state,
553 me,
554 });
555 });
556 it('covers valid', async function () {
557 ctx.queryParams['state'] = state;
558 ctx.queryParams['code'] = 'codeCodeCode';
559 ctx.queryParams['iss'] = 'https://example.com/';
560
561 await manager.getAdminIA(res, ctx);
562
563 assert.strictEqual(res.statusCode, 302);
564 });
565 it('covers mis-matched issuer', async function () {
566 ctx.queryParams['state'] = state;
567 ctx.queryParams['code'] = 'codeCodeCode';
568
569 await manager.getAdminIA(res, ctx);
570
571 assert(ctx.errors.length);
572 });
573 }); // living-standard-20220212
574 }); // getAdminIA
575
576 describe('getAdminSettings', function () {
577 it('covers success', async function () {
578 manager.db.authenticationGet.resolves({});
579 await manager.getAdminSettings(res, ctx);
580 assert(!ctx.errors.length);
581 });
582 it('covers no user', async function () {
583 manager.db.authenticationGet.resolves();
584 await manager.getAdminSettings(res, ctx);
585 assert(ctx.errors.length);
586 });
587 it('covers db failure', async function () {
588 manager.db.authenticationGet.throws();
589 await manager.getAdminSettings(res, ctx);
590 assert(ctx.errors.length);
591 });
592 }); // getAdminSettings
593
594 describe('postAdminSettings', function () {
595 let authData;
596 beforeEach(function () {
597 authData = {
598 identifier: 'user',
599 credential: 'password',
600 otpKey: '12345678901234567890123456789012',
601 };
602 manager.db.authenticationGet.resolves(authData);
603 sinon.stub(manager, '_credentialUpdate');
604 sinon.stub(manager, '_otpEnable');
605 sinon.stub(manager, '_otpConfirm');
606 sinon.stub(manager, '_otpDisable');
607 });
608 it('covers no action', async function () {
609 await manager.postAdminSettings(res, ctx);
610 assert(!ctx.errors.length);
611 });
612 it('covers db empty', async function () {
613 manager.db.authenticationGet.resolves();
614 await manager.postAdminSettings(res, ctx);
615 assert(ctx.errors.length);
616 });
617 it('covers db error', async function () {
618 manager.db.authenticationGet.throws();
619 await manager.postAdminSettings(res, ctx);
620 assert(ctx.errors.length);
621 });
622 it('covers credential update', async function () {
623 ctx.parsedBody.credential = 'update';
624 await manager.postAdminSettings(res, ctx);
625 assert(manager._credentialUpdate.called);
626 });
627 it('covers otp enabling', async function () {
628 ctx.parsedBody.otp = 'enable';
629 await manager.postAdminSettings(res, ctx);
630 assert(manager._otpEnable.called);
631 });
632 it('covers otp confirmation', async function () {
633 ctx.parsedBody.otp = 'confirm';
634 await manager.postAdminSettings(res, ctx);
635 assert(manager._otpConfirm.called);
636 });
637 it('covers otp disabling', async function () {
638 ctx.parsedBody.otp = 'disable';
639 await manager.postAdminSettings(res, ctx);
640 assert(manager._otpDisable.called);
641 });
642 }); // postAdminSettings
643
644 describe('_otpDisable', function () {
645 let dbCtx, authData;
646 beforeEach(function () {
647 ctx.otpKey = '12345678901234567890123456789012';
648 dbCtx = {};
649 authData = {
650 otpKey: '12345678901234567890123456789012',
651 };
652 });
653 it('covers success', async function () {
654 await manager._otpDisable(dbCtx, ctx, authData);
655 assert(!ctx.otpKey);
656 assert(!authData.otpKey);
657 assert(manager.db.authenticationUpdateOTPKey.called);
658 assert(ctx.notifications.length);
659 assert(!ctx.errors.length);
660 });
661 it('covers db failure', async function () {
662 manager.db.authenticationUpdateOTPKey.throws();
663 await manager._otpDisable(dbCtx, ctx, authData);
664 assert(!ctx.notifications.length);
665 assert(ctx.errors.length);
666 });
667 }); // _otpDisable
668
669 describe('_otpEnsable', function () {
670 it('covers success', async function () {
671 await manager._otpEnable(ctx);
672 assert('otpConfirmKey' in ctx);
673 assert('otpConfirmBox' in ctx);
674 assert(!ctx.errors.length);
675 });
676 it('covers failure', async function () {
677 sinon.stub(manager.mysteryBox, 'pack').throws();
678 await manager._otpEnable(ctx);
679 assert(!('otpConfirmKey' in ctx));
680 assert(!('otpConfirmBox' in ctx));
681 assert(ctx.errors.length);
682 });
683 }); // _otpEnsable
684
685 describe('_otpConfirm', function () {
686 let dbCtx, otpState;
687 beforeEach(function () {
688 sinon.stub(Date, 'now').returns(1710435655000);
689 dbCtx = {};
690 ctx.parsedBody = {
691 'otp-box': 'xxxBoxedStatexxx',
692 'otp-token': '350876',
693 };
694 otpState = {
695 otpKey: 'CDBGB3U3B2ILECQORMINGGSZN7LXY565',
696 otpAttempt: 0,
697 otpInitiatedMs: 1710434052084,
698 };
699 sinon.stub(manager.mysteryBox, 'unpack').resolves(otpState);
700 });
701 it('covers success', async function () {
702 await manager._otpConfirm(dbCtx, ctx);
703 assert(manager.db.authenticationUpdateOTPKey.called);
704 assert(ctx.notifications.length);
705 assert(!ctx.errors.length);
706 });
707 it('covers bad state', async function () {
708 manager.mysteryBox.unpack.throws();
709 await manager._otpConfirm(dbCtx, ctx);
710 assert(ctx.errors.length);
711 assert(manager.db.authenticationUpdateOTPKey.notCalled);
712 });
713 it('covers no token entered', async function () {
714 ctx.parsedBody['otp-token'] = '';
715 await manager._otpConfirm(dbCtx, ctx);
716 assert(!ctx.errors.length);
717 assert(manager.db.authenticationUpdateOTPKey.notCalled);
718 });
719 it('covers bad token entered', async function () {
720 ctx.parsedBody['otp-token'] = '123456';
721 await manager._otpConfirm(dbCtx, ctx);
722 assert(ctx.errors.length);
723 assert(manager.db.authenticationUpdateOTPKey.notCalled);
724 });
725 it('covers db error', async function () {
726 manager.db.authenticationUpdateOTPKey.throws();
727 await manager._otpConfirm(dbCtx, ctx);
728 assert(ctx.errors.length);
729 });
730 }); // _otpConfirm
731
732 describe('_credentialUpdate', function () {
733 let dbCtx, authData;
734 beforeEach(function () {
735 ctx.parsedBody = {
736 'credential-new': 'abc',
737 'credential-new-2': 'abc',
738 'credential-current': '123',
739 };
740 authData = {};
741 manager.authenticator._validateAuthDataCredential.resolves(true);
742 });
743 it('covers success', async function () {
744 await manager._credentialUpdate(dbCtx, ctx, authData);
745 assert(ctx.notifications.length);
746 assert(!ctx.errors.length);
747 });
748 it('covers invalid current password', async function () {
749 manager.authenticator._validateAuthDataCredential.resolves(false);
750 await manager._credentialUpdate(dbCtx, ctx, authData);
751 assert(!ctx.notifications.length);
752 assert(ctx.errors.length);
753 });
754 it('covers empty new password', async function () {
755 delete ctx.parsedBody['credential-new'];
756 manager.authenticator._validateAuthDataCredential.resolves(false);
757 await manager._credentialUpdate(dbCtx, ctx, authData);
758 assert(!ctx.notifications.length);
759 assert(ctx.errors.length);
760 });
761 it('covers mismatched new password', async function () {
762 ctx.parsedBody['credential-new'] = 'cde';
763 manager.authenticator._validateAuthDataCredential.resolves(false);
764 await manager._credentialUpdate(dbCtx, ctx, authData);
765 assert(!ctx.notifications.length);
766 assert(ctx.errors.length);
767 });
768 it('covers db failure', async function () {
769 manager.authenticator.updateCredential.throws();
770 await manager._credentialUpdate(dbCtx, ctx, authData);
771 assert(!ctx.notifications.length);
772 assert(ctx.errors.length);
773 assert(manager.logger.error.called);
774 });
775 }); // _credentialUpdate
776
777 }); // SessionManager