8d5b61553d24f3b72e4ec6d39fd73534de7617cd
[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 await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
332 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
333 assert.strictEqual(Number(subscription.deliveryAttemptsSinceSuccess), 0);
334 });
335 });
336 step('subscription delete', async function () {
337 const { callback } = testData.subscriptionUpsert;
338 await db.context(async (dbCtx) => {
339 const result = await db.subscriptionDelete(dbCtx, callback, topicId);
340 assert.strictEqual(result.changes, 1);
341 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
342 assert(!subscription);
343 });
344 });
345 step('create subscription', async function () {
346 const data = {
347 ...testData.subscriptionUpsert,
348 secret: 'newSecret',
349 topicId,
350 }
351 await db.context(async (dbCtx) => {
352 const result = await db.subscriptionUpsert(dbCtx, data);
353 assert(result.lastInsertRowid);
354 assert.notStrictEqual(result.lastInsertRowid, subscriptionId);
355 subscriptionId = result.lastInsertRowid;
356 assert.strictEqual(result.changes, 1);
357 });
358 });
359 step('update subscription', async function () {
360 const data = {
361 subscriptionId,
362 signatureAlgorithm: 'sha256',
363 };
364 await db.context(async (dbCtx) => {
365 await db.subscriptionUpdate(dbCtx, data);
366 });
367 });
368 step('claim subscription', async function () {
369 const claimTimeoutSeconds = 10;
370 const wanted = 5;
371 let subscriptionIds;
372 await db.context(async (dbCtx) => {
373 subscriptionIds = await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
374 });
375 assert(subscriptionIds.includes(subscriptionId));
376 });
377 step('subscription gone', async function () {
378 const { callback } = testData.subscriptionUpsert;
379 await db.context(async (dbCtx) => {
380 await db.subscriptionDeliveryGone(dbCtx, callback, topicId);
381 const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
382 assert(!subscription);
383 });
384 });
385 step('create expired subscription', async function () {
386 const data = {
387 ...testData.subscriptionUpsert,
388 secret: 'newSecret',
389 topicId,
390 leaseSeconds: -1,
391 };
392 await db.context(async (dbCtx) => {
393 const result = await db.subscriptionUpsert(dbCtx, data);
394 assert(result.lastInsertRowid);
395 assert.notStrictEqual(result.lastInsertRowid, subscriptionId);
396 subscriptionId = result.lastInsertRowid;
397 assert.strictEqual(result.changes, 1);
398 });
399 });
400 step('delete expired subscriptions', async function() {
401 await db.context(async (dbCtx) => {
402 await db.subscriptionDeleteExpired(dbCtx, topicId)
403 const subscription = await db.subscriptionGet(dbCtx, testData.subscriptionUpsert.callback, topicId);
404 assert(!subscription);
405 });
406 });
407 }); // Subscription
408
409 describe('Verification', function () {
410 step('requires data', async function() {
411 try {
412 await db.context(async (dbCtx) => {
413 await db.verificationInsert(dbCtx);
414 });
415 assert.fail(noExpectedException);
416 } catch (e) {
417 assert(e instanceof DBErrors.DataValidation);
418 }
419 });
420 step('creates verification', async function() {
421 const verificationData = {
422 ...testData.verificationInsert,
423 topicId,
424 };
425 await db.context(async (dbCtx) => {
426 verificationId = await db.verificationInsert(dbCtx, verificationData);
427 assert(verificationId);
428 });
429 });
430 step('gets verification', async function() {
431 await db.context(async (dbCtx) => {
432 const verification = await db.verificationGetById(dbCtx, verificationId);
433 assert.strictEqual(verification.mode, testData.verificationInsert.mode);
434 });
435 });
436 step('validates verification', async function() {
437 await db.context(async (dbCtx) => {
438 await db.verificationValidated(dbCtx, verificationId);
439 const verification = await db.verificationGetById(dbCtx, verificationId);
440 assert.strictEqual(verification.isPublisherValidated, true);
441 });
442 });
443 step('claims verification', async function() {
444 const claimTimeoutSeconds = 10;
445 const wanted = 5;
446 let verificationIds;
447 await db.context(async (dbCtx) => {
448 verificationIds = await db.verificationClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
449 });
450 assert(verificationIds.includes(verificationId));
451 });
452 step('releases verification', async function() {
453 await db.context(async (dbCtx) => {
454 await db.verificationRelease(dbCtx, verificationId);
455 });
456 });
457 step('updates verification', async function() {
458 const verificationData = {
459 ...testData.verificationUpdate,
460 };
461 await db.context(async (dbCtx) => {
462 db.verificationUpdate(dbCtx, verificationId, verificationData);
463 const verification = await db.verificationGetById(dbCtx, verificationId);
464 assert.strictEqual(verification.isPublisherValidated, testData.verificationUpdate.isPublisherValidated);
465 });
466 });
467 step('claims verification by id', async function() {
468 const claimTimeoutSeconds = 10;
469 await db.context(async (dbCtx) => {
470 const result = await db.verificationClaimById(dbCtx, verificationId, claimTimeoutSeconds, claimant);
471 assert.strictEqual(result.changes, 1);
472 });
473 });
474 step('incompletes verification', async function() {
475 await db.context(async (dbCtx) => {
476 await db.verificationIncomplete(dbCtx, verificationId);
477 });
478 });
479 step('claims verification by id', async function() {
480 const claimTimeoutSeconds = 10;
481 await db.context(async (dbCtx) => {
482 const result = await db.verificationClaimById(dbCtx, verificationId, claimTimeoutSeconds, claimant);
483 assert.strictEqual(result.changes, 1);
484 });
485 });
486 step('completes verification', async function() {
487 await db.context(async (dbCtx) => {
488 const verification = await db.verificationGetById(dbCtx, verificationId);
489 await db.subscriptionUpsert(dbCtx, verification);
490 await db.verificationComplete(dbCtx, verificationId, testData.verificationInsert.callback, topicId);
491 const count = await db.subscriptionCountByTopicUrl(dbCtx, testData.topicSet.url);
492 assert.strictEqual(Number(count.count), 1);
493 });
494 });
495
496 }); // Verification
497
498 }); // specific implementation
499 }); // foreach
500
501 }); // Database Integration