f6935943eb1f088174185c15aac0ea5f9e373441
[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('authenticationGet', function () {
220 let identifier, credential;
221 beforeEach(function () {
222 identifier = 'username';
223 credential = '$z$foo';
224 sinon.stub(db.statement.authenticationGet, 'get');
225 });
226 it('success', function() {
227 const expected = {
228 identifier,
229 credential,
230 };
231 db.statement.authenticationGet.get.returns(expected);
232 const result = db.authenticationGet(dbCtx, identifier);
233 assert.deepStrictEqual(result, expected);
234 });
235 it('failure', function () {
236 db.statement.authenticationGet.get.throws(expectedException);
237 assert.throws(() => db.authenticationGet(dbCtx, identifier), expectedException);
238 });
239 }); // authenticationGet
240
241 describe('authenticationSuccess', function () {
242 let dbResult, identifier;
243 beforeEach(function () {
244 identifier = 'username';
245 sinon.stub(db.statement.authenticationSuccess, 'run');
246 dbResult = {
247 changes: 1,
248 lastInsertRowid: undefined,
249 };
250 });
251 it('success', function() {
252 db.statement.authenticationSuccess.run.returns(dbResult);
253 db.authenticationSuccess(dbCtx, identifier);
254 });
255 it('failure', function () {
256 dbResult.changes = 0;
257 db.statement.authenticationSuccess.run.returns(dbResult);
258 assert.throws(() => db.authenticationSuccess(dbCtx, identifier), DBErrors.UnexpectedResult);
259 });
260 }); // authenticationSuccess
261
262 describe('authenticationUpsert', function () {
263 let identifier, credential;
264 beforeEach(function () {
265 identifier = 'username';
266 credential = '$z$foo';
267 });
268 it('success', function() {
269 const dbResult = {
270 changes: 1,
271 lastInsertRowid: undefined,
272 };
273 sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult);
274 db.authenticationUpsert(dbCtx, identifier, credential);
275 });
276 it('failure', function () {
277 const dbResult = {
278 changes: 0,
279 lastInsertRowid: undefined,
280 };
281 sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult);
282 assert.throws(() => db.authenticationUpsert(dbCtx, identifier, credential), DBErrors.UnexpectedResult);
283 });
284 }); // authenticationUpsert
285
286 describe('profileIdentifierInsert', function () {
287 let profile, identifier;
288 beforeEach(function () {
289 profile = 'https://profile.example.com/';
290 identifier = 'identifier';
291 sinon.stub(db.statement.profileIdentifierInsert, 'run');
292 });
293 it('success', function () {
294 db.statement.profileIdentifierInsert.run.returns({ changes: 1 });
295 db.profileIdentifierInsert(dbCtx, profile, identifier);
296 });
297 it('failure', function () {
298 db.statement.profileIdentifierInsert.run.returns({ changes: 0 });
299 assert.throws(() => db.profileIdentifierInsert(dbCtx, profile, identifier), DBErrors.UnexpectedResult);
300 });
301 }); // profileIdentifierInsert
302
303 describe('profileScopeInsert', function () {
304 let profile, scope;
305 beforeEach(function () {
306 profile = 'https://profile.example.com/';
307 scope = 'scope';
308 sinon.stub(db.statement.profileScopeInsert, 'run');
309 });
310 it('success', function () {
311 db.statement.profileScopeInsert.run.returns({ changes: 1 });
312 db.profileScopeInsert(dbCtx, profile, scope);
313 });
314 it('failure', function () {
315 db.statement.profileScopeInsert.run.returns({ changes: 2 });
316 assert.throws(() => db.profileScopeInsert(dbCtx, profile, scope), DBErrors.UnexpectedResult);
317 });
318 }); // profileScopeInsert
319
320 describe('profileIsValid', function () {
321 let profile;
322 beforeEach(function () {
323 profile = 'https://profile.exmaple.com';
324 });
325 it('valid profile', function () {
326 sinon.stub(db.statement.profileGet, 'get').returns({ profile });
327 const result = db.profileIsValid(dbCtx, profile);
328 assert.deepStrictEqual(result, true);
329 });
330 it('invalid profile', function () {
331 sinon.stub(db.statement.profileGet, 'get').returns();
332 const result = db.profileIsValid(dbCtx, profile);
333 assert.deepStrictEqual(result, false);
334 });
335 it('failure', function() {
336 sinon.stub(db.statement.profileGet, 'get').throws(expectedException);
337 assert.throws(() => db.profileIsValid(dbCtx, profile), expectedException);
338 });
339 }); // profileIsValid
340
341 describe('profilesScopesByIdentifier', function () {
342 let identifier, scopeIndex, profileScopes, profiles;
343 beforeEach(function () {
344 identifier = 'identifier';
345 scopeIndex = {
346 'scope': {
347 description: 'A scope.',
348 application: 'test',
349 isPermanent: false,
350 isManuallyAdded: false,
351 profiles: ['https://first.example.com/', 'https://second.example.com/'],
352 },
353 'another_scope': {
354 description: 'Another scope.',
355 application: 'another test',
356 isPermanent: false,
357 isManuallyAdded: false,
358 profiles: ['https://first.example.com/'],
359 },
360 };
361 profileScopes = {
362 'https://first.example.com/': {
363 'scope': scopeIndex['scope'],
364 'another_scope': scopeIndex['another_scope'],
365 },
366 'https://second.example.com/': {
367 'scope': scopeIndex['scope'],
368 },
369 };
370 profiles = ['https://first.example.com/', 'https://second.example.com/'];
371 });
372 it('success', function () {
373 const dbResult = [
374 { profile: 'https://first.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: false },
375 { profile: 'https://first.example.com/', scope: 'another_scope', application: 'another test', description: 'Another scope.', isPermanent: false, isManuallyAdded: false },
376 { profile: 'https://second.example.com/', scope: 'scope', application: 'test', description: 'A scope.', isPermanent: false, isManuallyAdded: false },
377 ];
378 const expected = {
379 scopeIndex,
380 profileScopes,
381 profiles,
382 };
383 sinon.stub(db.statement.profilesScopesByIdentifier, 'all').returns(dbResult);
384 const result = db.profilesScopesByIdentifier(dbCtx, identifier);
385 assert.deepStrictEqual(result, expected);
386 });
387 it('failure', function() {
388 sinon.stub(db.statement.profilesScopesByIdentifier, 'all').throws(expectedException);
389 assert.throws(() => db.profilesScopesByIdentifier(dbCtx, identifier), expectedException);
390 });
391 }); // profilesScopesByIdentifier
392
393 describe('profileScopesSetAll', function () {
394 let profile, scopes;
395 beforeEach(function () {
396 profile = 'https://example.com/';
397 scopes = ['scope1', 'scope2'];
398 sinon.stub(db.statement.profileScopesClear, 'run').returns();
399 sinon.stub(db.statement.profileScopeInsert, 'run');
400 });
401 it('success, no scopes', function () {
402 db.statement.profileScopeInsert.run.returns();
403 scopes = [];
404 db.profileScopesSetAll(dbCtx, profile, scopes);
405 });
406 it('success, scopes', function () {
407 db.statement.profileScopeInsert.run.returns();
408 scopes.push('profile', 'email', 'create');
409 db.profileScopesSetAll(dbCtx, profile, scopes);
410 });
411 it('failure', function () {
412 db.statement.profileScopeInsert.run.throws(expectedException);
413 assert.throws(() => db.profileScopesSetAll(dbCtx, profile, scopes), expectedException);
414 });
415
416 }); // profileScopesSetAll
417
418 describe('redeemCode', function () {
419 let codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, profileData;
420 beforeEach(function () {
421 codeId = '2f226616-3e79-11ec-ad0f-0025905f714a';
422 isToken = false;
423 clientId = 'https://app.exmaple.com/';
424 profile = 'https://profile.example.com/';
425 identifier = 'username';
426 scopes = ['scope1', 'scope2'];
427 lifespanSeconds = 600;
428 profileData = undefined;
429 created = new Date();
430
431 sinon.stub(db.statement.scopeInsert, 'run');
432 sinon.stub(db.statement.tokenScopeSet, 'run');
433 sinon.stub(db.statement.redeemCode, 'get');
434 });
435 it('success', function() {
436 const dbResult = {
437 changes: 1,
438 lastInsertRowid: undefined,
439 };
440 const dbGet = {
441 isRevoked: false,
442 };
443 db.statement.scopeInsert.run.returns(dbResult);
444 db.statement.tokenScopeSet.run.returns(dbResult);
445 db.statement.redeemCode.get.returns(dbGet);
446 profileData = {
447 name: 'Some Name',
448 };
449 const result = db.redeemCode(dbCtx, { codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, profileData });
450 assert.strictEqual(result, true);
451 });
452 it('success (revoked)', function() {
453 const dbResult = {
454 changes: 1,
455 lastInsertRowid: undefined,
456 };
457 const dbGet = {
458 isRevoked: true,
459 };
460 db.statement.scopeInsert.run.returns(dbResult);
461 db.statement.tokenScopeSet.run.returns(dbResult);
462 db.statement.redeemCode.get.returns(dbGet);
463 const result = db.redeemCode(dbCtx, { codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, profileData });
464 assert.strictEqual(result, false);
465 });
466 it('failure', function () {
467 db.statement.scopeInsert.run.throws();
468 db.statement.tokenScopeSet.run.throws();
469 db.statement.redeemCode.get.returns();
470 assert.throws(() => db.redeemCode(dbCtx, { codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds }), DBErrors.UnexpectedResult);
471 });
472 }); // redeemCode
473
474 describe('refreshCode', function () {
475 let refreshResponse, removeResponse, scopesResponse, codeId, refreshed, removeScopes;
476 beforeEach(function () {
477 sinon.stub(db.statement.refreshCode, 'get');
478 sinon.stub(db.statement.tokenScopeRemove, 'run');
479 sinon.stub(db.statement.tokenScopesGetByCodeId, 'all');
480 codeId = '73db7b18-27bb-11ed-8edd-0025905f714a';
481 refreshed = new Date();
482 removeScopes = ['foop'];
483 const refreshedEpoch = Math.ceil(refreshed.getTime() / 1000);
484 refreshResponse = {
485 expires: refreshedEpoch + 86400,
486 refreshExpires: refreshedEpoch + 172800,
487 };
488 removeResponse = {
489 changes: removeScopes.length,
490 };
491 scopesResponse = [
492 { scope: 'blah' },
493 ];
494 });
495 it('success', function () {
496 db.statement.refreshCode.get.returns(refreshResponse);
497 db.statement.tokenScopeRemove.run.returns(removeResponse);
498 db.statement.tokenScopesGetByCodeId.all.returns(scopesResponse);
499 const expectedResponse = {
500 expires: new Date(refreshResponse.expires * 1000),
501 refreshExpires: new Date(refreshResponse.refreshExpires * 1000),
502 scopes: ['blah'],
503 }
504 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
505 assert.deepStrictEqual(response, expectedResponse);
506 });
507 it('success without scope removal', function () {
508 db.statement.refreshCode.get.returns(refreshResponse);
509 db.statement.tokenScopeRemove.run.returns(removeResponse);
510 const expectedResponse = {
511 expires: new Date(refreshResponse.expires * 1000),
512 refreshExpires: new Date(refreshResponse.refreshExpires * 1000),
513 }
514 removeScopes = [];
515 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
516 assert.deepStrictEqual(response, expectedResponse);
517 });
518 it('success with no scopes left', function () {
519 db.statement.refreshCode.get.returns(refreshResponse);
520 db.statement.tokenScopeRemove.run.returns(removeResponse);
521 const expectedResponse = {
522 expires: new Date(refreshResponse.expires * 1000),
523 refreshExpires: new Date(refreshResponse.refreshExpires * 1000),
524 scopes: [],
525 }
526 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
527 assert.deepStrictEqual(response, expectedResponse);
528 });
529 it('no code', function () {
530 db.statement.refreshCode.get.returns();
531 removeResponse.changes = 0;
532 db.statement.tokenScopeRemove.run.returns();
533 const expectedResponse = undefined;
534 const response = db.refreshCode(dbCtx, codeId, refreshed, removeScopes);
535 assert.deepStrictEqual(response, expectedResponse);
536 });
537 it('failure', function () {
538 db.statement.refreshCode.get.throws(expectedException);
539 assert.throws(() => db.refreshCode(dbCtx, codeId, refreshed, removeScopes), expectedException);
540 });
541 it('scope removal failure', function () {
542 removeResponse.changes = 0;
543 db.statement.tokenScopeRemove.run.returns(removeResponse);
544 db.statement.refreshCode.get.returns(refreshResponse);
545 assert.throws(() => db.refreshCode(dbCtx, codeId, refreshed, removeScopes), DBErrors.UnexpectedResult);
546 });
547
548 describe('_refreshCodeResponseToNative', function () {
549 it('coverage', function () {
550 const expected = { foo: 'bar' };
551 const result = DB._refreshCodeResponseToNative(expected);
552 assert.deepStrictEqual(result, expected);
553 });
554 it('coverage', function () {
555 const result = DB._refreshCodeResponseToNative();
556 assert.strictEqual(result, undefined);
557 });
558 });
559 }); // refreshCode
560
561 describe('resourceGet', function () {
562 let identifier;
563 beforeEach(function () {
564 sinon.stub(db.statement.resourceGet, 'get');
565 identifier = '05b81112-b224-11ec-a9c6-0025905f714a';
566 });
567 it('success', function () {
568 const dbResult = {
569 identifier,
570 secret: 'secrety',
571 };
572 db.statement.resourceGet.get.returns(dbResult);
573 const result = db.resourceGet(dbCtx, identifier);
574 assert.deepStrictEqual(result, dbResult);
575 });
576 it('failure', function() {
577 db.statement.resourceGet.get.throws(expectedException);
578 assert.throws(() => db.resourceGet(dbCtx, identifier), expectedException);
579 });
580 }); // resourceGet
581
582 describe('resourceUpsert', function () {
583 let resourceId, secret, description;
584 beforeEach(function () {
585 resourceId = '4086661a-f980-11ec-ba19-0025905f714a';
586 secret = 'secret';
587 description = 'some application';
588 });
589 it('success', function() {
590 const dbResult = {
591 changes: 1,
592 lastInsertRowid: undefined,
593 };
594 sinon.stub(db.statement.resourceUpsert, 'run').returns(dbResult);
595 db.resourceUpsert(dbCtx, resourceId, secret, description);
596 });
597 it('creates id if not provided', function () {
598 resourceId = undefined;
599 const dbResult = {
600 changes: 1,
601 lastInsertRowid: undefined,
602 };
603 sinon.stub(db.statement.resourceUpsert, 'run').returns(dbResult);
604 db.resourceUpsert(dbCtx, resourceId, secret, description);
605 });
606 it('failure', function () {
607 const dbResult = {
608 changes: 0,
609 lastInsertRowid: undefined,
610 };
611 sinon.stub(db.statement.resourceUpsert, 'run').returns(dbResult);
612 assert.throws(() => db.resourceUpsert(dbCtx, resourceId, secret, description), DBErrors.UnexpectedResult);
613 });
614 }); // resourceUpsert
615
616 describe('scopeCleanup', function () {
617 let atLeastMsSinceLast;
618 beforeEach(function () {
619 atLeastMsSinceLast = 86400000;
620 sinon.stub(db.statement.scopeCleanup, 'run');
621 sinon.stub(db.statement.almanacGet, 'get');
622 sinon.stub(db.statement.almanacUpsert, 'run');
623 });
624 it('success, empty almanac', function () {
625 const cleaned = 10n;
626 db.statement.almanacGet.get.returns();
627 db.statement.scopeCleanup.run.returns({ changes: cleaned });
628 db.statement.almanacUpsert.run.returns({ changes: 1 });
629 const result = db.scopeCleanup(dbCtx, atLeastMsSinceLast);
630 assert.strictEqual(result, cleaned);
631 });
632 it('success, too soon', function () {
633 db.statement.almanacGet.get.returns({ epoch: BigInt(Math.ceil(Date.now() / 1000) - 4) });
634 const result = db.scopeCleanup(dbCtx, atLeastMsSinceLast);
635 assert.strictEqual(result, undefined);
636 assert(db.statement.scopeCleanup.run.notCalled);
637 });
638 it('failure', function () {
639 db.statement.almanacGet.get.returns({ epoch: 0n });
640 db.statement.scopeCleanup.run.returns({ changes: 1 });
641 db.statement.almanacUpsert.run.returns({ changes: 0 });
642 assert.throws(() => db.scopeCleanup(dbCtx, atLeastMsSinceLast), DBErrors.UnexpectedResult);
643 });
644 }); // scopeCleanup
645
646 describe('scopeDelete', function () {
647 let dbGetResult, dbRunResult, scope;
648 beforeEach(function () {
649 sinon.stub(db.statement.scopeInUse, 'get');
650 dbGetResult = {
651 inUse: false,
652 }
653 sinon.stub(db.statement.scopeDelete, 'run');
654 dbRunResult = {
655 changes: 1,
656 };
657 scope = 'some_scope';
658 });
659 it('success', function () {
660 db.statement.scopeInUse.get.returns(dbGetResult);
661 db.statement.scopeDelete.run.returns(dbRunResult);
662 const result = db.scopeDelete(dbCtx, scope);
663 assert.strictEqual(result, true);
664 });
665 it('in use', function () {
666 dbGetResult.inUse = true;
667 db.statement.scopeInUse.get.returns(dbGetResult);
668 db.statement.scopeDelete.run.returns(dbRunResult);
669 const result = db.scopeDelete(dbCtx, scope);
670 assert.strictEqual(result, false);
671 });
672 it('no scope', function () {
673 dbRunResult.changes = 0;
674 db.statement.scopeInUse.get.returns(dbGetResult);
675 db.statement.scopeDelete.run.returns(dbRunResult);
676 const result = db.scopeDelete(dbCtx, scope);
677 assert.strictEqual(result, true);
678 });
679 it('failure', function () {
680 db.statement.scopeInUse.get.throws(expectedException);
681 assert.throws(() => db.scopeDelete(dbCtx, scope), expectedException);
682 });
683 }); // scopeDelete
684
685 describe('scopeUpsert', function () {
686 let dbResult, scope, application, description;
687 beforeEach(function () {
688 scope = 'scope';
689 application = undefined;
690 description = 'description';
691 sinon.stub(db.statement.scopeUpsert, 'run');
692 dbResult = {
693 changes: 1,
694 lastInsertRowid: undefined,
695 };
696 });
697 it('success', function() {
698 db.statement.scopeUpsert.run.returns(dbResult);
699 db.scopeUpsert(dbCtx, scope, application, description);
700 });
701 it('failure', function () {
702 dbResult.changes = 0;
703 db.statement.scopeUpsert.run.returns(dbResult);
704 assert.throws(() => db.scopeUpsert(dbCtx, scope, application, description), DBErrors.UnexpectedResult);
705 });
706 it('failure, error', function () {
707 db.statement.scopeUpsert.run.throws(expectedException);
708 assert.throws(() => db.scopeUpsert(dbCtx, scope, application, description), expectedException);
709 });
710 }); // scopeUpsert
711
712 describe('tokenCleanup', function () {
713 let codeLifespanSeconds, atLeastMsSinceLast;
714 beforeEach(function () {
715 codeLifespanSeconds = 600;
716 atLeastMsSinceLast = 86400000;
717 sinon.stub(db.statement.tokenCleanup, 'run');
718 sinon.stub(db.statement.almanacGet, 'get');
719 sinon.stub(db.statement.almanacUpsert, 'run');
720 });
721 it('success, empty almanac', function() {
722 const cleaned = 10n;
723 db.statement.almanacGet.get.returns();
724 db.statement.tokenCleanup.run.returns({ changes: cleaned });
725 db.statement.almanacUpsert.run.returns({ changes: 1 });
726 const result = db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast);
727 assert.strictEqual(result, cleaned);
728 });
729 it('success, too soon', function () {
730 db.statement.almanacGet.get.returns({ epoch: BigInt(Math.ceil(Date.now() / 1000) - 4) });
731 const result = db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast);
732 assert.strictEqual(result, undefined);
733 assert(db.statement.tokenCleanup.run.notCalled);
734 });
735 it('failure', function () {
736 db.statement.almanacGet.get.returns({ epoch: 0n });
737 db.statement.tokenCleanup.run.returns({ changes: 10 });
738 db.statement.almanacUpsert.run.returns({ changes: 0 });
739 assert.throws(() => db.tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast), DBErrors.UnexpectedResult);
740 });
741 }); // tokenCleanup
742
743 describe('tokenGetByCodeId', function () {
744 let codeId, token;
745 beforeEach(function () {
746 codeId = '184a26f6-2612-11ec-9e88-0025905f714a';
747 token = 'TokenTokenTokenToken';
748 sinon.stub(db.statement.tokenGetByCodeId, 'get');
749 sinon.stub(db.statement.tokenScopesGetByCodeId, 'all');
750 });
751 it('success', function() {
752 const now = new Date();
753 const nowEpoch = Math.ceil(now / 1000);
754 const expected = {
755 created: new Date(nowEpoch * 1000),
756 expires: null,
757 refreshExpires: null,
758 refreshed: null,
759 isRevoked: false,
760 isToken: false,
761 token,
762 codeId,
763 scopes: [],
764 profileData: {
765 name: 'Some Name',
766 },
767 };
768 const dbResult = {
769 created: Math.ceil(nowEpoch),
770 expires: null,
771 refreshExpires: null,
772 refreshed: null,
773 isToken: 0,
774 token,
775 codeId,
776 profileData: '{"name":"Some Name"}',
777 };
778 db.statement.tokenGetByCodeId.get.returns(dbResult);
779 const result = db.tokenGetByCodeId(dbCtx, codeId);
780 assert.deepStrictEqual(result, expected);
781 });
782 it('success without profile data', function () {
783 const now = new Date();
784 const nowEpoch = Math.ceil(now / 1000);
785 const expected = {
786 created: new Date(nowEpoch * 1000),
787 expires: null,
788 refreshExpires: null,
789 refreshed: null,
790 isRevoked: false,
791 isToken: false,
792 token,
793 codeId,
794 scopes: ['foop', 'baa'],
795 };
796 const dbResult = {
797 created: Math.ceil(nowEpoch),
798 expires: null,
799 refreshExpires: null,
800 refreshed: null,
801 isToken: 0,
802 token,
803 codeId,
804 };
805 db.statement.tokenGetByCodeId.get.returns(dbResult);
806 db.statement.tokenScopesGetByCodeId.all.returns([{ scope: 'foop' }, { scope: 'baa' }]);
807 const result = db.tokenGetByCodeId(dbCtx, codeId);
808 assert.deepStrictEqual(result, expected);
809 });
810 it('failure', function () {
811 db.statement.tokenGetByCodeId.get.throws(expectedException);
812 assert.throws(() => db.tokenGetByCodeId(dbCtx, codeId), expectedException);
813 });
814
815 describe('_tokenToNative', function () {
816 it('covers', function () {
817 const result = DB._tokenToNative();
818 assert.strictEqual(result, undefined);
819 });
820 }); // _tokenToNative
821 }); // tokenGetByCodeId
822
823 describe('tokenRevokeByCodeId', function () {
824 let dbResult, codeId;
825 beforeEach(function () {
826 codeId = '2f226616-3e79-11ec-ad0f-0025905f714a';
827 sinon.stub(db.statement.tokenRevokeByCodeId, 'run')
828 dbResult = {
829 changes: 1,
830 lastInsertRowid: undefined,
831 };
832 });
833 it('success', function() {
834 db.statement.tokenRevokeByCodeId.run.returns(dbResult);
835 db.tokenRevokeByCodeId(dbCtx, codeId);
836 });
837 it('failure', function () {
838 dbResult.changes = 0;
839 db.statement.tokenRevokeByCodeId.run.returns(dbResult);
840 assert.throws(() => db.tokenRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
841 });
842 it('failure, error', function () {
843 db.statement.tokenRevokeByCodeId.run.throws(expectedException);
844 assert.throws(() => db.tokenRevokeByCodeId(dbCtx, codeId), expectedException);
845 });
846 }); // tokenRevokeByCodeId
847
848 describe('tokenRefreshRevokeByCodeId', function () {
849 let dbResult, codeId;
850 beforeEach(function () {
851 dbResult = {
852 changes: 1,
853 lastInsertRowid: undefined,
854 };
855 codeId = 'eabba58e-2633-11ed-bbad-0025905f714a';
856 sinon.stub(db.statement.tokenRefreshRevokeByCodeId, 'run');
857 });
858 it('success', function () {
859 db.statement.tokenRefreshRevokeByCodeId.run.returns(dbResult);
860 db.tokenRefreshRevokeByCodeId(dbCtx, codeId);
861 });
862 it('failure', function () {
863 dbResult.changes = 0;
864 db.statement.tokenRefreshRevokeByCodeId.run.returns(dbResult);
865 assert.throws(() => db.tokenRefreshRevokeByCodeId(dbCtx, codeId), DBErrors.UnexpectedResult);
866 });
867 it('failure, error', function () {
868 const expected = new Error('oh no');
869 db.statement.tokenRefreshRevokeByCodeId.run.throws(expected);
870 assert.throws(() => db.tokenRefreshRevokeByCodeId(dbCtx, codeId), expected);
871 });
872 }); // tokenRefreshRevokeByCodeId
873
874 describe('tokensGetByIdentifier', function () {
875 let identifier;
876 beforeEach(function () {
877 identifier = 'identifier';
878 sinon.stub(db.statement.tokensGetByIdentifier, 'all');
879 });
880 it('success', function () {
881 const nowEpoch = Math.ceil(Date.now() / 1000);
882 const dbResult = [
883 {
884 created: nowEpoch,
885 expires: nowEpoch + 86400,
886 duration: 86400,
887 refreshed: nowEpoch + 600,
888 refreshExpires: nowEpoch + 172800,
889 isRevoked: false,
890 isToken: true,
891 codeId: 'c0a7cef4-2637-11ed-a830-0025905f714a',
892 profile: 'https://profile.example.com/',
893 profileData: '{"name":"Some Name"}',
894 identifier: 'username',
895 },
896 ];
897 const expected = [
898 Object.assign({}, dbResult[0], {
899 created: new Date(dbResult[0].created * 1000),
900 expires: new Date(dbResult[0].expires * 1000),
901 refreshed: new Date(dbResult[0].refreshed * 1000),
902 refreshExpires: new Date(dbResult[0].refreshExpires * 1000),
903 profileData: {
904 name: 'Some Name',
905 },
906 }),
907 ];
908 db.statement.tokensGetByIdentifier.all.returns(dbResult);
909 const result = db.tokensGetByIdentifier(dbCtx, identifier);
910 assert.deepStrictEqual(result, expected);
911 });
912 it('failure', function() {
913 db.statement.tokensGetByIdentifier.all.throws(expectedException);
914 assert.throws(() => db.tokensGetByIdentifier(dbCtx, identifier), expectedException);
915 });
916 }); // tokensGetByIdentifier
917
918 }); // DatabaseSQLite