4 * These are LIVE FIRE tests to exercise actual database operations.
5 * They should be configured to use local test databases, as they
6 * perform DESTRUCTIVE ACTIONS on all tables, beginning with a COMPLETE
9 * They will only run if all the appropriate environmental settings exist:
10 * - INTEGRATION_TESTS must be set
11 * - <ENGINE>_TEST_PATH must point to the endpoint/db
13 * These tests are sequential, relying on the state created along the way.
17 const assert
= require('node:assert');
18 const { step
} = require('mocha-steps');
19 const stubLogger
= require('../../stub-logger');
20 const DBErrors
= require('../../../src/db/errors');
21 const testData
= require('../../test-data/db-integration');
23 describe('Database Integration', function () {
24 const noExpectedException
= 'did not receive expected exception';
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',
59 implementations
.forEach(function (i
) {
60 describe(i
.name
, function () {
62 let topicId
, subscriptionId
, verificationId
;
63 const claimant
= '96bff010-d9e6-11eb-b95d-0025905f714a';
65 before(async
function () {
66 this.timeout(10 * 1000); // Allow some time for creating tables et cetera.
67 // eslint-disable-next-line security/detect-non-literal-require
68 DB
= require(i
.module
);
69 db
= new DB(stubLogger
, i
.config
);
70 await db
.initialize();
71 await db
._purgeTables(true);
73 after(async
function () {
74 await db
._closeConnection();
76 it('instantiated', function () {
80 it('is healthy', async
function () {
81 const result
= await db
.healthCheck();
85 describe('Authentication', function () {
86 let identifier
, credential
, otpKey
;
87 beforeEach(function () {
88 identifier
= 'username';
89 credential
= 'myEncryptedPassword';
90 otpKey
= '1234567890123456789012';
92 step('create auth entry', async
function () {
93 await db
.context(async (dbCtx
) => {
94 await db
.authenticationUpsert(dbCtx
, identifier
, credential
);
97 step('get auth entry', async
function () {
98 await db
.context(async (dbCtx
) => {
99 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
100 assert
.strictEqual(authInfo
.credential
, credential
);
103 step('valid auth event', async
function () {
104 await db
.context(async (dbCtx
) => {
105 await db
.authenticationSuccess(dbCtx
, identifier
);
106 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
107 assert
.notStrictEqual(authInfo
.lastAuthentication
, undefined);
110 step('update auth entry', async
function () {
111 await db
.context(async (dbCtx
) => {
112 credential
= 'myNewPassword';
113 await db
.authenticationUpsert(dbCtx
, identifier
, credential
, otpKey
);
114 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
115 assert
.strictEqual(authInfo
.credential
, credential
);
116 assert
.strictEqual(authInfo
.otpKey
, otpKey
);
119 step('update auth otp key', async
function () {
120 await db
.context(async (dbCtx
) => {
121 const removedOTPKey
= null;
122 await db
.authenticationUpdateOTPKey(dbCtx
, identifier
, removedOTPKey
);
123 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
124 assert
.strictEqual(authInfo
.otpKey
, removedOTPKey
);
127 step('update credential', async
function () {
128 await db
.context(async (dbCtx
) => {
129 credential
= '$plain$anotherCredential';
130 await db
.authenticationUpdateCredential(dbCtx
, identifier
, credential
);
131 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
132 assert
.strictEqual(authInfo
.credential
, credential
);
135 }); // Authentication
137 describe('Topic', function () {
139 step('requires data', async
function () {
141 await db
.context(async (dbCtx
) => {
142 await db
.topicSet(dbCtx
);
144 assert
.fail(noExpectedException
);
146 assert(e
instanceof DBErrors
.DataValidation
);
149 step('creates topic', async
function () {
150 await db
.context(async (dbCtx
) => {
151 const result
= await db
.topicSet(dbCtx
, testData
.topicSet
);
152 topicId
= result
.lastInsertRowid
;
153 assert
.strictEqual(result
.changes
, 1);
156 step('gets topic by url', async
function () {
157 await db
.context(async (dbCtx
) => {
158 const topic
= await db
.topicGetByUrl(dbCtx
, testData
.topicSet
.url
);
159 assert
.strictEqual(topic
.url
, testData
.topicSet
.url
);
162 step('updates topic', async
function () {
163 await db
.context(async(dbCtx
) => {
164 const result
= await db
.topicSet(dbCtx
, testData
.topicUpdate
);
165 assert
.strictEqual(result
.changes
, 1);
168 step('also updates topic', async
function () {
173 await db
.context(async(dbCtx
) => {
174 const expected
= await db
.topicGetByUrl(dbCtx
, testData
.topicSet
.url
, true);
175 expected
.leaseSecondsMin
= data
.leaseSecondsMin
;
176 let topic
= await db
.topicGetByUrl(dbCtx
, testData
.topicSet
.url
, false);
177 await db
.topicUpdate(dbCtx
, { ...topic
, ...data
});
178 topic
= await db
.topicGetByUrl(dbCtx
, testData
.topicSet
.url
);
179 assert
.strictEqual(Number(topic
.leaseSecondsMin
), data
.leaseSecondsMin
);
180 assert
.deepEqual(topic
, expected
);
183 step('gets topic by id', async
function () {
184 await db
.context(async (dbCtx
) => {
185 const topic
= await db
.topicGetById(dbCtx
, topicId
);
186 assert
.strictEqual(topic
.url
, testData
.topicSet
.url
);
187 assert
.strictEqual(Number(topic
.leaseSecondsPreferred
), testData
.topicUpdate
.leaseSecondsPreferred
);
190 step('sets topic content', async
function () {
192 ...testData
.topicSetContent
,
195 await db
.context(async (dbCtx
) => {
196 const result
= await db
.topicSetContent(dbCtx
, data
);
197 assert
.strictEqual(result
.changes
, 1);
200 step('gets topic content', async
function () {
201 await db
.context(async (dbCtx
) => {
202 const topic
= await db
.topicGetContentById(dbCtx
, topicId
);
203 assert
.strictEqual(topic
.contentHash
, testData
.topicSetContent
.contentHash
);
206 step('sets publish request', async
function() {
207 await db
.context(async (dbCtx
) => {
208 const result
= await db
.topicFetchRequested(dbCtx
, topicId
);
209 assert
.strictEqual(result
.changes
, 1);
211 const topic
= await db
.topicGetById(dbCtx
, topicId
);
212 assert(topic
.lastPublish
);
215 step('claims topic fetch', async
function () {
216 const claimTimeoutSeconds
= 10;
219 await db
.context(async (dbCtx
) => {
220 topicIds
= await db
.topicFetchClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
);
222 assert(topicIds
.includes(topicId
));
224 step('incompletes topic fetch', async
function () {
225 await db
.context(async (dbCtx
) => {
226 const result
= await db
.topicFetchIncomplete(dbCtx
, topicId
);
227 assert
.strictEqual(result
.changes
, 1);
228 const topic
= await db
.topicGetById(dbCtx
, topicId
);
229 assert
.strictEqual(Number(topic
.contentFetchAttemptsSinceSuccess
), 1);
232 step('claims topic fetch by id', async
function () {
233 const claimTimeoutSeconds
= 10;
234 await db
.context(async (dbCtx
) => {
235 const result
= await db
.topicFetchClaimById(dbCtx
, topicId
, claimTimeoutSeconds
, claimant
);
236 assert
.strictEqual(result
.changes
, 1);
239 step('completes topic fetch', async
function () {
240 await db
.context(async (dbCtx
) => {
241 const result
= await db
.topicFetchComplete(dbCtx
, topicId
);
242 assert
.strictEqual(result
.changes
, 1);
243 const topic
= await db
.topicGetById(dbCtx
, topicId
);
244 assert
.strictEqual(Number(topic
.contentFetchAttemptsSinceSuccess
), 0);
247 step('gets publish history', async
function () {
248 await db
.context(async (dbCtx
) => {
249 const result
= (await db
.topicPublishHistory(dbCtx
, topicId
, 7))
250 .map((x
) => Number(x
));
251 const expected
= [1, 0, 0, 0, 0, 0, 0];
252 assert
.deepStrictEqual(result
, expected
);
255 step('deletes a topic', async
function () {
256 await db
.context(async (dbCtx
) => {
257 const result
= await db
.topicSet(dbCtx
, testData
.anotherTopicSet
);
258 anotherTopicId
= result
.lastInsertRowid
;
259 await db
.topicDeleted(dbCtx
, anotherTopicId
);
260 const topic
= await db
.topicGetById(dbCtx
, anotherTopicId
);
261 assert
.strictEqual(topic
.isDeleted
, true);
264 step('update un-deletes a topic', async
function () {
265 await db
.context(async (dbCtx
) => {
266 const result
= await db
.topicSet(dbCtx
, testData
.anotherTopicSet
);
267 assert
.strictEqual(result
.lastInsertRowid
, anotherTopicId
);
268 const topic
= await db
.topicGetById(dbCtx
, anotherTopicId
);
269 assert
.strictEqual(topic
.isDeleted
, false);
272 step('gets all topics', async
function() {
273 await db
.context(async (dbCtx
) => {
274 const topics
= await db
.topicGetAll(dbCtx
);
275 assert(topics
.length
);
278 // pending delete of deleted topic with no subscriptions
279 step('really deletes unsubscribed deleted topic', async
function() {
280 await db
.context(async (dbCtx
) => {
281 await db
.topicDeleted(dbCtx
, anotherTopicId
);
282 await db
.topicPendingDelete(dbCtx
, anotherTopicId
);
283 const topic
= await db
.topicGetById(dbCtx
, anotherTopicId
);
289 describe('Subscription', function () {
290 step('requires data', async
function () {
292 await db
.context(async (dbCtx
) => {
293 await db
.subscriptionUpsert(dbCtx
);
295 assert
.fail(noExpectedException
);
297 assert(e
instanceof DBErrors
.DataValidation
);
300 step('creates subscription', async
function () {
302 ...testData
.subscriptionUpsert
,
305 await db
.context(async (dbCtx
) => {
306 const result
= await db
.subscriptionUpsert(dbCtx
, data
);
307 assert(result
.lastInsertRowid
);
308 subscriptionId
= result
.lastInsertRowid
;
309 assert
.strictEqual(result
.changes
, 1);
312 step('gets subscription', async
function () {
313 await db
.context(async (dbCtx
) => {
314 const subscription
= await db
.subscriptionGet(dbCtx
, testData
.subscriptionUpsert
.callback
, topicId
);
315 assert
.strictEqual(subscription
.secret
, testData
.subscriptionUpsert
.secret
);
318 step('gets subscription by id', async
function () {
319 await db
.context(async (dbCtx
) => {
320 const subscription
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
321 assert
.strictEqual(subscription
.secret
, testData
.subscriptionUpsert
.secret
);
324 step('gets subscriptions by topic', async
function() {
325 await db
.context(async (dbCtx
) => {
326 const subscriptions
= await db
.subscriptionsByTopicId(dbCtx
, topicId
);
327 assert(subscriptions
.length
);
330 step('count subscriptions', async
function () {
331 await db
.context(async (dbCtx
) => {
332 const count
= await db
.subscriptionCountByTopicUrl(dbCtx
, testData
.topicSet
.url
);
333 assert
.strictEqual(Number(count
.count
), 1);
336 step('claim subscription', async
function () {
337 const claimTimeoutSeconds
= 10;
340 await db
.context(async (dbCtx
) => {
341 subscriptionIds
= await db
.subscriptionDeliveryClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
);
343 assert(subscriptionIds
.includes(subscriptionId
));
345 step('incompletes subscription', async
function () {
346 const { callback
} = testData
.subscriptionUpsert
;
347 await db
.context(async (dbCtx
) => {
348 await db
.subscriptionDeliveryIncomplete(dbCtx
, callback
, topicId
);
349 const topic
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
350 assert
.strictEqual(Number(topic
.deliveryAttemptsSinceSuccess
), 1);
353 step('claim subscription by id', async
function () {
354 const claimTimeoutSeconds
= 10;
355 await db
.context(async (dbCtx
) => {
356 const result
= await db
.subscriptionDeliveryClaimById(dbCtx
, subscriptionId
, claimTimeoutSeconds
, claimant
);
357 assert
.strictEqual(result
.changes
, 1);
360 step('complete subscription', async
function () {
361 const { callback
} = testData
.subscriptionUpsert
;
362 await db
.context(async (dbCtx
) => {
363 const topic
= await db
.topicGetById(dbCtx
, topicId
);
364 await db
.subscriptionDeliveryComplete(dbCtx
, callback
, topicId
, topic
.contentUpdated
);
365 const subscription
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
366 assert
.strictEqual(Number(subscription
.deliveryAttemptsSinceSuccess
), 0);
369 step('subscription delete', async
function () {
370 const { callback
} = testData
.subscriptionUpsert
;
371 await db
.context(async (dbCtx
) => {
372 const result
= await db
.subscriptionDelete(dbCtx
, callback
, topicId
);
373 assert
.strictEqual(result
.changes
, 1);
374 const subscription
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
375 assert(!subscription
);
378 step('create subscription', async
function () {
380 ...testData
.subscriptionUpsert
,
384 await db
.context(async (dbCtx
) => {
385 const result
= await db
.subscriptionUpsert(dbCtx
, data
);
386 assert(result
.lastInsertRowid
);
387 assert
.notStrictEqual(result
.lastInsertRowid
, subscriptionId
);
388 subscriptionId
= result
.lastInsertRowid
;
389 assert
.strictEqual(result
.changes
, 1);
392 step('update subscription', async
function () {
395 signatureAlgorithm: 'sha256',
397 await db
.context(async (dbCtx
) => {
398 await db
.subscriptionUpdate(dbCtx
, data
);
401 step('claim subscription', async
function () {
402 const claimTimeoutSeconds
= 10;
405 await db
.context(async (dbCtx
) => {
406 subscriptionIds
= await db
.subscriptionDeliveryClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
);
408 assert(subscriptionIds
.includes(subscriptionId
));
410 step('subscription gone', async
function () {
411 const { callback
} = testData
.subscriptionUpsert
;
412 await db
.context(async (dbCtx
) => {
413 await db
.subscriptionDeliveryGone(dbCtx
, callback
, topicId
);
414 const subscription
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
415 assert(!subscription
);
418 step('create expired subscription', async
function () {
420 ...testData
.subscriptionUpsert
,
425 await db
.context(async (dbCtx
) => {
426 const result
= await db
.subscriptionUpsert(dbCtx
, data
);
427 assert(result
.lastInsertRowid
);
428 assert
.notStrictEqual(result
.lastInsertRowid
, subscriptionId
);
429 subscriptionId
= result
.lastInsertRowid
;
430 assert
.strictEqual(result
.changes
, 1);
433 step('delete expired subscriptions', async
function() {
434 await db
.context(async (dbCtx
) => {
435 await db
.subscriptionDeleteExpired(dbCtx
, topicId
);
436 const subscription
= await db
.subscriptionGet(dbCtx
, testData
.subscriptionUpsert
.callback
, topicId
);
437 assert(!subscription
);
442 describe('Verification', function () {
443 step('requires data', async
function() {
445 await db
.context(async (dbCtx
) => {
446 await db
.verificationInsert(dbCtx
);
448 assert
.fail(noExpectedException
);
450 assert(e
instanceof DBErrors
.DataValidation
);
453 step('creates verification', async
function() {
454 const verificationData
= {
455 ...testData
.verificationInsert
,
458 await db
.context(async (dbCtx
) => {
459 verificationId
= await db
.verificationInsert(dbCtx
, verificationData
);
460 assert(verificationId
);
463 step('gets verification', async
function() {
464 await db
.context(async (dbCtx
) => {
465 const verification
= await db
.verificationGetById(dbCtx
, verificationId
);
466 assert
.strictEqual(verification
.mode
, testData
.verificationInsert
.mode
);
469 step('validates verification', async
function() {
470 await db
.context(async (dbCtx
) => {
471 await db
.verificationValidated(dbCtx
, verificationId
);
472 const verification
= await db
.verificationGetById(dbCtx
, verificationId
);
473 assert
.strictEqual(verification
.isPublisherValidated
, true);
476 step('claims verification', async
function() {
477 const claimTimeoutSeconds
= 10;
480 await db
.context(async (dbCtx
) => {
481 verificationIds
= await db
.verificationClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
);
483 assert(verificationIds
.includes(verificationId
));
485 step('releases verification', async
function() {
486 await db
.context(async (dbCtx
) => {
487 await db
.verificationRelease(dbCtx
, verificationId
);
490 step('updates verification', async
function() {
491 const verificationData
= {
492 ...testData
.verificationUpdate
,
494 await db
.context(async (dbCtx
) => {
495 db
.verificationUpdate(dbCtx
, verificationId
, verificationData
);
496 const verification
= await db
.verificationGetById(dbCtx
, verificationId
);
497 assert
.strictEqual(verification
.isPublisherValidated
, testData
.verificationUpdate
.isPublisherValidated
);
500 step('claims verification by id', async
function() {
501 const claimTimeoutSeconds
= 10;
502 await db
.context(async (dbCtx
) => {
503 const result
= await db
.verificationClaimById(dbCtx
, verificationId
, claimTimeoutSeconds
, claimant
);
504 assert
.strictEqual(result
.changes
, 1);
507 step('incompletes verification', async
function() {
508 await db
.context(async (dbCtx
) => {
509 await db
.verificationIncomplete(dbCtx
, verificationId
);
512 step('claims verification by id', async
function() {
513 const claimTimeoutSeconds
= 10;
514 await db
.context(async (dbCtx
) => {
515 const result
= await db
.verificationClaimById(dbCtx
, verificationId
, claimTimeoutSeconds
, claimant
);
516 assert
.strictEqual(result
.changes
, 1);
519 step('completes verification', async
function() {
520 await db
.context(async (dbCtx
) => {
521 const verification
= await db
.verificationGetById(dbCtx
, verificationId
);
522 await db
.subscriptionUpsert(dbCtx
, verification
);
523 await db
.verificationComplete(dbCtx
, verificationId
, testData
.verificationInsert
.callback
, topicId
);
524 const count
= await db
.subscriptionCountByTopicUrl(dbCtx
, testData
.topicSet
.url
);
525 assert
.strictEqual(Number(count
.count
), 1);
531 }); // specific implementation
534 }); // Database Integration