f2d95461b439915005833267a8890d60f82ff94e
[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('authenticationUpdateCredential', function () {
313 let identifier, credential;
314 beforeEach(function () {
315 identifier = 'username';
316 credential = '$z$foo';
317 });
318 it('success', function() {
319 const dbResult = {
320 changes: 1,
321 lastInsertRowid: undefined,
322 };
323 sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult);
324 db.authenticationUpdateCredential(dbCtx, identifier, credential);
325 });
326 it('failure', function () {
327 const dbResult = {
328 changes: 0,
329 lastInsertRowid: undefined,
330 };
331 sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult);
332 assert.throws(() => db.authenticationUpdateCredential(dbCtx, identifier, credential), DBErrors.UnexpectedResult);
333 });
334 }); // authenticationUpdateCredential
335
336 describe('authenticationUpdateOTPKey', function () {
337 let identifier, otpKey;
338 beforeEach(function () {
339 identifier = 'username';
340 otpKey = '1234567890123456789012';
341 });
342 it('success', function() {
343 const dbResult = {
344 changes: 1,
345 lastInsertRowid: undefined,
346 };
347 sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult);
348 db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
349 });
350 it('failure', function () {
351 const dbResult = {
352 changes: 0,
353 lastInsertRowid: undefined,
354 };
355 sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult);
356 assert.throws(() => db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey), DBErrors.UnexpectedResult);
357 });
358 }); // authenticationUpdateOTPKey
359
360 describe('profileIdentifierInsert', function () {
361 let profile, identifier;
362 beforeEach(function () {
363 profile = 'https://profile.example.com/';
364 identifier = 'identifier';
365 sinon.stub(db.statement.profileIdentifierInsert, 'run');
366 });
367 it('success', function () {
368 db.statement.profileIdentifierInsert.run.returns({ changes: 1 });
369 db.profileIdentifierInsert(dbCtx, profile, identifier);
370 });
371 it('failure', function () {
372 db.statement.profileIdentifierInsert.run.returns({ changes: 0 });
373 assert.throws(() => db.profileIdentifierInsert(dbCtx, profile, identifier), DBErrors.UnexpectedResult);
374 });
375 }); // profileIdentifierInsert
376
377 describe('profileScopeInsert', function () {
378 let profile, scope;
379 beforeEach(function () {
380 profile = 'https://profile.example.com/';
381 scope = 'scope';
382 sinon.stub(db.statement.profileScopeInsert, 'run');
383 });
384 it('success', function () {
385 db.statement.profileScopeInsert.run.returns({ changes: 1 });
386 db.profileScopeInsert(dbCtx, profile, scope);
387 });
388 it('failure', function () {
389 db.statement.profileScopeInsert.run.returns({ changes: 2 });
390 assert.throws(() => db.profileScopeInsert(dbCtx, profile, scope), DBErrors.UnexpectedResult);
391 });
392 }); // profileScopeInsert
393
394 describe('profileIsValid', function () {
395 let profile;
396 beforeEach(function () {
397 profile = 'https://profile.exmaple.com';
398 });
399 it('valid profile', function () {
400 sinon.stub(db.statement.profileGet, 'get').returns({ profile });
401 const result = db.profileIsValid(dbCtx, profile);
402 assert.deepStrictEqual(result, true);
403 });
404 it('invalid profile', function () {
405 sinon.stub(db.statement.profileGet, 'get').returns();
406 const result = db.profileIsValid(dbCtx, profile);
407 assert.deepStrictEqual(result, false);
408 });
409 it('failure', function() {
410 sinon.stub(db.statement.profileGet, 'get').throws(expectedException);
411 assert.throws(() => db.profileIsValid(dbCtx, profile), expectedException);
412 });
413 }); // profileIsValid
414
415 describe('profilesScopesByIdentifier', function () {
416 let identifier, scopeIndex, profileScopes, profiles;
417 beforeEach(function () {
418 identifier = 'identifier';
419 scopeIndex = {
420 'scope': {
421 description: 'A scope.',
422 application: 'test',
423 isPermanent: false,
424 isManuallyAdded: false,
425 profiles: ['https://first.example.com/', 'https://second.example.com/'],
426 },
427 'another_scope': {
428 description: 'Another scope.',
429 application: 'another test',
430 isPermanent: false,
431 isManuallyAdded: false,
432 profiles: ['https://first.example.com/'],
433 },
434 };
435 profileScopes = {
436 'https://first.example.com/': {
437 'scope': scopeIndex['scope'],
438 'another_scope': scopeIndex['another_scope'],
439 },
440 'https://second.example.com/': {
441 'scope': scopeIndex['scope'],
442 },
443 };
444 profiles = ['https://first.example.com/', 'https://second.example.com/'];
445 });
446 it('success', function () {
447 const dbResult = [
448 { profile: 'https://first.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: false },
449 { profile: 'https://first.example.com/', scope: 'another_scope', application: 'another test', description: 'Another scope.', isPermanent: false, isManuallyAdded: false },
450 { profile: 'https://second.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: false },
451 ];
452 const expected = {
453 scopeIndex,
454 profileScopes,
455 profiles,
456 };
457 sinon.stub(db.statement.profilesScopesByIdentifier, 'all').returns(dbResult);
458 const result = db.profilesScopesByIdentifier(dbCtx, identifier);
459 assert.deepStrictEqual(result, expected);
460 });
461 it('failure', function() {
462 sinon.stub(db.statement.profilesScopesByIdentifier, 'all').throws(expectedException);
463 assert.throws(() => db.profilesScopesByIdentifier(dbCtx, identifier), expectedException);
464 });
465 }); // profilesScopesByIdentifier
466
467 describe('profileScopesSetAll', function () {
468 let profile, scopes;
469 beforeEach(function () {
470 profile = 'https://example.com/';
471 scopes = ['scope1', 'scope2'];
472 sinon.stub(db.statement.profileScopesClear, 'run').returns();
473 sinon.stub(db.statement.profileScopeInsert, 'run');
474 });
475 it('success, no scopes', function () {
476 db.statement.profileScopeInsert.run.returns();
477 scopes = [];
478 db.profileScopesSetAll(dbCtx, profile, scopes);
479 });
480 it('success, scopes', function () {
481 db.statement.profileScopeInsert.run.returns();
482 scopes.push('profile', 'email', 'create');
483 db.profileScopesSetAll(dbCtx, profile, scopes);
484 });
485 it('failure', function () {
486 db.statement.profileScopeInsert.run.throws(expectedException);
487 assert.throws(() => db.profileScopesSetAll(dbCtx, profile, scopes), expectedException);
488 });
489
490 }); // profileScopesSetAll
491
492 describe('redeemCode', function () {
493 let codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, profileData;
494 beforeEach(function () {
495 codeId = '2f226616-3e79-11ec-ad0f-0025905f714a';
496 isToken = false;
497 clientId = 'https://app.exmaple.com/';
498 profile = 'https://profile.example.com/';
499 identifier = 'username';
500 scopes = ['scope1', 'scope2'];
501 lifespanSeconds = 600;
502 profileData = undefined;
503 created = new Date();
504
505 sinon.stub(db.statement.scopeInsert, 'run');
506 sinon.stub(db.statement.tokenScopeSet, 'run');
507 sinon.stub(db.statement.redeemCode, 'get');
508 });
509 it('success', function() {
510 const dbResult = {
511 changes: 1,
512 lastInsertRowid: undefined,
513 };
514 const dbGet = {
515 isRevoked: false,
516 };
517 db.statement.scopeInsert.run.returns(dbResult);
518 db.statement.tokenScopeSet.run.returns(dbResult);
519 db.statement.redeemCode.get.returns(dbGet);
520 profileData = {
521 name: 'Some Name',
522 };
523 const result = db.redeemCode(dbCtx, { codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, profileData });
524 assert.strictEqual(result, true);
525 });
526 it('success (revoked)', function() {
527 const dbResult = {
528 changes: 1,
529 lastInsertRowid: undefined,
530 };
531 const dbGet = {
532 isRevoked: true,
533 };
534 db.statement.scopeInsert.run.returns(dbResult);
535 db.statement.tokenScopeSet.run.returns(dbResult);
536 db.statement.redeemCode.get.returns(dbGet);
537 const result = db.redeemCode(dbCtx, { codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, profileData });
538 assert.strictEqual(result, false);
539 });
540 it('failure', function () {
541 db.statement.scopeInsert.run.throws();
542 db.statement.tokenScopeSet.run.throws();
543 db.statement.redeemCode.get.returns();
544 assert.throws(() => db.redeemCode(dbCtx, { codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds }), DBErrors.UnexpectedResult);
545 });
546 }); // redeemCode
547
548 describe('refreshCode', function () {
549 let refreshResponse, removeResponse, scopesResponse, codeId, refreshed, removeScopes;
550 beforeEach(function () {
551 sinon.stub(db.statement.refreshCode, 'get');
552 sinon.stub(db.statement.tokenScopeRemove, 'run');
553 sinon.stub(db.statement.tokenScopesGetByCodeId, 'all');
554 codeId = '73db7b18-27bb-11ed-8edd-0025905f714a';
555 refreshed = new Date();
556 removeScopes = ['foop'];
557 const refreshedEpoch = Math.ceil(refreshed.getTime() / 1000);
558 refreshResponse = {
559 expires: refreshedEpoch + 86400,
560 refreshExpires: refreshedEpoch + 172800,
561 };
562 removeResponse = {
563 changes: removeScopes.length,
564 };
565 scopesResponse = [
566 { scope: 'blah' },
567 ];
568 });
569 it('success', function () {
570 db.statement.refreshCode.get.returns(refreshResponse);
571 db.statement.tokenScopeRemove.run.returns(removeResponse);
572 db.statement.tokenScopesGetByCodeId.all.returns(scopesResponse);
573 const expectedResponse = {
574 expires: new Date(refreshResponse.expires * 1000),
575 refreshExpires: new Date(refreshResponse.refreshExpires * 1000),
576 scopes: ['blah'],
577 }
578 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
579 assert.deepStrictEqual(response, expectedResponse);
580 });
581 it('success without scope removal', function () {
582 db.statement.refreshCode.get.returns(refreshResponse);
583 db.statement.tokenScopeRemove.run.returns(removeResponse);
584 const expectedResponse = {
585 expires: new Date(refreshResponse.expires * 1000),
586 refreshExpires: new Date(refreshResponse.refreshExpires * 1000),
587 }
588 removeScopes = [];
589 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
590 assert.deepStrictEqual(response, expectedResponse);
591 });
592 it('success with no scopes left', function () {
593 db.statement.refreshCode.get.returns(refreshResponse);
594 db.statement.tokenScopeRemove.run.returns(removeResponse);
595 const expectedResponse = {
596 expires: new Date(refreshResponse.expires * 1000),
597 refreshExpires: new Date(refreshResponse.refreshExpires * 1000),
598 scopes: [],
599 }
600 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
601 assert.deepStrictEqual(response, expectedResponse);
602 });
603 it('no code', function () {
604 db.statement.refreshCode.get.returns();
605 removeResponse.changes = 0;
606 db.statement.tokenScopeRemove.run.returns();
607 const expectedResponse = undefined;
608 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
609 assert.deepStrictEqual(response, expectedResponse);
610 });
611 it('failure', function () {
612 db.statement.refreshCode.get.throws(expectedException);
613 assert.throws(() => db.refreshCode(dbCtx, codeId, refreshed, removeScopes), expectedException);
614 });
615 it('scope removal failure', function () {
616 removeResponse.changes = 0;
617 db.statement.tokenScopeRemove.run.returns(removeResponse);
618 db.statement.refreshCode.get.returns(refreshResponse);
619 assert.throws(() => db.refreshCode(dbCtx, codeId, refreshed, removeScopes), DBErrors.UnexpectedResult);
620 });
621
622 describe('_refreshCodeResponseToNative', function () {
623 it('coverage', function () {
624 const expected = { foo: 'bar' };
625 const result = DB._refreshCodeResponseToNative(expected);
626 assert.deepStrictEqual(result, expected);
627 });
628 it('coverage', function () {
629 const result = DB._refreshCodeResponseToNative();
630 assert.strictEqual(result, undefined);
631 });
632 });
633 }); // refreshCode
634
635 describe('resourceGet', function () {
636 let identifier;
637 beforeEach(function () {
638 sinon.stub(db.statement.resourceGet, 'get');
639 identifier = '05b81112-b224-11ec-a9c6-0025905f714a';
640 });
641 it('success', function () {
642 const dbResult = {
643 identifier,
644 secret: 'secrety',
645 };
646 db.statement.resourceGet.get.returns(dbResult);
647 const result = db.resourceGet(dbCtx, identifier);
648 assert.deepStrictEqual(result, dbResult);
649 });
650 it('failure', function() {
651 db.statement.resourceGet.get.throws(expectedException);
652 assert.throws(() => db.resourceGet(dbCtx, identifier), expectedException);
653 });
654 }); // resourceGet
655
656 describe('resourceUpsert', function () {
657 let resourceId, secret, description;
658 beforeEach(function () {
659 resourceId = '4086661a-f980-11ec-ba19-0025905f714a';
660 secret = 'secret';
661 description = 'some application';
662 });
663 it('success', function() {
664 const dbResult = {
665 changes: 1,
666 lastInsertRowid: undefined,
667 };
668 sinon.stub(db.statement.resourceUpsert, 'run').returns(dbResult);
669 db.resourceUpsert(dbCtx, resourceId, secret, description);
670 });
671 it('creates id if not provided', function () {
672 resourceId = undefined;
673 const dbResult = {
674 changes: 1,
675 lastInsertRowid: undefined,
676 };
677 sinon.stub(db.statement.resourceUpsert, 'run').returns(dbResult);
678 db.resourceUpsert(dbCtx, resourceId, secret, description);
679 });
680 it('failure', function () {
681 const dbResult = {
682 changes: 0,
683 lastInsertRowid: undefined,
684 };
685 sinon.stub(db.statement.resourceUpsert, 'run').returns(dbResult);
686 assert.throws(() => db.resourceUpsert(dbCtx, resourceId, secret, description), DBErrors.UnexpectedResult);
687 });
688 }); // resourceUpsert
689
690 describe('scopeCleanup', function () {
691 let atLeastMsSinceLast;
692 beforeEach(function () {
693 atLeastMsSinceLast = 86400000;
694 sinon.stub(db.statement.scopeCleanup, 'run');
695 sinon.stub(db.statement.almanacGet, 'get');
696 sinon.stub(db.statement.almanacUpsert, 'run');
697 });
698 it('success, empty almanac', function () {
699 const cleaned = 10n;
700 db.statement.almanacGet.get.returns();
701 db.statement.scopeCleanup.run.returns({ changes: cleaned });
702 db.statement.almanacUpsert.run.returns({ changes: 1 });
703 const result = db.scopeCleanup(dbCtx, atLeastMsSinceLast);
704 assert.strictEqual(result, cleaned);
705 });
706 it('success, too soon', function () {
707 db.statement.almanacGet.get.returns({ epoch: BigInt(Math.ceil(Date.now() / 1000) - 4) });
708 const result = db.scopeCleanup(dbCtx, atLeastMsSinceLast);
709 assert.strictEqual(result, undefined);
710 assert(db.statement.scopeCleanup.run.notCalled);
711 });
712 it('failure', function () {
713 db.statement.almanacGet.get.returns({ epoch: 0n });
714 db.statement.scopeCleanup.run.returns({ changes: 1 });
715 db.statement.almanacUpsert.run.returns({ changes: 0 });
716 assert.throws(() => db.scopeCleanup(dbCtx, atLeastMsSinceLast), DBErrors.UnexpectedResult);
717 });
718 }); // scopeCleanup
719
720 describe('scopeDelete', function () {
721 let dbGetResult, dbRunResult, scope;
722 beforeEach(function () {
723 sinon.stub(db.statement.scopeInUse, 'get');
724 dbGetResult = {
725 inUse: false,
726 }
727 sinon.stub(db.statement.scopeDelete, 'run');
728 dbRunResult = {
729 changes: 1,
730 };
731 scope = 'some_scope';
732 });
733 it('success', function () {
734 db.statement.scopeInUse.get.returns(dbGetResult);
735 db.statement.scopeDelete.run.returns(dbRunResult);
736 const result = db.scopeDelete(dbCtx, scope);
737 assert.strictEqual(result, true);
738 });
739 it('in use', function () {
740 dbGetResult.inUse = true;
741 db.statement.scopeInUse.get.returns(dbGetResult);
742 db.statement.scopeDelete.run.returns(dbRunResult);
743 const result = db.scopeDelete(dbCtx, scope);
744 assert.strictEqual(result, false);
745 });
746 it('no scope', function () {
747 dbRunResult.changes = 0;
748 db.statement.scopeInUse.get.returns(dbGetResult);
749 db.statement.scopeDelete.run.returns(dbRunResult);
750 const result = db.scopeDelete(dbCtx, scope);
751 assert.strictEqual(result, true);
752 });
753 it('failure', function () {
754 db.statement.scopeInUse.get.throws(expectedException);
755 assert.throws(() => db.scopeDelete(dbCtx, scope), expectedException);
756 });
757 }); // scopeDelete
758
759 describe('scopeUpsert', function () {
760 let dbResult, scope, application, description;
761 beforeEach(function () {
762 scope = 'scope';
763 application = undefined;
764 description = 'description';
765 sinon.stub(db.statement.scopeUpsert, 'run');
766 dbResult = {
767 changes: 1,
768 lastInsertRowid: undefined,
769 };
770 });
771 it('success', function() {
772 db.statement.scopeUpsert.run.returns(dbResult);
773 db.scopeUpsert(dbCtx, scope, application, description);
774 });
775 it('failure', function () {
776 dbResult.changes = 0;
777 db.statement.scopeUpsert.run.returns(dbResult);
778 assert.throws(() => db.scopeUpsert(dbCtx, scope, application, description), DBErrors.UnexpectedResult);
779 });
780 it('failure, error', function () {
781 db.statement.scopeUpsert.run.throws(expectedException);
782 assert.throws(() => db.scopeUpsert(dbCtx, scope, application, description), expectedException);
783 });
784 }); // scopeUpsert
785
786 describe('tokenCleanup', function () {
787 let codeLifespanSeconds, atLeastMsSinceLast;
788 beforeEach(function () {
789 codeLifespanSeconds = 600;
790 atLeastMsSinceLast = 86400000;
791 sinon.stub(db.statement.tokenCleanup, 'run');
792 sinon.stub(db.statement.almanacGet, 'get');
793 sinon.stub(db.statement.almanacUpsert, 'run');
794 });
795 it('success, empty almanac', function() {
796 const cleaned = 10n;
797 db.statement.almanacGet.get.returns();
798 db.statement.tokenCleanup.run.returns({ changes: cleaned });
799 db.statement.almanacUpsert.run.returns({ changes: 1 });
800 const result = db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast);
801 assert.strictEqual(result, cleaned);
802 });
803 it('success, too soon', function () {
804 db.statement.almanacGet.get.returns({ epoch: BigInt(Math.ceil(Date.now() / 1000) - 4) });
805 const result = db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast);
806 assert.strictEqual(result, undefined);
807 assert(db.statement.tokenCleanup.run.notCalled);
808 });
809 it('failure', function () {
810 db.statement.almanacGet.get.returns({ epoch: 0n });
811 db.statement.tokenCleanup.run.returns({ changes: 10 });
812 db.statement.almanacUpsert.run.returns({ changes: 0 });
813 assert.throws(() => db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast), DBErrors.UnexpectedResult);
814 });
815 }); // tokenCleanup
816
817 describe('tokenGetByCodeId', function () {
818 let codeId, token;
819 beforeEach(function () {
820 codeId = '184a26f6-2612-11ec-9e88-0025905f714a';
821 token = 'TokenTokenTokenToken';
822 sinon.stub(db.statement.tokenGetByCodeId, 'get');
823 sinon.stub(db.statement.tokenScopesGetByCodeId, 'all');
824 });
825 it('success', function() {
826 const now = new Date();
827 const nowEpoch = Math.ceil(now / 1000);
828 const expected = {
829 created: new Date(nowEpoch * 1000),
830 expires: null,
831 refreshExpires: null,
832 refreshed: null,
833 isRevoked: false,
834 isToken: false,
835 token,
836 codeId,
837 scopes: [],
838 profileData: {
839 name: 'Some Name',
840 },
841 };
842 const dbResult = {
843 created: Math.ceil(nowEpoch),
844 expires: null,
845 refreshExpires: null,
846 refreshed: null,
847 isToken: 0,
848 token,
849 codeId,
850 profileData: '{"name":"Some Name"}',
851 };
852 db.statement.tokenGetByCodeId.get.returns(dbResult);
853 const result = db.tokenGetByCodeId(dbCtx, codeId);
854 assert.deepStrictEqual(result, expected);
855 });
856 it('success without profile data', function () {
857 const now = new Date();
858 const nowEpoch = Math.ceil(now / 1000);
859 const expected = {
860 created: new Date(nowEpoch * 1000),
861 expires: null,
862 refreshExpires: null,
863 refreshed: null,
864 isRevoked: false,
865 isToken: false,
866 token,
867 codeId,
868 scopes: ['foop', 'baa'],
869 };
870 const dbResult = {
871 created: Math.ceil(nowEpoch),
872 expires: null,
873 refreshExpires: null,
874 refreshed: null,
875 isToken: 0,
876 token,
877 codeId,
878 };
879 db.statement.tokenGetByCodeId.get.returns(dbResult);
880 db.statement.tokenScopesGetByCodeId.all.returns([{ scope: 'foop' }, { scope: 'baa' }]);
881 const result = db.tokenGetByCodeId(dbCtx, codeId);
882 assert.deepStrictEqual(result, expected);
883 });
884 it('failure', function () {
885 db.statement.tokenGetByCodeId.get.throws(expectedException);
886 assert.throws(() => db.tokenGetByCodeId(dbCtx, codeId), expectedException);
887 });
888
889 describe('_tokenToNative', function () {
890 it('covers', function () {
891 const result = DB._tokenToNative();
892 assert.strictEqual(result, undefined);
893 });
894 }); // _tokenToNative
895 }); // tokenGetByCodeId
896
897 describe('tokenRevokeByCodeId', function () {
898 let dbResult, codeId;
899 beforeEach(function () {
900 codeId = '2f226616-3e79-11ec-ad0f-0025905f714a';
901 sinon.stub(db.statement.tokenRevokeByCodeId, 'run')
902 dbResult = {
903 changes: 1,
904 lastInsertRowid: undefined,
905 };
906 });
907 it('success', function() {
908 db.statement.tokenRevokeByCodeId.run.returns(dbResult);
909 db.tokenRevokeByCodeId(dbCtx, codeId);
910 });
911 it('failure', function () {
912 dbResult.changes = 0;
913 db.statement.tokenRevokeByCodeId.run.returns(dbResult);
914 assert.throws(() => db.tokenRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
915 });
916 it('failure, error', function () {
917 db.statement.tokenRevokeByCodeId.run.throws(expectedException);
918 assert.throws(() => db.tokenRevokeByCodeId(dbCtx, codeId), expectedException);
919 });
920 }); // tokenRevokeByCodeId
921
922 describe('tokenRefreshRevokeByCodeId', function () {
923 let dbResult, codeId;
924 beforeEach(function () {
925 dbResult = {
926 changes: 1,
927 lastInsertRowid: undefined,
928 };
929 codeId = 'eabba58e-2633-11ed-bbad-0025905f714a';
930 sinon.stub(db.statement.tokenRefreshRevokeByCodeId, 'run');
931 });
932 it('success', function () {
933 db.statement.tokenRefreshRevokeByCodeId.run.returns(dbResult);
934 db.tokenRefreshRevokeByCodeId(dbCtx, codeId);
935 });
936 it('failure', function () {
937 dbResult.changes = 0;
938 db.statement.tokenRefreshRevokeByCodeId.run.returns(dbResult);
939 assert.throws(() => db.tokenRefreshRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
940 });
941 it('failure, error', function () {
942 const expected = new Error('oh no');
943 db.statement.tokenRefreshRevokeByCodeId.run.throws(expected);
944 assert.throws(() => db.tokenRefreshRevokeByCodeId(dbCtx, codeId), expected);
945 });
946 }); // tokenRefreshRevokeByCodeId
947
948 describe('tokensGetByIdentifier', function () {
949 let identifier;
950 beforeEach(function () {
951 identifier = 'identifier';
952 sinon.stub(db.statement.tokensGetByIdentifier, 'all');
953 });
954 it('success', function () {
955 const nowEpoch = Math.ceil(Date.now() / 1000);
956 const dbResult = [
957 {
958 created: nowEpoch,
959 expires: nowEpoch + 86400,
960 duration: 86400,
961 refreshed: nowEpoch + 600,
962 refreshExpires: nowEpoch + 172800,
963 isRevoked: false,
964 isToken: true,
965 codeId: 'c0a7cef4-2637-11ed-a830-0025905f714a',
966 profile: 'https://profile.example.com/',
967 profileData: '{"name":"Some Name"}',
968 identifier: 'username',
969 },
970 ];
971 const expected = [
972 Object.assign({}, dbResult[0], {
973 created: new Date(dbResult[0].created * 1000),
974 expires: new Date(dbResult[0].expires * 1000),
975 refreshed: new Date(dbResult[0].refreshed * 1000),
976 refreshExpires: new Date(dbResult[0].refreshExpires * 1000),
977 profileData: {
978 name: 'Some Name',
979 },
980 }),
981 ];
982 db.statement.tokensGetByIdentifier.all.returns(dbResult);
983 const result = db.tokensGetByIdentifier(dbCtx, identifier);
984 assert.deepStrictEqual(result, expected);
985 });
986 it('failure', function() {
987 db.statement.tokensGetByIdentifier.all.throws(expectedException);
988 assert.throws(() => db.tokensGetByIdentifier(dbCtx, identifier), expectedException);
989 });
990 }); // tokensGetByIdentifier
991
992 describe('ticketRedeemed', function () {
993 let redeemedData, dbResult;
994 beforeEach(function () {
995 redeemedData = {
996 resource: 'https://resource.example.com/',
997 subject: 'https://subject.example.com/',
998 iss: 'https://idp.example.com/',
999 ticket: 'xxxTICKETxxx',
1000 token: 'xxxTOKENxxx',
1001 };
1002 sinon.stub(db.statement.ticketRedeemed, 'run');
1003 dbResult = {
1004 changes: 1,
1005 lastInsertRowid: undefined,
1006 };
1007 });
1008 it('success', function () {
1009 db.statement.ticketRedeemed.run.returns(dbResult);
1010 db.ticketRedeemed(dbCtx, redeemedData);
1011 });
1012 it('failure', function () {
1013 dbResult.changes = 0;
1014 db.statement.ticketRedeemed.run.returns(dbResult);
1015 assert.throws(() => db.ticketRedeemed(dbCtx, redeemedData), DBErrors.UnexpectedResult);
1016 });
1017 }); // ticketRedeemed
1018
1019 describe('ticketTokenPublished', function () {
1020 let redeemedData, dbResult;
1021 beforeEach(function () {
1022 redeemedData = {
1023 resource: 'https://resource.example.com/',
1024 subject: 'https://subject.example.com/',
1025 iss: 'https://idp.example.com/',
1026 ticket: 'xxxTICKETxxx',
1027 token: 'xxxTOKENxxx',
1028 };
1029 sinon.stub(db.statement.ticketTokenPublished, 'run');
1030 sinon.stub(db.statement.almanacUpsert, 'run');
1031 dbResult = {
1032 changes: 1,
1033 lastInsertRowid: undefined,
1034 };
1035 });
1036 it('success', function () {
1037 db.statement.ticketTokenPublished.run.returns(dbResult);
1038 db.statement.almanacUpsert.run.returns(dbResult);
1039 db.ticketTokenPublished(dbCtx, redeemedData);
1040 assert(db.statement.ticketTokenPublished.run.called);
1041 assert(db.statement.almanacUpsert.run.called);
1042 });
1043 it('failure', function () {
1044 dbResult.changes = 0;
1045 db.statement.ticketTokenPublished.run.returns(dbResult);
1046 assert.throws(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult);
1047 });
1048 it('failure of almanac', function () {
1049 const dbResultAlmanac = {
1050 ...dbResult,
1051 changes: 0,
1052 }
1053 db.statement.ticketTokenPublished.run.returns(dbResult);
1054 db.statement.almanacUpsert.run.returns(dbResultAlmanac);
1055 assert.throws(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult);
1056 });
1057 }); // ticketTokenPublished
1058
1059 describe('ticketTokenGetUnpublished', function () {
1060 beforeEach(function () {
1061 sinon.stub(db.statement.ticketTokenGetUnpublished, 'all');
1062 });
1063 it('success', function () {
1064 db.statement.ticketTokenGetUnpublished.all.returns([]);
1065 const result = db.ticketTokenGetUnpublished();
1066 assert.deepStrictEqual(result, []);
1067 });
1068 it('failure', function () {
1069 db.statement.ticketTokenGetUnpublished.all.throws(expectedException);
1070 assert.throws(() => db.ticketTokenGetUnpublished(), expectedException);
1071 });
1072 }); // ticketTokenGetUnpublished
1073
1074 describe('_redeemedTicketToNative', function () {
1075 let redeemedData;
1076 beforeEach(function () {
1077 redeemedData = {
1078 resource: 'https://resource.example.com/',
1079 subject: 'https://subject.example.com/',
1080 iss: 'https://idp.example.com/',
1081 ticket: 'xxxTICKETxxx',
1082 token: 'xxxTOKENxxx',
1083 created: 1701970607n,
1084 published: 1701970670n,
1085 };
1086 });
1087 it('covers', function () {
1088 const expected = {
1089 ...redeemedData,
1090 created: new Date('2023-12-07T17:36:47.000Z'),
1091 published: new Date('2023-12-07T17:37:50.000Z'),
1092 };
1093 const result = DB._redeemedTicketToNative(redeemedData);
1094 assert.deepStrictEqual(result, expected);
1095 });
1096 it('covers no published', function () {
1097 redeemedData.published = null;
1098 const expected = {
1099 ...redeemedData,
1100 created: new Date('2023-12-07T17:36:47.000Z'),
1101 published: null,
1102 };
1103 const result = DB._redeemedTicketToNative(redeemedData);
1104 assert.deepStrictEqual(result, expected);
1105 });
1106 }); // _redeemedTicketToNative
1107
1108 }); // DatabaseSQLite