db migration 1.0.2, now stores and indexes date of content delivered to subscriber...
[websub-hub] / test / src / db / integration.js
1 /* eslint-env mocha */
2 /* eslint-disable sonarjs/no-identical-functions */
3 'use strict';
4
5 /**
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
9 * DATA WIPE.
10 *
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
14 *
15 * These tests are sequential, relying on the state created along the way.
16 *
17 */
18
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');
24
25 describe('Database Integration', function () {
26 const noExpectedException = 'did not receive expected exception';
27 const implementations = [];
28
29 if (!process.env.INTEGRATION_TESTS) {
30 it.skip('integration tests not requested');
31 return;
32 }
33
34 if (process.env.POSTGRES_TEST_PATH) {
35 implementations.push({
36 name: 'PostgreSQL',
37 module: '../../../src/db/postgres',
38 config: {
39 db: {
40 connectionString: `postgresql://${process.env.POSTGRES_TEST_PATH}`,
41 queryLogLevel: 'debug',
42 noWarnings: true,
43 },
44 },
45 });
46 }
47
48 if (process.env.SQLITE_TEST_PATH) {
49 implementations.push({
50 name: 'SQLite',
51 module: '../../../src/db/sqlite',
52 config: {
53 db: {
54 connectionString: `sqlite://${process.env.SQLITE_TEST_PATH}`,
55 queryLogLevel: 'debug',
56 },
57 },
58 });
59 }
60
61 implementations.forEach(function (i) {
62 describe(i.name, function () {
63 let DB, db;
64 let topicId, subscriptionId, verificationId;
65 const claimant = '96bff010-d9e6-11eb-b95d-0025905f714a';
66
67 before(async function () {
68 this.timeout(10 * 1000); // Allow some time for creating tables et cetera.
69 // eslint-disable-next-line security/detect-non-literal-require
70 DB = require(i.module);
71 db = new DB(stubLogger, i.config);
72 await db.initialize();
73 await db._purgeTables(true);
74 });
75 after(async function () {
76 await db._closeConnection();
77 });
78 it('instantiated', function () {
79 assert(db);
80 });
81
82 describe('Authentication', function () {
83 let identifier, credential;
84 beforeEach(function () {
85 identifier = 'username';
86 credential = 'myEncryptedPassword';
87 });
88 step('create auth entry', async function() {
89 await db.context(async (dbCtx) => {
90 await db.authenticationUpsert(dbCtx, identifier, credential);
91 });
92 });
93 step('get auth entry', async function() {
94 await db.context(async (dbCtx) => {
95 const authInfo = await db.authenticationGet(dbCtx, identifier);
96 assert.strictEqual(authInfo.credential, credential);
97 });
98 });
99 step('valid auth event', async function() {
100 await db.context(async (dbCtx) => {
101 await db.authenticationSuccess(dbCtx, identifier);
102 const authInfo = await db.authenticationGet(dbCtx, identifier);
103 assert.notStrictEqual(authInfo.lastAuthentication, undefined);
104 });
105 });
106 step('update auth entry', async function() {
107 await db.context(async (dbCtx) => {
108 credential = 'myNewPassword';
109 await db.authenticationUpsert(dbCtx, identifier, credential);
110 const authInfo = await db.authenticationGet(dbCtx, identifier);
111 assert.strictEqual(authInfo.credential, credential);
112 });
113 });
114 }); // Authentication
115
116 describe('Topic', function () {
117 let anotherTopicId;
118 step('requires data', async function () {
119 try {
120 await db.context(async (dbCtx) => {
121 await db.topicSet(dbCtx);
122 });
123 assert.fail(noExpectedException);
124 } catch (e) {
125 assert(e instanceof DBErrors.DataValidation);
126 }
127 });
128 step('creates topic', async function () {
129 await db.context(async (dbCtx) => {
130 const result = await db.topicSet(dbCtx, testData.topicSet);
131 topicId = result.lastInsertRowid;
132 assert.strictEqual(result.changes, 1);
133 });
134 });
135 step('gets topic by url', async function () {
136 await db.context(async (dbCtx) => {
137 const topic = await db.topicGetByUrl(dbCtx, testData.topicSet.url);
138 assert.strictEqual(topic.url, testData.topicSet.url);
139 });
140 });
141 step('updates topic', async function () {
142 await db.context(async(dbCtx) => {
143 const result = await db.topicSet(dbCtx, testData.topicUpdate);
144 assert.strictEqual(result.changes, 1);
145 });
146 });
147 step('also updates topic', async function () {
148 const data = {
149 topicId,
150 leaseSecondsMin: 60,
151 }
152 await db.context(async(dbCtx) => {
153 let topic = await db.topicGetByUrl(dbCtx, testData.topicSet.url);
154 await db.topicUpdate(dbCtx, { ...topic, ...data });
155 topic = await db.topicGetByUrl(dbCtx, testData.topicSet.url);
156 assert.strictEqual(Number(topic.leaseSecondsMin), data.leaseSecondsMin);
157 });
158 });
159 step('gets topic by id', async function () {
160 await db.context(async (dbCtx) => {
161 const topic = await db.topicGetById(dbCtx, topicId);
162 assert.strictEqual(topic.url, testData.topicSet.url);
163 assert.strictEqual(Number(topic.leaseSecondsPreferred), testData.topicUpdate.leaseSecondsPreferred);
164 });
165 });
166 step('sets topic content', async function () {
167 const data = {
168 ...testData.topicSetContent,
169 topicId,
170 };
171 await db.context(async (dbCtx) => {
172 const result = await db.topicSetContent(dbCtx, data);
173 assert.strictEqual(result.changes, 1);
174 });
175 });
176 step('gets topic content', async function () {
177 await db.context(async (dbCtx) => {
178 const topic = await db.topicGetContentById(dbCtx, topicId);
179 assert.strictEqual(topic.contentHash, testData.topicSetContent.contentHash);
180 });
181 });
182 step('sets publish request', async function() {
183 await db.context(async (dbCtx) => {
184 const result = await db.topicFetchRequested(dbCtx, topicId);
185 assert.strictEqual(result.changes, 1);
186
187 const topic = await db.topicGetById(dbCtx, topicId);
188 assert(topic.lastPublish);
189 });
190 });
191 step('claims topic fetch', async function () {
192 const claimTimeoutSeconds = 10;
193 const wanted = 5;
194 let topicIds;
195 await db.context(async (dbCtx) => {
196 topicIds = await db.topicFetchClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
197 });
198 assert(topicIds.includes(topicId));
199 });
200 step('incompletes topic fetch', async function () {
201 await db.context(async (dbCtx) => {
202 const result = await db.topicFetchIncomplete(dbCtx, topicId);
203 assert.strictEqual(result.changes, 1);
204 const topic = await db.topicGetById(dbCtx, topicId);
205 assert.strictEqual(Number(topic.contentFetchAttemptsSinceSuccess), 1);
206 });
207 });
208 step('claims topic fetch by id', async function () {
209 const claimTimeoutSeconds = 10;
210 await db.context(async (dbCtx) => {
211 const result = await db.topicFetchClaimById(dbCtx, topicId, claimTimeoutSeconds, claimant);
212 assert.strictEqual(result.changes, 1);
213 });
214 });
215 step('completes topic fetch', async function () {
216 await db.context(async (dbCtx) => {
217 const result = await db.topicFetchComplete(dbCtx, topicId);
218 assert.strictEqual(result.changes, 1);
219 const topic = await db.topicGetById(dbCtx, topicId);
220 assert.strictEqual(Number(topic.contentFetchAttemptsSinceSuccess), 0);
221 });
222 });
223 step('deletes a topic', async function () {
224 await db.context(async (dbCtx) => {
225 const result = await db.topicSet(dbCtx, testData.anotherTopicSet);
226 anotherTopicId = result.lastInsertRowid;
227 await db.topicDeleted(dbCtx, anotherTopicId);
228 const topic = await db.topicGetById(dbCtx, anotherTopicId);
229 assert.strictEqual(topic.isDeleted, true);
230 });
231 });
232 step('update un-deletes a topic', async function () {
233 await db.context(async (dbCtx) => {
234 const result = await db.topicSet(dbCtx, testData.anotherTopicSet);
235 assert.strictEqual(result.lastInsertRowid, anotherTopicId);
236 const topic = await db.topicGetById(dbCtx, anotherTopicId);
237 assert.strictEqual(topic.isDeleted, false);
238 });
239 });
240 step('gets all topics', async function() {
241 await db.context(async (dbCtx) => {
242 const topics = await db.topicGetAll(dbCtx);
243 assert(topics.length);
244 });
245 });
246 // pending delete of deleted topic with no subscriptions
247 step('really deletes unsubscribed deleted topic', async function() {
248 await db.context(async (dbCtx) => {
249 await db.topicDeleted(dbCtx, anotherTopicId);
250 await db.topicPendingDelete(dbCtx, anotherTopicId);
251 const topic = await db.topicGetById(dbCtx, anotherTopicId);
252 assert(!topic);
253 });
254 });
255 }); // Topic
256
257 describe('Subscription', function () {
258 step('requires data', async function () {
259 try {
260 await db.context(async (dbCtx) => {
261 await db.subscriptionUpsert(dbCtx);
262 });
263 assert.fail(noExpectedException);
264 } catch (e) {
265 assert(e instanceof DBErrors.DataValidation);
266 }
267 });
268 step('creates subscription', async function () {
269 const data = {
270 ...testData.subscriptionUpsert,
271 topicId,
272 }
273 await db.context(async (dbCtx) => {
274 const result = await db.subscriptionUpsert(dbCtx, data);
275 assert(result.lastInsertRowid);
276 subscriptionId = result.lastInsertRowid;
277 assert.strictEqual(result.changes, 1);
278 });
279 });
280 step('gets subscription', async function () {
281 await db.context(async (dbCtx) => {
282 const subscription = await db.subscriptionGet(dbCtx, testData.subscriptionUpsert.callback, topicId);
283 assert.strictEqual(subscription.secret, testData.subscriptionUpsert.secret);
284 });
285 });
286 step('gets subscription by id', async function () {
287 await db.context(async (dbCtx) => {
288 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
289 assert.strictEqual(subscription.secret, testData.subscriptionUpsert.secret);
290 });
291 });
292 step('gets subscriptions by topic', async function() {
293 await db.context(async (dbCtx) => {
294 const subscriptions = await db.subscriptionsByTopicId(dbCtx, topicId);
295 assert(subscriptions.length);
296 });
297 });
298 step('count subscriptions', async function () {
299 await db.context(async (dbCtx) => {
300 const count = await db.subscriptionCountByTopicUrl(dbCtx, testData.topicSet.url);
301 assert.strictEqual(Number(count.count), 1);
302 });
303 });
304 step('claim subscription', async function () {
305 const claimTimeoutSeconds = 10;
306 const wanted = 5;
307 let subscriptionIds;
308 await db.context(async (dbCtx) => {
309 subscriptionIds = await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
310 });
311 assert(subscriptionIds.includes(subscriptionId));
312 });
313 step('incompletes subscription', async function () {
314 const { callback } = testData.subscriptionUpsert;
315 await db.context(async (dbCtx) => {
316 await db.subscriptionDeliveryIncomplete(dbCtx, callback, topicId);
317 const topic = await db.subscriptionGetById(dbCtx, subscriptionId);
318 assert.strictEqual(Number(topic.deliveryAttemptsSinceSuccess), 1);
319 });
320 });
321 step('claim subscription by id', async function () {
322 const claimTimeoutSeconds = 10;
323 await db.context(async (dbCtx) => {
324 const result = await db.subscriptionDeliveryClaimById(dbCtx, subscriptionId, claimTimeoutSeconds, claimant);
325 assert.strictEqual(result.changes, 1);
326 });
327 });
328 step('complete subscription', async function () {
329 const { callback } = testData.subscriptionUpsert;
330 await db.context(async (dbCtx) => {
331 const topic = await db.topicGetById(dbCtx, topicId);
332 await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topic.contentUpdated);
333 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
334 assert.strictEqual(Number(subscription.deliveryAttemptsSinceSuccess), 0);
335 });
336 });
337 step('subscription delete', async function () {
338 const { callback } = testData.subscriptionUpsert;
339 await db.context(async (dbCtx) => {
340 const result = await db.subscriptionDelete(dbCtx, callback, topicId);
341 assert.strictEqual(result.changes, 1);
342 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
343 assert(!subscription);
344 });
345 });
346 step('create subscription', async function () {
347 const data = {
348 ...testData.subscriptionUpsert,
349 secret: 'newSecret',
350 topicId,
351 }
352 await db.context(async (dbCtx) => {
353 const result = await db.subscriptionUpsert(dbCtx, data);
354 assert(result.lastInsertRowid);
355 assert.notStrictEqual(result.lastInsertRowid, subscriptionId);
356 subscriptionId = result.lastInsertRowid;
357 assert.strictEqual(result.changes, 1);
358 });
359 });
360 step('update subscription', async function () {
361 const data = {
362 subscriptionId,
363 signatureAlgorithm: 'sha256',
364 };
365 await db.context(async (dbCtx) => {
366 await db.subscriptionUpdate(dbCtx, data);
367 });
368 });
369 step('claim subscription', async function () {
370 const claimTimeoutSeconds = 10;
371 const wanted = 5;
372 let subscriptionIds;
373 await db.context(async (dbCtx) => {
374 subscriptionIds = await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
375 });
376 assert(subscriptionIds.includes(subscriptionId));
377 });
378 step('subscription gone', async function () {
379 const { callback } = testData.subscriptionUpsert;
380 await db.context(async (dbCtx) => {
381 await db.subscriptionDeliveryGone(dbCtx, callback, topicId);
382 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
383 assert(!subscription);
384 });
385 });
386 step('create expired subscription', async function () {
387 const data = {
388 ...testData.subscriptionUpsert,
389 secret: 'newSecret',
390 topicId,
391 leaseSeconds: -1,
392 };
393 await db.context(async (dbCtx) => {
394 const result = await db.subscriptionUpsert(dbCtx, data);
395 assert(result.lastInsertRowid);
396 assert.notStrictEqual(result.lastInsertRowid, subscriptionId);
397 subscriptionId = result.lastInsertRowid;
398 assert.strictEqual(result.changes, 1);
399 });
400 });
401 step('delete expired subscriptions', async function() {
402 await db.context(async (dbCtx) => {
403 await db.subscriptionDeleteExpired(dbCtx, topicId)
404 const subscription = await db.subscriptionGet(dbCtx, testData.subscriptionUpsert.callback, topicId);
405 assert(!subscription);
406 });
407 });
408 }); // Subscription
409
410 describe('Verification', function () {
411 step('requires data', async function() {
412 try {
413 await db.context(async (dbCtx) => {
414 await db.verificationInsert(dbCtx);
415 });
416 assert.fail(noExpectedException);
417 } catch (e) {
418 assert(e instanceof DBErrors.DataValidation);
419 }
420 });
421 step('creates verification', async function() {
422 const verificationData = {
423 ...testData.verificationInsert,
424 topicId,
425 };
426 await db.context(async (dbCtx) => {
427 verificationId = await db.verificationInsert(dbCtx, verificationData);
428 assert(verificationId);
429 });
430 });
431 step('gets verification', async function() {
432 await db.context(async (dbCtx) => {
433 const verification = await db.verificationGetById(dbCtx, verificationId);
434 assert.strictEqual(verification.mode, testData.verificationInsert.mode);
435 });
436 });
437 step('validates verification', async function() {
438 await db.context(async (dbCtx) => {
439 await db.verificationValidated(dbCtx, verificationId);
440 const verification = await db.verificationGetById(dbCtx, verificationId);
441 assert.strictEqual(verification.isPublisherValidated, true);
442 });
443 });
444 step('claims verification', async function() {
445 const claimTimeoutSeconds = 10;
446 const wanted = 5;
447 let verificationIds;
448 await db.context(async (dbCtx) => {
449 verificationIds = await db.verificationClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
450 });
451 assert(verificationIds.includes(verificationId));
452 });
453 step('releases verification', async function() {
454 await db.context(async (dbCtx) => {
455 await db.verificationRelease(dbCtx, verificationId);
456 });
457 });
458 step('updates verification', async function() {
459 const verificationData = {
460 ...testData.verificationUpdate,
461 };
462 await db.context(async (dbCtx) => {
463 db.verificationUpdate(dbCtx, verificationId, verificationData);
464 const verification = await db.verificationGetById(dbCtx, verificationId);
465 assert.strictEqual(verification.isPublisherValidated, testData.verificationUpdate.isPublisherValidated);
466 });
467 });
468 step('claims verification by id', async function() {
469 const claimTimeoutSeconds = 10;
470 await db.context(async (dbCtx) => {
471 const result = await db.verificationClaimById(dbCtx, verificationId, claimTimeoutSeconds, claimant);
472 assert.strictEqual(result.changes, 1);
473 });
474 });
475 step('incompletes verification', async function() {
476 await db.context(async (dbCtx) => {
477 await db.verificationIncomplete(dbCtx, verificationId);
478 });
479 });
480 step('claims verification by id', async function() {
481 const claimTimeoutSeconds = 10;
482 await db.context(async (dbCtx) => {
483 const result = await db.verificationClaimById(dbCtx, verificationId, claimTimeoutSeconds, claimant);
484 assert.strictEqual(result.changes, 1);
485 });
486 });
487 step('completes verification', async function() {
488 await db.context(async (dbCtx) => {
489 const verification = await db.verificationGetById(dbCtx, verificationId);
490 await db.subscriptionUpsert(dbCtx, verification);
491 await db.verificationComplete(dbCtx, verificationId, testData.verificationInsert.callback, topicId);
492 const count = await db.subscriptionCountByTopicUrl(dbCtx, testData.topicSet.url);
493 assert.strictEqual(Number(count.count), 1);
494 });
495 });
496
497 }); // Verification
498
499 }); // specific implementation
500 }); // foreach
501
502 }); // Database Integration