c26e277878f344573481c8ae0b1196bc43b51a7a
1 /* eslint-disable security/detect-object-injection */
5 * These are LIVE FIRE tests to exercise actual database operations.
6 * They should be configured to use local test databases, as they
7 * perform DESTRUCTIVE ACTIONS on all tables, beginning with a COMPLETE
10 * They will only run if all the appropriate environmental settings exist:
11 * - INTEGRATION_TESTS must be set
12 * - <ENGINE>_TEST_PATH must point to the endpoint/db
14 * These tests are sequential, relying on the state created along the way.
18 const assert
= require('assert');
19 const { step
} = require('mocha-steps');
20 const StubLogger
= require('../../stub-logger');
21 // const DBErrors = require('../../../src/db/errors');
22 // const testData = require('../../test-data/db-integration');
24 describe('Database Integration', function () {
25 const implementations
= [];
27 if (!process
.env
.INTEGRATION_TESTS
) {
28 it
.skip('integration tests not requested');
32 if (process
.env
.POSTGRES_TEST_PATH
) {
33 implementations
.push({
35 module: '../../../src/db/postgres',
38 connectionString: `postgresql://${process.env.POSTGRES_TEST_PATH}`,
39 queryLogLevel: 'debug',
46 if (process
.env
.SQLITE_TEST_PATH
) {
47 implementations
.push({
49 module: '../../../src/db/sqlite',
52 connectionString: `sqlite://${process.env.SQLITE_TEST_PATH}`,
53 queryLogLevel: 'debug',
54 sqliteOptimizeAfterChanges: 10,
60 if (!implementations
.length
) {
61 it('have some implementations to test', function () {
62 assert
.fail('No implementations have been configured for requested integration tests');
66 implementations
.forEach(function (i
) {
67 describe(i
.name
, function () {
70 let profile
, identifier
;
72 before(async
function () {
73 this.timeout(10 * 1000); // Allow some time for creating tables et cetera.
74 logger
= new StubLogger();
76 // eslint-disable-next-line security/detect-non-literal-require
77 DB
= require(i
.module
);
78 db
= new DB(logger
, i
.config
);
79 await db
.initialize();
80 await db
._purgeTables(true);
82 after(async
function () {
83 await db
._closeConnection();
86 beforeEach(function () {
87 identifier
= 'username';
88 profile
= 'https://example.com/profile';
91 describe('Healthcheck', function () {
92 it('should succeed', async
function () {
93 const result
= await db
.healthCheck();
98 describe('Resources', function () {
99 let resourceId
, secret
, description
;
101 secret
= 'shared secret';
102 description
= 'A resource server that needs to verify our tokens.';
104 step('returns nothing when resource does not exist', async
function () {
105 await db
.context(async (dbCtx
) => {
106 const badResourceId
= 'f1669969-c87e-46f8-83bb-a6712981d15d';
107 const result
= await db
.resourceGet(dbCtx
, badResourceId
);
111 step('creates resource', async
function () {
112 await db
.context(async (dbCtx
) => {
113 const result
= await db
.resourceUpsert(dbCtx
, undefined, secret
, description
);
114 assert(result
.resourceId
);
115 resourceId
= result
.resourceId
;
118 step('gets resource', async
function () {
119 await db
.context(async (dbCtx
) => {
120 const result
= await db
.resourceGet(dbCtx
, resourceId
);
121 assert
.strictEqual(result
.secret
, secret
);
122 db
._validateResource(result
);
125 step('updates resource', async
function () {
126 await db
.context(async (dbCtx
) => {
127 secret
= 'new shared secret';
128 description
= 'Still a resource server, but with a new description.';
129 await db
.resourceUpsert(dbCtx
, resourceId
, secret
, description
);
130 const result
= await db
.resourceGet(dbCtx
, resourceId
);
131 assert
.strictEqual(result
.resourceId
, resourceId
);
132 assert
.strictEqual(result
.secret
, secret
);
133 assert
.strictEqual(result
.description
, description
);
138 describe('Users and Profiles and Scopes', function () {
139 let credential
, otpKey
;
140 beforeEach(function () {
141 credential
= '$plain$myPassword';
142 otpKey
= '1234567890123456789012';
144 step('returns nothing when auth does not exist', async
function () {
145 await db
.context(async (dbCtx
) => {
146 const result
= await db
.authenticationGet(dbCtx
, identifier
);
150 step('create auth entry', async
function () {
151 await db
.context(async (dbCtx
) => {
152 await db
.authenticationUpsert(dbCtx
, identifier
, credential
);
155 step('get auth entry', async
function () {
156 await db
.context(async (dbCtx
) => {
157 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
158 assert
.strictEqual(authInfo
.credential
, credential
);
159 db
._validateAuthentication(authInfo
);
162 step('valid auth event', async
function () {
163 await db
.context(async (dbCtx
) => {
164 await db
.authenticationSuccess(dbCtx
, identifier
);
165 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
166 db
._validateAuthentication(authInfo
);
167 assert
.notStrictEqual(authInfo
.lastAuthentication
, undefined);
170 step('update auth entry', async
function () {
171 await db
.context(async (dbCtx
) => {
172 credential
= '$plain$myNewPassword';
173 await db
.authenticationUpsert(dbCtx
, identifier
, credential
, otpKey
);
174 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
175 assert
.strictEqual(authInfo
.credential
, credential
);
176 assert
.strictEqual(authInfo
.otpKey
, otpKey
);
179 step('update auth credential', async
function () {
180 await db
.context(async (dbCtx
) => {
181 credential
= '$plain$anotherNewPassword';
182 await db
.authenticationUpdateCredential(dbCtx
, identifier
, credential
);
183 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
184 assert
.strictEqual(authInfo
.credential
, credential
);
187 step('update auth otp', async
function () {
188 await db
.context(async (dbCtx
) => {
189 await db
.authenticationUpdateOTPKey(dbCtx
, identifier
, otpKey
);
190 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
191 assert
.strictEqual(authInfo
.otpKey
, otpKey
);
194 step('profile is not valid', async
function () {
195 await db
.context(async (dbCtx
) => {
196 const isValid
= await db
.profileIsValid(dbCtx
, profile
);
197 assert
.strictEqual(isValid
, false);
200 step('user-profile relation does not exist', async
function () {
201 await db
.context(async (dbCtx
) => {
202 const { profiles
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
203 const exists
= profiles
.includes(profile
);
204 assert
.strictEqual(exists
, false);
207 step('create user-profile relation', async
function () {
208 await db
.context(async (dbCtx
) => {
209 await db
.profileIdentifierInsert(dbCtx
, profile
, identifier
);
212 step('profile is valid', async
function () {
213 await db
.context(async (dbCtx
) => {
214 const isValid
= await db
.profileIsValid(dbCtx
, profile
);
215 assert
.strictEqual(isValid
, true);
218 step('user-profile relation does exist', async
function () {
219 await db
.context(async (dbCtx
) => {
220 const { profiles
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
221 const exists
= profiles
.includes(profile
);
222 assert
.strictEqual(exists
, true);
225 step('create scope', async
function () {
226 await db
.context(async (dbCtx
) => {
227 await db
.scopeUpsert(dbCtx
, 'new_scope', '', 'Allows something to happen.');
230 step('create and delete scope', async
function () {
231 await db
.context(async (dbCtx
) => {
232 await db
.scopeUpsert(dbCtx
, 'sacrificial', 'No App', 'Exists to be destroyed.', true);
233 const result
= await db
.scopeDelete(dbCtx
, 'sacrificial');
234 assert
.strictEqual(result
, true);
237 step('do not delete in-use scope', async
function () {
238 await db
.context(async (dbCtx
) => {
239 const result
= await db
.scopeDelete(dbCtx
, 'profile');
240 assert
.strictEqual(result
, false);
243 step('ignore delete of non-existent scope', async
function () {
244 await db
.context(async (dbCtx
) => {
245 await db
.scopeDelete(dbCtx
, 'non-existent');
248 step('assign scope to profile', async
function () {
249 const scope
= 'new_scope';
250 await db
.context(async (dbCtx
) => {
251 await db
.profileScopeInsert(dbCtx
, profile
, scope
);
252 const { scopeIndex
, profileScopes
, profiles
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
253 const scopeExistsInProfile
= scope
in profileScopes
[profile
];
254 const profileExistsInScope
= scopeIndex
[scope
].profiles
.includes(profile
);
255 const profileExists
= profiles
.includes(profile
);
256 assert
.strictEqual(scopeExistsInProfile
, true);
257 assert
.strictEqual(profileExistsInScope
, true);
258 assert
.strictEqual(profileExists
, true);
261 step('update scope', async
function () {
262 await db
.context(async (dbCtx
) => {
263 await db
.scopeUpsert(dbCtx
, 'new_scope', 'Application', 'Updated description.');
266 step('re-assigning scope to profile is ignored', async
function () {
267 const scope
= 'new_scope';
268 await db
.context(async (dbCtx
) => {
269 await db
.profileScopeInsert(dbCtx
, profile
, scope
);
270 const { scopeIndex
, profileScopes
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
271 const scopeExistsInProfile
= scope
in profileScopes
[profile
];
272 const profileExistsInScope
= scopeIndex
[scope
].profiles
.includes(profile
);
273 assert
.strictEqual(scopeExistsInProfile
, true);
274 assert
.strictEqual(profileExistsInScope
, true);
277 step('clear all scopes for a profile', async
function () {
279 await db
.context(async (dbCtx
) => {
280 await db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
281 const { profileScopes
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
282 const exists
= profile
in profileScopes
;
284 const numScopes
= Object
.keys(profileScopes
[profile
]).length
;
285 assert
.strictEqual(numScopes
, 0);
288 step('set multiple scopes for a profile', async
function () {
289 const scopes
= ['profile', 'email', 'create'];
290 await db
.context(async (dbCtx
) => {
291 await db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
292 const { profileScopes
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
293 assert
.strictEqual(Object
.keys(profileScopes
[profile
]).length
, scopes
.length
);
296 step('garbage-collect client scopes', async
function () {
297 await db
.context(async (dbCtx
) => {
298 await db
.scopeUpsert(dbCtx
, 'extra_scope', 'useless', 'useless');
299 const result
= await db
.scopeCleanup(dbCtx
, 0);
303 step('too-soon garbage-collect skips', async
function () {
304 await db
.context(async (dbCtx
) => {
305 const result
= await db
.scopeCleanup(dbCtx
, 86400000);
306 assert
.strictEqual(result
, undefined);
309 }); // Users and Profiles and Scopes
311 describe('Token', function () {
312 let created
, codeId
, profileCodeId
, ticketCodeId
, scopes
, clientId
, lifespanSeconds
, resource
;
313 beforeEach(function () {
314 created
= new Date();
315 codeId
= '907a95fc-384b-11ec-a541-0025905f714a';
316 profileCodeId
= '93d6314a-384e-11ec-94e4-0025905f714a';
317 ticketCodeId
= 'bc5c39a8-5ca0-11ed-94cd-0025905f714a';
318 clientId
= 'https://app.example.com/';
319 scopes
= ['create', 'email', 'profile'];
320 lifespanSeconds
= 600;
321 resource
= 'https://example.com/profile/feed';
323 step('redeems code for token', async
function () {
324 await db
.context(async (dbCtx
) => {
325 lifespanSeconds
= null;
326 const result
= await db
.redeemCode(dbCtx
, {
335 refreshLifespanSeconds: null,
338 assert
.strictEqual(result
, true);
339 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
341 db
._validateToken(t
);
344 step('revokes token', async
function () {
345 await db
.context(async (dbCtx
) => {
346 await db
.tokenRevokeByCodeId(dbCtx
, codeId
, identifier
);
347 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
348 assert
.strictEqual(t
.isRevoked
, true);
351 step('redeems code for profile', async
function () {
352 await db
.context(async (dbCtx
) => {
353 await db
.redeemCode(dbCtx
, {
355 codeId: profileCodeId
,
363 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
365 db
._validateToken(t
);
368 step('redeems ticket', async
function () {
369 await db
.context(async (dbCtx
) => {
370 await db
.redeemCode(dbCtx
, {
372 codeId: ticketCodeId
,
382 step('gets tokens', async
function () {
383 await db
.context(async (dbCtx
) => {
384 const tokens
= await db
.tokensGetByIdentifier(dbCtx
, identifier
);
385 assert(tokens
.length
);
388 step('revokes multiply-redeemed code', async
function () {
389 await db
.context(async (dbCtx
) => {
390 const result
= await db
.redeemCode(dbCtx
, {
399 assert
.strictEqual(result
, false);
400 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
401 assert
.strictEqual(t
.isRevoked
, true);
404 step('garbage-collect tokens', async
function () {
405 await db
.context(async (dbCtx
) => {
406 const result
= await db
.tokenCleanup(dbCtx
, -86400, 0);
410 step('too-soon garbage-collect skips', async
function () {
411 await db
.context(async (dbCtx
) => {
412 const result
= await db
.tokenCleanup(dbCtx
, 0, 86400000);
413 assert
.strictEqual(result
, undefined);
416 step('garbage collection is recorded', async
function () {
417 await db
.context(async (dbCtx
) => {
418 const result
= await db
.almanacGetAll(dbCtx
);
419 assert(result
?.length
);
424 describe('Ticket Token Tracking', function () {
426 beforeEach(function () {
428 subject: 'https://entity.example.com/',
429 resource: 'https://blog.example.com/secret_entry',
430 iss: 'https://idp.example.com/',
431 ticket: 'xxxTICKETxxx',
432 token: 'xxxTOKENxxx',
435 step('stores redeemed ticket data', async
function () {
436 await db
.context(async (dbCtx
) => {
437 await db
.ticketRedeemed(dbCtx
, redeemedData
);
440 step('gets one pending-publish ticket tokens', async
function () {
441 await db
.context(async (dbCtx
) => {
442 const unpublished
= await db
.ticketTokenGetUnpublished(dbCtx
);
443 assert
.strictEqual(unpublished
.length
, 1);
444 const record
= unpublished
[0];
445 assert(record
.created
);
446 assert(!record
.published
);
447 assert(record
.ticketId
);
448 delete record
.created
;
449 delete record
.published
;
450 delete record
.ticketId
;
451 assert
.deepStrictEqual(record
, redeemedData
);
454 step('stores published ticket token data', async
function () {
455 await db
.context(async (dbCtx
) => {
456 await db
.ticketTokenPublished(dbCtx
, redeemedData
);
459 step('gets no pending-publish ticket tokens', async
function () {
460 await db
.context(async (dbCtx
) => {
461 const unpublished
= await db
.ticketTokenGetUnpublished(dbCtx
);
462 assert
.strictEqual(unpublished
.length
, 0);
465 }); // Ticket Token Tracking
467 describe('Bookkeeping', function () {
469 beforeEach(function () {
470 event
= 'integrationTestEvent';
471 date
= new Date('Fri Dec 22 03:27 UTC 2023');
473 step('inserts event', async
function () {
474 await db
.context(async (dbCtx
) => {
475 await db
.almanacUpsert(dbCtx
, event
, date
);
476 const result
= await db
.almanacGetAll(dbCtx
);
477 const [storedEvent
] = result
.filter((e
) => e
.event
=== event
);
478 assert
.deepStrictEqual(storedEvent
.date
, date
);
483 describe('Refreshable Token', function () {
484 let created
, codeId
, scopes
, clientId
, profileData
, lifespanSeconds
, refreshLifespanSeconds
, removeScopes
;
485 beforeEach(function () {
486 created
= new Date();
487 codeId
= '20ff1c5e-24d9-11ed-83b9-0025905f714a';
488 scopes
= ['profile', 'email', 'create', 'fancy:scope'];
489 clientId
= 'https://app.example.com/';
490 lifespanSeconds
= 86400;
491 refreshLifespanSeconds
= 172800;
493 url: 'https://profile.example.com/',
494 name: 'Namey McUser',
495 photo: 'https://profile.example.com/picture.jpg',
496 email: 'usey@example.com',
500 step('redeems code for refreshable token', async
function () {
501 await db
.context(async (dbCtx
) => {
502 const result
= await db
.redeemCode(dbCtx
, {
511 refreshLifespanSeconds
,
514 assert
.strictEqual(result
, true);
515 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
517 db
._validateToken(t
);
518 const requestedScopesSet
= new Set(scopes
);
519 const tokenScopesSet
= new Set(t
.scopes
);
520 for (const s
of tokenScopesSet
) {
521 if (requestedScopesSet
.has(s
)) {
522 requestedScopesSet
.delete(s
);
524 requestedScopesSet
.add(s
);
527 assert(!requestedScopesSet
.size
, [...requestedScopesSet
].toString());
530 step('refreshes token', async
function () {
531 await db
.context(async (dbCtx
) => {
532 const result
= await db
.refreshCode(dbCtx
, codeId
, new Date(), removeScopes
);
534 assert(result
.expires
);
535 assert(result
.refreshExpires
);
536 assert(!result
.scopes
);
539 step('refreshes token and reduces scope', async
function () {
540 await db
.context(async (dbCtx
) => {
541 removeScopes
= ['create', 'fancy:scope'];
542 const result
= await db
.refreshCode(dbCtx
, codeId
, new Date(), removeScopes
);
544 assert(result
.scopes
);
545 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
546 const remainingScopesSet
= new Set(scopes
);
547 removeScopes
.forEach((s
) => remainingScopesSet
.delete(s
));
548 const tokenScopesSet
= new Set(t
.scopes
);
549 for (const s
of tokenScopesSet
) {
550 if (remainingScopesSet
.has(s
)) {
551 remainingScopesSet
.delete(s
);
553 remainingScopesSet
.add(s
);
556 assert(!remainingScopesSet
.size
, [...remainingScopesSet
].toString());
560 step('revokes token refreshability', async
function () {
561 await db
.context(async (dbCtx
) => {
562 await db
.tokenRefreshRevokeByCodeId(dbCtx
, codeId
);
563 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
564 assert(!t
.refreshExpires
);
567 step('token not refreshable', async
function () {
568 await db
.context(async (dbCtx
) => {
569 const result
= await db
.refreshCode(dbCtx
, codeId
, new Date(), removeScopes
);
573 }); // Refreshable Token
575 }); // specific implementation
578 }); // Database Integration