update dependencies and devDependencies, fix pg-promise breaking changes
[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('authenticationSuccess', function () {
318 let identifier;
319 beforeEach(function () {
320 identifier = 'username';
321 });
322 it('success', async function () {
323 const dbResult = {
324 rowCount: 1,
325 rows: undefined,
326 duration: 22,
327 };
328 sinon.stub(db.db, 'result').resolves(dbResult);
329 await db.authenticationSuccess(dbCtx, identifier);
330 });
331 it('failure', async function() {
332 const dbResult = {
333 rowCount: 0,
334 rows: undefined,
335 duration: 22,
336 };
337 sinon.stub(db.db, 'result').resolves(dbResult);
338 await assert.rejects(() => db.authenticationSuccess(dbCtx, identifier), DBErrors.UnexpectedResult);
339 });
340 }); // authenticationSuccess
341
342 describe('authenticationGet', function () {
343 let identifier, credential;
344 beforeEach(function () {
345 identifier = 'username';
346 credential = '$z$foo';
347 });
348 it('success', async function () {
349 const dbResult = { identifier, credential };
350 sinon.stub(db.db, 'oneOrNone').resolves(dbResult);
351 const result = await db.authenticationGet(dbCtx, identifier);
352 assert.deepStrictEqual(result, dbResult);
353 });
354 it('failure', async function() {
355 sinon.stub(db.db, 'oneOrNone').rejects(expectedException);
356 await assert.rejects(() => db.authenticationGet(dbCtx, identifier, credential), expectedException);
357 });
358 }); // authenticationGet
359
360 describe('authenticationUpsert', function () {
361 let identifier, credential;
362 beforeEach(function () {
363 identifier = 'username';
364 credential = '$z$foo';
365 });
366 it('success', async function () {
367 const dbResult = {
368 rowCount: 1,
369 rows: undefined,
370 duration: 22,
371 };
372 sinon.stub(db.db, 'result').resolves(dbResult);
373 await db.authenticationUpsert(dbCtx, identifier, credential);
374 });
375 it('failure', async function() {
376 credential = undefined;
377 const dbResult = {
378 rowCount: 0,
379 rows: undefined,
380 duration: 22,
381 };
382 sinon.stub(db.db, 'result').resolves(dbResult);
383 await assert.rejects(() => db.authenticationUpsert(dbCtx, identifier, credential), DBErrors.UnexpectedResult);
384 });
385 }); // authenticationUpsert
386
387 describe('profileIdentifierInsert', function () {
388 let profile, identifier;
389 beforeEach(function () {
390 profile = 'https://profile.example.com/';
391 identifier = 'username';
392 });
393 it('success', async function () {
394 const dbResult = {
395 rowCount: 1,
396 };
397 sinon.stub(db.db, 'result').resolves(dbResult);
398 await db.profileIdentifierInsert(dbCtx, profile, identifier);
399 });
400 it('failure', async function () {
401 const dbResult = {
402 rowCount: 0,
403 };
404 sinon.stub(db.db, 'result').resolves(dbResult);
405 await assert.rejects(() => db.profileIdentifierInsert(dbCtx, profile, identifier), DBErrors.UnexpectedResult);
406 });
407 }); // profileIdentifierInsert
408
409 describe('profileIsValid', function () {
410 let profile;
411 beforeEach(function () {
412 profile = 'https://profile.exmaple.com';
413 });
414 it('valid profile', async function () {
415 sinon.stub(db.db, 'oneOrNone').resolves({ profile });
416 const result = await db.profileIsValid(dbCtx, profile);
417 assert.strictEqual(result, true);
418 });
419 it('invalid profile', async function () {
420 sinon.stub(db.db, 'oneOrNone').resolves();
421 const result = await db.profileIsValid(dbCtx, profile);
422 assert.strictEqual(result, false);
423 });
424 it('failure', async function () {
425 sinon.stub(db.db, 'oneOrNone').rejects(expectedException);
426 await assert.rejects(() => db.profileIsValid(dbCtx, profile), expectedException);
427 });
428 }); // profileIsValid
429
430 describe('tokenGetByCodeId', function () {
431 let codeId;
432 beforeEach(function () {
433 sinon.stub(db.db, 'oneOrNone');
434 codeId = 'xxxxxxxx';
435 });
436 it('success', async function() {
437 const dbResult = {
438 token: '',
439 codeId,
440 created: new Date(),
441 expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
442 };
443 db.db.oneOrNone.resolves(dbResult);
444 const result = await db.tokenGetByCodeId(dbCtx, codeId);
445 assert.deepStrictEqual(result, dbResult);
446 });
447 it('failure', async function () {
448 db.db.oneOrNone.rejects(expectedException);
449 await assert.rejects(() => db.tokenGetByCodeId(dbCtx, codeId), expectedException);
450 });
451 }); // tokenGetByCodeId
452
453 describe('profileScopeInsert', function () {
454 let profile, scope;
455 beforeEach(function () {
456 profile = 'https://profile.example.com/';
457 scope = 'scope';
458 });
459 it('success', async function () {
460 const dbResult = {
461 rowCount: 1,
462 };
463 sinon.stub(db.db, 'result').resolves(dbResult);
464 await db.profileScopeInsert(dbCtx, profile, scope);
465 });
466 it('failure', async function () {
467 sinon.stub(db.db, 'result').rejects(expectedException);
468 await assert.rejects(() => db.profileScopeInsert(dbCtx, profile, scope), expectedException);
469 });
470 it('failure', async function () {
471 const dbResult = {
472 rowCount: 2,
473 };
474 sinon.stub(db.db, 'result').resolves(dbResult);
475 await assert.rejects(() => db.profileScopeInsert(dbCtx, profile, scope), DBErrors.UnexpectedResult);
476 });
477 }); // profileScopeInsert
478
479 describe('profileScopesSetAll', function () {
480 let profile, scopes;
481 beforeEach(function () {
482 profile = 'https://example.com/';
483 scopes = [];
484 sinon.stub(db.db, 'result');
485 });
486 it('success, no scopes', async function () {
487 db.db.result.resolves();
488 await db.profileScopesSetAll(dbCtx, profile, scopes);
489 });
490 it('success, scopes', async function () {
491 db.db.result.resolves();
492 scopes.push('profile', 'email', 'create');
493 await db.profileScopesSetAll(dbCtx, profile, scopes);
494 });
495 it('failure', async function () {
496 db.db.result.rejects(expectedException);
497 await assert.rejects(() => db.profileScopesSetAll(dbCtx, profile, scopes), expectedException);
498 });
499 }); // profileScopesSetAll
500
501 describe('profilesScopesByIdentifier', function () {
502 let identifier, scopeIndex, profileScopes, profiles;
503 beforeEach(function () {
504 identifier = 'identifier';
505 scopeIndex = {
506 'scope': {
507 description: 'A scope.',
508 application: 'test',
509 isPermanent: false,
510 isManuallyAdded: true,
511 profiles: ['https://first.example.com/', 'https://second.example.com/'],
512 },
513 'another_scope': {
514 description: 'Another scope.',
515 application: 'another test',
516 isPermanent: true,
517 isManuallyAdded: false,
518 profiles: ['https://first.example.com/'],
519 },
520 'no_app_scope': {
521 description: 'A scope without application.',
522 application: '',
523 isPermanent: false,
524 isManuallyAdded: false,
525 profiles: ['https://second.example.com/'],
526 },
527 'no_profile_scope': {
528 description: 'A scope without profiles.',
529 application: 'test',
530 isPermanent: false,
531 isManuallyAdded: false,
532 profiles: [],
533 },
534 };
535 profileScopes = {
536 'https://first.example.com/': {
537 'scope': scopeIndex['scope'],
538 'another_scope': scopeIndex['another_scope'],
539 },
540 'https://second.example.com/': {
541 'scope': scopeIndex['scope'],
542 'no_app_scope': scopeIndex['no_app_scope'],
543 },
544 'https://scopeless.example.com/': {},
545 };
546 profiles = [
547 'https://first.example.com/',
548 'https://second.example.com/',
549 'https://scopeless.example.com/',
550 ];
551 });
552 it('success', async function () {
553 const dbResult = [
554 { profile: 'https://first.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: true },
555 { profile: 'https://first.example.com/', scope: 'another_scope', application: 'another test', description: 'Another scope.', isPermanent: true, isManuallyAdded: false },
556 { profile: 'https://second.example.com/', scope: 'no_app_scope', application: '', description: 'A scope without application.', isPermanent: false, isManuallyAdded: false },
557 { profile: 'https://second.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: true },
558 { profile: null, scope: 'no_profile_scope', application: 'test', description: 'A scope without profiles.', isPermanent: false, isManuallyAdded: false },
559 { profile: 'https://scopeless.example.com/', scope: null, application: null, description: null, isPermanent: null, isManuallyAdded: null },
560 ];
561 const expected = {
562 scopeIndex,
563 profileScopes,
564 profiles,
565 };
566 sinon.stub(db.db, 'manyOrNone').resolves(dbResult);
567 const result = await db.profilesScopesByIdentifier(dbCtx, identifier);
568 assert.deepStrictEqual(result, expected);
569 });
570 it('failure', async function () {
571 sinon.stub(db.db, 'manyOrNone').rejects(expectedException);
572 await assert.rejects(() => db.profilesScopesByIdentifier(dbCtx, identifier), expectedException);
573 });
574 }); // profilesScopesByIdentifier
575
576 describe('redeemCode', function () {
577 let codeId, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData;
578 beforeEach(function () {
579 codeId = '41945b8e-3e82-11ec-82d1-0025905f714a';
580 isToken = false;
581 clientId = 'https://app.example.com/';
582 profile = 'https://profile.example.com/';
583 identifier = 'username';
584 scopes = ['scope1', 'scope2'];
585 lifespanSeconds = 600;
586 refreshId = undefined;
587 profileData = undefined;
588 });
589 it('success redeem', async function () {
590 const dbResult = {
591 rowCount: 1,
592 rows: [{ isRevoked: false }],
593 duration: 22,
594 };
595 const dbResultScopes = {
596 rowCount: scopes.length,
597 rows: [],
598 duration: 22,
599 };
600 sinon.stub(db.db, 'result').resolves(dbResult).onCall(2).resolves(dbResultScopes);
601 const result = await db.redeemCode(dbCtx, { codeId, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData });
602 assert.strictEqual(result, true);
603 });
604 it('success redeem, no scopes', async function () {
605 scopes = [];
606 const dbResult = {
607 rowCount: 1,
608 rows: [{ isRevoked: false }],
609 duration: 22,
610 };
611 const dbResultScopes = {
612 rowCount: scopes.length,
613 rows: [],
614 duration: 22,
615 };
616 sinon.stub(db.db, 'result').resolves(dbResult).onCall(1).resolves(dbResultScopes);
617 const result = await db.redeemCode(dbCtx, { codeId, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData });
618 assert.strictEqual(result, true);
619 });
620 it('success revoke', async function () {
621 const dbResult = {
622 rowCount: 1,
623 rows: [{ isRevoked: true }],
624 duration: 22,
625 };
626 sinon.stub(db.db, 'result').resolves(dbResult);
627 const result = await db.redeemCode(dbCtx, { codeId, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData });
628 assert.strictEqual(result, false);
629 });
630 it('failure', async function() {
631 const dbResult = {
632 rowCount: 0,
633 rows: undefined,
634 duration: 22,
635 };
636 sinon.stub(db.db, 'result').resolves(dbResult);
637 await assert.rejects(() => db.redeemCode(dbCtx, { codeId, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData }), DBErrors.UnexpectedResult);
638 });
639 it('failure token scopes', async function () {
640 const dbResult = {
641 rowCount: 1,
642 rows: [{ isRevoked: false }],
643 duration: 22,
644 };
645 const dbResultNone = {
646 rowCount: 0,
647 rows: undefined,
648 duration: 22,
649 };
650 sinon.stub(db.db, 'result').resolves(dbResult).onCall(2).resolves(dbResultNone);
651 await assert.rejects(() => db.redeemCode(dbCtx, { codeId, clientId, profile, identifier, scopes, lifespanSeconds, refreshId, profileData }), DBErrors.UnexpectedResult);
652 });
653 }); // redeemCode
654
655 describe('refreshCode', function () {
656 let codeId, now, removeScopes;
657 beforeEach(function () {
658 codeId = '41945b8e-3e82-11ec-82d1-0025905f714a';
659 now = new Date();
660 removeScopes = [];
661 sinon.stub(db.db, 'result').resolves({ rowCount: removeScopes.length });
662 sinon.stub(db.db, 'oneOrNone');
663 });
664 it('success', async function () {
665 db.db.oneOrNone.resolves({
666 expires: now,
667 refreshExpires: now,
668 });
669 const result = await db.refreshCode(dbCtx, codeId, now, removeScopes);
670 assert(db.db.result.notCalled);
671 assert(result);
672 assert(result.expires);
673 assert(result.refreshExpires);
674 assert(!result.scopes);
675 });
676 it('success with scope reduction', async function () {
677 removeScopes = ['create'];
678 db.db.oneOrNone.resolves({
679 expires: now,
680 refreshExpires: now,
681 scopes: [],
682 });
683 db.db.result.resolves({ rowCount: removeScopes.length });
684 const result = await db.refreshCode(dbCtx, codeId, now, removeScopes);
685 assert(result);
686 assert(result.expires);
687 assert(result.refreshExpires);
688 assert(!result.scopes.includes('create'));
689 });
690 it('failure', async function () {
691 db.db.oneOrNone.rejects(expectedException);
692 await assert.rejects(async () => db.refreshCode(dbCtx, codeId, now, removeScopes), expectedException);
693 });
694 it('failure with scope reduction', async function () {
695 removeScopes = ['create'];
696 db.db.oneOrNone.resolves({});
697 db.db.result.resolves({ rowCount: 0 });
698 await assert.rejects(async () => db.refreshCode(dbCtx, codeId, now, removeScopes), DBErrors.UnexpectedResult);
699 });
700 }); // refreshCode
701
702 describe('resourceGet', function () {
703 let identifier;
704 beforeEach(function () {
705 sinon.stub(db.db, 'oneOrNone');
706 identifier = '05b81112-b224-11ec-a9c6-0025905f714a';
707 });
708 it('success', async function () {
709 const dbResult = {
710 identifier,
711 secret: 'secrety',
712 };
713 db.db.oneOrNone.resolves(dbResult);
714 const result = await db.resourceGet(dbCtx, identifier);
715 assert.deepStrictEqual(result, dbResult);
716 });
717 it('failure', async function() {
718 db.db.oneOrNone.rejects(expectedException);
719 await assert.rejects(() => db.resourceGet(dbCtx, identifier), expectedException);
720 });
721 }); // resourceGet
722
723 describe('resourceUpsert', function () {
724 let resourceId, secret, description;
725 beforeEach(function () {
726 resourceId = '98b8d9ec-f8e2-11ec-aceb-0025905f714a';
727 secret = 'supersecret';
728 description = 'some service';
729 });
730 it('success', async function () {
731 const dbResult = {
732 rowCount: 1,
733 rows: [],
734 duration: 22,
735 };
736 sinon.stub(db.db, 'result').resolves(dbResult);
737 await db.resourceUpsert(dbCtx, resourceId, secret, description)
738 });
739 it('failure', async function () {
740 const dbResult = {
741 rowCount: 0,
742 rows: undefined,
743 duration: 22,
744 };
745 sinon.stub(db.db, 'result').resolves(dbResult);
746 await assert.rejects(() => db.resourceUpsert(dbCtx, resourceId, undefined, description), DBErrors.UnexpectedResult);
747 });
748 }); // resourceUpsert
749
750 describe('scopeCleanup', function () {
751 let atLeastMsSinceLast;
752 beforeEach(function () {
753 sinon.stub(db.db, 'result');
754 sinon.stub(db.db, 'oneOrNone');
755 atLeastMsSinceLast = 86400000;
756 });
757 it('success, empty almanac', async function () {
758 const cleaned = 10;
759 db.db.result
760 .onFirstCall().resolves({ rowCount: cleaned })
761 .onSecondCall().resolves({ rowCount: 1 });
762 const result = await db.scopeCleanup(dbCtx, atLeastMsSinceLast);
763 assert.strictEqual(result, cleaned);
764 });
765 it('success, too soon', async function () {
766 db.db.oneOrNone.resolves({ date: new Date(Date.now() - 4000) });
767 const result = await db.scopeCleanup(dbCtx, atLeastMsSinceLast);
768 assert.strictEqual(result, undefined);
769 assert(db.db.result.notCalled);
770 });
771 it('failure', async function () {
772 db.db.result.resolves({ rowCount: 0 });
773 await assert.rejects(async () => db.scopeCleanup(dbCtx, atLeastMsSinceLast), DBErrors.UnexpectedResult);
774 });
775 }); // scopeCleanup
776
777 describe('scopeDelete', function () {
778 let scope;
779 beforeEach(function () {
780 scope = 'somescope';
781 });
782 it('success', async function () {
783 const dbResult = {
784 rowCount: 1,
785 rows: undefined,
786 duration: 22,
787 };
788 sinon.stub(db.db, 'one').resolves({ inUse: false });
789 sinon.stub(db.db, 'result').resolves(dbResult);
790 const result = await db.scopeDelete(dbCtx, scope);
791 assert(db.db.result.called);
792 assert.strictEqual(result, true);
793 });
794 it('success, no scope', async function () {
795 const dbResult = {
796 rowCount: 0,
797 rows: undefined,
798 duration: 22,
799 };
800 sinon.stub(db.db, 'one').resolves({ inUse: false });
801 sinon.stub(db.db, 'result').resolves(dbResult);
802 const result = await db.scopeDelete(dbCtx, scope);
803 assert(db.db.result.called);
804 assert.strictEqual(result, true);
805 });
806 it('scope in use', async function () {
807 const dbResult = {
808 rowCount: 0,
809 rows: undefined,
810 duration: 22,
811 };
812 sinon.stub(db.db, 'one').resolves({ inUse: true });
813 sinon.stub(db.db, 'result').resolves(dbResult);
814 const result = await db.scopeDelete(dbCtx, scope);
815 assert(db.db.result.notCalled);
816 assert.strictEqual(result, false);
817 });
818 it('failure', async function () {
819 sinon.stub(db.db, 'one').rejects(expectedException);
820 await assert.rejects(() => db.scopeDelete(dbCtx, scope), expectedException);
821 });
822 }); // scopeDelete
823
824 describe('scopeUpsert', function () {
825 let scope, description;
826 beforeEach(function () {
827 scope = 'username';
828 description = '$z$foo';
829 });
830 it('success', async function () {
831 const dbResult = {
832 rowCount: 1,
833 rows: undefined,
834 duration: 22,
835 };
836 sinon.stub(db.db, 'result').resolves(dbResult);
837 await db.scopeUpsert(dbCtx, scope, description);
838 });
839 it('failure', async function() {
840 scope = undefined;
841 const dbResult = {
842 rowCount: 0,
843 rows: undefined,
844 duration: 22,
845 };
846 sinon.stub(db.db, 'result').resolves(dbResult);
847 await assert.rejects(() => db.scopeUpsert(dbCtx, scope, description), DBErrors.UnexpectedResult);
848 });
849 }); // scopeUpsert
850
851 describe('tokenCleanup', function () {
852 let codeLifespanSeconds, atLeastMsSinceLast;
853 beforeEach(function () {
854 sinon.stub(db.db, 'result');
855 sinon.stub(db.db, 'oneOrNone');
856 codeLifespanSeconds = 600000;
857 atLeastMsSinceLast = 86400000;
858 });
859 it('success, empty almanac', async function () {
860 const cleaned = 10;
861 db.db.result
862 .onFirstCall().resolves({ rowCount: cleaned })
863 .onSecondCall().resolves({ rowCount: 1 });
864 const result = await db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast);
865 assert.strictEqual(result, cleaned);
866 });
867 it('success, too soon', async function () {
868 db.db.oneOrNone.resolves({ date: new Date(Date.now() - 4000) });
869 const result = await db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast);
870 assert.strictEqual(result, undefined);
871 assert(db.db.result.notCalled);
872 });
873 it('failure', async function () {
874 db.db.result.resolves({ rowCount: 0 });
875 await assert.rejects(() => db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast), DBErrors.UnexpectedResult);
876 });
877 }); // tokenCleanup
878
879 describe('tokenRevokeByCodeId', function () {
880 let codeId;
881 beforeEach(function () {
882 codeId = 'a74bda94-3dae-11ec-8908-0025905f714a';
883 });
884 it('success', async function () {
885 const dbResult = {
886 rowCount: 1,
887 rows: undefined,
888 duration: 22,
889 };
890 sinon.stub(db.db, 'result').resolves(dbResult);
891 await db.tokenRevokeByCodeId(dbCtx, codeId);
892 });
893 it('failure', async function() {
894 const dbResult = {
895 rowCount: 0,
896 rows: undefined,
897 duration: 22,
898 };
899 sinon.stub(db.db, 'result').resolves(dbResult);
900 await assert.rejects(() => db.tokenRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
901 });
902 }); // tokenRevokeByCodeId
903
904 describe('tokenRefreshRevokeByCodeId', function () {
905 let codeId;
906 beforeEach(function () {
907 codeId = '279947c8-2584-11ed-a2d6-0025905f714a';
908 sinon.stub(db.db, 'result');
909 });
910 it('success', async function () {
911 db.db.result.resolves({ rowCount: 1 });
912 await db.tokenRefreshRevokeByCodeId(dbCtx, codeId);
913 });
914 it('failure, no code', async function () {
915 db.db.result.resolves({ rowCount: 0 });
916 assert.rejects(async () => db.tokenRefreshRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
917 });
918 it('failure', async function () {
919 db.db.result.rejects(expectedException);
920 assert.rejects(async () => db.tokenRefreshRevokeByCodeId(dbCtx, codeId), expectedException);
921 });
922 }); // tokenRefreshRevokeByCodeId
923
924 describe('tokensGetByIdentifier', function () {
925 let identifier;
926 beforeEach(function () {
927 identifier = 'identifier';
928 });
929 it('success', async function () {
930 const dbResult = [
931 {
932 'created': new Date(),
933 'expires': new Date(),
934 'isRevoked': false,
935 'token': '',
936 'codeId': '',
937 'profile': '',
938 'identifier': '',
939 },
940 ];
941 const expected = dbResult;
942 sinon.stub(db.db, 'manyOrNone').resolves(dbResult);
943 const result = await db.tokensGetByIdentifier(dbCtx, identifier);
944 assert.deepStrictEqual(result, expected);
945 });
946 it('failure', async function () {
947 sinon.stub(db.db, 'manyOrNone').rejects(expectedException);
948 await assert.rejects(() => db.tokensGetByIdentifier(dbCtx, identifier), expectedException);
949 });
950 }); // tokensGetByIdentifier
951
952
953 }); // DatabasePostgres