display history of topic updates on topic details page
[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 it('is healthy', async function () {
83 const result = await db.healthCheck();
84 assert(result);
85 });
86
87 describe('Authentication', function () {
88 let identifier, credential;
89 beforeEach(function () {
90 identifier = 'username';
91 credential = 'myEncryptedPassword';
92 });
93 step('create auth entry', async function() {
94 await db.context(async (dbCtx) => {
95 await db.authenticationUpsert(dbCtx, identifier, credential);
96 });
97 });
98 step('get auth entry', async function() {
99 await db.context(async (dbCtx) => {
100 const authInfo = await db.authenticationGet(dbCtx, identifier);
101 assert.strictEqual(authInfo.credential, credential);
102 });
103 });
104 step('valid auth event', async function() {
105 await db.context(async (dbCtx) => {
106 await db.authenticationSuccess(dbCtx, identifier);
107 const authInfo = await db.authenticationGet(dbCtx, identifier);
108 assert.notStrictEqual(authInfo.lastAuthentication, undefined);
109 });
110 });
111 step('update auth entry', async function() {
112 await db.context(async (dbCtx) => {
113 credential = 'myNewPassword';
114 await db.authenticationUpsert(dbCtx, identifier, credential);
115 const authInfo = await db.authenticationGet(dbCtx, identifier);
116 assert.strictEqual(authInfo.credential, credential);
117 });
118 });
119 }); // Authentication
120
121 describe('Topic', function () {
122 let anotherTopicId;
123 step('requires data', async function () {
124 try {
125 await db.context(async (dbCtx) => {
126 await db.topicSet(dbCtx);
127 });
128 assert.fail(noExpectedException);
129 } catch (e) {
130 assert(e instanceof DBErrors.DataValidation);
131 }
132 });
133 step('creates topic', async function () {
134 await db.context(async (dbCtx) => {
135 const result = await db.topicSet(dbCtx, testData.topicSet);
136 topicId = result.lastInsertRowid;
137 assert.strictEqual(result.changes, 1);
138 });
139 });
140 step('gets topic by url', async function () {
141 await db.context(async (dbCtx) => {
142 const topic = await db.topicGetByUrl(dbCtx, testData.topicSet.url);
143 assert.strictEqual(topic.url, testData.topicSet.url);
144 });
145 });
146 step('updates topic', async function () {
147 await db.context(async(dbCtx) => {
148 const result = await db.topicSet(dbCtx, testData.topicUpdate);
149 assert.strictEqual(result.changes, 1);
150 });
151 });
152 step('also updates topic', async function () {
153 const data = {
154 topicId,
155 leaseSecondsMin: 60,
156 }
157 await db.context(async(dbCtx) => {
158 let topic = await db.topicGetByUrl(dbCtx, testData.topicSet.url);
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 });
163 });
164 step('gets topic by id', async function () {
165 await db.context(async (dbCtx) => {
166 const topic = await db.topicGetById(dbCtx, topicId);
167 assert.strictEqual(topic.url, testData.topicSet.url);
168 assert.strictEqual(Number(topic.leaseSecondsPreferred), testData.topicUpdate.leaseSecondsPreferred);
169 });
170 });
171 step('sets topic content', async function () {
172 const data = {
173 ...testData.topicSetContent,
174 topicId,
175 };
176 await db.context(async (dbCtx) => {
177 const result = await db.topicSetContent(dbCtx, data);
178 assert.strictEqual(result.changes, 1);
179 });
180 });
181 step('gets topic content', async function () {
182 await db.context(async (dbCtx) => {
183 const topic = await db.topicGetContentById(dbCtx, topicId);
184 assert.strictEqual(topic.contentHash, testData.topicSetContent.contentHash);
185 });
186 });
187 step('sets publish request', async function() {
188 await db.context(async (dbCtx) => {
189 const result = await db.topicFetchRequested(dbCtx, topicId);
190 assert.strictEqual(result.changes, 1);
191
192 const topic = await db.topicGetById(dbCtx, topicId);
193 assert(topic.lastPublish);
194 });
195 });
196 step('claims topic fetch', async function () {
197 const claimTimeoutSeconds = 10;
198 const wanted = 5;
199 let topicIds;
200 await db.context(async (dbCtx) => {
201 topicIds = await db.topicFetchClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
202 });
203 assert(topicIds.includes(topicId));
204 });
205 step('incompletes topic fetch', async function () {
206 await db.context(async (dbCtx) => {
207 const result = await db.topicFetchIncomplete(dbCtx, topicId);
208 assert.strictEqual(result.changes, 1);
209 const topic = await db.topicGetById(dbCtx, topicId);
210 assert.strictEqual(Number(topic.contentFetchAttemptsSinceSuccess), 1);
211 });
212 });
213 step('claims topic fetch by id', async function () {
214 const claimTimeoutSeconds = 10;
215 await db.context(async (dbCtx) => {
216 const result = await db.topicFetchClaimById(dbCtx, topicId, claimTimeoutSeconds, claimant);
217 assert.strictEqual(result.changes, 1);
218 });
219 });
220 step('completes topic fetch', async function () {
221 await db.context(async (dbCtx) => {
222 const result = await db.topicFetchComplete(dbCtx, topicId);
223 assert.strictEqual(result.changes, 1);
224 const topic = await db.topicGetById(dbCtx, topicId);
225 assert.strictEqual(Number(topic.contentFetchAttemptsSinceSuccess), 0);
226 });
227 });
228 step('gets publish history', async function () {
229 await db.context(async (dbCtx) => {
230 const result = (await db.topicPublishHistory(dbCtx, topicId, 7))
231 .map((x) => Number(x));
232 const expected = [1, 0, 0, 0, 0, 0, 0];
233 assert.deepStrictEqual(result, expected);
234 });
235 });
236 step('deletes a topic', async function () {
237 await db.context(async (dbCtx) => {
238 const result = await db.topicSet(dbCtx, testData.anotherTopicSet);
239 anotherTopicId = result.lastInsertRowid;
240 await db.topicDeleted(dbCtx, anotherTopicId);
241 const topic = await db.topicGetById(dbCtx, anotherTopicId);
242 assert.strictEqual(topic.isDeleted, true);
243 });
244 });
245 step('update un-deletes a topic', async function () {
246 await db.context(async (dbCtx) => {
247 const result = await db.topicSet(dbCtx, testData.anotherTopicSet);
248 assert.strictEqual(result.lastInsertRowid, anotherTopicId);
249 const topic = await db.topicGetById(dbCtx, anotherTopicId);
250 assert.strictEqual(topic.isDeleted, false);
251 });
252 });
253 step('gets all topics', async function() {
254 await db.context(async (dbCtx) => {
255 const topics = await db.topicGetAll(dbCtx);
256 assert(topics.length);
257 });
258 });
259 // pending delete of deleted topic with no subscriptions
260 step('really deletes unsubscribed deleted topic', async function() {
261 await db.context(async (dbCtx) => {
262 await db.topicDeleted(dbCtx, anotherTopicId);
263 await db.topicPendingDelete(dbCtx, anotherTopicId);
264 const topic = await db.topicGetById(dbCtx, anotherTopicId);
265 assert(!topic);
266 });
267 });
268 }); // Topic
269
270 describe('Subscription', function () {
271 step('requires data', async function () {
272 try {
273 await db.context(async (dbCtx) => {
274 await db.subscriptionUpsert(dbCtx);
275 });
276 assert.fail(noExpectedException);
277 } catch (e) {
278 assert(e instanceof DBErrors.DataValidation);
279 }
280 });
281 step('creates subscription', async function () {
282 const data = {
283 ...testData.subscriptionUpsert,
284 topicId,
285 }
286 await db.context(async (dbCtx) => {
287 const result = await db.subscriptionUpsert(dbCtx, data);
288 assert(result.lastInsertRowid);
289 subscriptionId = result.lastInsertRowid;
290 assert.strictEqual(result.changes, 1);
291 });
292 });
293 step('gets subscription', async function () {
294 await db.context(async (dbCtx) => {
295 const subscription = await db.subscriptionGet(dbCtx, testData.subscriptionUpsert.callback, topicId);
296 assert.strictEqual(subscription.secret, testData.subscriptionUpsert.secret);
297 });
298 });
299 step('gets subscription by id', async function () {
300 await db.context(async (dbCtx) => {
301 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
302 assert.strictEqual(subscription.secret, testData.subscriptionUpsert.secret);
303 });
304 });
305 step('gets subscriptions by topic', async function() {
306 await db.context(async (dbCtx) => {
307 const subscriptions = await db.subscriptionsByTopicId(dbCtx, topicId);
308 assert(subscriptions.length);
309 });
310 });
311 step('count subscriptions', async function () {
312 await db.context(async (dbCtx) => {
313 const count = await db.subscriptionCountByTopicUrl(dbCtx, testData.topicSet.url);
314 assert.strictEqual(Number(count.count), 1);
315 });
316 });
317 step('claim subscription', async function () {
318 const claimTimeoutSeconds = 10;
319 const wanted = 5;
320 let subscriptionIds;
321 await db.context(async (dbCtx) => {
322 subscriptionIds = await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
323 });
324 assert(subscriptionIds.includes(subscriptionId));
325 });
326 step('incompletes subscription', async function () {
327 const { callback } = testData.subscriptionUpsert;
328 await db.context(async (dbCtx) => {
329 await db.subscriptionDeliveryIncomplete(dbCtx, callback, topicId);
330 const topic = await db.subscriptionGetById(dbCtx, subscriptionId);
331 assert.strictEqual(Number(topic.deliveryAttemptsSinceSuccess), 1);
332 });
333 });
334 step('claim subscription by id', async function () {
335 const claimTimeoutSeconds = 10;
336 await db.context(async (dbCtx) => {
337 const result = await db.subscriptionDeliveryClaimById(dbCtx, subscriptionId, claimTimeoutSeconds, claimant);
338 assert.strictEqual(result.changes, 1);
339 });
340 });
341 step('complete subscription', async function () {
342 const { callback } = testData.subscriptionUpsert;
343 await db.context(async (dbCtx) => {
344 const topic = await db.topicGetById(dbCtx, topicId);
345 await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topic.contentUpdated);
346 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
347 assert.strictEqual(Number(subscription.deliveryAttemptsSinceSuccess), 0);
348 });
349 });
350 step('subscription delete', async function () {
351 const { callback } = testData.subscriptionUpsert;
352 await db.context(async (dbCtx) => {
353 const result = await db.subscriptionDelete(dbCtx, callback, topicId);
354 assert.strictEqual(result.changes, 1);
355 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
356 assert(!subscription);
357 });
358 });
359 step('create subscription', async function () {
360 const data = {
361 ...testData.subscriptionUpsert,
362 secret: 'newSecret',
363 topicId,
364 }
365 await db.context(async (dbCtx) => {
366 const result = await db.subscriptionUpsert(dbCtx, data);
367 assert(result.lastInsertRowid);
368 assert.notStrictEqual(result.lastInsertRowid, subscriptionId);
369 subscriptionId = result.lastInsertRowid;
370 assert.strictEqual(result.changes, 1);
371 });
372 });
373 step('update subscription', async function () {
374 const data = {
375 subscriptionId,
376 signatureAlgorithm: 'sha256',
377 };
378 await db.context(async (dbCtx) => {
379 await db.subscriptionUpdate(dbCtx, data);
380 });
381 });
382 step('claim subscription', async function () {
383 const claimTimeoutSeconds = 10;
384 const wanted = 5;
385 let subscriptionIds;
386 await db.context(async (dbCtx) => {
387 subscriptionIds = await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
388 });
389 assert(subscriptionIds.includes(subscriptionId));
390 });
391 step('subscription gone', async function () {
392 const { callback } = testData.subscriptionUpsert;
393 await db.context(async (dbCtx) => {
394 await db.subscriptionDeliveryGone(dbCtx, callback, topicId);
395 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
396 assert(!subscription);
397 });
398 });
399 step('create expired subscription', async function () {
400 const data = {
401 ...testData.subscriptionUpsert,
402 secret: 'newSecret',
403 topicId,
404 leaseSeconds: -1,
405 };
406 await db.context(async (dbCtx) => {
407 const result = await db.subscriptionUpsert(dbCtx, data);
408 assert(result.lastInsertRowid);
409 assert.notStrictEqual(result.lastInsertRowid, subscriptionId);
410 subscriptionId = result.lastInsertRowid;
411 assert.strictEqual(result.changes, 1);
412 });
413 });
414 step('delete expired subscriptions', async function() {
415 await db.context(async (dbCtx) => {
416 await db.subscriptionDeleteExpired(dbCtx, topicId)
417 const subscription = await db.subscriptionGet(dbCtx, testData.subscriptionUpsert.callback, topicId);
418 assert(!subscription);
419 });
420 });
421 }); // Subscription
422
423 describe('Verification', function () {
424 step('requires data', async function() {
425 try {
426 await db.context(async (dbCtx) => {
427 await db.verificationInsert(dbCtx);
428 });
429 assert.fail(noExpectedException);
430 } catch (e) {
431 assert(e instanceof DBErrors.DataValidation);
432 }
433 });
434 step('creates verification', async function() {
435 const verificationData = {
436 ...testData.verificationInsert,
437 topicId,
438 };
439 await db.context(async (dbCtx) => {
440 verificationId = await db.verificationInsert(dbCtx, verificationData);
441 assert(verificationId);
442 });
443 });
444 step('gets verification', async function() {
445 await db.context(async (dbCtx) => {
446 const verification = await db.verificationGetById(dbCtx, verificationId);
447 assert.strictEqual(verification.mode, testData.verificationInsert.mode);
448 });
449 });
450 step('validates verification', async function() {
451 await db.context(async (dbCtx) => {
452 await db.verificationValidated(dbCtx, verificationId);
453 const verification = await db.verificationGetById(dbCtx, verificationId);
454 assert.strictEqual(verification.isPublisherValidated, true);
455 });
456 });
457 step('claims verification', async function() {
458 const claimTimeoutSeconds = 10;
459 const wanted = 5;
460 let verificationIds;
461 await db.context(async (dbCtx) => {
462 verificationIds = await db.verificationClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
463 });
464 assert(verificationIds.includes(verificationId));
465 });
466 step('releases verification', async function() {
467 await db.context(async (dbCtx) => {
468 await db.verificationRelease(dbCtx, verificationId);
469 });
470 });
471 step('updates verification', async function() {
472 const verificationData = {
473 ...testData.verificationUpdate,
474 };
475 await db.context(async (dbCtx) => {
476 db.verificationUpdate(dbCtx, verificationId, verificationData);
477 const verification = await db.verificationGetById(dbCtx, verificationId);
478 assert.strictEqual(verification.isPublisherValidated, testData.verificationUpdate.isPublisherValidated);
479 });
480 });
481 step('claims verification by id', async function() {
482 const claimTimeoutSeconds = 10;
483 await db.context(async (dbCtx) => {
484 const result = await db.verificationClaimById(dbCtx, verificationId, claimTimeoutSeconds, claimant);
485 assert.strictEqual(result.changes, 1);
486 });
487 });
488 step('incompletes verification', async function() {
489 await db.context(async (dbCtx) => {
490 await db.verificationIncomplete(dbCtx, verificationId);
491 });
492 });
493 step('claims verification by id', async function() {
494 const claimTimeoutSeconds = 10;
495 await db.context(async (dbCtx) => {
496 const result = await db.verificationClaimById(dbCtx, verificationId, claimTimeoutSeconds, claimant);
497 assert.strictEqual(result.changes, 1);
498 });
499 });
500 step('completes verification', async function() {
501 await db.context(async (dbCtx) => {
502 const verification = await db.verificationGetById(dbCtx, verificationId);
503 await db.subscriptionUpsert(dbCtx, verification);
504 await db.verificationComplete(dbCtx, verificationId, testData.verificationInsert.callback, topicId);
505 const count = await db.subscriptionCountByTopicUrl(dbCtx, testData.topicSet.url);
506 assert.strictEqual(Number(count.count), 1);
507 });
508 });
509
510 }); // Verification
511
512 }); // specific implementation
513 }); // foreach
514
515 }); // Database Integration