1 /* eslint-disable sonarjs/no-duplicate-string */
4 /* This provides implementation coverage, stubbing parts of better-sqlite3. */
6 const assert
= require('assert');
7 const sinon
= require('sinon');
8 const StubDatabase
= require('../../stub-db');
9 const StubLogger
= require('../../stub-logger');
10 const DB
= require('../../../src/db/sqlite');
11 const DBErrors
= require('../../../src/db/errors');
12 const common
= require('../../../src/common');
13 const Config
= require('../../../config');
15 const expectedException
= new Error('oh no');
17 describe('DatabaseSQLite', function () {
18 let db
, options
, logger
, stubDb
;
21 logger
= new StubLogger();
23 stubDb
= new StubDatabase();
25 beforeEach(function () {
26 options
= new Config('test');
27 options
.db
.connectionString
= 'sqlite://:memory:';
28 db
= new DB(logger
, options
);
31 afterEach(function () {
35 it('covers constructor options', function () {
36 delete options
.db
.connectionString
;
37 db
= new DB(logger
, options
);
40 // Ensure all interface methods are implemented
41 describe('Implementation', function () {
42 it('implements interface', async
function () {
43 const results
= await Promise
.allSettled(stubDb
._implementation
.map((fn
) => {
45 // eslint-disable-next-line security/detect-object-injection
48 assert(!(e
instanceof DBErrors
.NotImplemented
), `${fn} not implemented`);
51 const failures
= results
.filter((x
) => x
.status
=== 'rejected');
52 assert(!failures
.length
, failures
.map((x
) => {
53 x
= x
.reason
.toString();
54 return x
.slice(x
.indexOf(': '));
59 describe('_currentSchema', function () {
60 it('covers', function () {
61 const version
= { major: 1, minor: 0, patch: 0 };
62 sinon
.stub(db
.db
, 'prepare').returns({
65 const result
= db
._currentSchema();
66 assert
.deepStrictEqual(result
, version
);
70 describe('_closeConnection', function () {
71 it('success', function () {
72 sinon
.stub(db
.db
, 'close');
73 db
._closeConnection();
74 assert(db
.db
.close
.called
);
76 it('failure', function () {
77 sinon
.stub(db
.db
, 'close').throws(expectedException
);
78 assert
.throws(() => db
._closeConnection(), expectedException
);
80 }); // _closeConnection
82 describe('_purgeTables', function () {
83 beforeEach(function () {
84 sinon
.stub(db
.db
, 'prepare').returns({
88 it('covers not really', function () {
89 db
._purgeTables(false);
90 assert(!db
.db
.prepare
.called
);
92 it('success', function () {
93 db
._purgeTables(true);
94 assert(db
.db
.prepare
.called
);
96 it('failure', function () {
97 db
.db
.prepare
.restore();
98 sinon
.stub(db
.db
, 'prepare').throws(expectedException
);
99 assert
.throws(() => db
._purgeTables(true), expectedException
);
103 describe('_optimize', function () {
104 beforeEach(function () {
105 sinon
.stub(db
.statement
._optimize
, 'all');
106 sinon
.stub(db
.db
, 'pragma');
108 it('covers', function () {
109 db
.changesSinceLastOptimize
= BigInt(20);
111 assert(db
.db
.pragma
.called
);
112 assert(db
.statement
._optimize
.all
.called
);
113 assert
.strictEqual(db
.changesSinceLastOptimize
, 0n
);
117 describe('_updateChanges', function () {
119 beforeEach(function () {
123 sinon
.stub(db
, '_optimize');
125 it('does not optimize if not wanted', function () {
126 db
.optimizeAfterChanges
= 0n
;
127 db
._updateChanges(dbResult
);
128 assert(db
._optimize
.notCalled
);
130 it('does not optimize if under threshold', function () {
131 db
.optimizeAfterChanges
= 100n
;
132 db
._updateChanges(dbResult
);
133 assert(db
._optimize
.notCalled
);
135 it('optimizes over threshold', function () {
136 db
.optimizeAfterChanges
= 1n
;
137 db
._updateChanges(dbResult
);
138 assert(db
._optimize
.called
);
140 }); // _updateChanges
142 describe('_deOphidiate', function () {
143 it('covers non-array', function () {
150 const result
= DB
._deOphidiate(obj
);
151 assert
.deepStrictEqual(result
, expected
);
153 it('covers array', function () {
170 const result
= DB
._deOphidiate(rows
);
171 assert
.deepStrictEqual(result
, expected
);
175 describe('healthCheck', function () {
176 it('covers', function () {
179 it('covers failure', function () {
180 db
.db
= { open: false };
181 assert
.throws(() => db
.healthCheck(), DBErrors
.UnexpectedResult
);
185 describe('context', function () {
186 it('covers', function () {
187 db
.context(common
.nop
);
191 describe('transaction', function () {
192 it('covers', function () {
193 db
.transaction(db
.db
, common
.nop
);
195 it('covers no context', function () {
196 db
.transaction(undefined, common
.nop
);
200 describe('almanacGetAll', function () {
201 beforeEach(function () {
202 sinon
.stub(db
.statement
.almanacGetAll
, 'all');
204 it('success', function () {
205 const dbResult
= [{ event: 'someEvent', epoch: '1668887796' } ];
206 const expected
= [{ event: 'someEvent', date: new Date('Sat Nov 19 11:56:36 AM PST 2022') }];
207 db
.statement
.almanacGetAll
.all
.returns(dbResult
);
208 const result
= db
.almanacGetAll(dbCtx
);
209 assert
.deepStrictEqual(result
, expected
);
211 it('failure', function () {
212 db
.statement
.almanacGetAll
.all
.throws(expectedException
);
213 assert
.throws(() => db
.almanacGetAll(dbCtx
), expectedException
);
217 describe('almanacUpsert', function () {
218 let event
, date
, dbResult
;
219 beforeEach(function () {
220 event
= 'test_event';
221 date
= new Date('Fri Dec 22 03:27 UTC 2023');
222 sinon
.stub(db
.statement
.almanacUpsert
, 'run');
225 lastInsertRowid: undefined,
228 it('success', function () {
229 db
.statement
.almanacUpsert
.run
.returns(dbResult
);
230 db
.almanacUpsert(dbCtx
, event
, date
);
232 it('success with default date', function () {
233 db
.statement
.almanacUpsert
.run
.returns(dbResult
);
234 db
.almanacUpsert(dbCtx
, event
);
236 it('failure', function () {
237 dbResult
.changes
= 0;
238 db
.statement
.almanacUpsert
.run
.returns(dbResult
);
239 assert
.throws(() => db
.almanacUpsert(dbCtx
, { event
, date
}), DBErrors
.UnexpectedResult
);
243 describe('authenticationGet', function () {
244 let identifier
, credential
;
245 beforeEach(function () {
246 identifier
= 'username';
247 credential
= '$z$foo';
248 sinon
.stub(db
.statement
.authenticationGet
, 'get');
250 it('success', function() {
255 db
.statement
.authenticationGet
.get.returns(expected
);
256 const result
= db
.authenticationGet(dbCtx
, identifier
);
257 assert
.deepStrictEqual(result
, expected
);
259 it('failure', function () {
260 db
.statement
.authenticationGet
.get.throws(expectedException
);
261 assert
.throws(() => db
.authenticationGet(dbCtx
, identifier
), expectedException
);
263 }); // authenticationGet
265 describe('authenticationSuccess', function () {
266 let dbResult
, identifier
;
267 beforeEach(function () {
268 identifier
= 'username';
269 sinon
.stub(db
.statement
.authenticationSuccess
, 'run');
272 lastInsertRowid: undefined,
275 it('success', function() {
276 db
.statement
.authenticationSuccess
.run
.returns(dbResult
);
277 db
.authenticationSuccess(dbCtx
, identifier
);
279 it('failure', function () {
280 dbResult
.changes
= 0;
281 db
.statement
.authenticationSuccess
.run
.returns(dbResult
);
282 assert
.throws(() => db
.authenticationSuccess(dbCtx
, identifier
), DBErrors
.UnexpectedResult
);
284 }); // authenticationSuccess
286 describe('authenticationUpsert', function () {
287 let identifier
, credential
;
288 beforeEach(function () {
289 identifier
= 'username';
290 credential
= '$z$foo';
292 it('success', function() {
295 lastInsertRowid: undefined,
297 sinon
.stub(db
.statement
.authenticationUpsert
, 'run').returns(dbResult
);
298 db
.authenticationUpsert(dbCtx
, identifier
, credential
);
300 it('failure', function () {
303 lastInsertRowid: undefined,
305 sinon
.stub(db
.statement
.authenticationUpsert
, 'run').returns(dbResult
);
306 assert
.throws(() => db
.authenticationUpsert(dbCtx
, identifier
, credential
), DBErrors
.UnexpectedResult
);
308 }); // authenticationUpsert
310 describe('authenticationUpdateCredential', function () {
311 let identifier
, credential
;
312 beforeEach(function () {
313 identifier
= 'username';
314 credential
= '$z$foo';
316 it('success', function() {
319 lastInsertRowid: undefined,
321 sinon
.stub(db
.statement
.authenticationUpdateCredential
, 'run').returns(dbResult
);
322 db
.authenticationUpdateCredential(dbCtx
, identifier
, credential
);
324 it('failure', function () {
327 lastInsertRowid: undefined,
329 sinon
.stub(db
.statement
.authenticationUpdateCredential
, 'run').returns(dbResult
);
330 assert
.throws(() => db
.authenticationUpdateCredential(dbCtx
, identifier
, credential
), DBErrors
.UnexpectedResult
);
332 }); // authenticationUpdateCredential
334 describe('authenticationUpdateOTPKey', function () {
335 let identifier
, otpKey
;
336 beforeEach(function () {
337 identifier
= 'username';
338 otpKey
= '1234567890123456789012';
340 it('success', function() {
343 lastInsertRowid: undefined,
345 sinon
.stub(db
.statement
.authenticationUpdateOtpKey
, 'run').returns(dbResult
);
346 db
.authenticationUpdateOTPKey(dbCtx
, identifier
, otpKey
);
348 it('failure', function () {
351 lastInsertRowid: undefined,
353 sinon
.stub(db
.statement
.authenticationUpdateOtpKey
, 'run').returns(dbResult
);
354 assert
.throws(() => db
.authenticationUpdateOTPKey(dbCtx
, identifier
, otpKey
), DBErrors
.UnexpectedResult
);
356 }); // authenticationUpdateOTPKey
358 describe('profileIdentifierInsert', function () {
359 let profile
, identifier
;
360 beforeEach(function () {
361 profile
= 'https://profile.example.com/';
362 identifier
= 'identifier';
363 sinon
.stub(db
.statement
.profileIdentifierInsert
, 'run');
365 it('success', function () {
366 db
.statement
.profileIdentifierInsert
.run
.returns({ changes: 1 });
367 db
.profileIdentifierInsert(dbCtx
, profile
, identifier
);
369 it('failure', function () {
370 db
.statement
.profileIdentifierInsert
.run
.returns({ changes: 0 });
371 assert
.throws(() => db
.profileIdentifierInsert(dbCtx
, profile
, identifier
), DBErrors
.UnexpectedResult
);
373 }); // profileIdentifierInsert
375 describe('profileScopeInsert', function () {
377 beforeEach(function () {
378 profile
= 'https://profile.example.com/';
380 sinon
.stub(db
.statement
.profileScopeInsert
, 'run');
382 it('success', function () {
383 db
.statement
.profileScopeInsert
.run
.returns({ changes: 1 });
384 db
.profileScopeInsert(dbCtx
, profile
, scope
);
386 it('failure', function () {
387 db
.statement
.profileScopeInsert
.run
.returns({ changes: 2 });
388 assert
.throws(() => db
.profileScopeInsert(dbCtx
, profile
, scope
), DBErrors
.UnexpectedResult
);
390 }); // profileScopeInsert
392 describe('profileIsValid', function () {
394 beforeEach(function () {
395 profile
= 'https://profile.exmaple.com';
397 it('valid profile', function () {
398 sinon
.stub(db
.statement
.profileGet
, 'get').returns({ profile
});
399 const result
= db
.profileIsValid(dbCtx
, profile
);
400 assert
.deepStrictEqual(result
, true);
402 it('invalid profile', function () {
403 sinon
.stub(db
.statement
.profileGet
, 'get').returns();
404 const result
= db
.profileIsValid(dbCtx
, profile
);
405 assert
.deepStrictEqual(result
, false);
407 it('failure', function() {
408 sinon
.stub(db
.statement
.profileGet
, 'get').throws(expectedException
);
409 assert
.throws(() => db
.profileIsValid(dbCtx
, profile
), expectedException
);
411 }); // profileIsValid
413 describe('profilesScopesByIdentifier', function () {
414 let identifier
, scopeIndex
, profileScopes
, profiles
;
415 beforeEach(function () {
416 identifier
= 'identifier';
419 description: 'A scope.',
422 isManuallyAdded: false,
423 profiles: ['https://first.example.com/', 'https://second.example.com/'],
426 description: 'Another scope.',
427 application: 'another test',
429 isManuallyAdded: false,
430 profiles: ['https://first.example.com/'],
434 'https://first.example.com/': {
435 'scope': scopeIndex
['scope'],
436 'another_scope': scopeIndex
['another_scope'],
438 'https://second.example.com/': {
439 'scope': scopeIndex
['scope'],
442 profiles
= ['https://first.example.com/', 'https://second.example.com/'];
444 it('success', function () {
446 { profile: 'https://first.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: false },
447 { profile: 'https://first.example.com/', scope: 'another_scope', application: 'another test', description: 'Another scope.', isPermanent: false, isManuallyAdded: false },
448 { profile: 'https://second.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: false },
455 sinon
.stub(db
.statement
.profilesScopesByIdentifier
, 'all').returns(dbResult
);
456 const result
= db
.profilesScopesByIdentifier(dbCtx
, identifier
);
457 assert
.deepStrictEqual(result
, expected
);
459 it('failure', function() {
460 sinon
.stub(db
.statement
.profilesScopesByIdentifier
, 'all').throws(expectedException
);
461 assert
.throws(() => db
.profilesScopesByIdentifier(dbCtx
, identifier
), expectedException
);
463 }); // profilesScopesByIdentifier
465 describe('profileScopesSetAll', function () {
467 beforeEach(function () {
468 profile
= 'https://example.com/';
469 scopes
= ['scope1', 'scope2'];
470 sinon
.stub(db
.statement
.profileScopesClear
, 'run').returns();
471 sinon
.stub(db
.statement
.profileScopeInsert
, 'run');
473 it('success, no scopes', function () {
474 db
.statement
.profileScopeInsert
.run
.returns();
476 db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
478 it('success, scopes', function () {
479 db
.statement
.profileScopeInsert
.run
.returns();
480 scopes
.push('profile', 'email', 'create');
481 db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
483 it('failure', function () {
484 db
.statement
.profileScopeInsert
.run
.throws(expectedException
);
485 assert
.throws(() => db
.profileScopesSetAll(dbCtx
, profile
, scopes
), expectedException
);
488 }); // profileScopesSetAll
490 describe('redeemCode', function () {
491 let codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, profileData
;
492 beforeEach(function () {
493 codeId
= '2f226616-3e79-11ec-ad0f-0025905f714a';
495 clientId
= 'https://app.exmaple.com/';
496 profile
= 'https://profile.example.com/';
497 identifier
= 'username';
498 scopes
= ['scope1', 'scope2'];
499 lifespanSeconds
= 600;
500 profileData
= undefined;
501 created
= new Date();
503 sinon
.stub(db
.statement
.scopeInsert
, 'run');
504 sinon
.stub(db
.statement
.tokenScopeSet
, 'run');
505 sinon
.stub(db
.statement
.redeemCode
, 'get');
507 it('success', function() {
510 lastInsertRowid: undefined,
515 db
.statement
.scopeInsert
.run
.returns(dbResult
);
516 db
.statement
.tokenScopeSet
.run
.returns(dbResult
);
517 db
.statement
.redeemCode
.get.returns(dbGet
);
521 const result
= db
.redeemCode(dbCtx
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, profileData
});
522 assert
.strictEqual(result
, true);
524 it('success (revoked)', function() {
527 lastInsertRowid: undefined,
532 db
.statement
.scopeInsert
.run
.returns(dbResult
);
533 db
.statement
.tokenScopeSet
.run
.returns(dbResult
);
534 db
.statement
.redeemCode
.get.returns(dbGet
);
535 const result
= db
.redeemCode(dbCtx
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, profileData
});
536 assert
.strictEqual(result
, false);
538 it('failure', function () {
539 db
.statement
.scopeInsert
.run
.throws();
540 db
.statement
.tokenScopeSet
.run
.throws();
541 db
.statement
.redeemCode
.get.returns();
542 assert
.throws(() => db
.redeemCode(dbCtx
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
}), DBErrors
.UnexpectedResult
);
546 describe('refreshCode', function () {
547 let refreshResponse
, removeResponse
, scopesResponse
, codeId
, refreshed
, removeScopes
;
548 beforeEach(function () {
549 sinon
.stub(db
.statement
.refreshCode
, 'get');
550 sinon
.stub(db
.statement
.tokenScopeRemove
, 'run');
551 sinon
.stub(db
.statement
.tokenScopesGetByCodeId
, 'all');
552 codeId
= '73db7b18-27bb-11ed-8edd-0025905f714a';
553 refreshed
= new Date();
554 removeScopes
= ['foop'];
555 const refreshedEpoch
= Math
.ceil(refreshed
.getTime() / 1000);
557 expires: refreshedEpoch
+ 86400,
558 refreshExpires: refreshedEpoch
+ 172800,
561 changes: removeScopes
.length
,
567 it('success', function () {
568 db
.statement
.refreshCode
.get.returns(refreshResponse
);
569 db
.statement
.tokenScopeRemove
.run
.returns(removeResponse
);
570 db
.statement
.tokenScopesGetByCodeId
.all
.returns(scopesResponse
);
571 const expectedResponse
= {
572 expires: new Date(refreshResponse
.expires
* 1000),
573 refreshExpires: new Date(refreshResponse
.refreshExpires
* 1000),
576 const response
= db
.refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
);
577 assert
.deepStrictEqual(response
, expectedResponse
);
579 it('success without scope removal', function () {
580 db
.statement
.refreshCode
.get.returns(refreshResponse
);
581 db
.statement
.tokenScopeRemove
.run
.returns(removeResponse
);
582 const expectedResponse
= {
583 expires: new Date(refreshResponse
.expires
* 1000),
584 refreshExpires: new Date(refreshResponse
.refreshExpires
* 1000),
587 const response
= db
.refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
);
588 assert
.deepStrictEqual(response
, expectedResponse
);
590 it('success with no scopes left', function () {
591 db
.statement
.refreshCode
.get.returns(refreshResponse
);
592 db
.statement
.tokenScopeRemove
.run
.returns(removeResponse
);
593 const expectedResponse
= {
594 expires: new Date(refreshResponse
.expires
* 1000),
595 refreshExpires: new Date(refreshResponse
.refreshExpires
* 1000),
598 const response
= db
.refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
);
599 assert
.deepStrictEqual(response
, expectedResponse
);
601 it('no code', function () {
602 db
.statement
.refreshCode
.get.returns();
603 removeResponse
.changes
= 0;
604 db
.statement
.tokenScopeRemove
.run
.returns();
605 const expectedResponse
= undefined;
606 const response
= db
.refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
);
607 assert
.deepStrictEqual(response
, expectedResponse
);
609 it('failure', function () {
610 db
.statement
.refreshCode
.get.throws(expectedException
);
611 assert
.throws(() => db
.refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
), expectedException
);
613 it('scope removal failure', function () {
614 removeResponse
.changes
= 0;
615 db
.statement
.tokenScopeRemove
.run
.returns(removeResponse
);
616 db
.statement
.refreshCode
.get.returns(refreshResponse
);
617 assert
.throws(() => db
.refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
), DBErrors
.UnexpectedResult
);
620 describe('_refreshCodeResponseToNative', function () {
621 it('coverage', function () {
622 const expected
= { foo: 'bar' };
623 const result
= DB
._refreshCodeResponseToNative(expected
);
624 assert
.deepStrictEqual(result
, expected
);
626 it('coverage', function () {
627 const result
= DB
._refreshCodeResponseToNative();
628 assert
.strictEqual(result
, undefined);
633 describe('resourceGet', function () {
635 beforeEach(function () {
636 sinon
.stub(db
.statement
.resourceGet
, 'get');
637 identifier
= '05b81112-b224-11ec-a9c6-0025905f714a';
639 it('success', function () {
644 db
.statement
.resourceGet
.get.returns(dbResult
);
645 const result
= db
.resourceGet(dbCtx
, identifier
);
646 assert
.deepStrictEqual(result
, dbResult
);
648 it('failure', function() {
649 db
.statement
.resourceGet
.get.throws(expectedException
);
650 assert
.throws(() => db
.resourceGet(dbCtx
, identifier
), expectedException
);
654 describe('resourceUpsert', function () {
655 let resourceId
, secret
, description
;
656 beforeEach(function () {
657 resourceId
= '4086661a-f980-11ec-ba19-0025905f714a';
659 description
= 'some application';
661 it('success', function() {
664 lastInsertRowid: undefined,
666 sinon
.stub(db
.statement
.resourceUpsert
, 'run').returns(dbResult
);
667 db
.resourceUpsert(dbCtx
, resourceId
, secret
, description
);
669 it('creates id if not provided', function () {
670 resourceId
= undefined;
673 lastInsertRowid: undefined,
675 sinon
.stub(db
.statement
.resourceUpsert
, 'run').returns(dbResult
);
676 db
.resourceUpsert(dbCtx
, resourceId
, secret
, description
);
678 it('failure', function () {
681 lastInsertRowid: undefined,
683 sinon
.stub(db
.statement
.resourceUpsert
, 'run').returns(dbResult
);
684 assert
.throws(() => db
.resourceUpsert(dbCtx
, resourceId
, secret
, description
), DBErrors
.UnexpectedResult
);
686 }); // resourceUpsert
688 describe('scopeCleanup', function () {
689 let atLeastMsSinceLast
;
690 beforeEach(function () {
691 atLeastMsSinceLast
= 86400000;
692 sinon
.stub(db
.statement
.scopeCleanup
, 'run');
693 sinon
.stub(db
.statement
.almanacGet
, 'get');
694 sinon
.stub(db
.statement
.almanacUpsert
, 'run');
696 it('success, empty almanac', function () {
698 db
.statement
.almanacGet
.get.returns();
699 db
.statement
.scopeCleanup
.run
.returns({ changes: cleaned
});
700 db
.statement
.almanacUpsert
.run
.returns({ changes: 1 });
701 const result
= db
.scopeCleanup(dbCtx
, atLeastMsSinceLast
);
702 assert
.strictEqual(result
, cleaned
);
704 it('success, too soon', function () {
705 db
.statement
.almanacGet
.get.returns({ epoch: BigInt(Math
.ceil(Date
.now() / 1000) - 4) });
706 const result
= db
.scopeCleanup(dbCtx
, atLeastMsSinceLast
);
707 assert
.strictEqual(result
, undefined);
708 assert(db
.statement
.scopeCleanup
.run
.notCalled
);
710 it('failure', function () {
711 db
.statement
.almanacGet
.get.returns({ epoch: 0n
});
712 db
.statement
.scopeCleanup
.run
.returns({ changes: 1 });
713 db
.statement
.almanacUpsert
.run
.returns({ changes: 0 });
714 assert
.throws(() => db
.scopeCleanup(dbCtx
, atLeastMsSinceLast
), DBErrors
.UnexpectedResult
);
718 describe('scopeDelete', function () {
719 let dbGetResult
, dbRunResult
, scope
;
720 beforeEach(function () {
721 sinon
.stub(db
.statement
.scopeInUse
, 'get');
725 sinon
.stub(db
.statement
.scopeDelete
, 'run');
729 scope
= 'some_scope';
731 it('success', function () {
732 db
.statement
.scopeInUse
.get.returns(dbGetResult
);
733 db
.statement
.scopeDelete
.run
.returns(dbRunResult
);
734 const result
= db
.scopeDelete(dbCtx
, scope
);
735 assert
.strictEqual(result
, true);
737 it('in use', function () {
738 dbGetResult
.inUse
= true;
739 db
.statement
.scopeInUse
.get.returns(dbGetResult
);
740 db
.statement
.scopeDelete
.run
.returns(dbRunResult
);
741 const result
= db
.scopeDelete(dbCtx
, scope
);
742 assert
.strictEqual(result
, false);
744 it('no scope', function () {
745 dbRunResult
.changes
= 0;
746 db
.statement
.scopeInUse
.get.returns(dbGetResult
);
747 db
.statement
.scopeDelete
.run
.returns(dbRunResult
);
748 const result
= db
.scopeDelete(dbCtx
, scope
);
749 assert
.strictEqual(result
, true);
751 it('failure', function () {
752 db
.statement
.scopeInUse
.get.throws(expectedException
);
753 assert
.throws(() => db
.scopeDelete(dbCtx
, scope
), expectedException
);
757 describe('scopeUpsert', function () {
758 let dbResult
, scope
, application
, description
;
759 beforeEach(function () {
761 application
= undefined;
762 description
= 'description';
763 sinon
.stub(db
.statement
.scopeUpsert
, 'run');
766 lastInsertRowid: undefined,
769 it('success', function() {
770 db
.statement
.scopeUpsert
.run
.returns(dbResult
);
771 db
.scopeUpsert(dbCtx
, scope
, application
, description
);
773 it('failure', function () {
774 dbResult
.changes
= 0;
775 db
.statement
.scopeUpsert
.run
.returns(dbResult
);
776 assert
.throws(() => db
.scopeUpsert(dbCtx
, scope
, application
, description
), DBErrors
.UnexpectedResult
);
778 it('failure, error', function () {
779 db
.statement
.scopeUpsert
.run
.throws(expectedException
);
780 assert
.throws(() => db
.scopeUpsert(dbCtx
, scope
, application
, description
), expectedException
);
784 describe('tokenCleanup', function () {
785 let codeLifespanSeconds
, atLeastMsSinceLast
;
786 beforeEach(function () {
787 codeLifespanSeconds
= 600;
788 atLeastMsSinceLast
= 86400000;
789 sinon
.stub(db
.statement
.tokenCleanup
, 'run');
790 sinon
.stub(db
.statement
.almanacGet
, 'get');
791 sinon
.stub(db
.statement
.almanacUpsert
, 'run');
793 it('success, empty almanac', function() {
795 db
.statement
.almanacGet
.get.returns();
796 db
.statement
.tokenCleanup
.run
.returns({ changes: cleaned
});
797 db
.statement
.almanacUpsert
.run
.returns({ changes: 1 });
798 const result
= db
.tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
);
799 assert
.strictEqual(result
, cleaned
);
801 it('success, too soon', function () {
802 db
.statement
.almanacGet
.get.returns({ epoch: BigInt(Math
.ceil(Date
.now() / 1000) - 4) });
803 const result
= db
.tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
);
804 assert
.strictEqual(result
, undefined);
805 assert(db
.statement
.tokenCleanup
.run
.notCalled
);
807 it('failure', function () {
808 db
.statement
.almanacGet
.get.returns({ epoch: 0n
});
809 db
.statement
.tokenCleanup
.run
.returns({ changes: 10 });
810 db
.statement
.almanacUpsert
.run
.returns({ changes: 0 });
811 assert
.throws(() => db
.tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
), DBErrors
.UnexpectedResult
);
815 describe('tokenGetByCodeId', function () {
817 beforeEach(function () {
818 codeId
= '184a26f6-2612-11ec-9e88-0025905f714a';
819 token
= 'TokenTokenTokenToken';
820 sinon
.stub(db
.statement
.tokenGetByCodeId
, 'get');
821 sinon
.stub(db
.statement
.tokenScopesGetByCodeId
, 'all');
823 it('success', function() {
824 const now
= new Date();
825 const nowEpoch
= Math
.ceil(now
/ 1000);
827 created: new Date(nowEpoch
* 1000),
829 refreshExpires: null,
841 created: Math
.ceil(nowEpoch
),
843 refreshExpires: null,
848 profileData: '{"name":"Some Name"}',
850 db
.statement
.tokenGetByCodeId
.get.returns(dbResult
);
851 const result
= db
.tokenGetByCodeId(dbCtx
, codeId
);
852 assert
.deepStrictEqual(result
, expected
);
854 it('success without profile data', function () {
855 const now
= new Date();
856 const nowEpoch
= Math
.ceil(now
/ 1000);
858 created: new Date(nowEpoch
* 1000),
860 refreshExpires: null,
866 scopes: ['foop', 'baa'],
869 created: Math
.ceil(nowEpoch
),
871 refreshExpires: null,
877 db
.statement
.tokenGetByCodeId
.get.returns(dbResult
);
878 db
.statement
.tokenScopesGetByCodeId
.all
.returns([{ scope: 'foop' }, { scope: 'baa' }]);
879 const result
= db
.tokenGetByCodeId(dbCtx
, codeId
);
880 assert
.deepStrictEqual(result
, expected
);
882 it('failure', function () {
883 db
.statement
.tokenGetByCodeId
.get.throws(expectedException
);
884 assert
.throws(() => db
.tokenGetByCodeId(dbCtx
, codeId
), expectedException
);
887 describe('_tokenToNative', function () {
888 it('covers', function () {
889 const result
= DB
._tokenToNative();
890 assert
.strictEqual(result
, undefined);
892 }); // _tokenToNative
893 }); // tokenGetByCodeId
895 describe('tokenRevokeByCodeId', function () {
896 let dbResult
, codeId
;
897 beforeEach(function () {
898 codeId
= '2f226616-3e79-11ec-ad0f-0025905f714a';
899 sinon
.stub(db
.statement
.tokenRevokeByCodeId
, 'run');
902 lastInsertRowid: undefined,
905 it('success', function() {
906 db
.statement
.tokenRevokeByCodeId
.run
.returns(dbResult
);
907 db
.tokenRevokeByCodeId(dbCtx
, codeId
);
909 it('failure', function () {
910 dbResult
.changes
= 0;
911 db
.statement
.tokenRevokeByCodeId
.run
.returns(dbResult
);
912 assert
.throws(() => db
.tokenRevokeByCodeId(dbCtx
, codeId
), DBErrors
.UnexpectedResult
);
914 it('failure, error', function () {
915 db
.statement
.tokenRevokeByCodeId
.run
.throws(expectedException
);
916 assert
.throws(() => db
.tokenRevokeByCodeId(dbCtx
, codeId
), expectedException
);
918 }); // tokenRevokeByCodeId
920 describe('tokenRefreshRevokeByCodeId', function () {
921 let dbResult
, codeId
;
922 beforeEach(function () {
925 lastInsertRowid: undefined,
927 codeId
= 'eabba58e-2633-11ed-bbad-0025905f714a';
928 sinon
.stub(db
.statement
.tokenRefreshRevokeByCodeId
, 'run');
930 it('success', function () {
931 db
.statement
.tokenRefreshRevokeByCodeId
.run
.returns(dbResult
);
932 db
.tokenRefreshRevokeByCodeId(dbCtx
, codeId
);
934 it('failure', function () {
935 dbResult
.changes
= 0;
936 db
.statement
.tokenRefreshRevokeByCodeId
.run
.returns(dbResult
);
937 assert
.throws(() => db
.tokenRefreshRevokeByCodeId(dbCtx
, codeId
), DBErrors
.UnexpectedResult
);
939 it('failure, error', function () {
940 const expected
= new Error('oh no');
941 db
.statement
.tokenRefreshRevokeByCodeId
.run
.throws(expected
);
942 assert
.throws(() => db
.tokenRefreshRevokeByCodeId(dbCtx
, codeId
), expected
);
944 }); // tokenRefreshRevokeByCodeId
946 describe('tokensGetByIdentifier', function () {
948 beforeEach(function () {
949 identifier
= 'identifier';
950 sinon
.stub(db
.statement
.tokensGetByIdentifier
, 'all');
952 it('success', function () {
953 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
957 expires: nowEpoch
+ 86400,
959 refreshed: nowEpoch
+ 600,
960 refreshExpires: nowEpoch
+ 172800,
963 codeId: 'c0a7cef4-2637-11ed-a830-0025905f714a',
964 profile: 'https://profile.example.com/',
965 profileData: '{"name":"Some Name"}',
966 identifier: 'username',
970 Object
.assign({}, dbResult
[0], {
971 created: new Date(dbResult
[0].created
* 1000),
972 expires: new Date(dbResult
[0].expires
* 1000),
973 refreshed: new Date(dbResult
[0].refreshed
* 1000),
974 refreshExpires: new Date(dbResult
[0].refreshExpires
* 1000),
980 db
.statement
.tokensGetByIdentifier
.all
.returns(dbResult
);
981 const result
= db
.tokensGetByIdentifier(dbCtx
, identifier
);
982 assert
.deepStrictEqual(result
, expected
);
984 it('failure', function() {
985 db
.statement
.tokensGetByIdentifier
.all
.throws(expectedException
);
986 assert
.throws(() => db
.tokensGetByIdentifier(dbCtx
, identifier
), expectedException
);
988 }); // tokensGetByIdentifier
990 describe('ticketRedeemed', function () {
991 let redeemedData
, dbResult
;
992 beforeEach(function () {
994 resource: 'https://resource.example.com/',
995 subject: 'https://subject.example.com/',
996 iss: 'https://idp.example.com/',
997 ticket: 'xxxTICKETxxx',
998 token: 'xxxTOKENxxx',
1000 sinon
.stub(db
.statement
.ticketRedeemed
, 'run');
1003 lastInsertRowid: undefined,
1006 it('success', function () {
1007 db
.statement
.ticketRedeemed
.run
.returns(dbResult
);
1008 db
.ticketRedeemed(dbCtx
, redeemedData
);
1010 it('failure', function () {
1011 dbResult
.changes
= 0;
1012 db
.statement
.ticketRedeemed
.run
.returns(dbResult
);
1013 assert
.throws(() => db
.ticketRedeemed(dbCtx
, redeemedData
), DBErrors
.UnexpectedResult
);
1015 }); // ticketRedeemed
1017 describe('ticketTokenPublished', function () {
1018 let redeemedData
, dbResult
;
1019 beforeEach(function () {
1021 resource: 'https://resource.example.com/',
1022 subject: 'https://subject.example.com/',
1023 iss: 'https://idp.example.com/',
1024 ticket: 'xxxTICKETxxx',
1025 token: 'xxxTOKENxxx',
1027 sinon
.stub(db
.statement
.ticketTokenPublished
, 'run');
1028 sinon
.stub(db
.statement
.almanacUpsert
, 'run');
1031 lastInsertRowid: undefined,
1034 it('success', function () {
1035 db
.statement
.ticketTokenPublished
.run
.returns(dbResult
);
1036 db
.statement
.almanacUpsert
.run
.returns(dbResult
);
1037 db
.ticketTokenPublished(dbCtx
, redeemedData
);
1038 assert(db
.statement
.ticketTokenPublished
.run
.called
);
1039 assert(db
.statement
.almanacUpsert
.run
.called
);
1041 it('failure', function () {
1042 dbResult
.changes
= 0;
1043 db
.statement
.ticketTokenPublished
.run
.returns(dbResult
);
1044 assert
.throws(() => db
.ticketTokenPublished(dbCtx
, redeemedData
), DBErrors
.UnexpectedResult
);
1046 it('failure of almanac', function () {
1047 const dbResultAlmanac
= {
1051 db
.statement
.ticketTokenPublished
.run
.returns(dbResult
);
1052 db
.statement
.almanacUpsert
.run
.returns(dbResultAlmanac
);
1053 assert
.throws(() => db
.ticketTokenPublished(dbCtx
, redeemedData
), DBErrors
.UnexpectedResult
);
1055 }); // ticketTokenPublished
1057 describe('ticketTokenGetUnpublished', function () {
1058 beforeEach(function () {
1059 sinon
.stub(db
.statement
.ticketTokenGetUnpublished
, 'all');
1061 it('success', function () {
1062 db
.statement
.ticketTokenGetUnpublished
.all
.returns([]);
1063 const result
= db
.ticketTokenGetUnpublished();
1064 assert
.deepStrictEqual(result
, []);
1066 it('failure', function () {
1067 db
.statement
.ticketTokenGetUnpublished
.all
.throws(expectedException
);
1068 assert
.throws(() => db
.ticketTokenGetUnpublished(), expectedException
);
1070 }); // ticketTokenGetUnpublished
1072 describe('_redeemedTicketToNative', 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',
1081 created: 1701970607n
,
1082 published: 1701970670n
,
1085 it('covers', function () {
1088 created: new Date('2023-12-07T17:36:47.000Z'),
1089 published: new Date('2023-12-07T17:37:50.000Z'),
1091 const result
= DB
._redeemedTicketToNative(redeemedData
);
1092 assert
.deepStrictEqual(result
, expected
);
1094 it('covers no published', function () {
1095 redeemedData
.published
= null;
1098 created: new Date('2023-12-07T17:36:47.000Z'),
1101 const result
= DB
._redeemedTicketToNative(redeemedData
);
1102 assert
.deepStrictEqual(result
, expected
);
1104 }); // _redeemedTicketToNative
1106 }); // DatabaseSQLite