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