7634b30c748a05726332457baf098e9cc7749bb6
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 () {
141 beforeEach(function () {
142 credential
= '$plain$myPassword';
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
);
174 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
175 assert
.strictEqual(authInfo
.credential
, credential
);
178 step('profile is not valid', async
function () {
179 await db
.context(async (dbCtx
) => {
180 const isValid
= await db
.profileIsValid(dbCtx
, profile
);
181 assert
.strictEqual(isValid
, false);
184 step('user-profile relation does not exist', async
function () {
185 await db
.context(async (dbCtx
) => {
186 const { profiles
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
187 const exists
= profiles
.includes(profile
);
188 assert
.strictEqual(exists
, false);
191 step('create user-profile relation', async
function () {
192 await db
.context(async (dbCtx
) => {
193 await db
.profileIdentifierInsert(dbCtx
, profile
, identifier
);
196 step('profile is valid', async
function () {
197 await db
.context(async (dbCtx
) => {
198 const isValid
= await db
.profileIsValid(dbCtx
, profile
);
199 assert
.strictEqual(isValid
, true);
202 step('user-profile relation does exist', async
function () {
203 await db
.context(async (dbCtx
) => {
204 const { profiles
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
205 const exists
= profiles
.includes(profile
);
206 assert
.strictEqual(exists
, true);
209 step('create scope', async
function () {
210 await db
.context(async (dbCtx
) => {
211 await db
.scopeUpsert(dbCtx
, 'new_scope', '', 'Allows something to happen.');
214 step('create and delete scope', async
function () {
215 await db
.context(async (dbCtx
) => {
216 await db
.scopeUpsert(dbCtx
, 'sacrificial', 'No App', 'Exists to be destroyed.', true);
217 const result
= await db
.scopeDelete(dbCtx
, 'sacrificial');
218 assert
.strictEqual(result
, true);
221 step('do not delete in-use scope', async
function () {
222 await db
.context(async (dbCtx
) => {
223 const result
= await db
.scopeDelete(dbCtx
, 'profile');
224 assert
.strictEqual(result
, false);
227 step('ignore delete of non-existent scope', async
function () {
228 await db
.context(async (dbCtx
) => {
229 await db
.scopeDelete(dbCtx
, 'non-existent');
232 step('assign scope to profile', async
function () {
233 const scope
= 'new_scope';
234 await db
.context(async (dbCtx
) => {
235 await db
.profileScopeInsert(dbCtx
, profile
, scope
);
236 const { scopeIndex
, profileScopes
, profiles
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
237 const scopeExistsInProfile
= scope
in profileScopes
[profile
];
238 const profileExistsInScope
= scopeIndex
[scope
].profiles
.includes(profile
);
239 const profileExists
= profiles
.includes(profile
);
240 assert
.strictEqual(scopeExistsInProfile
, true);
241 assert
.strictEqual(profileExistsInScope
, true);
242 assert
.strictEqual(profileExists
, true);
245 step('update scope', async
function () {
246 await db
.context(async (dbCtx
) => {
247 await db
.scopeUpsert(dbCtx
, 'new_scope', 'Application', 'Updated description.');
250 step('re-assigning scope to profile is ignored', async
function () {
251 const scope
= 'new_scope';
252 await db
.context(async (dbCtx
) => {
253 await db
.profileScopeInsert(dbCtx
, profile
, scope
);
254 const { scopeIndex
, profileScopes
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
255 const scopeExistsInProfile
= scope
in profileScopes
[profile
];
256 const profileExistsInScope
= scopeIndex
[scope
].profiles
.includes(profile
);
257 assert
.strictEqual(scopeExistsInProfile
, true);
258 assert
.strictEqual(profileExistsInScope
, true);
261 step('clear all scopes for a profile', async
function () {
263 await db
.context(async (dbCtx
) => {
264 await db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
265 const { profileScopes
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
266 const exists
= profile
in profileScopes
;
268 const numScopes
= Object
.keys(profileScopes
[profile
]).length
;
269 assert
.strictEqual(numScopes
, 0);
272 step('set multiple scopes for a profile', async
function () {
273 const scopes
= ['profile', 'email', 'create'];
274 await db
.context(async (dbCtx
) => {
275 await db
.profileScopesSetAll(dbCtx
, profile
, scopes
);
276 const { profileScopes
} = await db
.profilesScopesByIdentifier(dbCtx
, identifier
);
277 assert
.strictEqual(Object
.keys(profileScopes
[profile
]).length
, scopes
.length
);
280 step('garbage-collect client scopes', async
function () {
281 await db
.context(async (dbCtx
) => {
282 await db
.scopeUpsert(dbCtx
, 'extra_scope', 'useless', 'useless');
283 const result
= await db
.scopeCleanup(dbCtx
, 0);
287 step('too-soon garbage-collect skips', async
function () {
288 await db
.context(async (dbCtx
) => {
289 const result
= await db
.scopeCleanup(dbCtx
, 86400000);
290 assert
.strictEqual(result
, undefined);
293 }); // Users and Profiles and Scopes
295 describe('Token', function () {
296 let created
, codeId
, profileCodeId
, ticketCodeId
, scopes
, clientId
, lifespanSeconds
, resource
;
297 beforeEach(function () {
298 created
= new Date();
299 codeId
= '907a95fc-384b-11ec-a541-0025905f714a';
300 profileCodeId
= '93d6314a-384e-11ec-94e4-0025905f714a';
301 ticketCodeId
= 'bc5c39a8-5ca0-11ed-94cd-0025905f714a';
302 clientId
= 'https://app.example.com/';
303 scopes
= ['create', 'email', 'profile'];
304 lifespanSeconds
= 600;
305 resource
= 'https://example.com/profile/feed';
307 step('redeems code for token', async
function () {
308 await db
.context(async (dbCtx
) => {
309 lifespanSeconds
= null;
310 const result
= await db
.redeemCode(dbCtx
, {
319 refreshLifespanSeconds: null,
322 assert
.strictEqual(result
, true);
323 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
325 db
._validateToken(t
);
328 step('revokes token', async
function () {
329 await db
.context(async (dbCtx
) => {
330 await db
.tokenRevokeByCodeId(dbCtx
, codeId
, identifier
);
331 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
332 assert
.strictEqual(t
.isRevoked
, true);
335 step('redeems code for profile', async
function () {
336 await db
.context(async (dbCtx
) => {
337 await db
.redeemCode(dbCtx
, {
339 codeId: profileCodeId
,
347 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
349 db
._validateToken(t
);
352 step('redeems ticket', async
function () {
353 await db
.context(async (dbCtx
) => {
354 await db
.redeemCode(dbCtx
, {
356 codeId: ticketCodeId
,
366 step('gets tokens', async
function () {
367 await db
.context(async (dbCtx
) => {
368 const tokens
= await db
.tokensGetByIdentifier(dbCtx
, identifier
);
369 assert(tokens
.length
);
372 step('revokes multiply-redeemed code', async
function () {
373 await db
.context(async (dbCtx
) => {
374 const result
= await db
.redeemCode(dbCtx
, {
383 assert
.strictEqual(result
, false);
384 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
385 assert
.strictEqual(t
.isRevoked
, true);
388 step('garbage-collect tokens', async
function () {
389 await db
.context(async (dbCtx
) => {
390 const result
= await db
.tokenCleanup(dbCtx
, -86400, 0);
394 step('too-soon garbage-collect skips', async
function () {
395 await db
.context(async (dbCtx
) => {
396 const result
= await db
.tokenCleanup(dbCtx
, 0, 86400000);
397 assert
.strictEqual(result
, undefined);
400 step('garbage collection is recorded', async
function () {
401 await db
.context(async (dbCtx
) => {
402 const result
= await db
.almanacGetAll(dbCtx
);
403 assert(result
?.length
);
408 describe('Refreshable Token', function () {
409 let created
, codeId
, scopes
, clientId
, profileData
, lifespanSeconds
, refreshLifespanSeconds
, removeScopes
;
410 beforeEach(function () {
411 created
= new Date();
412 codeId
= '20ff1c5e-24d9-11ed-83b9-0025905f714a';
413 scopes
= ['profile', 'email', 'create', 'fancy:scope'];
414 clientId
= 'https://app.example.com/';
415 lifespanSeconds
= 86400;
416 refreshLifespanSeconds
= 172800;
418 url: 'https://profile.example.com/',
419 name: 'Namey McUser',
420 photo: 'https://profile.example.com/picture.jpg',
421 email: 'usey@example.com',
425 step('redeems code for refreshable token', async
function () {
426 await db
.context(async (dbCtx
) => {
427 const result
= await db
.redeemCode(dbCtx
, {
436 refreshLifespanSeconds
,
439 assert
.strictEqual(result
, true);
440 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
442 db
._validateToken(t
);
443 const requestedScopesSet
= new Set(scopes
);
444 const tokenScopesSet
= new Set(t
.scopes
);
445 for (const s
of tokenScopesSet
) {
446 if (requestedScopesSet
.has(s
)) {
447 requestedScopesSet
.delete(s
);
449 requestedScopesSet
.add(s
);
452 assert(!requestedScopesSet
.size
, [...requestedScopesSet
].toString());
455 step('refreshes token', async
function () {
456 await db
.context(async (dbCtx
) => {
457 const result
= await db
.refreshCode(dbCtx
, codeId
, new Date(), removeScopes
);
459 assert(result
.expires
);
460 assert(result
.refreshExpires
);
461 assert(!result
.scopes
);
464 step('refreshes token and reduces scope', async
function () {
465 await db
.context(async (dbCtx
) => {
466 removeScopes
= ['create', 'fancy:scope'];
467 const result
= await db
.refreshCode(dbCtx
, codeId
, new Date(), removeScopes
);
469 assert(result
.scopes
);
470 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
471 const remainingScopesSet
= new Set(scopes
);
472 removeScopes
.forEach((s
) => remainingScopesSet
.delete(s
));
473 const tokenScopesSet
= new Set(t
.scopes
);
474 for (const s
of tokenScopesSet
) {
475 if (remainingScopesSet
.has(s
)) {
476 remainingScopesSet
.delete(s
);
478 remainingScopesSet
.add(s
);
481 assert(!remainingScopesSet
.size
, [...remainingScopesSet
].toString());
485 step('revokes token refreshability', async
function () {
486 await db
.context(async (dbCtx
) => {
487 await db
.tokenRefreshRevokeByCodeId(dbCtx
, codeId
);
488 const t
= await db
.tokenGetByCodeId(dbCtx
, codeId
);
489 assert(!t
.refreshExpires
);
492 step('token not refreshable', async
function () {
493 await db
.context(async (dbCtx
) => {
494 const result
= await db
.refreshCode(dbCtx
, codeId
, new Date(), removeScopes
);
498 }); // Refreshable Token
500 }); // specific implementation
503 }); // Database Integration