update dependencies and devDependencies, address lint issues
[squeep-indie-auther] / test / src / db / sqlite.js
1 /* eslint-disable sonarjs/no-duplicate-string */
2 'use strict';
3
4 /* This provides implementation coverage, stubbing parts of better-sqlite3. */
5
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');
14
15 const expectedException = new Error('oh no');
16
17 describe('DatabaseSQLite', function () {
18 let db, options, logger, stubDb;
19 let dbCtx;
20 before(function () {
21 logger = new StubLogger();
22 logger._reset();
23 stubDb = new StubDatabase();
24 });
25 beforeEach(function () {
26 options = new Config('test');
27 options.db.connectionString = 'sqlite://:memory:';
28 db = new DB(logger, options);
29 dbCtx = db.db;
30 });
31 afterEach(function () {
32 sinon.restore();
33 });
34
35 it('covers constructor options', function () {
36 delete options.db.connectionString;
37 db = new DB(logger, options);
38 });
39
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) => {
44 try {
45 // eslint-disable-next-line security/detect-object-injection
46 db[fn](db.db);
47 } catch (e) {
48 assert(!(e instanceof DBErrors.NotImplemented), `${fn} not implemented`);
49 }
50 }));
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(': '));
55 }));
56 });
57 }); // Implementation
58
59 describe('_currentSchema', function () {
60 it('covers', function () {
61 const version = { major: 1, minor: 0, patch: 0 };
62 sinon.stub(db.db, 'prepare').returns({
63 get: () => version,
64 });
65 const result = db._currentSchema();
66 assert.deepStrictEqual(result, version);
67 });
68 }); // _currentSchema
69
70 describe('_closeConnection', function () {
71 it('success', function () {
72 sinon.stub(db.db, 'close');
73 db._closeConnection();
74 assert(db.db.close.called);
75 });
76 it('failure', function () {
77 sinon.stub(db.db, 'close').throws(expectedException);
78 assert.throws(() => db._closeConnection(), expectedException);
79 });
80 }); // _closeConnection
81
82 describe('_purgeTables', function () {
83 beforeEach(function () {
84 sinon.stub(db.db, 'prepare').returns({
85 run: sinon.stub(),
86 });
87 });
88 it('covers not really', function () {
89 db._purgeTables(false);
90 assert(!db.db.prepare.called);
91 });
92 it('success', function () {
93 db._purgeTables(true);
94 assert(db.db.prepare.called);
95 });
96 it('failure', function () {
97 db.db.prepare.restore();
98 sinon.stub(db.db, 'prepare').throws(expectedException);
99 assert.throws(() => db._purgeTables(true), expectedException);
100 });
101 }); // _purgeTables
102
103 describe('_optimize', function () {
104 beforeEach(function () {
105 sinon.stub(db.statement._optimize, 'all');
106 sinon.stub(db.db, 'pragma');
107 });
108 it('covers', function () {
109 db.changesSinceLastOptimize = BigInt(20);
110 db._optimize();
111 assert(db.db.pragma.called);
112 assert(db.statement._optimize.all.called);
113 assert.strictEqual(db.changesSinceLastOptimize, 0n);
114 });
115 }); // _optimize
116
117 describe('_updateChanges', function () {
118 let dbResult;
119 beforeEach(function () {
120 dbResult = {
121 changes: 4,
122 };
123 sinon.stub(db, '_optimize');
124 });
125 it('does not optimize if not wanted', function () {
126 db.optimizeAfterChanges = 0n;
127 db._updateChanges(dbResult);
128 assert(db._optimize.notCalled);
129 });
130 it('does not optimize if under threshold', function () {
131 db.optimizeAfterChanges = 100n;
132 db._updateChanges(dbResult);
133 assert(db._optimize.notCalled);
134 });
135 it('optimizes over threshold', function () {
136 db.optimizeAfterChanges = 1n;
137 db._updateChanges(dbResult);
138 assert(db._optimize.called);
139 });
140 }); // _updateChanges
141
142 describe('_deOphidiate', function () {
143 it('covers non-array', function () {
144 const obj = {
145 'snake_case': 1,
146 };
147 const expected = {
148 snakeCase: 1,
149 };
150 const result = DB._deOphidiate(obj);
151 assert.deepStrictEqual(result, expected);
152 });
153 it('covers array', function () {
154 const rows = [
155 {
156 'snek_field': 'foo',
157 },
158 {
159 'snek_field': 'bar',
160 },
161 ];
162 const expected = [
163 {
164 snekField: 'foo',
165 },
166 {
167 snekField: 'bar',
168 },
169 ];
170 const result = DB._deOphidiate(rows);
171 assert.deepStrictEqual(result, expected);
172 });
173 }); // _deOphidiate
174
175 describe('healthCheck', function () {
176 it('covers', function () {
177 db.healthCheck();
178 });
179 it('covers failure', function () {
180 db.db = { open: false };
181 assert.throws(() => db.healthCheck(), DBErrors.UnexpectedResult);
182 });
183 }); // healthCheck
184
185 describe('context', function () {
186 it('covers', function () {
187 db.context(common.nop);
188 });
189 }); // context
190
191 describe('transaction', function () {
192 it('covers', function () {
193 db.transaction(db.db, common.nop);
194 });
195 it('covers no context', function () {
196 db.transaction(undefined, common.nop);
197 });
198 }); // transaction
199
200 describe('almanacGetAll', function () {
201 beforeEach(function () {
202 sinon.stub(db.statement.almanacGetAll, 'all');
203 });
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);
210 });
211 it('failure', function () {
212 db.statement.almanacGetAll.all.throws(expectedException);
213 assert.throws(() => db.almanacGetAll(dbCtx), expectedException);
214 });
215 }); // almanacGetAll
216
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');
223 dbResult = {
224 changes: 1,
225 lastInsertRowid: undefined,
226 };
227 });
228 it('success', function () {
229 db.statement.almanacUpsert.run.returns(dbResult);
230 db.almanacUpsert(dbCtx, event, date);
231 });
232 it('success with default date', function () {
233 db.statement.almanacUpsert.run.returns(dbResult);
234 db.almanacUpsert(dbCtx, event);
235 });
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);
240 });
241 }); // almanacUpsert
242
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');
249 });
250 it('success', function() {
251 const expected = {
252 identifier,
253 credential,
254 };
255 db.statement.authenticationGet.get.returns(expected);
256 const result = db.authenticationGet(dbCtx, identifier);
257 assert.deepStrictEqual(result, expected);
258 });
259 it('failure', function () {
260 db.statement.authenticationGet.get.throws(expectedException);
261 assert.throws(() => db.authenticationGet(dbCtx, identifier), expectedException);
262 });
263 }); // authenticationGet
264
265 describe('authenticationSuccess', function () {
266 let dbResult, identifier;
267 beforeEach(function () {
268 identifier = 'username';
269 sinon.stub(db.statement.authenticationSuccess, 'run');
270 dbResult = {
271 changes: 1,
272 lastInsertRowid: undefined,
273 };
274 });
275 it('success', function() {
276 db.statement.authenticationSuccess.run.returns(dbResult);
277 db.authenticationSuccess(dbCtx, identifier);
278 });
279 it('failure', function () {
280 dbResult.changes = 0;
281 db.statement.authenticationSuccess.run.returns(dbResult);
282 assert.throws(() => db.authenticationSuccess(dbCtx, identifier), DBErrors.UnexpectedResult);
283 });
284 }); // authenticationSuccess
285
286 describe('authenticationUpsert', function () {
287 let identifier, credential;
288 beforeEach(function () {
289 identifier = 'username';
290 credential = '$z$foo';
291 });
292 it('success', function() {
293 const dbResult = {
294 changes: 1,
295 lastInsertRowid: undefined,
296 };
297 sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult);
298 db.authenticationUpsert(dbCtx, identifier, credential);
299 });
300 it('failure', function () {
301 const dbResult = {
302 changes: 0,
303 lastInsertRowid: undefined,
304 };
305 sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult);
306 assert.throws(() => db.authenticationUpsert(dbCtx, identifier, credential), DBErrors.UnexpectedResult);
307 });
308 }); // authenticationUpsert
309
310 describe('authenticationUpdateCredential', function () {
311 let identifier, credential;
312 beforeEach(function () {
313 identifier = 'username';
314 credential = '$z$foo';
315 });
316 it('success', function() {
317 const dbResult = {
318 changes: 1,
319 lastInsertRowid: undefined,
320 };
321 sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult);
322 db.authenticationUpdateCredential(dbCtx, identifier, credential);
323 });
324 it('failure', function () {
325 const dbResult = {
326 changes: 0,
327 lastInsertRowid: undefined,
328 };
329 sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult);
330 assert.throws(() => db.authenticationUpdateCredential(dbCtx, identifier, credential), DBErrors.UnexpectedResult);
331 });
332 }); // authenticationUpdateCredential
333
334 describe('authenticationUpdateOTPKey', function () {
335 let identifier, otpKey;
336 beforeEach(function () {
337 identifier = 'username';
338 otpKey = '1234567890123456789012';
339 });
340 it('success', function() {
341 const dbResult = {
342 changes: 1,
343 lastInsertRowid: undefined,
344 };
345 sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult);
346 db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
347 });
348 it('failure', function () {
349 const dbResult = {
350 changes: 0,
351 lastInsertRowid: undefined,
352 };
353 sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult);
354 assert.throws(() => db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey), DBErrors.UnexpectedResult);
355 });
356 }); // authenticationUpdateOTPKey
357
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');
364 });
365 it('success', function () {
366 db.statement.profileIdentifierInsert.run.returns({ changes: 1 });
367 db.profileIdentifierInsert(dbCtx, profile, identifier);
368 });
369 it('failure', function () {
370 db.statement.profileIdentifierInsert.run.returns({ changes: 0 });
371 assert.throws(() => db.profileIdentifierInsert(dbCtx, profile, identifier), DBErrors.UnexpectedResult);
372 });
373 }); // profileIdentifierInsert
374
375 describe('profileScopeInsert', function () {
376 let profile, scope;
377 beforeEach(function () {
378 profile = 'https://profile.example.com/';
379 scope = 'scope';
380 sinon.stub(db.statement.profileScopeInsert, 'run');
381 });
382 it('success', function () {
383 db.statement.profileScopeInsert.run.returns({ changes: 1 });
384 db.profileScopeInsert(dbCtx, profile, scope);
385 });
386 it('failure', function () {
387 db.statement.profileScopeInsert.run.returns({ changes: 2 });
388 assert.throws(() => db.profileScopeInsert(dbCtx, profile, scope), DBErrors.UnexpectedResult);
389 });
390 }); // profileScopeInsert
391
392 describe('profileIsValid', function () {
393 let profile;
394 beforeEach(function () {
395 profile = 'https://profile.exmaple.com';
396 });
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);
401 });
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);
406 });
407 it('failure', function() {
408 sinon.stub(db.statement.profileGet, 'get').throws(expectedException);
409 assert.throws(() => db.profileIsValid(dbCtx, profile), expectedException);
410 });
411 }); // profileIsValid
412
413 describe('profilesScopesByIdentifier', function () {
414 let identifier, scopeIndex, profileScopes, profiles;
415 beforeEach(function () {
416 identifier = 'identifier';
417 scopeIndex = {
418 'scope': {
419 description: 'A scope.',
420 application: 'test',
421 isPermanent: false,
422 isManuallyAdded: false,
423 profiles: ['https://first.example.com/', 'https://second.example.com/'],
424 },
425 'another_scope': {
426 description: 'Another scope.',
427 application: 'another test',
428 isPermanent: false,
429 isManuallyAdded: false,
430 profiles: ['https://first.example.com/'],
431 },
432 };
433 profileScopes = {
434 'https://first.example.com/': {
435 'scope': scopeIndex['scope'],
436 'another_scope': scopeIndex['another_scope'],
437 },
438 'https://second.example.com/': {
439 'scope': scopeIndex['scope'],
440 },
441 };
442 profiles = ['https://first.example.com/', 'https://second.example.com/'];
443 });
444 it('success', function () {
445 const dbResult = [
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 },
449 ];
450 const expected = {
451 scopeIndex,
452 profileScopes,
453 profiles,
454 };
455 sinon.stub(db.statement.profilesScopesByIdentifier, 'all').returns(dbResult);
456 const result = db.profilesScopesByIdentifier(dbCtx, identifier);
457 assert.deepStrictEqual(result, expected);
458 });
459 it('failure', function() {
460 sinon.stub(db.statement.profilesScopesByIdentifier, 'all').throws(expectedException);
461 assert.throws(() => db.profilesScopesByIdentifier(dbCtx, identifier), expectedException);
462 });
463 }); // profilesScopesByIdentifier
464
465 describe('profileScopesSetAll', function () {
466 let profile, scopes;
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');
472 });
473 it('success, no scopes', function () {
474 db.statement.profileScopeInsert.run.returns();
475 scopes = [];
476 db.profileScopesSetAll(dbCtx, profile, scopes);
477 });
478 it('success, scopes', function () {
479 db.statement.profileScopeInsert.run.returns();
480 scopes.push('profile', 'email', 'create');
481 db.profileScopesSetAll(dbCtx, profile, scopes);
482 });
483 it('failure', function () {
484 db.statement.profileScopeInsert.run.throws(expectedException);
485 assert.throws(() => db.profileScopesSetAll(dbCtx, profile, scopes), expectedException);
486 });
487
488 }); // profileScopesSetAll
489
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';
494 isToken = false;
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();
502
503 sinon.stub(db.statement.scopeInsert, 'run');
504 sinon.stub(db.statement.tokenScopeSet, 'run');
505 sinon.stub(db.statement.redeemCode, 'get');
506 });
507 it('success', function() {
508 const dbResult = {
509 changes: 1,
510 lastInsertRowid: undefined,
511 };
512 const dbGet = {
513 isRevoked: false,
514 };
515 db.statement.scopeInsert.run.returns(dbResult);
516 db.statement.tokenScopeSet.run.returns(dbResult);
517 db.statement.redeemCode.get.returns(dbGet);
518 profileData = {
519 name: 'Some Name',
520 };
521 const result = db.redeemCode(dbCtx, { codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, profileData });
522 assert.strictEqual(result, true);
523 });
524 it('success (revoked)', function() {
525 const dbResult = {
526 changes: 1,
527 lastInsertRowid: undefined,
528 };
529 const dbGet = {
530 isRevoked: true,
531 };
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);
537 });
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);
543 });
544 }); // redeemCode
545
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);
556 refreshResponse = {
557 expires: refreshedEpoch + 86400,
558 refreshExpires: refreshedEpoch + 172800,
559 };
560 removeResponse = {
561 changes: removeScopes.length,
562 };
563 scopesResponse = [
564 { scope: 'blah' },
565 ];
566 });
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),
574 scopes: ['blah'],
575 };
576 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
577 assert.deepStrictEqual(response, expectedResponse);
578 });
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),
585 };
586 removeScopes = [];
587 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
588 assert.deepStrictEqual(response, expectedResponse);
589 });
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),
596 scopes: [],
597 };
598 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
599 assert.deepStrictEqual(response, expectedResponse);
600 });
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);
608 });
609 it('failure', function () {
610 db.statement.refreshCode.get.throws(expectedException);
611 assert.throws(() => db.refreshCode(dbCtx, codeId, refreshed, removeScopes), expectedException);
612 });
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);
618 });
619
620 describe('_refreshCodeResponseToNative', function () {
621 it('coverage', function () {
622 const expected = { foo: 'bar' };
623 const result = DB._refreshCodeResponseToNative(expected);
624 assert.deepStrictEqual(result, expected);
625 });
626 it('coverage', function () {
627 const result = DB._refreshCodeResponseToNative();
628 assert.strictEqual(result, undefined);
629 });
630 });
631 }); // refreshCode
632
633 describe('resourceGet', function () {
634 let identifier;
635 beforeEach(function () {
636 sinon.stub(db.statement.resourceGet, 'get');
637 identifier = '05b81112-b224-11ec-a9c6-0025905f714a';
638 });
639 it('success', function () {
640 const dbResult = {
641 identifier,
642 secret: 'secrety',
643 };
644 db.statement.resourceGet.get.returns(dbResult);
645 const result = db.resourceGet(dbCtx, identifier);
646 assert.deepStrictEqual(result, dbResult);
647 });
648 it('failure', function() {
649 db.statement.resourceGet.get.throws(expectedException);
650 assert.throws(() => db.resourceGet(dbCtx, identifier), expectedException);
651 });
652 }); // resourceGet
653
654 describe('resourceUpsert', function () {
655 let resourceId, secret, description;
656 beforeEach(function () {
657 resourceId = '4086661a-f980-11ec-ba19-0025905f714a';
658 secret = 'secret';
659 description = 'some application';
660 });
661 it('success', function() {
662 const dbResult = {
663 changes: 1,
664 lastInsertRowid: undefined,
665 };
666 sinon.stub(db.statement.resourceUpsert, 'run').returns(dbResult);
667 db.resourceUpsert(dbCtx, resourceId, secret, description);
668 });
669 it('creates id if not provided', function () {
670 resourceId = undefined;
671 const dbResult = {
672 changes: 1,
673 lastInsertRowid: undefined,
674 };
675 sinon.stub(db.statement.resourceUpsert, 'run').returns(dbResult);
676 db.resourceUpsert(dbCtx, resourceId, secret, description);
677 });
678 it('failure', function () {
679 const dbResult = {
680 changes: 0,
681 lastInsertRowid: undefined,
682 };
683 sinon.stub(db.statement.resourceUpsert, 'run').returns(dbResult);
684 assert.throws(() => db.resourceUpsert(dbCtx, resourceId, secret, description), DBErrors.UnexpectedResult);
685 });
686 }); // resourceUpsert
687
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');
695 });
696 it('success, empty almanac', function () {
697 const cleaned = 10n;
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);
703 });
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);
709 });
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);
715 });
716 }); // scopeCleanup
717
718 describe('scopeDelete', function () {
719 let dbGetResult, dbRunResult, scope;
720 beforeEach(function () {
721 sinon.stub(db.statement.scopeInUse, 'get');
722 dbGetResult = {
723 inUse: false,
724 };
725 sinon.stub(db.statement.scopeDelete, 'run');
726 dbRunResult = {
727 changes: 1,
728 };
729 scope = 'some_scope';
730 });
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);
736 });
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);
743 });
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);
750 });
751 it('failure', function () {
752 db.statement.scopeInUse.get.throws(expectedException);
753 assert.throws(() => db.scopeDelete(dbCtx, scope), expectedException);
754 });
755 }); // scopeDelete
756
757 describe('scopeUpsert', function () {
758 let dbResult, scope, application, description;
759 beforeEach(function () {
760 scope = 'scope';
761 application = undefined;
762 description = 'description';
763 sinon.stub(db.statement.scopeUpsert, 'run');
764 dbResult = {
765 changes: 1,
766 lastInsertRowid: undefined,
767 };
768 });
769 it('success', function() {
770 db.statement.scopeUpsert.run.returns(dbResult);
771 db.scopeUpsert(dbCtx, scope, application, description);
772 });
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);
777 });
778 it('failure, error', function () {
779 db.statement.scopeUpsert.run.throws(expectedException);
780 assert.throws(() => db.scopeUpsert(dbCtx, scope, application, description), expectedException);
781 });
782 }); // scopeUpsert
783
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');
792 });
793 it('success, empty almanac', function() {
794 const cleaned = 10n;
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);
800 });
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);
806 });
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);
812 });
813 }); // tokenCleanup
814
815 describe('tokenGetByCodeId', function () {
816 let codeId, token;
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');
822 });
823 it('success', function() {
824 const now = new Date();
825 const nowEpoch = Math.ceil(now / 1000);
826 const expected = {
827 created: new Date(nowEpoch * 1000),
828 expires: null,
829 refreshExpires: null,
830 refreshed: null,
831 isRevoked: false,
832 isToken: false,
833 token,
834 codeId,
835 scopes: [],
836 profileData: {
837 name: 'Some Name',
838 },
839 };
840 const dbResult = {
841 created: Math.ceil(nowEpoch),
842 expires: null,
843 refreshExpires: null,
844 refreshed: null,
845 isToken: 0,
846 token,
847 codeId,
848 profileData: '{"name":"Some Name"}',
849 };
850 db.statement.tokenGetByCodeId.get.returns(dbResult);
851 const result = db.tokenGetByCodeId(dbCtx, codeId);
852 assert.deepStrictEqual(result, expected);
853 });
854 it('success without profile data', function () {
855 const now = new Date();
856 const nowEpoch = Math.ceil(now / 1000);
857 const expected = {
858 created: new Date(nowEpoch * 1000),
859 expires: null,
860 refreshExpires: null,
861 refreshed: null,
862 isRevoked: false,
863 isToken: false,
864 token,
865 codeId,
866 scopes: ['foop', 'baa'],
867 };
868 const dbResult = {
869 created: Math.ceil(nowEpoch),
870 expires: null,
871 refreshExpires: null,
872 refreshed: null,
873 isToken: 0,
874 token,
875 codeId,
876 };
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);
881 });
882 it('failure', function () {
883 db.statement.tokenGetByCodeId.get.throws(expectedException);
884 assert.throws(() => db.tokenGetByCodeId(dbCtx, codeId), expectedException);
885 });
886
887 describe('_tokenToNative', function () {
888 it('covers', function () {
889 const result = DB._tokenToNative();
890 assert.strictEqual(result, undefined);
891 });
892 }); // _tokenToNative
893 }); // tokenGetByCodeId
894
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');
900 dbResult = {
901 changes: 1,
902 lastInsertRowid: undefined,
903 };
904 });
905 it('success', function() {
906 db.statement.tokenRevokeByCodeId.run.returns(dbResult);
907 db.tokenRevokeByCodeId(dbCtx, codeId);
908 });
909 it('failure', function () {
910 dbResult.changes = 0;
911 db.statement.tokenRevokeByCodeId.run.returns(dbResult);
912 assert.throws(() => db.tokenRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
913 });
914 it('failure, error', function () {
915 db.statement.tokenRevokeByCodeId.run.throws(expectedException);
916 assert.throws(() => db.tokenRevokeByCodeId(dbCtx, codeId), expectedException);
917 });
918 }); // tokenRevokeByCodeId
919
920 describe('tokenRefreshRevokeByCodeId', function () {
921 let dbResult, codeId;
922 beforeEach(function () {
923 dbResult = {
924 changes: 1,
925 lastInsertRowid: undefined,
926 };
927 codeId = 'eabba58e-2633-11ed-bbad-0025905f714a';
928 sinon.stub(db.statement.tokenRefreshRevokeByCodeId, 'run');
929 });
930 it('success', function () {
931 db.statement.tokenRefreshRevokeByCodeId.run.returns(dbResult);
932 db.tokenRefreshRevokeByCodeId(dbCtx, codeId);
933 });
934 it('failure', function () {
935 dbResult.changes = 0;
936 db.statement.tokenRefreshRevokeByCodeId.run.returns(dbResult);
937 assert.throws(() => db.tokenRefreshRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
938 });
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);
943 });
944 }); // tokenRefreshRevokeByCodeId
945
946 describe('tokensGetByIdentifier', function () {
947 let identifier;
948 beforeEach(function () {
949 identifier = 'identifier';
950 sinon.stub(db.statement.tokensGetByIdentifier, 'all');
951 });
952 it('success', function () {
953 const nowEpoch = Math.ceil(Date.now() / 1000);
954 const dbResult = [
955 {
956 created: nowEpoch,
957 expires: nowEpoch + 86400,
958 duration: 86400,
959 refreshed: nowEpoch + 600,
960 refreshExpires: nowEpoch + 172800,
961 isRevoked: false,
962 isToken: true,
963 codeId: 'c0a7cef4-2637-11ed-a830-0025905f714a',
964 profile: 'https://profile.example.com/',
965 profileData: '{"name":"Some Name"}',
966 identifier: 'username',
967 },
968 ];
969 const expected = [
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),
975 profileData: {
976 name: 'Some Name',
977 },
978 }),
979 ];
980 db.statement.tokensGetByIdentifier.all.returns(dbResult);
981 const result = db.tokensGetByIdentifier(dbCtx, identifier);
982 assert.deepStrictEqual(result, expected);
983 });
984 it('failure', function() {
985 db.statement.tokensGetByIdentifier.all.throws(expectedException);
986 assert.throws(() => db.tokensGetByIdentifier(dbCtx, identifier), expectedException);
987 });
988 }); // tokensGetByIdentifier
989
990 describe('ticketRedeemed', function () {
991 let redeemedData, dbResult;
992 beforeEach(function () {
993 redeemedData = {
994 resource: 'https://resource.example.com/',
995 subject: 'https://subject.example.com/',
996 iss: 'https://idp.example.com/',
997 ticket: 'xxxTICKETxxx',
998 token: 'xxxTOKENxxx',
999 };
1000 sinon.stub(db.statement.ticketRedeemed, 'run');
1001 dbResult = {
1002 changes: 1,
1003 lastInsertRowid: undefined,
1004 };
1005 });
1006 it('success', function () {
1007 db.statement.ticketRedeemed.run.returns(dbResult);
1008 db.ticketRedeemed(dbCtx, redeemedData);
1009 });
1010 it('failure', function () {
1011 dbResult.changes = 0;
1012 db.statement.ticketRedeemed.run.returns(dbResult);
1013 assert.throws(() => db.ticketRedeemed(dbCtx, redeemedData), DBErrors.UnexpectedResult);
1014 });
1015 }); // ticketRedeemed
1016
1017 describe('ticketTokenPublished', function () {
1018 let redeemedData, dbResult;
1019 beforeEach(function () {
1020 redeemedData = {
1021 resource: 'https://resource.example.com/',
1022 subject: 'https://subject.example.com/',
1023 iss: 'https://idp.example.com/',
1024 ticket: 'xxxTICKETxxx',
1025 token: 'xxxTOKENxxx',
1026 };
1027 sinon.stub(db.statement.ticketTokenPublished, 'run');
1028 sinon.stub(db.statement.almanacUpsert, 'run');
1029 dbResult = {
1030 changes: 1,
1031 lastInsertRowid: undefined,
1032 };
1033 });
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);
1040 });
1041 it('failure', function () {
1042 dbResult.changes = 0;
1043 db.statement.ticketTokenPublished.run.returns(dbResult);
1044 assert.throws(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult);
1045 });
1046 it('failure of almanac', function () {
1047 const dbResultAlmanac = {
1048 ...dbResult,
1049 changes: 0,
1050 };
1051 db.statement.ticketTokenPublished.run.returns(dbResult);
1052 db.statement.almanacUpsert.run.returns(dbResultAlmanac);
1053 assert.throws(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult);
1054 });
1055 }); // ticketTokenPublished
1056
1057 describe('ticketTokenGetUnpublished', function () {
1058 beforeEach(function () {
1059 sinon.stub(db.statement.ticketTokenGetUnpublished, 'all');
1060 });
1061 it('success', function () {
1062 db.statement.ticketTokenGetUnpublished.all.returns([]);
1063 const result = db.ticketTokenGetUnpublished();
1064 assert.deepStrictEqual(result, []);
1065 });
1066 it('failure', function () {
1067 db.statement.ticketTokenGetUnpublished.all.throws(expectedException);
1068 assert.throws(() => db.ticketTokenGetUnpublished(), expectedException);
1069 });
1070 }); // ticketTokenGetUnpublished
1071
1072 describe('_redeemedTicketToNative', function () {
1073 let redeemedData;
1074 beforeEach(function () {
1075 redeemedData = {
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,
1083 };
1084 });
1085 it('covers', function () {
1086 const expected = {
1087 ...redeemedData,
1088 created: new Date('2023-12-07T17:36:47.000Z'),
1089 published: new Date('2023-12-07T17:37:50.000Z'),
1090 };
1091 const result = DB._redeemedTicketToNative(redeemedData);
1092 assert.deepStrictEqual(result, expected);
1093 });
1094 it('covers no published', function () {
1095 redeemedData.published = null;
1096 const expected = {
1097 ...redeemedData,
1098 created: new Date('2023-12-07T17:36:47.000Z'),
1099 published: null,
1100 };
1101 const result = DB._redeemedTicketToNative(redeemedData);
1102 assert.deepStrictEqual(result, expected);
1103 });
1104 }); // _redeemedTicketToNative
1105
1106 }); // DatabaseSQLite