update depedencies, changes to support updated authentication-module
[squeep-indie-auther] / test / src / db / postgres.js
1 /* eslint-disable sonarjs/no-identical-functions */
2 /* eslint-env mocha */
3 /* eslint-disable sonarjs/no-duplicate-string */
4 'use strict';
5
6 /* This provides implementation coverage, stubbing pg-promise. */
7
8 const assert = require('assert');
9 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
10 const StubLogger = require('../../stub-logger');
11 const StubDatabase = require('../../stub-db');
12 const DB = require('../../../src/db/postgres');
13 const DBErrors = require('../../../src/db/errors');
14 const common = require('../../../src/common');
15 const Config = require('../../../config');
16
17 const expectedException = new Error('oh no');
18
19 describe('DatabasePostgres', function () {
20 let db, logger, options, pgpStub;
21 let dbCtx;
22 before(function () {
23 pgpStub = () => {
24 const stub = {
25 result: () => ({ rows: [] }),
26 all: common.nop,
27 get: common.nop,
28 run: common.nop,
29 one: common.nop,
30 manyOrNone: common.nop,
31 oneOrNone: common.nop,
32 query: common.nop,
33 batch: common.nop,
34 multiResult: common.nop,
35 connect: common.nop,
36 };
37 stub.tx = (fn) => fn(stub);
38 stub.txIf = (fn) => fn(stub);
39 stub.task = (fn) => fn(stub);
40 return stub;
41 };
42 pgpStub.utils = {
43 enumSql: () => ({}),
44 };
45 pgpStub.QueryFile = class {};
46 pgpStub.end = common.nop;
47 });
48 beforeEach(function () {
49 logger = new StubLogger();
50 logger._reset();
51 options = new Config('test');
52 db = new DB(logger, options, pgpStub);
53 dbCtx = db.db;
54 });
55 afterEach(function () {
56 sinon.restore();
57 });
58
59 it('covers no query logging', function () {
60 delete options.db.queryLogLevel;
61 db = new DB(logger, options, pgpStub);
62 });
63
64
65 // Ensure all interface methods are implemented
66 describe('Implementation', function () {
67 it('implements interface', async function () {
68 const stubDb = new StubDatabase();
69 const results = await Promise.allSettled(stubDb._implementation.map(async (fn) => {
70 try {
71 // eslint-disable-next-line security/detect-object-injection
72 await db[fn](db.db);
73 } catch (e) {
74 assert(!(e instanceof DBErrors.NotImplemented), `${fn} not implemented`);
75 }
76 }));
77 const failures = results.filter((x) => x.status === 'rejected');
78 assert(!failures.length, failures.map((x) => {
79 x = x.reason.toString();
80 return x.slice(x.indexOf(': '));
81 }));
82 });
83 }); // Implementation
84
85 describe('pgpInitOptions', function () {
86 describe('error', function () {
87 it('covers', function () {
88 const err = {};
89 const event = {};
90 db.pgpInitOptions.error(err, event);
91 assert(db.logger.error.called);
92 });
93 }); // error
94 describe('query', function () {
95 it('covers', function () {
96 const event = {};
97 db.pgpInitOptions.query(event);
98 assert(db.logger.debug.called);
99 });
100 }); // query
101 describe('receive', function () {
102 it('covers', function () {
103 const data = [
104 {
105 column_one: 'one', // eslint-disable-line camelcase
106 column_two: 2, // eslint-disable-line camelcase
107 },
108 {
109 column_one: 'foo', // eslint-disable-line camelcase
110 column_two: 4, // eslint-disable-line camelcase
111 },
112 ];
113 const result = {};
114 const event = {};
115 const expectedData = [
116 {
117 columnOne: 'one',
118 columnTwo: 2,
119 },
120 {
121 columnOne: 'foo',
122 columnTwo: 4,
123 },
124 ];
125 db.pgpInitOptions.receive({ data, result, ctx: event });
126 assert(db.logger.debug.called);
127 assert.deepStrictEqual(data, expectedData);
128 });
129 it('covers no query logging', function () {
130 delete options.db.queryLogLevel;
131 db = new DB(logger, options, pgpStub);
132 const data = [
133 {
134 column_one: 'one', // eslint-disable-line camelcase
135 column_two: 2, // eslint-disable-line camelcase
136 },
137 {
138 column_one: 'foo', // eslint-disable-line camelcase
139 column_two: 4, // eslint-disable-line camelcase
140 },
141 ];
142 const result = {};
143 const event = {};
144 const expectedData = [
145 {
146 columnOne: 'one',
147 columnTwo: 2,
148 },
149 {
150 columnOne: 'foo',
151 columnTwo: 4,
152 },
153 ];
154 db.pgpInitOptions.receive({ data, result, ctx: event });
155 assert(db.logger.debug.called);
156 assert.deepStrictEqual(data, expectedData);
157 });
158
159 }); // receive
160 }); // pgpInitOptions
161
162 describe('_initTables', function () {
163 beforeEach(function () {
164 sinon.stub(db.db, 'oneOrNone');
165 sinon.stub(db.db, 'multiResult');
166 sinon.stub(db, '_currentSchema');
167 });
168
169 it('covers apply', async function() {
170 db.db.oneOrNone.onCall(0).resolves(null).onCall(1).resolves({});
171 db._currentSchema.resolves({ major: 0, minor: 0, patch: 0 });
172 await db._initTables();
173 });
174 it('covers exists', async function() {
175 db.db.oneOrNone.resolves({});
176 db._currentSchema.resolves(db.schemaVersionsSupported.max);
177 await db._initTables();
178 });
179 }); // _initTables
180
181 describe('initialize', function () {
182 after(function () {
183 delete db.listener;
184 });
185 it('passes supported version', async function () {
186 const version = { major: 1, minor: 0, patch: 0 };
187 sinon.stub(db.db, 'one').resolves(version);
188 await db.initialize(false);
189 });
190 it('fails low version', async function () {
191 const version = { major: 0, minor: 0, patch: 0 };
192 sinon.stub(db.db, 'one').resolves(version);
193 await assert.rejects(() => db.initialize(false), DBErrors.MigrationNeeded);
194 });
195 it('fails high version', async function () {
196 const version = { major: 100, minor: 100, patch: 100 };
197 sinon.stub(db.db, 'one').resolves(version);
198 await assert.rejects(() => db.initialize(false));
199 });
200 it('covers migration', async function() {
201 sinon.stub(db.db, 'oneOrNone').resolves({});
202 sinon.stub(db.db, 'multiResult');
203 sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.max);
204 sinon.stub(db.db, 'one').resolves(db.schemaVersionsSupported.max);
205 await db.initialize();
206 });
207 it('covers listener', async function() {
208 db.listener = {
209 start: sinon.stub(),
210 };
211 const version = { major: 1, minor: 0, patch: 0 };
212 sinon.stub(db.db, 'one').resolves(version);
213 await db.initialize(false);
214 assert(db.listener.start.called);
215 });
216 }); // initialize
217
218 describe('healthCheck', function () {
219 beforeEach(function () {
220 sinon.stub(db.db, 'connect').resolves({
221 done: () => {},
222 client: {
223 serverVersion: '0.0',
224 },
225 });
226 });
227 it('covers', async function () {
228 const result = await db.healthCheck();
229 assert.deepStrictEqual(result, { serverVersion: '0.0' });
230 });
231 }); // healthCheck
232
233 describe('_queryFileHelper', function () {
234 it('covers success', function () {
235 const _queryFile = db._queryFileHelper(pgpStub);
236 _queryFile();
237 });
238 it('covers failure', function () {
239 pgpStub.QueryFile = class {
240 constructor() {
241 this.error = expectedException;
242 }
243 };
244 const _queryFile = db._queryFileHelper(pgpStub);
245 assert.throws(() => _queryFile(), expectedException);
246 });
247 }); // _queryFileHelper
248
249 describe('_closeConnection', function () {
250 after(function () {
251 delete db.listener;
252 });
253 it('success', async function () {
254 sinon.stub(db._pgp, 'end');
255 await db._closeConnection();
256 assert(db._pgp.end.called);
257 });
258 it('failure', async function () {
259 sinon.stub(db._pgp, 'end').throws(expectedException);
260 await assert.rejects(() => db._closeConnection(), expectedException);
261 });
262 it('covers listener', async function () {
263 db.listener = {
264 stop: sinon.stub(),
265 };
266 sinon.stub(db._pgp, 'end');
267 await db._closeConnection();
268 assert(db._pgp.end.called);
269 });
270 }); // _closeConnection
271
272 describe('_purgeTables', function () {
273 it('covers not really', async function () {
274 sinon.stub(db.db, 'tx');
275 await db._purgeTables(false);
276 assert(!db.db.tx.called);
277 });
278 it('success', async function () {
279 sinon.stub(db.db, 'batch');
280 await db._purgeTables(true);
281 assert(db.db.batch.called);
282 });
283 it('failure', async function () {
284 sinon.stub(db.db, 'tx').rejects(expectedException)
285 await assert.rejects(() => db._purgeTables(true), expectedException);
286 });
287 }); // _purgeTables
288
289 describe('context', function () {
290 it('covers', async function () {
291 await db.context(common.nop);
292 });
293 }); // context
294
295 describe('transaction', function () {
296 it('covers', async function () {
297 await db.transaction(db.db, common.nop);
298 });
299 }); // transaction
300
301 describe('almanacGetAll', function () {
302 beforeEach(function () {
303 sinon.stub(db.db, 'manyOrNone');
304 });
305 it('success', async function () {
306 const expected = [{ event: 'someEvent', date: new Date() }];
307 db.db.manyOrNone.resolves(expected);
308 const result = await db.almanacGetAll(dbCtx);
309 assert.deepStrictEqual(result, expected);
310 });
311 it('failure', async function () {
312 db.db.manyOrNone.rejects(expectedException);
313 await assert.rejects(() => db.almanacGetAll(dbCtx), expectedException);
314 });
315 }); // almanacGetAll
316
317 describe('almanacUpsert', function () {
318 let event, date;
319 beforeEach(function () {
320 event = 'test_event';
321 date = new Date('Fri Dec 22 03:27 UTC 2023')
322 });
323 it('success', async function () {
324 const dbResult = {
325 rowCount: 1,
326 rows: undefined,
327 duration: 22,
328 };
329 sinon.stub(db.db, 'result').resolves(dbResult);
330 await db.almanacUpsert(dbCtx, event, date);
331 });
332 it('success with default date', async function () {
333 const dbResult = {
334 rowCount: 1,
335 rows: undefined,
336 duration: 22,
337 };
338 sinon.stub(db.db, 'result').resolves(dbResult);
339 await db.almanacUpsert(dbCtx, event);
340 });
341 it('failure', async function () {
342 const dbResult = {
343 rowCount: 0,
344 rows: undefined,
345 duration: 22,
346 };
347 sinon.stub(db.db, 'result').resolves(dbResult);
348 await assert.rejects(() => db.almanacUpsert(dbCtx, event, date), DBErrors.UnexpectedResult);
349 });
350 }); // almanacUpsert
351
352 describe('authenticationSuccess', function () {
353 let identifier;
354 beforeEach(function () {
355 identifier = 'username';
356 });
357 it('success', async function () {
358 const dbResult = {
359 rowCount: 1,
360 rows: undefined,
361 duration: 22,
362 };
363 sinon.stub(db.db, 'result').resolves(dbResult);
364 await db.authenticationSuccess(dbCtx, identifier);
365 });
366 it('failure', async function() {
367 const dbResult = {
368 rowCount: 0,
369 rows: undefined,
370 duration: 22,
371 };
372 sinon.stub(db.db, 'result').resolves(dbResult);
373 await assert.rejects(() => db.authenticationSuccess(dbCtx, identifier), DBErrors.UnexpectedResult);
374 });
375 }); // authenticationSuccess
376
377 describe('authenticationGet', function () {
378 let identifier, credential;
379 beforeEach(function () {
380 identifier = 'username';
381 credential = '$z$foo';
382 });
383 it('success', async function () {
384 const dbResult = { identifier, credential };
385 sinon.stub(db.db, 'oneOrNone').resolves(dbResult);
386 const result = await db.authenticationGet(dbCtx, identifier);
387 assert.deepStrictEqual(result, dbResult);
388 });
389 it('failure', async function() {
390 sinon.stub(db.db, 'oneOrNone').rejects(expectedException);
391 await assert.rejects(() => db.authenticationGet(dbCtx, identifier, credential), expectedException);
392 });
393 }); // authenticationGet
394
395 describe('authenticationUpsert', function () {
396 let identifier, credential;
397 beforeEach(function () {
398 identifier = 'username';
399 credential = '$z$foo';
400 });
401 it('success', async function () {
402 const dbResult = {
403 rowCount: 1,
404 rows: undefined,
405 duration: 22,
406 };
407 sinon.stub(db.db, 'result').resolves(dbResult);
408 await db.authenticationUpsert(dbCtx, identifier, credential);
409 });
410 it('failure', async function() {
411 credential = undefined;
412 const dbResult = {
413 rowCount: 0,
414 rows: undefined,
415 duration: 22,
416 };
417 sinon.stub(db.db, 'result').resolves(dbResult);
418 await assert.rejects(() => db.authenticationUpsert(dbCtx, identifier, credential), DBErrors.UnexpectedResult);
419 });
420 }); // authenticationUpsert
421
422 describe('authenticationUpdateCredential', function () {
423 let identifier, credential;
424 beforeEach(function () {
425 identifier = 'username';
426 credential = '$z$foo';
427 });
428 it('success', async function () {
429 const dbResult = {
430 rowCount: 1,
431 rows: undefined,
432 duration: 22,
433 };
434 sinon.stub(db.db, 'result').resolves(dbResult);
435 await db.authenticationUpdateCredential(dbCtx, identifier, credential);
436 });
437 it('failure', async function () {
438 credential = undefined;
439 const dbResult = {
440 rowCount: 0,
441 rows: undefined,
442 duration: 22,
443 };
444 sinon.stub(db.db, 'result').resolves(dbResult);
445 await assert.rejects(() => db.authenticationUpdateCredential(dbCtx, identifier, credential), DBErrors.UnexpectedResult);
446
447 });
448 }); // authenticationUpdateCredential
449
450 describe('authenticationUpdateOTPKey', function () {
451 let identifier, otpKey;
452 beforeEach(function () {
453 identifier = 'username';
454 otpKey = '1234567890123456789012';
455 });
456 it('success', async function () {
457 const dbResult = {
458 rowCount: 1,
459 rows: undefined,
460 duration: 22,
461 };
462 sinon.stub(db.db, 'result').resolves(dbResult);
463 await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
464 });
465 it('failure', async function () {
466 const dbResult = {
467 rowCount: 0,
468 rows: undefined,
469 duration: 22,
470 };
471 sinon.stub(db.db, 'result').resolves(dbResult);
472 await assert.rejects(() => db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey), DBErrors.UnexpectedResult);
473 });
474 }); // authenticationUpdateOTPKey
475
476 describe('profileIdentifierInsert', function () {
477 let profile, identifier;
478 beforeEach(function () {
479 profile = 'https://profile.example.com/';
480 identifier = 'username';
481 });
482 it('success', async function () {
483 const dbResult = {
484 rowCount: 1,
485 };
486 sinon.stub(db.db, 'result').resolves(dbResult);
487 await db.profileIdentifierInsert(dbCtx, profile, identifier);
488 });
489 it('failure', async function () {
490 const dbResult = {
491 rowCount: 0,
492 };
493 sinon.stub(db.db, 'result').resolves(dbResult);
494 await assert.rejects(() => db.profileIdentifierInsert(dbCtx, profile, identifier), DBErrors.UnexpectedResult);
495 });
496 }); // profileIdentifierInsert
497
498 describe('profileIsValid', function () {
499 let profile;
500 beforeEach(function () {
501 profile = 'https://profile.exmaple.com';
502 });
503 it('valid profile', async function () {
504 sinon.stub(db.db, 'oneOrNone').resolves({ profile });
505 const result = await db.profileIsValid(dbCtx, profile);
506 assert.strictEqual(result, true);
507 });
508 it('invalid profile', async function () {
509 sinon.stub(db.db, 'oneOrNone').resolves();
510 const result = await db.profileIsValid(dbCtx, profile);
511 assert.strictEqual(result, false);
512 });
513 it('failure', async function () {
514 sinon.stub(db.db, 'oneOrNone').rejects(expectedException);
515 await assert.rejects(() => db.profileIsValid(dbCtx, profile), expectedException);
516 });
517 }); // profileIsValid
518
519 describe('tokenGetByCodeId', function () {
520 let codeId;
521 beforeEach(function () {
522 sinon.stub(db.db, 'oneOrNone');
523 codeId = 'xxxxxxxx';
524 });
525 it('success', async function() {
526 const dbResult = {
527 token: '',
528 codeId,
529 created: new Date(),
530 expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
531 };
532 db.db.oneOrNone.resolves(dbResult);
533 const result = await db.tokenGetByCodeId(dbCtx, codeId);
534 assert.deepStrictEqual(result, dbResult);
535 });
536 it('failure', async function () {
537 db.db.oneOrNone.rejects(expectedException);
538 await assert.rejects(() => db.tokenGetByCodeId(dbCtx, codeId), expectedException);
539 });
540 }); // tokenGetByCodeId
541
542 describe('profileScopeInsert', function () {
543 let profile, scope;
544 beforeEach(function () {
545 profile = 'https://profile.example.com/';
546 scope = 'scope';
547 });
548 it('success', async function () {
549 const dbResult = {
550 rowCount: 1,
551 };
552 sinon.stub(db.db, 'result').resolves(dbResult);
553 await db.profileScopeInsert(dbCtx, profile, scope);
554 });
555 it('failure', async function () {
556 sinon.stub(db.db, 'result').rejects(expectedException);
557 await assert.rejects(() => db.profileScopeInsert(dbCtx, profile, scope), expectedException);
558 });
559 it('failure', async function () {
560 const dbResult = {
561 rowCount: 2,
562 };
563 sinon.stub(db.db, 'result').resolves(dbResult);
564 await assert.rejects(() => db.profileScopeInsert(dbCtx, profile, scope), DBErrors.UnexpectedResult);
565 });
566 }); // profileScopeInsert
567
568 describe('profileScopesSetAll', function () {
569 let profile, scopes;
570 beforeEach(function () {
571 profile = 'https://example.com/';
572 scopes = [];
573 sinon.stub(db.db, 'result');
574 });
575 it('success, no scopes', async function () {
576 db.db.result.resolves();
577 await db.profileScopesSetAll(dbCtx, profile, scopes);
578 });
579 it('success, scopes', async function () {
580 db.db.result.resolves();
581 scopes.push('profile', 'email', 'create');
582 await db.profileScopesSetAll(dbCtx, profile, scopes);
583 });
584 it('failure', async function () {
585 db.db.result.rejects(expectedException);
586 await assert.rejects(() => db.profileScopesSetAll(dbCtx, profile, scopes), expectedException);
587 });
588 }); // profileScopesSetAll
589
590 describe('profilesScopesByIdentifier', function () {
591 let identifier, scopeIndex, profileScopes, profiles;
592 beforeEach(function () {
593 identifier = 'identifier';
594 scopeIndex = {
595 'scope': {
596 description: 'A scope.',
597 application: 'test',
598 isPermanent: false,
599 isManuallyAdded: true,
600 profiles: ['https://first.example.com/', 'https://second.example.com/'],
601 },
602 'another_scope': {
603 description: 'Another scope.',
604 application: 'another test',
605 isPermanent: true,
606 isManuallyAdded: false,
607 profiles: ['https://first.example.com/'],
608 },
609 'no_app_scope': {
610 description: 'A scope without application.',
611 application: '',
612 isPermanent: false,
613 isManuallyAdded: false,
614 profiles: ['https://second.example.com/'],
615 },
616 'no_profile_scope': {
617 description: 'A scope without profiles.',
618 application: 'test',
619 isPermanent: false,
620 isManuallyAdded: false,
621 profiles: [],
622 },
623 };
624 profileScopes = {
625 'https://first.example.com/': {
626 'scope': scopeIndex['scope'],
627 'another_scope': scopeIndex['another_scope'],
628 },
629 'https://second.example.com/': {
630 'scope': scopeIndex['scope'],
631 'no_app_scope': scopeIndex['no_app_scope'],
632 },
633 'https://scopeless.example.com/': {},
634 };
635 profiles = [
636 'https://first.example.com/',
637 'https://second.example.com/',
638 'https://scopeless.example.com/',
639 ];
640 });
641 it('success', async function () {
642 const dbResult = [
643 { profile: 'https://first.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: true },
644 { profile: 'https://first.example.com/', scope: 'another_scope', application: 'another test', description: 'Another scope.', isPermanent: true, isManuallyAdded: false },
645 { profile: 'https://second.example.com/', scope: 'no_app_scope', application: '', description: 'A scope without application.', isPermanent: false, isManuallyAdded: false },
646 { profile: 'https://second.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: true },
647 { profile: null, scope: 'no_profile_scope', application: 'test', description: 'A scope without profiles.', isPermanent: false, isManuallyAdded: false },
648 { profile: 'https://scopeless.example.com/', scope: null, application: null, description: null, isPermanent: null, isManuallyAdded: null },
649 ];
650 const expected = {
651 scopeIndex,
652 profileScopes,
653 profiles,
654 };
655 sinon.stub(db.db, 'manyOrNone').resolves(dbResult);
656 const result = await db.profilesScopesByIdentifier(dbCtx, identifier);
657 assert.deepStrictEqual(result, expected);
658 });
659 it('failure', async function () {
660 sinon.stub(db.db, 'manyOrNone').rejects(expectedException);
661 await assert.rejects(() => db.profilesScopesByIdentifier(dbCtx, identifier), expectedException);
662 });
663 }); // profilesScopesByIdentifier
664
665 describe('redeemCode', function () {
666 let codeId, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData;
667 beforeEach(function () {
668 codeId = '41945b8e-3e82-11ec-82d1-0025905f714a';
669 isToken = false;
670 clientId = 'https://app.example.com/';
671 profile = 'https://profile.example.com/';
672 identifier = 'username';
673 scopes = ['scope1', 'scope2'];
674 lifespanSeconds = 600;
675 refreshId = undefined;
676 profileData = undefined;
677 });
678 it('success redeem', async function () {
679 const dbResult = {
680 rowCount: 1,
681 rows: [{ isRevoked: false }],
682 duration: 22,
683 };
684 const dbResultScopes = {
685 rowCount: scopes.length,
686 rows: [],
687 duration: 22,
688 };
689 sinon.stub(db.db, 'result').resolves(dbResult).onCall(2).resolves(dbResultScopes);
690 const result = await db.redeemCode(dbCtx, { codeId, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData });
691 assert.strictEqual(result, true);
692 });
693 it('success redeem, no scopes', async function () {
694 scopes = [];
695 const dbResult = {
696 rowCount: 1,
697 rows: [{ isRevoked: false }],
698 duration: 22,
699 };
700 const dbResultScopes = {
701 rowCount: scopes.length,
702 rows: [],
703 duration: 22,
704 };
705 sinon.stub(db.db, 'result').resolves(dbResult).onCall(1).resolves(dbResultScopes);
706 const result = await db.redeemCode(dbCtx, { codeId, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData });
707 assert.strictEqual(result, true);
708 });
709 it('success revoke', async function () {
710 const dbResult = {
711 rowCount: 1,
712 rows: [{ isRevoked: true }],
713 duration: 22,
714 };
715 sinon.stub(db.db, 'result').resolves(dbResult);
716 const result = await db.redeemCode(dbCtx, { codeId, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData });
717 assert.strictEqual(result, false);
718 });
719 it('failure', async function() {
720 const dbResult = {
721 rowCount: 0,
722 rows: undefined,
723 duration: 22,
724 };
725 sinon.stub(db.db, 'result').resolves(dbResult);
726 await assert.rejects(() => db.redeemCode(dbCtx, { codeId, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData }), DBErrors.UnexpectedResult);
727 });
728 it('failure token scopes', async function () {
729 const dbResult = {
730 rowCount: 1,
731 rows: [{ isRevoked: false }],
732 duration: 22,
733 };
734 const dbResultNone = {
735 rowCount: 0,
736 rows: undefined,
737 duration: 22,
738 };
739 sinon.stub(db.db, 'result').resolves(dbResult).onCall(2).resolves(dbResultNone);
740 await assert.rejects(() => db.redeemCode(dbCtx, { codeId, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData }), DBErrors.UnexpectedResult);
741 });
742 }); // redeemCode
743
744 describe('refreshCode', function () {
745 let codeId, now, removeScopes;
746 beforeEach(function () {
747 codeId = '41945b8e-3e82-11ec-82d1-0025905f714a';
748 now = new Date();
749 removeScopes = [];
750 sinon.stub(db.db, 'result').resolves({ rowCount: removeScopes.length });
751 sinon.stub(db.db, 'oneOrNone');
752 });
753 it('success', async function () {
754 db.db.oneOrNone.resolves({
755 expires: now,
756 refreshExpires: now,
757 });
758 const result = await db.refreshCode(dbCtx, codeId, now, removeScopes);
759 assert(db.db.result.notCalled);
760 assert(result);
761 assert(result.expires);
762 assert(result.refreshExpires);
763 assert(!result.scopes);
764 });
765 it('success with scope reduction', async function () {
766 removeScopes = ['create'];
767 db.db.oneOrNone.resolves({
768 expires: now,
769 refreshExpires: now,
770 scopes: [],
771 });
772 db.db.result.resolves({ rowCount: removeScopes.length });
773 const result = await db.refreshCode(dbCtx, codeId, now, removeScopes);
774 assert(result);
775 assert(result.expires);
776 assert(result.refreshExpires);
777 assert(!result.scopes.includes('create'));
778 });
779 it('failure', async function () {
780 db.db.oneOrNone.rejects(expectedException);
781 await assert.rejects(async () => db.refreshCode(dbCtx, codeId, now, removeScopes), expectedException);
782 });
783 it('failure with scope reduction', async function () {
784 removeScopes = ['create'];
785 db.db.oneOrNone.resolves({});
786 db.db.result.resolves({ rowCount: 0 });
787 await assert.rejects(async () => db.refreshCode(dbCtx, codeId, now, removeScopes), DBErrors.UnexpectedResult);
788 });
789 }); // refreshCode
790
791 describe('resourceGet', function () {
792 let identifier;
793 beforeEach(function () {
794 sinon.stub(db.db, 'oneOrNone');
795 identifier = '05b81112-b224-11ec-a9c6-0025905f714a';
796 });
797 it('success', async function () {
798 const dbResult = {
799 identifier,
800 secret: 'secrety',
801 };
802 db.db.oneOrNone.resolves(dbResult);
803 const result = await db.resourceGet(dbCtx, identifier);
804 assert.deepStrictEqual(result, dbResult);
805 });
806 it('failure', async function() {
807 db.db.oneOrNone.rejects(expectedException);
808 await assert.rejects(() => db.resourceGet(dbCtx, identifier), expectedException);
809 });
810 }); // resourceGet
811
812 describe('resourceUpsert', function () {
813 let resourceId, secret, description;
814 beforeEach(function () {
815 resourceId = '98b8d9ec-f8e2-11ec-aceb-0025905f714a';
816 secret = 'supersecret';
817 description = 'some service';
818 });
819 it('success', async function () {
820 const dbResult = {
821 rowCount: 1,
822 rows: [],
823 duration: 22,
824 };
825 sinon.stub(db.db, 'result').resolves(dbResult);
826 await db.resourceUpsert(dbCtx, resourceId, secret, description)
827 });
828 it('failure', async function () {
829 const dbResult = {
830 rowCount: 0,
831 rows: undefined,
832 duration: 22,
833 };
834 sinon.stub(db.db, 'result').resolves(dbResult);
835 await assert.rejects(() => db.resourceUpsert(dbCtx, resourceId, undefined, description), DBErrors.UnexpectedResult);
836 });
837 }); // resourceUpsert
838
839 describe('scopeCleanup', function () {
840 let atLeastMsSinceLast;
841 beforeEach(function () {
842 sinon.stub(db.db, 'result');
843 sinon.stub(db.db, 'oneOrNone');
844 atLeastMsSinceLast = 86400000;
845 });
846 it('success, empty almanac', async function () {
847 const cleaned = 10;
848 db.db.result
849 .onFirstCall().resolves({ rowCount: cleaned })
850 .onSecondCall().resolves({ rowCount: 1 });
851 const result = await db.scopeCleanup(dbCtx, atLeastMsSinceLast);
852 assert.strictEqual(result, cleaned);
853 });
854 it('success, too soon', async function () {
855 db.db.oneOrNone.resolves({ date: new Date(Date.now() - 4000) });
856 const result = await db.scopeCleanup(dbCtx, atLeastMsSinceLast);
857 assert.strictEqual(result, undefined);
858 assert(db.db.result.notCalled);
859 });
860 it('failure', async function () {
861 db.db.result.resolves({ rowCount: 0 });
862 await assert.rejects(async () => db.scopeCleanup(dbCtx, atLeastMsSinceLast), DBErrors.UnexpectedResult);
863 });
864 }); // scopeCleanup
865
866 describe('scopeDelete', function () {
867 let scope;
868 beforeEach(function () {
869 scope = 'somescope';
870 });
871 it('success', async function () {
872 const dbResult = {
873 rowCount: 1,
874 rows: undefined,
875 duration: 22,
876 };
877 sinon.stub(db.db, 'one').resolves({ inUse: false });
878 sinon.stub(db.db, 'result').resolves(dbResult);
879 const result = await db.scopeDelete(dbCtx, scope);
880 assert(db.db.result.called);
881 assert.strictEqual(result, true);
882 });
883 it('success, no scope', async function () {
884 const dbResult = {
885 rowCount: 0,
886 rows: undefined,
887 duration: 22,
888 };
889 sinon.stub(db.db, 'one').resolves({ inUse: false });
890 sinon.stub(db.db, 'result').resolves(dbResult);
891 const result = await db.scopeDelete(dbCtx, scope);
892 assert(db.db.result.called);
893 assert.strictEqual(result, true);
894 });
895 it('scope in use', async function () {
896 const dbResult = {
897 rowCount: 0,
898 rows: undefined,
899 duration: 22,
900 };
901 sinon.stub(db.db, 'one').resolves({ inUse: true });
902 sinon.stub(db.db, 'result').resolves(dbResult);
903 const result = await db.scopeDelete(dbCtx, scope);
904 assert(db.db.result.notCalled);
905 assert.strictEqual(result, false);
906 });
907 it('failure', async function () {
908 sinon.stub(db.db, 'one').rejects(expectedException);
909 await assert.rejects(() => db.scopeDelete(dbCtx, scope), expectedException);
910 });
911 }); // scopeDelete
912
913 describe('scopeUpsert', function () {
914 let scope, description;
915 beforeEach(function () {
916 scope = 'username';
917 description = '$z$foo';
918 });
919 it('success', async function () {
920 const dbResult = {
921 rowCount: 1,
922 rows: undefined,
923 duration: 22,
924 };
925 sinon.stub(db.db, 'result').resolves(dbResult);
926 await db.scopeUpsert(dbCtx, scope, description);
927 });
928 it('failure', async function() {
929 scope = undefined;
930 const dbResult = {
931 rowCount: 0,
932 rows: undefined,
933 duration: 22,
934 };
935 sinon.stub(db.db, 'result').resolves(dbResult);
936 await assert.rejects(() => db.scopeUpsert(dbCtx, scope, description), DBErrors.UnexpectedResult);
937 });
938 }); // scopeUpsert
939
940 describe('tokenCleanup', function () {
941 let codeLifespanSeconds, atLeastMsSinceLast;
942 beforeEach(function () {
943 sinon.stub(db.db, 'result');
944 sinon.stub(db.db, 'oneOrNone');
945 codeLifespanSeconds = 600000;
946 atLeastMsSinceLast = 86400000;
947 });
948 it('success, empty almanac', async function () {
949 const cleaned = 10;
950 db.db.result
951 .onFirstCall().resolves({ rowCount: cleaned })
952 .onSecondCall().resolves({ rowCount: 1 });
953 const result = await db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast);
954 assert.strictEqual(result, cleaned);
955 });
956 it('success, too soon', async function () {
957 db.db.oneOrNone.resolves({ date: new Date(Date.now() - 4000) });
958 const result = await db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast);
959 assert.strictEqual(result, undefined);
960 assert(db.db.result.notCalled);
961 });
962 it('failure', async function () {
963 db.db.result.resolves({ rowCount: 0 });
964 await assert.rejects(() => db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast), DBErrors.UnexpectedResult);
965 });
966 }); // tokenCleanup
967
968 describe('tokenRevokeByCodeId', function () {
969 let codeId;
970 beforeEach(function () {
971 codeId = 'a74bda94-3dae-11ec-8908-0025905f714a';
972 });
973 it('success', async function () {
974 const dbResult = {
975 rowCount: 1,
976 rows: undefined,
977 duration: 22,
978 };
979 sinon.stub(db.db, 'result').resolves(dbResult);
980 await db.tokenRevokeByCodeId(dbCtx, codeId);
981 });
982 it('failure', async function() {
983 const dbResult = {
984 rowCount: 0,
985 rows: undefined,
986 duration: 22,
987 };
988 sinon.stub(db.db, 'result').resolves(dbResult);
989 await assert.rejects(() => db.tokenRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
990 });
991 }); // tokenRevokeByCodeId
992
993 describe('tokenRefreshRevokeByCodeId', function () {
994 let codeId;
995 beforeEach(function () {
996 codeId = '279947c8-2584-11ed-a2d6-0025905f714a';
997 sinon.stub(db.db, 'result');
998 });
999 it('success', async function () {
1000 db.db.result.resolves({ rowCount: 1 });
1001 await db.tokenRefreshRevokeByCodeId(dbCtx, codeId);
1002 });
1003 it('failure, no code', async function () {
1004 db.db.result.resolves({ rowCount: 0 });
1005 assert.rejects(async () => db.tokenRefreshRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
1006 });
1007 it('failure', async function () {
1008 db.db.result.rejects(expectedException);
1009 assert.rejects(async () => db.tokenRefreshRevokeByCodeId(dbCtx, codeId), expectedException);
1010 });
1011 }); // tokenRefreshRevokeByCodeId
1012
1013 describe('tokensGetByIdentifier', function () {
1014 let identifier;
1015 beforeEach(function () {
1016 identifier = 'identifier';
1017 });
1018 it('success', async function () {
1019 const dbResult = [
1020 {
1021 'created': new Date(),
1022 'expires': new Date(),
1023 'isRevoked': false,
1024 'token': '',
1025 'codeId': '',
1026 'profile': '',
1027 'identifier': '',
1028 },
1029 ];
1030 const expected = dbResult;
1031 sinon.stub(db.db, 'manyOrNone').resolves(dbResult);
1032 const result = await db.tokensGetByIdentifier(dbCtx, identifier);
1033 assert.deepStrictEqual(result, expected);
1034 });
1035 it('failure', async function () {
1036 sinon.stub(db.db, 'manyOrNone').rejects(expectedException);
1037 await assert.rejects(() => db.tokensGetByIdentifier(dbCtx, identifier), expectedException);
1038 });
1039 }); // tokensGetByIdentifier
1040
1041 describe('ticketRedeemed', function () {
1042 let redeemedData;
1043 beforeEach(function () {
1044 redeemedData = {
1045 resource: 'https://resource.example.com/',
1046 subject: 'https://subject.example.com/',
1047 iss: 'https://idp.example.com/',
1048 ticket: 'xxxTICKETxxx',
1049 token: 'xxxTOKENxxx',
1050 };
1051 });
1052 it('success', async function () {
1053 const dbResult = {
1054 rowCount: 1,
1055 rows: undefined,
1056 duration: 22,
1057 };
1058 sinon.stub(db.db, 'result').resolves(dbResult);
1059 await db.ticketRedeemed(dbCtx, redeemedData);
1060 });
1061 it('failure', async function () {
1062 const dbResult = {
1063 rowCount: 0,
1064 rows: undefined,
1065 duration: 22,
1066 };
1067 sinon.stub(db.db, 'result').resolves(dbResult);
1068 await assert.rejects(() => db.ticketRedeemed(dbCtx, redeemedData), DBErrors.UnexpectedResult);
1069 });
1070 }); // ticketRedeemed
1071
1072 describe('ticketTokenPublished', function () {
1073 let redeemedData;
1074 beforeEach(function () {
1075 redeemedData = {
1076 resource: 'https://resource.example.com/',
1077 subject: 'https://subject.example.com/',
1078 iss: 'https://idp.example.com/',
1079 ticket: 'xxxTICKETxxx',
1080 token: 'xxxTOKENxxx',
1081 };
1082 sinon.stub(db.db, 'result');
1083 });
1084 it('success', async function () {
1085 const dbResult = {
1086 rowCount: 1,
1087 rows: undefined,
1088 duration: 22,
1089 };
1090 db.db.result.resolves(dbResult);
1091 await db.ticketTokenPublished(dbCtx, redeemedData);
1092 });
1093 it('failure', async function () {
1094 const dbResult = {
1095 rowCount: 0,
1096 rows: undefined,
1097 duration: 22,
1098 };
1099 db.db.result.resolves(dbResult);
1100 await assert.rejects(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult);
1101 });
1102 it('failure of almanac', async function () {
1103 const dbResult = {
1104 rowCount: 1,
1105 rows: undefined,
1106 duration: 22,
1107 };
1108 const dbResultAlmanac = {
1109 ...dbResult,
1110 rowCount: 0,
1111 };
1112 db.db.result.resolves(dbResult).onCall(1).resolves(dbResultAlmanac);
1113 await assert.rejects(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult);
1114 });
1115 }); // ticketTokenPublished
1116
1117 describe('ticketTokenGetUnpublished', function () {
1118 it('success', async function () {
1119 const expected = [{
1120 resource: 'https://resource.example.com/',
1121 subject: 'https://subject.example.com/',
1122 iss: 'https://idp.example.com/',
1123 ticket: 'xxxTICKETxxx',
1124 token: 'xxxTOKENxxx',
1125 created: new Date(),
1126 published: null,
1127 }];
1128 sinon.stub(db.db, 'manyOrNone').resolves(expected);
1129 const result = await db.ticketTokenGetUnpublished(dbCtx);
1130 assert.deepStrictEqual(result, expected);
1131 });
1132 it('failure', async function () {
1133 sinon.stub(db.db, 'manyOrNone').rejects(expectedException);
1134 await assert.rejects(() => db.ticketTokenGetUnpublished(dbCtx), expectedException);
1135 });
1136 }); // ticketTokenGetUnpublished
1137
1138 }); // DatabasePostgres