10e88dfc06c40c84d7e65d48b0d27109fc6b44e7
2 /* eslint-disable sonarjs/no-identical-functions */
6 * These are LIVE FIRE tests to exercise actual database operations.
7 * They should be configured to use local test databases, as they
8 * perform DESTRUCTIVE ACTIONS on all tables, beginning with a COMPLETE
11 * They will only run if all the appropriate environmental settings exist:
12 * - INTEGRATION_TESTS must be set
13 * - <ENGINE>_TEST_PATH must point to the endpoint/db
15 * These tests are sequential, relying on the state created along the way.
19 const assert
= require('assert');
20 const { step
} = require('mocha-steps'); // eslint-disable-line node/no-unpublished-require
21 const StubLogger
= require('../../stub-logger');
22 // const DBErrors = require('../../../src/db/errors');
23 // const testData = require('../../test-data/db-integration');
25 describe('Database Integration', function () {
26 const implementations
= [];
28 if (!process
.env
.INTEGRATION_TESTS
) {
29 it
.skip('integration tests not requested');
33 if (process
.env
.POSTGRES_TEST_PATH
) {
34 implementations
.push({
36 module: '../../../src/db/postgres',
39 connectionString: `postgresql://${process.env.POSTGRES_TEST_PATH}`,
40 queryLogLevel: 'debug',
47 if (process
.env
.SQLITE_TEST_PATH
) {
48 implementations
.push({
50 module: '../../../src/db/sqlite',
53 connectionString: `sqlite://${process.env.SQLITE_TEST_PATH}`,
54 queryLogLevel: 'debug',
55 sqliteOptimizeAfterChanges: 10,
61 if (!implementations
.length
) {
62 it('have some implementations to test', function () {
63 assert
.fail('No implementations have been configured for requested integration tests');
67 implementations
.forEach(function (i
) {
68 describe(i
.name
, function () {
71 let profile
, identifier
;
73 before(async
function () {
74 this.timeout(10 * 1000); // Allow some time for creating tables et cetera.
75 logger
= new StubLogger();
77 // eslint-disable-next-line security/detect-non-literal-require
78 DB
= require(i
.module
);
79 db
= new DB(logger
, i
.config
);
80 await db
.initialize();
81 await db
._purgeTables(true);
83 after(async
function () {
84 await db
._closeConnection();
87 beforeEach(function () {
88 identifier
= 'username';
89 profile
= 'https://example.com/profile';
92 describe('Healthcheck', function () {
93 it('should succeed', async
function () {
94 const result
= await db
.healthCheck();
99 describe('Resources', function () {
100 let resourceId
, secret
, description
;
102 secret
= 'shared secret';
103 description
= 'A resource server that needs to verify our tokens.';
105 step('returns nothing when resource does not exist', async
function () {
106 await db
.context(async (dbCtx
) => {
107 const badResourceId
= 'f1669969-c87e-46f8-83bb-a6712981d15d';
108 const result
= await db
.resourceGet(dbCtx
, badResourceId
);
112 step('creates resource', async
function () {
113 await db
.context(async (dbCtx
) => {
114 const result
= await db
.resourceUpsert(dbCtx
, undefined, secret
, description
);
115 assert(result
.resourceId
);
116 resourceId
= result
.resourceId
;
119 step('gets resource', async
function () {
120 await db
.context(async (dbCtx
) => {
121 const result
= await db
.resourceGet(dbCtx
, resourceId
);
122 assert
.strictEqual(result
.secret
, secret
);
123 db
._validateResource(result
);
126 step('updates resource', async
function () {
127 await db
.context(async (dbCtx
) => {
128 secret
= 'new shared secret';
129 description
= 'Still a resource server, but with a new description.';
130 await db
.resourceUpsert(dbCtx
, resourceId
, secret
, description
);
131 const result
= await db
.resourceGet(dbCtx
, resourceId
);
132 assert
.strictEqual(result
.resourceId
, resourceId
);
133 assert
.strictEqual(result
.secret
, secret
);
134 assert
.strictEqual(result
.description
, description
);
139 describe('Users and Profiles and Scopes', function () {
140 let credential
, otpKey
;
141 beforeEach(function () {
142 credential
= '$plain$myPassword';
143 otpKey
= '1234567890123456789012';
145 step('returns nothing when auth does not exist', async
function () {
146 await db
.context(async (dbCtx
) => {
147 const result
= await db
.authenticationGet(dbCtx
, identifier
);
151 step('create auth entry', async
function () {
152 await db
.context(async (dbCtx
) => {
153 await db
.authenticationUpsert(dbCtx
, identifier
, credential
);
156 step('get auth entry', async
function () {
157 await db
.context(async (dbCtx
) => {
158 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
159 assert
.strictEqual(authInfo
.credential
, credential
);
160 db
._validateAuthentication(authInfo
);
163 step('valid auth event', async
function () {
164 await db
.context(async (dbCtx
) => {
165 await db
.authenticationSuccess(dbCtx
, identifier
);
166 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
167 db
._validateAuthentication(authInfo
);
168 assert
.notStrictEqual(authInfo
.lastAuthentication
, undefined);
171 step('update auth entry', async
function () {
172 await db
.context(async (dbCtx
) => {
173 credential
= '$plain$myNewPassword';
174 await db
.authenticationUpsert(dbCtx
, identifier
, credential
, otpKey
);
175 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
176 assert
.strictEqual(authInfo
.credential
, credential
);
177 assert
.strictEqual(authInfo
.otpKey
, otpKey
);
180 step('update auth credential', async
function () {
181 await db
.context(async (dbCtx
) => {
182 credential
= '$plain$anotherNewPassword';
183 await db
.authenticationUpdateCredential(dbCtx
, identifier
, credential
);
184 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
185 assert
.strictEqual(authInfo
.credential
, credential
);
188 step('update auth otp', async
function () {
189 await db
.context(async (dbCtx
) => {
190 await db
.authenticationUpdateOTPKey(dbCtx
, identifier
, otpKey
);
191 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
192 assert
.strictEqual(authInfo
.otpKey
, otpKey
);
195 step('profile is not valid', async
function () {
196 await db
.context(async (dbCtx
) => {
197 const isValid
= await db
.profileIsValid(dbCtx
, profile
);
198 assert
.strictEqual(isValid
, false);
201 step('user-profile relation does not exist', async
function () {
202 await db
.context(async (dbCtx
) => {
203 const { profiles
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
204 const exists
= profiles
.includes(profile
);
205 assert
.strictEqual(exists
, false);
208 step('create user-profile relation', async
function () {
209 await db
.context(async (dbCtx
) => {
210 await db
.profileIdentifierInsert(dbCtx
, profile
, identifier
);
213 step('profile is valid', async
function () {
214 await db
.context(async (dbCtx
) => {
215 const isValid
= await db
.profileIsValid(dbCtx
, profile
);
216 assert
.strictEqual(isValid
, true);
219 step('user-profile relation does exist', async
function () {
220 await db
.context(async (dbCtx
) => {
221 const { profiles
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
222 const exists
= profiles
.includes(profile
);
223 assert
.strictEqual(exists
, true);
226 step('create scope', async
function () {
227 await db
.context(async (dbCtx
) => {
228 await db
.scopeUpsert(dbCtx
, 'new_scope', '', 'Allows something to happen.');
231 step('create and delete scope', async
function () {
232 await db
.context(async (dbCtx
) => {
233 await db
.scopeUpsert(dbCtx
, 'sacrificial', 'No App', 'Exists to be destroyed.', true);
234 const result
= await db
.scopeDelete(dbCtx
, 'sacrificial');
235 assert
.strictEqual(result
, true);
238 step('do not delete in-use scope', async
function () {
239 await db
.context(async (dbCtx
) => {
240 const result
= await db
.scopeDelete(dbCtx
, 'profile');
241 assert
.strictEqual(result
, false);
244 step('ignore delete of non-existent scope', async
function () {
245 await db
.context(async (dbCtx
) => {
246 await db
.scopeDelete(dbCtx
, 'non-existent');
249 step('assign scope to profile', async
function () {
250 const scope
= 'new_scope';
251 await db
.context(async (dbCtx
) => {
252 await db
.profileScopeInsert(dbCtx
, profile
, scope
);
253 const { scopeIndex
, profileScopes
, profiles
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
254 const scopeExistsInProfile
= scope
in profileScopes
[profile
];
255 const profileExistsInScope
= scopeIndex
[scope
].profiles
.includes(profile
);
256 const profileExists
= profiles
.includes(profile
);
257 assert
.strictEqual(scopeExistsInProfile
, true);
258 assert
.strictEqual(profileExistsInScope
, true);
259 assert
.strictEqual(profileExists
, true);
262 step('update scope', async
function () {
263 await db
.context(async (dbCtx
) => {
264 await db
.scopeUpsert(dbCtx
, 'new_scope', 'Application', 'Updated description.');
267 step('re-assigning scope to profile is ignored', async
function () {
268 const scope
= 'new_scope';
269 await db
.context(async (dbCtx
) => {
270 await db
.profileScopeInsert(dbCtx
, profile
, scope
);
271 const { scopeIndex
, profileScopes
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
272 const scopeExistsInProfile
= scope
in profileScopes
[profile
];
273 const profileExistsInScope
= scopeIndex
[scope
].profiles
.includes(profile
);
274 assert
.strictEqual(scopeExistsInProfile
, true);
275 assert
.strictEqual(profileExistsInScope
, true);
278 step('clear all scopes for a profile', async
function () {
280 await db
.context(async (dbCtx
) => {
281 await db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
282 const { profileScopes
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
283 const exists
= profile
in profileScopes
;
285 const numScopes
= Object
.keys(profileScopes
[profile
]).length
;
286 assert
.strictEqual(numScopes
, 0);
289 step('set multiple scopes for a profile', async
function () {
290 const scopes
= ['profile', 'email', 'create'];
291 await db
.context(async (dbCtx
) => {
292 await db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
293 const { profileScopes
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
294 assert
.strictEqual(Object
.keys(profileScopes
[profile
]).length
, scopes
.length
);
297 step('garbage-collect client scopes', async
function () {
298 await db
.context(async (dbCtx
) => {
299 await db
.scopeUpsert(dbCtx
, 'extra_scope', 'useless', 'useless');
300 const result
= await db
.scopeCleanup(dbCtx
, 0);
304 step('too-soon garbage-collect skips', async
function () {
305 await db
.context(async (dbCtx
) => {
306 const result
= await db
.scopeCleanup(dbCtx
, 86400000);
307 assert
.strictEqual(result
, undefined);
310 }); // Users and Profiles and Scopes
312 describe('Token', function () {
313 let created
, codeId
, profileCodeId
, ticketCodeId
, scopes
, clientId
, lifespanSeconds
, resource
;
314 beforeEach(function () {
315 created
= new Date();
316 codeId
= '907a95fc-384b-11ec-a541-0025905f714a';
317 profileCodeId
= '93d6314a-384e-11ec-94e4-0025905f714a';
318 ticketCodeId
= 'bc5c39a8-5ca0-11ed-94cd-0025905f714a';
319 clientId
= 'https://app.example.com/';
320 scopes
= ['create', 'email', 'profile'];
321 lifespanSeconds
= 600;
322 resource
= 'https://example.com/profile/feed';
324 step('redeems code for token', async
function () {
325 await db
.context(async (dbCtx
) => {
326 lifespanSeconds
= null;
327 const result
= await db
.redeemCode(dbCtx
, {
336 refreshLifespanSeconds: null,
339 assert
.strictEqual(result
, true);
340 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
342 db
._validateToken(t
);
345 step('revokes token', async
function () {
346 await db
.context(async (dbCtx
) => {
347 await db
.tokenRevokeByCodeId(dbCtx
, codeId
, identifier
);
348 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
349 assert
.strictEqual(t
.isRevoked
, true);
352 step('redeems code for profile', async
function () {
353 await db
.context(async (dbCtx
) => {
354 await db
.redeemCode(dbCtx
, {
356 codeId: profileCodeId
,
364 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
366 db
._validateToken(t
);
369 step('redeems ticket', async
function () {
370 await db
.context(async (dbCtx
) => {
371 await db
.redeemCode(dbCtx
, {
373 codeId: ticketCodeId
,
383 step('gets tokens', async
function () {
384 await db
.context(async (dbCtx
) => {
385 const tokens
= await db
.tokensGetByIdentifier(dbCtx
, identifier
);
386 assert(tokens
.length
);
389 step('revokes multiply-redeemed code', async
function () {
390 await db
.context(async (dbCtx
) => {
391 const result
= await db
.redeemCode(dbCtx
, {
400 assert
.strictEqual(result
, false);
401 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
402 assert
.strictEqual(t
.isRevoked
, true);
405 step('garbage-collect tokens', async
function () {
406 await db
.context(async (dbCtx
) => {
407 const result
= await db
.tokenCleanup(dbCtx
, -86400, 0);
411 step('too-soon garbage-collect skips', async
function () {
412 await db
.context(async (dbCtx
) => {
413 const result
= await db
.tokenCleanup(dbCtx
, 0, 86400000);
414 assert
.strictEqual(result
, undefined);
417 step('garbage collection is recorded', async
function () {
418 await db
.context(async (dbCtx
) => {
419 const result
= await db
.almanacGetAll(dbCtx
);
420 assert(result
?.length
);
425 describe('Ticket Token Tracking', function () {
427 beforeEach(function () {
429 subject: 'https://entity.example.com/',
430 resource: 'https://blog.example.com/secret_entry',
431 iss: 'https://idp.example.com/',
432 ticket: 'xxxTICKETxxx',
433 token: 'xxxTOKENxxx',
436 step('stores redeemed ticket data', async
function () {
437 await db
.context(async (dbCtx
) => {
438 await db
.ticketRedeemed(dbCtx
, redeemedData
);
441 step('gets one pending-publish ticket tokens', async
function () {
442 await db
.context(async (dbCtx
) => {
443 const unpublished
= await db
.ticketTokenGetUnpublished(dbCtx
);
444 assert
.strictEqual(unpublished
.length
, 1);
445 const record
= unpublished
[0];
446 assert(record
.created
);
447 assert(!record
.published
);
448 assert(record
.ticketId
);
449 delete record
.created
;
450 delete record
.published
;
451 delete record
.ticketId
;
452 assert
.deepStrictEqual(record
, redeemedData
);
455 step('stores published ticket token data', async
function () {
456 await db
.context(async (dbCtx
) => {
457 await db
.ticketTokenPublished(dbCtx
, redeemedData
);
460 step('gets no pending-publish ticket tokens', async
function () {
461 await db
.context(async (dbCtx
) => {
462 const unpublished
= await db
.ticketTokenGetUnpublished(dbCtx
);
463 assert
.strictEqual(unpublished
.length
, 0);
466 }); // Ticket Token Tracking
468 describe('Bookkeeping', function () {
470 beforeEach(function () {
471 event
= 'integrationTestEvent';
472 date
= new Date('Fri Dec 22 03:27 UTC 2023');
474 step('inserts event', async
function () {
475 await db
.context(async (dbCtx
) => {
476 await db
.almanacUpsert(dbCtx
, event
, date
);
477 const result
= await db
.almanacGetAll(dbCtx
);
478 const [storedEvent
] = result
.filter((e
) => e
.event
=== event
);
479 assert
.deepStrictEqual(storedEvent
.date
, date
);
484 describe('Refreshable Token', function () {
485 let created
, codeId
, scopes
, clientId
, profileData
, lifespanSeconds
, refreshLifespanSeconds
, removeScopes
;
486 beforeEach(function () {
487 created
= new Date();
488 codeId
= '20ff1c5e-24d9-11ed-83b9-0025905f714a';
489 scopes
= ['profile', 'email', 'create', 'fancy:scope'];
490 clientId
= 'https://app.example.com/';
491 lifespanSeconds
= 86400;
492 refreshLifespanSeconds
= 172800;
494 url: 'https://profile.example.com/',
495 name: 'Namey McUser',
496 photo: 'https://profile.example.com/picture.jpg',
497 email: 'usey@example.com',
501 step('redeems code for refreshable token', async
function () {
502 await db
.context(async (dbCtx
) => {
503 const result
= await db
.redeemCode(dbCtx
, {
512 refreshLifespanSeconds
,
515 assert
.strictEqual(result
, true);
516 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
518 db
._validateToken(t
);
519 const requestedScopesSet
= new Set(scopes
);
520 const tokenScopesSet
= new Set(t
.scopes
);
521 for (const s
of tokenScopesSet
) {
522 if (requestedScopesSet
.has(s
)) {
523 requestedScopesSet
.delete(s
);
525 requestedScopesSet
.add(s
);
528 assert(!requestedScopesSet
.size
, [...requestedScopesSet
].toString());
531 step('refreshes token', async
function () {
532 await db
.context(async (dbCtx
) => {
533 const result
= await db
.refreshCode(dbCtx
, codeId
, new Date(), removeScopes
);
535 assert(result
.expires
);
536 assert(result
.refreshExpires
);
537 assert(!result
.scopes
);
540 step('refreshes token and reduces scope', async
function () {
541 await db
.context(async (dbCtx
) => {
542 removeScopes
= ['create', 'fancy:scope'];
543 const result
= await db
.refreshCode(dbCtx
, codeId
, new Date(), removeScopes
);
545 assert(result
.scopes
);
546 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
547 const remainingScopesSet
= new Set(scopes
);
548 removeScopes
.forEach((s
) => remainingScopesSet
.delete(s
));
549 const tokenScopesSet
= new Set(t
.scopes
);
550 for (const s
of tokenScopesSet
) {
551 if (remainingScopesSet
.has(s
)) {
552 remainingScopesSet
.delete(s
);
554 remainingScopesSet
.add(s
);
557 assert(!remainingScopesSet
.size
, [...remainingScopesSet
].toString());
561 step('revokes token refreshability', async
function () {
562 await db
.context(async (dbCtx
) => {
563 await db
.tokenRefreshRevokeByCodeId(dbCtx
, codeId
);
564 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
565 assert(!t
.refreshExpires
);
568 step('token not refreshable', async
function () {
569 await db
.context(async (dbCtx
) => {
570 const result
= await db
.refreshCode(dbCtx
, codeId
, new Date(), removeScopes
);
574 }); // Refreshable Token
576 }); // specific implementation
579 }); // Database Integration