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