update dependencies, fixes to support new authentication features
[websub-hub] / test / src / db / sqlite.js
index 0c96df364d1d4d5ecfb98b6de6e2a021a3c92a05..33f00282d9d80ff9b1e1d2278afe4e8cebdf4481 100644 (file)
@@ -1,12 +1,8 @@
-/* eslint-disable sonarjs/no-identical-functions */
-/* eslint-env mocha */
-/* eslint-disable sonarjs/no-duplicate-string */
 'use strict';
 
 /* This provides implementation coverage, stubbing parts of better-sqlite3. */
 
-const assert = require('assert');
-// eslint-disable-next-line node/no-unpublished-require
+const assert = require('node:assert');
 const sinon = require('sinon');
 const DBStub = require('../../stub-db');
 const stubLogger = require('../../stub-logger');
@@ -47,6 +43,12 @@ describe('DatabaseSQLite', function () {
     sinon.restore();
   });
 
+  it('covers options', function () {
+    const xoptions = new Config('test');
+    delete xoptions.db.connectionString;
+    db = new DB(stubLogger, xoptions);
+  });
+
   // Ensure all interface methods are implemented
   describe('Implementation', function () {
     it('implements interface', async function () {
@@ -66,6 +68,38 @@ describe('DatabaseSQLite', function () {
     });
   }); // Implementation
 
+  describe('_initTables', function () {
+    let preparedGet;
+    beforeEach(function () {
+      preparedGet = sinon.stub();
+      sinon.stub(db.db, 'prepare').returns({
+        pluck: () => ({
+          bind: () => ({
+            get: preparedGet,
+          }),
+        }),
+      });
+      sinon.stub(db, '_currentSchema').returns(db.schemaVersionsSupported.min);
+      sinon.stub(db.db, 'exec');
+    });
+    it('covers migration', async function() {
+      preparedGet.returns({});
+      await db._initTables();
+      assert(db.db.exec.called);
+    });
+    it('covers migration failure', async function() {
+      const expected = new Error('oh no');
+      preparedGet.returns({});
+      db.db.exec.throws(expected);
+      try {
+        await db._initTables();
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // _initTables
+
   describe('_currentSchema', function () {
     it('covers', async function () {
       const version = { major: 1, minor: 0, patch: 0 };
@@ -205,7 +239,7 @@ describe('DatabaseSQLite', function () {
         contentFetchNextAttempt: now,
         contentUpdated: now,
         url: topic.url,
-      }
+      };
       const result = DB._topicDataToNative(topic);
       assert.deepStrictEqual(result, expected);
     });
@@ -309,10 +343,11 @@ describe('DatabaseSQLite', function () {
   }); // authenticationGet
 
   describe('authenticationUpsert', function () {
-    let identifier, credential;
+    let identifier, credential, otpKey;
     beforeEach(function () {
       identifier = 'username';
       credential = '$z$foo';
+      otpKey = '12345678901234567890123456789012';
     });
     it('success', async function() {
       const dbResult = {
@@ -320,7 +355,7 @@ describe('DatabaseSQLite', function () {
         lastInsertRowid: undefined,
       };
       sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult);
-      await db.authenticationUpsert(dbCtx, identifier, credential);
+      await db.authenticationUpsert(dbCtx, identifier, credential, otpKey);
     });
     it('failure', async function () {
       const dbResult = {
@@ -329,7 +364,65 @@ describe('DatabaseSQLite', function () {
       };
       sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult);
       try {
-        await db.authenticationUpsert(dbCtx, identifier, credential);
+        await db.authenticationUpsert(dbCtx, identifier, credential, otpKey);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // authenticationUpsert
+
+  describe('authenticationUpdateCredential', function () {
+    let identifier, credential;
+    beforeEach(function () {
+      identifier = 'username';
+      credential = '$z$foo';
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult);
+      await db.authenticationUpdateCredential(dbCtx, identifier, credential);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult);
+      try {
+        await db.authenticationUpdateCredential(dbCtx, identifier, credential);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // authenticationUpdateCredential
+
+  describe('authenticationUpdateOTPKey', function () {
+    let identifier, otpKey;
+    beforeEach(function () {
+      identifier = 'username';
+      otpKey = '12345678901234567890123456789012';
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult);
+      await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult);
+      try {
+        await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
         assert.fail(noExpectedException);
       } catch (e) {
         assert(e instanceof DBErrors.UnexpectedResult);
@@ -404,8 +497,34 @@ describe('DatabaseSQLite', function () {
     });
   }); // subscriptionDelete
 
+  describe('subscriptionDeleteExpired', function () {
+    it('success', async function () {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const expected = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.subscriptionDeleteExpired, 'run').returns(dbResult);
+      const result = await db.subscriptionDeleteExpired(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.subscriptionDeleteExpired, 'run').throws(expected);
+      try {
+        await db.subscriptionDeleteExpired(dbCtx, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // subscriptionDeleteExpired
+
   describe('subscriptionDeliveryClaim', function () {
-    it('success', async function() {
+    it('success', async function () {
       const dbAllResult = [
         {
           id: 'c2e254c5-aa6e-4a8f-b1a1-e474b07392bb',
@@ -468,13 +587,17 @@ describe('DatabaseSQLite', function () {
   }); // subscriptionDeliveryClaimById
 
   describe('subscriptionDeliveryComplete', function () {
+    let topicContentUpdated;
+    before(function () {
+      topicContentUpdated = new Date();
+    });
     it('success', async function() {
       const dbResult = {
         changes: 1,
       };
       sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult);
       sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
-      await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+      await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
     });
     it('failure', async function () {
       const dbResult = {
@@ -483,7 +606,7 @@ describe('DatabaseSQLite', function () {
       sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult);
       sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
       try {
-        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
         assert.fail(noExpectedException);
       } catch (e) {
         assert(e instanceof DBErrors.UnexpectedResult);
@@ -499,7 +622,7 @@ describe('DatabaseSQLite', function () {
       sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult0);
       sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult1);
       try {
-        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
         assert.fail(noExpectedException);
       } catch (e) {
         assert(e instanceof DBErrors.UnexpectedResult);
@@ -819,7 +942,7 @@ describe('DatabaseSQLite', function () {
       const dbResult1 = {
         changes: 1,
         lastInsertRowid: undefined,
-      }
+      };
       const expected = {
         changes: 1,
         lastInsertRowid: undefined,
@@ -839,7 +962,7 @@ describe('DatabaseSQLite', function () {
       const dbResult1 = {
         changes: 1,
         lastInsertRowid: undefined,
-      }
+      };
       const expected = {
         changes: 1,
         lastInsertRowid: undefined,
@@ -859,7 +982,7 @@ describe('DatabaseSQLite', function () {
       const dbResult1 = {
         changes: 0,
         lastInsertRowid: undefined,
-      }
+      };
       sinon.stub(db.statement.topicAttempts, 'get').returns(dbGet);
       sinon.stub(db.statement.topicAttemptsIncrement, 'run').returns(dbResult0);
       sinon.stub(db.statement.topicContentFetchDone, 'run').returns(dbResult1);
@@ -879,7 +1002,7 @@ describe('DatabaseSQLite', function () {
       const dbResult1 = {
         changes: 0,
         lastInsertRowid: undefined,
-      }
+      };
       sinon.stub(db.statement.topicAttempts, 'get').returns(dbGet);
       sinon.stub(db.statement.topicAttemptsIncrement, 'run').returns(dbResult0);
       sinon.stub(db.statement.topicContentFetchDone, 'run').returns(dbResult1);
@@ -984,6 +1107,12 @@ describe('DatabaseSQLite', function () {
       const result = await db.topicGetByUrl(dbCtx, topicUrl);
       assert.deepStrictEqual(result, expected);
     });
+    it('success, no defaults', async function() {
+      const expected = [];
+      sinon.stub(db.statement.topicGetByUrl, 'get').returns(expected);
+      const result = await db.topicGetByUrl(dbCtx, topicUrl, false);
+      assert.deepStrictEqual(result, expected);
+    });
     it('failure', async function () {
       const expected = new Error();
       sinon.stub(db.statement.topicGetByUrl, 'get').throws(expected);
@@ -1021,6 +1150,91 @@ describe('DatabaseSQLite', function () {
     });
   }); // topicGetContentById
 
+  describe('topicPendingDelete', function () {
+    beforeEach(function () {
+      sinon.stub(db.statement.topicGetById, 'get');
+      sinon.stub(db.statement.subscriptionCountByTopicUrl, 'get');
+      sinon.stub(db.statement.topicDeleteById, 'run');
+    });
+    it('success', async function () {
+      db.statement.topicGetById.get.returns({
+        id: topicId,
+        isDeleted: true,
+      });
+      db.statement.subscriptionCountByTopicUrl.get.returns({
+        count: 0,
+      });
+      db.statement.topicDeleteById.run.returns({
+        changes: 1,
+      });
+      db.topicPendingDelete(dbCtx, topicId);
+      assert(db.statement.topicDeleteById.run.called);
+    });
+    it('does not delete non-deleted topic', async function () {
+      db.statement.topicGetById.get.returns({
+        id: topicId,
+        isDeleted: false,
+      });
+      db.statement.subscriptionCountByTopicUrl.get.returns({
+        count: 0,
+      });
+      db.statement.topicDeleteById.run.returns({
+        changes: 1,
+      });
+      db.topicPendingDelete(dbCtx, topicId);
+      assert(!db.statement.topicDeleteById.run.called);
+    });
+    it('does not delete topic with active subscriptions', async function () {
+      db.statement.topicGetById.get.returns({
+        id: topicId,
+        isDeleted: true,
+      });
+      db.statement.subscriptionCountByTopicUrl.get.returns({
+        count: 10,
+      });
+      db.statement.topicDeleteById.run.returns({
+        changes: 1,
+      });
+      db.topicPendingDelete(dbCtx, topicId);
+      assert(!db.statement.topicDeleteById.run.called);
+    });
+    it('covers no deletion', async function () {
+      db.statement.topicGetById.get.returns({
+        id: topicId,
+        isDeleted: true,
+      });
+      db.statement.subscriptionCountByTopicUrl.get.returns({
+        count: 0,
+      });
+      db.statement.topicDeleteById.run.returns({
+        changes: 0,
+      });
+      try {
+        db.topicPendingDelete(dbCtx, topicId);
+        assert.fail(noExpectedException);
+
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+      assert(db.statement.topicDeleteById.run.called);
+    });
+  }); // topicPendingDelete
+
+  describe('topicPublishHistory', function () {
+    beforeEach(function () {
+      sinon.stub(db.statement.topicPublishHistory, 'all');
+    });
+    it('success', function () {
+      db.statement.topicPublishHistory.all.returns([
+        { daysAgo: 1, contentUpdates: 1 },
+        { daysAgo: 3, contentUpdates: 2 },
+      ]);
+      const result = db.topicPublishHistory(dbCtx, topicId, 7);
+      const expected = [0, 1, 0, 2, 0, 0, 0];
+      assert.deepStrictEqual(result, expected);
+    });
+  }); // topicPublishHistory
+
   describe('topicSet', function () {
     let data;
     beforeEach(function () {
@@ -1088,6 +1302,8 @@ describe('DatabaseSQLite', function () {
         contentType: 'text/plain',
         contentHash: 'abc123',
       };
+      sinon.stub(db.statement.topicSetContent, 'run');
+      sinon.stub(db.statement.topicSetContentHistory, 'run');
     });
     it('success', async function() {
       const dbResult = {
@@ -1098,7 +1314,8 @@ describe('DatabaseSQLite', function () {
         changes: 1,
         lastInsertRowid: undefined,
       };
-      sinon.stub(db.statement.topicSetContent, 'run').returns(dbResult);
+      db.statement.topicSetContent.run.returns(dbResult);
+      db.statement.topicSetContentHistory.run.returns(dbResult);
       const result = await db.topicSetContent(dbCtx, data);
       assert.deepStrictEqual(result, expected);
     });
@@ -1107,7 +1324,25 @@ describe('DatabaseSQLite', function () {
         changes: 0,
         lastInsertRowid: undefined,
       };
-      sinon.stub(db.statement.topicSetContent, 'run').returns(dbResult);
+      db.statement.topicSetContent.run.returns(dbResult);
+      try {
+        await db.topicSetContent(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('failure 2', async function () {
+      const dbResultSuccess = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const dbResultFail = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      db.statement.topicSetContent.run.returns(dbResultSuccess);
+      db.statement.topicSetContentHistory.run.returns(dbResultFail);
       try {
         await db.topicSetContent(dbCtx, data);
         assert.fail(noExpectedException);
@@ -1366,7 +1601,7 @@ describe('DatabaseSQLite', function () {
       DB._verificationDataToEngine(data);
       assert.strictEqual(data.isPublisherValidated, 0);
     });
-  }) // _verificationDataToEngine
+  }); // _verificationDataToEngine
 
   describe('verificationInsert', function () {
     let verification;
@@ -1457,7 +1692,7 @@ describe('DatabaseSQLite', function () {
       const dbResult = {
         changes: 0,
         lastInsertRowid: undefined,
-      }
+      };
       sinon.stub(db.statement.verificationUpdate, 'run').returns(dbResult);
       try {
         await db.verificationUpdate(dbCtx, verificationId, data);
@@ -1482,7 +1717,7 @@ describe('DatabaseSQLite', function () {
       const dbResult = {
         changes: 1,
         lastInsertRowid: undefined,
-      }
+      };
       sinon.stub(db.statement.verificationValidate, 'run').returns(dbResult);
       await db.verificationValidated(dbCtx, verificationId);
     });
@@ -1490,7 +1725,7 @@ describe('DatabaseSQLite', function () {
       const dbResult = {
         changes: 0,
         lastInsertRowid: undefined,
-      }
+      };
       sinon.stub(db.statement.verificationValidate, 'run').returns(dbResult);
       try {
         await db.verificationValidated(dbCtx, verificationId);
@@ -1501,4 +1736,4 @@ describe('DatabaseSQLite', function () {
     });
   }); // verificationValidated
 
-}); // DatabasePostgres
+}); // DatabaseSQLite