1 /* eslint-disable sonarjs/no-identical-functions */
3 /* eslint-disable sonarjs/no-duplicate-string */
6 /* This provides implementation coverage, stubbing pg-promise. */
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');
17 const expectedException
= new Error('oh no');
19 describe('DatabasePostgres', function () {
20 let db
, logger
, options
, pgpStub
;
25 result: () => ({ rows: [] }),
30 manyOrNone: common
.nop
,
31 oneOrNone: common
.nop
,
34 multiResult: common
.nop
,
37 stub
.tx
= (fn
) => fn(stub
);
38 stub
.txIf
= (fn
) => fn(stub
);
39 stub
.task
= (fn
) => fn(stub
);
45 pgpStub
.QueryFile
= class {};
46 pgpStub
.end
= common
.nop
;
48 beforeEach(function () {
49 logger
= new StubLogger();
51 options
= new Config('test');
52 db
= new DB(logger
, options
, pgpStub
);
55 afterEach(function () {
59 it('covers no query logging', function () {
60 delete options
.db
.queryLogLevel
;
61 db
= new DB(logger
, options
, pgpStub
);
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
) => {
71 // eslint-disable-next-line security/detect-object-injection
74 assert(!(e
instanceof DBErrors
.NotImplemented
), `${fn} not implemented`);
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(': '));
85 describe('pgpInitOptions', function () {
86 describe('error', function () {
87 it('covers', function () {
90 db
.pgpInitOptions
.error(err
, event
);
91 assert(db
.logger
.error
.called
);
94 describe('query', function () {
95 it('covers', function () {
97 db
.pgpInitOptions
.query(event
);
98 assert(db
.logger
.debug
.called
);
101 describe('receive', function () {
102 it('covers', function () {
105 column_one: 'one', // eslint-disable-line camelcase
106 column_two: 2, // eslint-disable-line camelcase
109 column_one: 'foo', // eslint-disable-line camelcase
110 column_two: 4, // eslint-disable-line camelcase
115 const expectedData
= [
125 db
.pgpInitOptions
.receive({ data
, result
, ctx: event
});
126 assert(db
.logger
.debug
.called
);
127 assert
.deepStrictEqual(data
, expectedData
);
129 it('covers no query logging', function () {
130 delete options
.db
.queryLogLevel
;
131 db
= new DB(logger
, options
, pgpStub
);
134 column_one: 'one', // eslint-disable-line camelcase
135 column_two: 2, // eslint-disable-line camelcase
138 column_one: 'foo', // eslint-disable-line camelcase
139 column_two: 4, // eslint-disable-line camelcase
144 const expectedData
= [
154 db
.pgpInitOptions
.receive({ data
, result
, ctx: event
});
155 assert(db
.logger
.debug
.called
);
156 assert
.deepStrictEqual(data
, expectedData
);
160 }); // pgpInitOptions
162 describe('_initTables', function () {
163 beforeEach(function () {
164 sinon
.stub(db
.db
, 'oneOrNone');
165 sinon
.stub(db
.db
, 'multiResult');
166 sinon
.stub(db
, '_currentSchema');
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();
174 it('covers exists', async
function() {
175 db
.db
.oneOrNone
.resolves({});
176 db
._currentSchema
.resolves(db
.schemaVersionsSupported
.max
);
177 await db
._initTables();
181 describe('initialize', function () {
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);
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
);
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));
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();
207 it('covers listener', async
function() {
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
);
218 describe('healthCheck', function () {
219 beforeEach(function () {
220 sinon
.stub(db
.db
, 'connect').resolves({
223 serverVersion: '0.0',
227 it('covers', async
function () {
228 const result
= await db
.healthCheck();
229 assert
.deepStrictEqual(result
, { serverVersion: '0.0' });
233 describe('_queryFileHelper', function () {
234 it('covers success', function () {
235 const _queryFile
= db
._queryFileHelper(pgpStub
);
238 it('covers failure', function () {
239 pgpStub
.QueryFile
= class {
241 this.error
= expectedException
;
244 const _queryFile
= db
._queryFileHelper(pgpStub
);
245 assert
.throws(() => _queryFile(), expectedException
);
247 }); // _queryFileHelper
249 describe('_closeConnection', function () {
253 it('success', async
function () {
254 sinon
.stub(db
._pgp
, 'end');
255 await db
._closeConnection();
256 assert(db
._pgp
.end
.called
);
258 it('failure', async
function () {
259 sinon
.stub(db
._pgp
, 'end').throws(expectedException
);
260 await assert
.rejects(() => db
._closeConnection(), expectedException
);
262 it('covers listener', async
function () {
266 sinon
.stub(db
._pgp
, 'end');
267 await db
._closeConnection();
268 assert(db
._pgp
.end
.called
);
270 }); // _closeConnection
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
);
278 it('success', async
function () {
279 sinon
.stub(db
.db
, 'batch');
280 await db
._purgeTables(true);
281 assert(db
.db
.batch
.called
);
283 it('failure', async
function () {
284 sinon
.stub(db
.db
, 'tx').rejects(expectedException
)
285 await assert
.rejects(() => db
._purgeTables(true), expectedException
);
289 describe('context', function () {
290 it('covers', async
function () {
291 await db
.context(common
.nop
);
295 describe('transaction', function () {
296 it('covers', async
function () {
297 await db
.transaction(db
.db
, common
.nop
);
301 describe('almanacGetAll', function () {
302 beforeEach(function () {
303 sinon
.stub(db
.db
, 'manyOrNone');
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
);
311 it('failure', async
function () {
312 db
.db
.manyOrNone
.rejects(expectedException
);
313 await assert
.rejects(() => db
.almanacGetAll(dbCtx
), expectedException
);
317 describe('almanacUpsert', function () {
319 beforeEach(function () {
320 event
= 'test_event';
321 date
= new Date('Fri Dec 22 03:27 UTC 2023')
323 it('success', async
function () {
329 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
330 await db
.almanacUpsert(dbCtx
, event
, date
);
332 it('success with default date', async
function () {
338 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
339 await db
.almanacUpsert(dbCtx
, event
);
341 it('failure', async
function () {
347 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
348 await assert
.rejects(() => db
.almanacUpsert(dbCtx
, event
, date
), DBErrors
.UnexpectedResult
);
352 describe('authenticationSuccess', function () {
354 beforeEach(function () {
355 identifier
= 'username';
357 it('success', async
function () {
363 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
364 await db
.authenticationSuccess(dbCtx
, identifier
);
366 it('failure', async
function() {
372 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
373 await assert
.rejects(() => db
.authenticationSuccess(dbCtx
, identifier
), DBErrors
.UnexpectedResult
);
375 }); // authenticationSuccess
377 describe('authenticationGet', function () {
378 let identifier
, credential
;
379 beforeEach(function () {
380 identifier
= 'username';
381 credential
= '$z$foo';
383 it('success', async
function () {
384 const dbResult
= { identifier
, credential
};
385 sinon
.stub(db
.db
, 'oneOrNone').resolves(dbResult
);
386 const result
= await db
.authenticationGet(dbCtx
, identifier
);
387 assert
.deepStrictEqual(result
, dbResult
);
389 it('failure', async
function() {
390 sinon
.stub(db
.db
, 'oneOrNone').rejects(expectedException
);
391 await assert
.rejects(() => db
.authenticationGet(dbCtx
, identifier
, credential
), expectedException
);
393 }); // authenticationGet
395 describe('authenticationUpsert', function () {
396 let identifier
, credential
;
397 beforeEach(function () {
398 identifier
= 'username';
399 credential
= '$z$foo';
401 it('success', async
function () {
407 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
408 await db
.authenticationUpsert(dbCtx
, identifier
, credential
);
410 it('failure', async
function() {
411 credential
= undefined;
417 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
418 await assert
.rejects(() => db
.authenticationUpsert(dbCtx
, identifier
, credential
), DBErrors
.UnexpectedResult
);
420 }); // authenticationUpsert
422 describe('authenticationUpdateCredential', function () {
423 let identifier
, credential
;
424 beforeEach(function () {
425 identifier
= 'username';
426 credential
= '$z$foo';
428 it('success', async
function () {
434 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
435 await db
.authenticationUpdateCredential(dbCtx
, identifier
, credential
);
437 it('failure', async
function () {
438 credential
= undefined;
444 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
445 await assert
.rejects(() => db
.authenticationUpdateCredential(dbCtx
, identifier
, credential
), DBErrors
.UnexpectedResult
);
448 }); // authenticationUpdateCredential
450 describe('authenticationUpdateOTPKey', function () {
451 let identifier
, otpKey
;
452 beforeEach(function () {
453 identifier
= 'username';
454 otpKey
= '1234567890123456789012';
456 it('success', async
function () {
462 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
463 await db
.authenticationUpdateOTPKey(dbCtx
, identifier
, otpKey
);
465 it('failure', async
function () {
471 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
472 await assert
.rejects(() => db
.authenticationUpdateOTPKey(dbCtx
, identifier
, otpKey
), DBErrors
.UnexpectedResult
);
474 }); // authenticationUpdateOTPKey
476 describe('profileIdentifierInsert', function () {
477 let profile
, identifier
;
478 beforeEach(function () {
479 profile
= 'https://profile.example.com/';
480 identifier
= 'username';
482 it('success', async
function () {
486 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
487 await db
.profileIdentifierInsert(dbCtx
, profile
, identifier
);
489 it('failure', async
function () {
493 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
494 await assert
.rejects(() => db
.profileIdentifierInsert(dbCtx
, profile
, identifier
), DBErrors
.UnexpectedResult
);
496 }); // profileIdentifierInsert
498 describe('profileIsValid', function () {
500 beforeEach(function () {
501 profile
= 'https://profile.exmaple.com';
503 it('valid profile', async
function () {
504 sinon
.stub(db
.db
, 'oneOrNone').resolves({ profile
});
505 const result
= await db
.profileIsValid(dbCtx
, profile
);
506 assert
.strictEqual(result
, true);
508 it('invalid profile', async
function () {
509 sinon
.stub(db
.db
, 'oneOrNone').resolves();
510 const result
= await db
.profileIsValid(dbCtx
, profile
);
511 assert
.strictEqual(result
, false);
513 it('failure', async
function () {
514 sinon
.stub(db
.db
, 'oneOrNone').rejects(expectedException
);
515 await assert
.rejects(() => db
.profileIsValid(dbCtx
, profile
), expectedException
);
517 }); // profileIsValid
519 describe('tokenGetByCodeId', function () {
521 beforeEach(function () {
522 sinon
.stub(db
.db
, 'oneOrNone');
525 it('success', async
function() {
530 expires: new Date(Date
.now() + 24 * 60 * 60 * 1000),
532 db
.db
.oneOrNone
.resolves(dbResult
);
533 const result
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
534 assert
.deepStrictEqual(result
, dbResult
);
536 it('failure', async
function () {
537 db
.db
.oneOrNone
.rejects(expectedException
);
538 await assert
.rejects(() => db
.tokenGetByCodeId(dbCtx
, codeId
), expectedException
);
540 }); // tokenGetByCodeId
542 describe('profileScopeInsert', function () {
544 beforeEach(function () {
545 profile
= 'https://profile.example.com/';
548 it('success', async
function () {
552 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
553 await db
.profileScopeInsert(dbCtx
, profile
, scope
);
555 it('failure', async
function () {
556 sinon
.stub(db
.db
, 'result').rejects(expectedException
);
557 await assert
.rejects(() => db
.profileScopeInsert(dbCtx
, profile
, scope
), expectedException
);
559 it('failure', async
function () {
563 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
564 await assert
.rejects(() => db
.profileScopeInsert(dbCtx
, profile
, scope
), DBErrors
.UnexpectedResult
);
566 }); // profileScopeInsert
568 describe('profileScopesSetAll', function () {
570 beforeEach(function () {
571 profile
= 'https://example.com/';
573 sinon
.stub(db
.db
, 'result');
575 it('success, no scopes', async
function () {
576 db
.db
.result
.resolves();
577 await db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
579 it('success, scopes', async
function () {
580 db
.db
.result
.resolves();
581 scopes
.push('profile', 'email', 'create');
582 await db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
584 it('failure', async
function () {
585 db
.db
.result
.rejects(expectedException
);
586 await assert
.rejects(() => db
.profileScopesSetAll(dbCtx
, profile
, scopes
), expectedException
);
588 }); // profileScopesSetAll
590 describe('profilesScopesByIdentifier', function () {
591 let identifier
, scopeIndex
, profileScopes
, profiles
;
592 beforeEach(function () {
593 identifier
= 'identifier';
596 description: 'A scope.',
599 isManuallyAdded: true,
600 profiles: ['https://first.example.com/', 'https://second.example.com/'],
603 description: 'Another scope.',
604 application: 'another test',
606 isManuallyAdded: false,
607 profiles: ['https://first.example.com/'],
610 description: 'A scope without application.',
613 isManuallyAdded: false,
614 profiles: ['https://second.example.com/'],
616 'no_profile_scope': {
617 description: 'A scope without profiles.',
620 isManuallyAdded: false,
625 'https://first.example.com/': {
626 'scope': scopeIndex
['scope'],
627 'another_scope': scopeIndex
['another_scope'],
629 'https://second.example.com/': {
630 'scope': scopeIndex
['scope'],
631 'no_app_scope': scopeIndex
['no_app_scope'],
633 'https://scopeless.example.com/': {},
636 'https://first.example.com/',
637 'https://second.example.com/',
638 'https://scopeless.example.com/',
641 it('success', async
function () {
643 { profile: 'https://first.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: true },
644 { profile: 'https://first.example.com/', scope: 'another_scope', application: 'another test', description: 'Another scope.', isPermanent: true, isManuallyAdded: false },
645 { profile: 'https://second.example.com/', scope: 'no_app_scope', application: '', description: 'A scope without application.', isPermanent: false, isManuallyAdded: false },
646 { profile: 'https://second.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: true },
647 { profile: null, scope: 'no_profile_scope', application: 'test', description: 'A scope without profiles.', isPermanent: false, isManuallyAdded: false },
648 { profile: 'https://scopeless.example.com/', scope: null, application: null, description: null, isPermanent: null, isManuallyAdded: null },
655 sinon
.stub(db
.db
, 'manyOrNone').resolves(dbResult
);
656 const result
= await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
657 assert
.deepStrictEqual(result
, expected
);
659 it('failure', async
function () {
660 sinon
.stub(db
.db
, 'manyOrNone').rejects(expectedException
);
661 await assert
.rejects(() => db
.profilesScopesByIdentifier(dbCtx
, identifier
), expectedException
);
663 }); // profilesScopesByIdentifier
665 describe('redeemCode', function () {
666 let codeId
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshId
, profileData
;
667 beforeEach(function () {
668 codeId
= '41945b8e-3e82-11ec-82d1-0025905f714a';
670 clientId
= 'https://app.example.com/';
671 profile
= 'https://profile.example.com/';
672 identifier
= 'username';
673 scopes
= ['scope1', 'scope2'];
674 lifespanSeconds
= 600;
675 refreshId
= undefined;
676 profileData
= undefined;
678 it('success redeem', async
function () {
681 rows: [{ isRevoked: false }],
684 const dbResultScopes
= {
685 rowCount: scopes
.length
,
689 sinon
.stub(db
.db
, 'result').resolves(dbResult
).onCall(2).resolves(dbResultScopes
);
690 const result
= await db
.redeemCode(dbCtx
, { codeId
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshId
, profileData
});
691 assert
.strictEqual(result
, true);
693 it('success redeem, no scopes', async
function () {
697 rows: [{ isRevoked: false }],
700 const dbResultScopes
= {
701 rowCount: scopes
.length
,
705 sinon
.stub(db
.db
, 'result').resolves(dbResult
).onCall(1).resolves(dbResultScopes
);
706 const result
= await db
.redeemCode(dbCtx
, { codeId
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshId
, profileData
});
707 assert
.strictEqual(result
, true);
709 it('success revoke', async
function () {
712 rows: [{ isRevoked: true }],
715 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
716 const result
= await db
.redeemCode(dbCtx
, { codeId
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshId
, profileData
});
717 assert
.strictEqual(result
, false);
719 it('failure', async
function() {
725 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
726 await assert
.rejects(() => db
.redeemCode(dbCtx
, { codeId
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshId
, profileData
}), DBErrors
.UnexpectedResult
);
728 it('failure token scopes', async
function () {
731 rows: [{ isRevoked: false }],
734 const dbResultNone
= {
739 sinon
.stub(db
.db
, 'result').resolves(dbResult
).onCall(2).resolves(dbResultNone
);
740 await assert
.rejects(() => db
.redeemCode(dbCtx
, { codeId
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshId
, profileData
}), DBErrors
.UnexpectedResult
);
744 describe('refreshCode', function () {
745 let codeId
, now
, removeScopes
;
746 beforeEach(function () {
747 codeId
= '41945b8e-3e82-11ec-82d1-0025905f714a';
750 sinon
.stub(db
.db
, 'result').resolves({ rowCount: removeScopes
.length
});
751 sinon
.stub(db
.db
, 'oneOrNone');
753 it('success', async
function () {
754 db
.db
.oneOrNone
.resolves({
758 const result
= await db
.refreshCode(dbCtx
, codeId
, now
, removeScopes
);
759 assert(db
.db
.result
.notCalled
);
761 assert(result
.expires
);
762 assert(result
.refreshExpires
);
763 assert(!result
.scopes
);
765 it('success with scope reduction', async
function () {
766 removeScopes
= ['create'];
767 db
.db
.oneOrNone
.resolves({
772 db
.db
.result
.resolves({ rowCount: removeScopes
.length
});
773 const result
= await db
.refreshCode(dbCtx
, codeId
, now
, removeScopes
);
775 assert(result
.expires
);
776 assert(result
.refreshExpires
);
777 assert(!result
.scopes
.includes('create'));
779 it('failure', async
function () {
780 db
.db
.oneOrNone
.rejects(expectedException
);
781 await assert
.rejects(async () => db
.refreshCode(dbCtx
, codeId
, now
, removeScopes
), expectedException
);
783 it('failure with scope reduction', async
function () {
784 removeScopes
= ['create'];
785 db
.db
.oneOrNone
.resolves({});
786 db
.db
.result
.resolves({ rowCount: 0 });
787 await assert
.rejects(async () => db
.refreshCode(dbCtx
, codeId
, now
, removeScopes
), DBErrors
.UnexpectedResult
);
791 describe('resourceGet', function () {
793 beforeEach(function () {
794 sinon
.stub(db
.db
, 'oneOrNone');
795 identifier
= '05b81112-b224-11ec-a9c6-0025905f714a';
797 it('success', async
function () {
802 db
.db
.oneOrNone
.resolves(dbResult
);
803 const result
= await db
.resourceGet(dbCtx
, identifier
);
804 assert
.deepStrictEqual(result
, dbResult
);
806 it('failure', async
function() {
807 db
.db
.oneOrNone
.rejects(expectedException
);
808 await assert
.rejects(() => db
.resourceGet(dbCtx
, identifier
), expectedException
);
812 describe('resourceUpsert', function () {
813 let resourceId
, secret
, description
;
814 beforeEach(function () {
815 resourceId
= '98b8d9ec-f8e2-11ec-aceb-0025905f714a';
816 secret
= 'supersecret';
817 description
= 'some service';
819 it('success', async
function () {
825 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
826 await db
.resourceUpsert(dbCtx
, resourceId
, secret
, description
)
828 it('failure', async
function () {
834 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
835 await assert
.rejects(() => db
.resourceUpsert(dbCtx
, resourceId
, undefined, description
), DBErrors
.UnexpectedResult
);
837 }); // resourceUpsert
839 describe('scopeCleanup', function () {
840 let atLeastMsSinceLast
;
841 beforeEach(function () {
842 sinon
.stub(db
.db
, 'result');
843 sinon
.stub(db
.db
, 'oneOrNone');
844 atLeastMsSinceLast
= 86400000;
846 it('success, empty almanac', async
function () {
849 .onFirstCall().resolves({ rowCount: cleaned
})
850 .onSecondCall().resolves({ rowCount: 1 });
851 const result
= await db
.scopeCleanup(dbCtx
, atLeastMsSinceLast
);
852 assert
.strictEqual(result
, cleaned
);
854 it('success, too soon', async
function () {
855 db
.db
.oneOrNone
.resolves({ date: new Date(Date
.now() - 4000) });
856 const result
= await db
.scopeCleanup(dbCtx
, atLeastMsSinceLast
);
857 assert
.strictEqual(result
, undefined);
858 assert(db
.db
.result
.notCalled
);
860 it('failure', async
function () {
861 db
.db
.result
.resolves({ rowCount: 0 });
862 await assert
.rejects(async () => db
.scopeCleanup(dbCtx
, atLeastMsSinceLast
), DBErrors
.UnexpectedResult
);
866 describe('scopeDelete', function () {
868 beforeEach(function () {
871 it('success', async
function () {
877 sinon
.stub(db
.db
, 'one').resolves({ inUse: false });
878 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
879 const result
= await db
.scopeDelete(dbCtx
, scope
);
880 assert(db
.db
.result
.called
);
881 assert
.strictEqual(result
, true);
883 it('success, no scope', async
function () {
889 sinon
.stub(db
.db
, 'one').resolves({ inUse: false });
890 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
891 const result
= await db
.scopeDelete(dbCtx
, scope
);
892 assert(db
.db
.result
.called
);
893 assert
.strictEqual(result
, true);
895 it('scope in use', async
function () {
901 sinon
.stub(db
.db
, 'one').resolves({ inUse: true });
902 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
903 const result
= await db
.scopeDelete(dbCtx
, scope
);
904 assert(db
.db
.result
.notCalled
);
905 assert
.strictEqual(result
, false);
907 it('failure', async
function () {
908 sinon
.stub(db
.db
, 'one').rejects(expectedException
);
909 await assert
.rejects(() => db
.scopeDelete(dbCtx
, scope
), expectedException
);
913 describe('scopeUpsert', function () {
914 let scope
, description
;
915 beforeEach(function () {
917 description
= '$z$foo';
919 it('success', async
function () {
925 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
926 await db
.scopeUpsert(dbCtx
, scope
, description
);
928 it('failure', async
function() {
935 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
936 await assert
.rejects(() => db
.scopeUpsert(dbCtx
, scope
, description
), DBErrors
.UnexpectedResult
);
940 describe('tokenCleanup', function () {
941 let codeLifespanSeconds
, atLeastMsSinceLast
;
942 beforeEach(function () {
943 sinon
.stub(db
.db
, 'result');
944 sinon
.stub(db
.db
, 'oneOrNone');
945 codeLifespanSeconds
= 600000;
946 atLeastMsSinceLast
= 86400000;
948 it('success, empty almanac', async
function () {
951 .onFirstCall().resolves({ rowCount: cleaned
})
952 .onSecondCall().resolves({ rowCount: 1 });
953 const result
= await db
.tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
);
954 assert
.strictEqual(result
, cleaned
);
956 it('success, too soon', async
function () {
957 db
.db
.oneOrNone
.resolves({ date: new Date(Date
.now() - 4000) });
958 const result
= await db
.tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
);
959 assert
.strictEqual(result
, undefined);
960 assert(db
.db
.result
.notCalled
);
962 it('failure', async
function () {
963 db
.db
.result
.resolves({ rowCount: 0 });
964 await assert
.rejects(() => db
.tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
), DBErrors
.UnexpectedResult
);
968 describe('tokenRevokeByCodeId', function () {
970 beforeEach(function () {
971 codeId
= 'a74bda94-3dae-11ec-8908-0025905f714a';
973 it('success', async
function () {
979 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
980 await db
.tokenRevokeByCodeId(dbCtx
, codeId
);
982 it('failure', async
function() {
988 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
989 await assert
.rejects(() => db
.tokenRevokeByCodeId(dbCtx
, codeId
), DBErrors
.UnexpectedResult
);
991 }); // tokenRevokeByCodeId
993 describe('tokenRefreshRevokeByCodeId', function () {
995 beforeEach(function () {
996 codeId
= '279947c8-2584-11ed-a2d6-0025905f714a';
997 sinon
.stub(db
.db
, 'result');
999 it('success', async
function () {
1000 db
.db
.result
.resolves({ rowCount: 1 });
1001 await db
.tokenRefreshRevokeByCodeId(dbCtx
, codeId
);
1003 it('failure, no code', async
function () {
1004 db
.db
.result
.resolves({ rowCount: 0 });
1005 assert
.rejects(async () => db
.tokenRefreshRevokeByCodeId(dbCtx
, codeId
), DBErrors
.UnexpectedResult
);
1007 it('failure', async
function () {
1008 db
.db
.result
.rejects(expectedException
);
1009 assert
.rejects(async () => db
.tokenRefreshRevokeByCodeId(dbCtx
, codeId
), expectedException
);
1011 }); // tokenRefreshRevokeByCodeId
1013 describe('tokensGetByIdentifier', function () {
1015 beforeEach(function () {
1016 identifier
= 'identifier';
1018 it('success', async
function () {
1021 'created': new Date(),
1022 'expires': new Date(),
1030 const expected
= dbResult
;
1031 sinon
.stub(db
.db
, 'manyOrNone').resolves(dbResult
);
1032 const result
= await db
.tokensGetByIdentifier(dbCtx
, identifier
);
1033 assert
.deepStrictEqual(result
, expected
);
1035 it('failure', async
function () {
1036 sinon
.stub(db
.db
, 'manyOrNone').rejects(expectedException
);
1037 await assert
.rejects(() => db
.tokensGetByIdentifier(dbCtx
, identifier
), expectedException
);
1039 }); // tokensGetByIdentifier
1041 describe('ticketRedeemed', function () {
1043 beforeEach(function () {
1045 resource: 'https://resource.example.com/',
1046 subject: 'https://subject.example.com/',
1047 iss: 'https://idp.example.com/',
1048 ticket: 'xxxTICKETxxx',
1049 token: 'xxxTOKENxxx',
1052 it('success', async
function () {
1058 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
1059 await db
.ticketRedeemed(dbCtx
, redeemedData
);
1061 it('failure', async
function () {
1067 sinon
.stub(db
.db
, 'result').resolves(dbResult
);
1068 await assert
.rejects(() => db
.ticketRedeemed(dbCtx
, redeemedData
), DBErrors
.UnexpectedResult
);
1070 }); // ticketRedeemed
1072 describe('ticketTokenPublished', function () {
1074 beforeEach(function () {
1076 resource: 'https://resource.example.com/',
1077 subject: 'https://subject.example.com/',
1078 iss: 'https://idp.example.com/',
1079 ticket: 'xxxTICKETxxx',
1080 token: 'xxxTOKENxxx',
1082 sinon
.stub(db
.db
, 'result');
1084 it('success', async
function () {
1090 db
.db
.result
.resolves(dbResult
);
1091 await db
.ticketTokenPublished(dbCtx
, redeemedData
);
1093 it('failure', async
function () {
1099 db
.db
.result
.resolves(dbResult
);
1100 await assert
.rejects(() => db
.ticketTokenPublished(dbCtx
, redeemedData
), DBErrors
.UnexpectedResult
);
1102 it('failure of almanac', async
function () {
1108 const dbResultAlmanac
= {
1112 db
.db
.result
.resolves(dbResult
).onCall(1).resolves(dbResultAlmanac
);
1113 await assert
.rejects(() => db
.ticketTokenPublished(dbCtx
, redeemedData
), DBErrors
.UnexpectedResult
);
1115 }); // ticketTokenPublished
1117 describe('ticketTokenGetUnpublished', function () {
1118 it('success', async
function () {
1120 resource: 'https://resource.example.com/',
1121 subject: 'https://subject.example.com/',
1122 iss: 'https://idp.example.com/',
1123 ticket: 'xxxTICKETxxx',
1124 token: 'xxxTOKENxxx',
1125 created: new Date(),
1128 sinon
.stub(db
.db
, 'manyOrNone').resolves(expected
);
1129 const result
= await db
.ticketTokenGetUnpublished(dbCtx
);
1130 assert
.deepStrictEqual(result
, expected
);
1132 it('failure', async
function () {
1133 sinon
.stub(db
.db
, 'manyOrNone').rejects(expectedException
);
1134 await assert
.rejects(() => db
.ticketTokenGetUnpublished(dbCtx
), expectedException
);
1136 }); // ticketTokenGetUnpublished
1138 }); // DatabasePostgres