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
;
87 beforeEach(function () {
88 identifier
= 'username';
89 credential
= 'myEncryptedPassword';
91 step('create auth entry', async
function() {
92 await db
.context(async (dbCtx
) => {
93 await db
.authenticationUpsert(dbCtx
, identifier
, credential
);
96 step('get auth entry', async
function() {
97 await db
.context(async (dbCtx
) => {
98 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
99 assert
.strictEqual(authInfo
.credential
, credential
);
102 step('valid auth event', async
function() {
103 await db
.context(async (dbCtx
) => {
104 await db
.authenticationSuccess(dbCtx
, identifier
);
105 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
106 assert
.notStrictEqual(authInfo
.lastAuthentication
, undefined);
109 step('update auth entry', async
function() {
110 await db
.context(async (dbCtx
) => {
111 credential
= 'myNewPassword';
112 await db
.authenticationUpsert(dbCtx
, identifier
, credential
);
113 const authInfo
= await db
.authenticationGet(dbCtx
, identifier
);
114 assert
.strictEqual(authInfo
.credential
, credential
);
117 }); // Authentication
119 describe('Topic', function () {
121 step('requires data', async
function () {
123 await db
.context(async (dbCtx
) => {
124 await db
.topicSet(dbCtx
);
126 assert
.fail(noExpectedException
);
128 assert(e
instanceof DBErrors
.DataValidation
);
131 step('creates topic', async
function () {
132 await db
.context(async (dbCtx
) => {
133 const result
= await db
.topicSet(dbCtx
, testData
.topicSet
);
134 topicId
= result
.lastInsertRowid
;
135 assert
.strictEqual(result
.changes
, 1);
138 step('gets topic by url', async
function () {
139 await db
.context(async (dbCtx
) => {
140 const topic
= await db
.topicGetByUrl(dbCtx
, testData
.topicSet
.url
);
141 assert
.strictEqual(topic
.url
, testData
.topicSet
.url
);
144 step('updates topic', async
function () {
145 await db
.context(async(dbCtx
) => {
146 const result
= await db
.topicSet(dbCtx
, testData
.topicUpdate
);
147 assert
.strictEqual(result
.changes
, 1);
150 step('also updates topic', async
function () {
155 await db
.context(async(dbCtx
) => {
156 const expected
= await db
.topicGetByUrl(dbCtx
, testData
.topicSet
.url
, true);
157 expected
.leaseSecondsMin
= data
.leaseSecondsMin
;
158 let topic
= await db
.topicGetByUrl(dbCtx
, testData
.topicSet
.url
, false);
159 await db
.topicUpdate(dbCtx
, { ...topic
, ...data
});
160 topic
= await db
.topicGetByUrl(dbCtx
, testData
.topicSet
.url
);
161 assert
.strictEqual(Number(topic
.leaseSecondsMin
), data
.leaseSecondsMin
);
162 assert
.deepEqual(topic
, expected
);
165 step('gets topic by id', async
function () {
166 await db
.context(async (dbCtx
) => {
167 const topic
= await db
.topicGetById(dbCtx
, topicId
);
168 assert
.strictEqual(topic
.url
, testData
.topicSet
.url
);
169 assert
.strictEqual(Number(topic
.leaseSecondsPreferred
), testData
.topicUpdate
.leaseSecondsPreferred
);
172 step('sets topic content', async
function () {
174 ...testData
.topicSetContent
,
177 await db
.context(async (dbCtx
) => {
178 const result
= await db
.topicSetContent(dbCtx
, data
);
179 assert
.strictEqual(result
.changes
, 1);
182 step('gets topic content', async
function () {
183 await db
.context(async (dbCtx
) => {
184 const topic
= await db
.topicGetContentById(dbCtx
, topicId
);
185 assert
.strictEqual(topic
.contentHash
, testData
.topicSetContent
.contentHash
);
188 step('sets publish request', async
function() {
189 await db
.context(async (dbCtx
) => {
190 const result
= await db
.topicFetchRequested(dbCtx
, topicId
);
191 assert
.strictEqual(result
.changes
, 1);
193 const topic
= await db
.topicGetById(dbCtx
, topicId
);
194 assert(topic
.lastPublish
);
197 step('claims topic fetch', async
function () {
198 const claimTimeoutSeconds
= 10;
201 await db
.context(async (dbCtx
) => {
202 topicIds
= await db
.topicFetchClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
);
204 assert(topicIds
.includes(topicId
));
206 step('incompletes topic fetch', async
function () {
207 await db
.context(async (dbCtx
) => {
208 const result
= await db
.topicFetchIncomplete(dbCtx
, topicId
);
209 assert
.strictEqual(result
.changes
, 1);
210 const topic
= await db
.topicGetById(dbCtx
, topicId
);
211 assert
.strictEqual(Number(topic
.contentFetchAttemptsSinceSuccess
), 1);
214 step('claims topic fetch by id', async
function () {
215 const claimTimeoutSeconds
= 10;
216 await db
.context(async (dbCtx
) => {
217 const result
= await db
.topicFetchClaimById(dbCtx
, topicId
, claimTimeoutSeconds
, claimant
);
218 assert
.strictEqual(result
.changes
, 1);
221 step('completes topic fetch', async
function () {
222 await db
.context(async (dbCtx
) => {
223 const result
= await db
.topicFetchComplete(dbCtx
, topicId
);
224 assert
.strictEqual(result
.changes
, 1);
225 const topic
= await db
.topicGetById(dbCtx
, topicId
);
226 assert
.strictEqual(Number(topic
.contentFetchAttemptsSinceSuccess
), 0);
229 step('gets publish history', async
function () {
230 await db
.context(async (dbCtx
) => {
231 const result
= (await db
.topicPublishHistory(dbCtx
, topicId
, 7))
232 .map((x
) => Number(x
));
233 const expected
= [1, 0, 0, 0, 0, 0, 0];
234 assert
.deepStrictEqual(result
, expected
);
237 step('deletes a topic', async
function () {
238 await db
.context(async (dbCtx
) => {
239 const result
= await db
.topicSet(dbCtx
, testData
.anotherTopicSet
);
240 anotherTopicId
= result
.lastInsertRowid
;
241 await db
.topicDeleted(dbCtx
, anotherTopicId
);
242 const topic
= await db
.topicGetById(dbCtx
, anotherTopicId
);
243 assert
.strictEqual(topic
.isDeleted
, true);
246 step('update un-deletes a topic', async
function () {
247 await db
.context(async (dbCtx
) => {
248 const result
= await db
.topicSet(dbCtx
, testData
.anotherTopicSet
);
249 assert
.strictEqual(result
.lastInsertRowid
, anotherTopicId
);
250 const topic
= await db
.topicGetById(dbCtx
, anotherTopicId
);
251 assert
.strictEqual(topic
.isDeleted
, false);
254 step('gets all topics', async
function() {
255 await db
.context(async (dbCtx
) => {
256 const topics
= await db
.topicGetAll(dbCtx
);
257 assert(topics
.length
);
260 // pending delete of deleted topic with no subscriptions
261 step('really deletes unsubscribed deleted topic', async
function() {
262 await db
.context(async (dbCtx
) => {
263 await db
.topicDeleted(dbCtx
, anotherTopicId
);
264 await db
.topicPendingDelete(dbCtx
, anotherTopicId
);
265 const topic
= await db
.topicGetById(dbCtx
, anotherTopicId
);
271 describe('Subscription', function () {
272 step('requires data', async
function () {
274 await db
.context(async (dbCtx
) => {
275 await db
.subscriptionUpsert(dbCtx
);
277 assert
.fail(noExpectedException
);
279 assert(e
instanceof DBErrors
.DataValidation
);
282 step('creates subscription', async
function () {
284 ...testData
.subscriptionUpsert
,
287 await db
.context(async (dbCtx
) => {
288 const result
= await db
.subscriptionUpsert(dbCtx
, data
);
289 assert(result
.lastInsertRowid
);
290 subscriptionId
= result
.lastInsertRowid
;
291 assert
.strictEqual(result
.changes
, 1);
294 step('gets subscription', async
function () {
295 await db
.context(async (dbCtx
) => {
296 const subscription
= await db
.subscriptionGet(dbCtx
, testData
.subscriptionUpsert
.callback
, topicId
);
297 assert
.strictEqual(subscription
.secret
, testData
.subscriptionUpsert
.secret
);
300 step('gets subscription by id', async
function () {
301 await db
.context(async (dbCtx
) => {
302 const subscription
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
303 assert
.strictEqual(subscription
.secret
, testData
.subscriptionUpsert
.secret
);
306 step('gets subscriptions by topic', async
function() {
307 await db
.context(async (dbCtx
) => {
308 const subscriptions
= await db
.subscriptionsByTopicId(dbCtx
, topicId
);
309 assert(subscriptions
.length
);
312 step('count subscriptions', async
function () {
313 await db
.context(async (dbCtx
) => {
314 const count
= await db
.subscriptionCountByTopicUrl(dbCtx
, testData
.topicSet
.url
);
315 assert
.strictEqual(Number(count
.count
), 1);
318 step('claim subscription', async
function () {
319 const claimTimeoutSeconds
= 10;
322 await db
.context(async (dbCtx
) => {
323 subscriptionIds
= await db
.subscriptionDeliveryClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
);
325 assert(subscriptionIds
.includes(subscriptionId
));
327 step('incompletes subscription', async
function () {
328 const { callback
} = testData
.subscriptionUpsert
;
329 await db
.context(async (dbCtx
) => {
330 await db
.subscriptionDeliveryIncomplete(dbCtx
, callback
, topicId
);
331 const topic
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
332 assert
.strictEqual(Number(topic
.deliveryAttemptsSinceSuccess
), 1);
335 step('claim subscription by id', async
function () {
336 const claimTimeoutSeconds
= 10;
337 await db
.context(async (dbCtx
) => {
338 const result
= await db
.subscriptionDeliveryClaimById(dbCtx
, subscriptionId
, claimTimeoutSeconds
, claimant
);
339 assert
.strictEqual(result
.changes
, 1);
342 step('complete subscription', async
function () {
343 const { callback
} = testData
.subscriptionUpsert
;
344 await db
.context(async (dbCtx
) => {
345 const topic
= await db
.topicGetById(dbCtx
, topicId
);
346 await db
.subscriptionDeliveryComplete(dbCtx
, callback
, topicId
, topic
.contentUpdated
);
347 const subscription
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
348 assert
.strictEqual(Number(subscription
.deliveryAttemptsSinceSuccess
), 0);
351 step('subscription delete', async
function () {
352 const { callback
} = testData
.subscriptionUpsert
;
353 await db
.context(async (dbCtx
) => {
354 const result
= await db
.subscriptionDelete(dbCtx
, callback
, topicId
);
355 assert
.strictEqual(result
.changes
, 1);
356 const subscription
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
357 assert(!subscription
);
360 step('create subscription', async
function () {
362 ...testData
.subscriptionUpsert
,
366 await db
.context(async (dbCtx
) => {
367 const result
= await db
.subscriptionUpsert(dbCtx
, data
);
368 assert(result
.lastInsertRowid
);
369 assert
.notStrictEqual(result
.lastInsertRowid
, subscriptionId
);
370 subscriptionId
= result
.lastInsertRowid
;
371 assert
.strictEqual(result
.changes
, 1);
374 step('update subscription', async
function () {
377 signatureAlgorithm: 'sha256',
379 await db
.context(async (dbCtx
) => {
380 await db
.subscriptionUpdate(dbCtx
, data
);
383 step('claim subscription', async
function () {
384 const claimTimeoutSeconds
= 10;
387 await db
.context(async (dbCtx
) => {
388 subscriptionIds
= await db
.subscriptionDeliveryClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
);
390 assert(subscriptionIds
.includes(subscriptionId
));
392 step('subscription gone', async
function () {
393 const { callback
} = testData
.subscriptionUpsert
;
394 await db
.context(async (dbCtx
) => {
395 await db
.subscriptionDeliveryGone(dbCtx
, callback
, topicId
);
396 const subscription
= await db
.subscriptionGetById(dbCtx
, subscriptionId
);
397 assert(!subscription
);
400 step('create expired subscription', async
function () {
402 ...testData
.subscriptionUpsert
,
407 await db
.context(async (dbCtx
) => {
408 const result
= await db
.subscriptionUpsert(dbCtx
, data
);
409 assert(result
.lastInsertRowid
);
410 assert
.notStrictEqual(result
.lastInsertRowid
, subscriptionId
);
411 subscriptionId
= result
.lastInsertRowid
;
412 assert
.strictEqual(result
.changes
, 1);
415 step('delete expired subscriptions', async
function() {
416 await db
.context(async (dbCtx
) => {
417 await db
.subscriptionDeleteExpired(dbCtx
, topicId
);
418 const subscription
= await db
.subscriptionGet(dbCtx
, testData
.subscriptionUpsert
.callback
, topicId
);
419 assert(!subscription
);
424 describe('Verification', function () {
425 step('requires data', async
function() {
427 await db
.context(async (dbCtx
) => {
428 await db
.verificationInsert(dbCtx
);
430 assert
.fail(noExpectedException
);
432 assert(e
instanceof DBErrors
.DataValidation
);
435 step('creates verification', async
function() {
436 const verificationData
= {
437 ...testData
.verificationInsert
,
440 await db
.context(async (dbCtx
) => {
441 verificationId
= await db
.verificationInsert(dbCtx
, verificationData
);
442 assert(verificationId
);
445 step('gets verification', async
function() {
446 await db
.context(async (dbCtx
) => {
447 const verification
= await db
.verificationGetById(dbCtx
, verificationId
);
448 assert
.strictEqual(verification
.mode
, testData
.verificationInsert
.mode
);
451 step('validates verification', async
function() {
452 await db
.context(async (dbCtx
) => {
453 await db
.verificationValidated(dbCtx
, verificationId
);
454 const verification
= await db
.verificationGetById(dbCtx
, verificationId
);
455 assert
.strictEqual(verification
.isPublisherValidated
, true);
458 step('claims verification', async
function() {
459 const claimTimeoutSeconds
= 10;
462 await db
.context(async (dbCtx
) => {
463 verificationIds
= await db
.verificationClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
);
465 assert(verificationIds
.includes(verificationId
));
467 step('releases verification', async
function() {
468 await db
.context(async (dbCtx
) => {
469 await db
.verificationRelease(dbCtx
, verificationId
);
472 step('updates verification', async
function() {
473 const verificationData
= {
474 ...testData
.verificationUpdate
,
476 await db
.context(async (dbCtx
) => {
477 db
.verificationUpdate(dbCtx
, verificationId
, verificationData
);
478 const verification
= await db
.verificationGetById(dbCtx
, verificationId
);
479 assert
.strictEqual(verification
.isPublisherValidated
, testData
.verificationUpdate
.isPublisherValidated
);
482 step('claims verification by id', async
function() {
483 const claimTimeoutSeconds
= 10;
484 await db
.context(async (dbCtx
) => {
485 const result
= await db
.verificationClaimById(dbCtx
, verificationId
, claimTimeoutSeconds
, claimant
);
486 assert
.strictEqual(result
.changes
, 1);
489 step('incompletes verification', async
function() {
490 await db
.context(async (dbCtx
) => {
491 await db
.verificationIncomplete(dbCtx
, verificationId
);
494 step('claims verification by id', async
function() {
495 const claimTimeoutSeconds
= 10;
496 await db
.context(async (dbCtx
) => {
497 const result
= await db
.verificationClaimById(dbCtx
, verificationId
, claimTimeoutSeconds
, claimant
);
498 assert
.strictEqual(result
.changes
, 1);
501 step('completes verification', async
function() {
502 await db
.context(async (dbCtx
) => {
503 const verification
= await db
.verificationGetById(dbCtx
, verificationId
);
504 await db
.subscriptionUpsert(dbCtx
, verification
);
505 await db
.verificationComplete(dbCtx
, verificationId
, testData
.verificationInsert
.callback
, topicId
);
506 const count
= await db
.subscriptionCountByTopicUrl(dbCtx
, testData
.topicSet
.url
);
507 assert
.strictEqual(Number(count
.count
), 1);
513 }); // specific implementation
516 }); // Database Integration