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