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