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