7eb7d91670295b0f4bafad18f15b2e8bb31e7f56
[websub-hub] / test / src / db / integration.js
1 'use strict';
2
3 /**
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
7 * DATA WIPE.
8 *
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
12 *
13 * These tests are sequential, relying on the state created along the way.
14 *
15 */
16
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');
22
23 describe('Database Integration', function () {
24 const noExpectedException = 'did not receive expected exception';
25 const implementations = [];
26
27 if (!process.env.INTEGRATION_TESTS) {
28 it.skip('integration tests not requested');
29 return;
30 }
31
32 if (process.env.POSTGRES_TEST_PATH) {
33 implementations.push({
34 name: 'PostgreSQL',
35 module: '../../../src/db/postgres',
36 config: {
37 db: {
38 connectionString: `postgresql://${process.env.POSTGRES_TEST_PATH}`,
39 queryLogLevel: 'debug',
40 noWarnings: true,
41 },
42 },
43 });
44 }
45
46 if (process.env.SQLITE_TEST_PATH) {
47 implementations.push({
48 name: 'SQLite',
49 module: '../../../src/db/sqlite',
50 config: {
51 db: {
52 connectionString: `sqlite://${process.env.SQLITE_TEST_PATH}`,
53 queryLogLevel: 'debug',
54 },
55 },
56 });
57 }
58
59 implementations.forEach(function (i) {
60 describe(i.name, function () {
61 let DB, db;
62 let topicId, subscriptionId, verificationId;
63 const claimant = '96bff010-d9e6-11eb-b95d-0025905f714a';
64
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);
72 });
73 after(async function () {
74 await db._closeConnection();
75 });
76 it('instantiated', function () {
77 assert(db);
78 });
79
80 it('is healthy', async function () {
81 const result = await db.healthCheck();
82 assert(result);
83 });
84
85 describe('Authentication', function () {
86 let identifier, credential;
87 beforeEach(function () {
88 identifier = 'username';
89 credential = 'myEncryptedPassword';
90 });
91 step('create auth entry', async function() {
92 await db.context(async (dbCtx) => {
93 await db.authenticationUpsert(dbCtx, identifier, credential);
94 });
95 });
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);
100 });
101 });
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);
107 });
108 });
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);
115 });
116 });
117 }); // Authentication
118
119 describe('Topic', function () {
120 let anotherTopicId;
121 step('requires data', async function () {
122 try {
123 await db.context(async (dbCtx) => {
124 await db.topicSet(dbCtx);
125 });
126 assert.fail(noExpectedException);
127 } catch (e) {
128 assert(e instanceof DBErrors.DataValidation);
129 }
130 });
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);
136 });
137 });
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);
142 });
143 });
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);
148 });
149 });
150 step('also updates topic', async function () {
151 const data = {
152 topicId,
153 leaseSecondsMin: 60,
154 };
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);
163 });
164 });
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);
170 });
171 });
172 step('sets topic content', async function () {
173 const data = {
174 ...testData.topicSetContent,
175 topicId,
176 };
177 await db.context(async (dbCtx) => {
178 const result = await db.topicSetContent(dbCtx, data);
179 assert.strictEqual(result.changes, 1);
180 });
181 });
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);
186 });
187 });
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);
192
193 const topic = await db.topicGetById(dbCtx, topicId);
194 assert(topic.lastPublish);
195 });
196 });
197 step('claims topic fetch', async function () {
198 const claimTimeoutSeconds = 10;
199 const wanted = 5;
200 let topicIds;
201 await db.context(async (dbCtx) => {
202 topicIds = await db.topicFetchClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
203 });
204 assert(topicIds.includes(topicId));
205 });
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);
212 });
213 });
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);
219 });
220 });
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);
227 });
228 });
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);
235 });
236 });
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);
244 });
245 });
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);
252 });
253 });
254 step('gets all topics', async function() {
255 await db.context(async (dbCtx) => {
256 const topics = await db.topicGetAll(dbCtx);
257 assert(topics.length);
258 });
259 });
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);
266 assert(!topic);
267 });
268 });
269 }); // Topic
270
271 describe('Subscription', function () {
272 step('requires data', async function () {
273 try {
274 await db.context(async (dbCtx) => {
275 await db.subscriptionUpsert(dbCtx);
276 });
277 assert.fail(noExpectedException);
278 } catch (e) {
279 assert(e instanceof DBErrors.DataValidation);
280 }
281 });
282 step('creates subscription', async function () {
283 const data = {
284 ...testData.subscriptionUpsert,
285 topicId,
286 };
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);
292 });
293 });
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);
298 });
299 });
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);
304 });
305 });
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);
310 });
311 });
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);
316 });
317 });
318 step('claim subscription', async function () {
319 const claimTimeoutSeconds = 10;
320 const wanted = 5;
321 let subscriptionIds;
322 await db.context(async (dbCtx) => {
323 subscriptionIds = await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
324 });
325 assert(subscriptionIds.includes(subscriptionId));
326 });
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);
333 });
334 });
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);
340 });
341 });
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);
349 });
350 });
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);
358 });
359 });
360 step('create subscription', async function () {
361 const data = {
362 ...testData.subscriptionUpsert,
363 secret: 'newSecret',
364 topicId,
365 };
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);
372 });
373 });
374 step('update subscription', async function () {
375 const data = {
376 subscriptionId,
377 signatureAlgorithm: 'sha256',
378 };
379 await db.context(async (dbCtx) => {
380 await db.subscriptionUpdate(dbCtx, data);
381 });
382 });
383 step('claim subscription', async function () {
384 const claimTimeoutSeconds = 10;
385 const wanted = 5;
386 let subscriptionIds;
387 await db.context(async (dbCtx) => {
388 subscriptionIds = await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
389 });
390 assert(subscriptionIds.includes(subscriptionId));
391 });
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);
398 });
399 });
400 step('create expired subscription', async function () {
401 const data = {
402 ...testData.subscriptionUpsert,
403 secret: 'newSecret',
404 topicId,
405 leaseSeconds: -1,
406 };
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);
413 });
414 });
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);
420 });
421 });
422 }); // Subscription
423
424 describe('Verification', function () {
425 step('requires data', async function() {
426 try {
427 await db.context(async (dbCtx) => {
428 await db.verificationInsert(dbCtx);
429 });
430 assert.fail(noExpectedException);
431 } catch (e) {
432 assert(e instanceof DBErrors.DataValidation);
433 }
434 });
435 step('creates verification', async function() {
436 const verificationData = {
437 ...testData.verificationInsert,
438 topicId,
439 };
440 await db.context(async (dbCtx) => {
441 verificationId = await db.verificationInsert(dbCtx, verificationData);
442 assert(verificationId);
443 });
444 });
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);
449 });
450 });
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);
456 });
457 });
458 step('claims verification', async function() {
459 const claimTimeoutSeconds = 10;
460 const wanted = 5;
461 let verificationIds;
462 await db.context(async (dbCtx) => {
463 verificationIds = await db.verificationClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
464 });
465 assert(verificationIds.includes(verificationId));
466 });
467 step('releases verification', async function() {
468 await db.context(async (dbCtx) => {
469 await db.verificationRelease(dbCtx, verificationId);
470 });
471 });
472 step('updates verification', async function() {
473 const verificationData = {
474 ...testData.verificationUpdate,
475 };
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);
480 });
481 });
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);
487 });
488 });
489 step('incompletes verification', async function() {
490 await db.context(async (dbCtx) => {
491 await db.verificationIncomplete(dbCtx, verificationId);
492 });
493 });
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);
499 });
500 });
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);
508 });
509 });
510
511 }); // Verification
512
513 }); // specific implementation
514 }); // foreach
515
516 }); // Database Integration