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