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