update dependencies, fixes to support new authentication features
[websub-hub] / test / src / db / postgres.js
index 5df49fd662680fb2137adc32d8f7c77f2da7d986..825a295fc93a2e7bdd515755b2ca6348f9b0ba1f 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 pg-promise. */
 
-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');
@@ -65,11 +61,18 @@ describe('DatabasePostgres', function () {
     httpRemoteAddr = '127.0.0.1';
     httpFrom = 'user@example.com';
     wanted = 5;
-});
+  });
   afterEach(function () {
     sinon.restore();
   });
 
+  it('covers listener', function () {
+    const listenerOptions = new Config('test');
+    listenerOptions.db.cacheEnabled = true;
+    const listenerDb = new DB(stubLogger, listenerOptions, pgpStub);
+    assert(listenerDb);
+  });
+
   // Ensure all interface methods are implemented
   describe('Implementation', function () {
     it('implements interface', async function () {
@@ -104,6 +107,11 @@ describe('DatabasePostgres', function () {
         db.pgpInitOptions.query(event);
         assert(db.logger.debug.called);
       });
+      it('covers NOTIFY', function () {
+        const event = { query: 'NOTIFY thing' };
+        db.pgpInitOptions.query(event);
+        assert(!db.logger.debug.called);
+      });
     }); // query
     describe('receive', function () {
       it('covers', function () {
@@ -129,10 +137,39 @@ describe('DatabasePostgres', function () {
             columnTwo: 4,
           },
         ];
-        db.pgpInitOptions.receive(data, result, event)
+        db.pgpInitOptions.receive({ data, result, ctx: event });
         assert(db.logger.debug.called);
         assert.deepStrictEqual(data, expectedData);
       });
+      it('covers NOTIFY', function () {
+        const data = [
+          {
+            column_one: 'one', // eslint-disable-line camelcase
+            column_two: 2, // eslint-disable-line camelcase
+          },
+          {
+            column_one: 'foo', // eslint-disable-line camelcase
+            column_two: 4, // eslint-disable-line camelcase
+          },
+        ];
+        const result = {
+          command: 'NOTIFY',
+        };
+        const event = {};
+        const expectedData = [
+          {
+            columnOne: 'one',
+            columnTwo: 2,
+          },
+          {
+            columnOne: 'foo',
+            columnTwo: 4,
+          },
+        ];
+        db.pgpInitOptions.receive({ data, result, ctx: event });
+        assert(!db.logger.debug.called);
+        assert.deepStrictEqual(data, expectedData);
+      });
     }); // receive
   }); // pgpInitOptions
 
@@ -155,17 +192,20 @@ describe('DatabasePostgres', function () {
     });
   }); // _initTables
 
-  describe('schemaCheck', function () {
+  describe('initialize', function () {
+    after(function () {
+      delete db.listener;
+    });
     it('passes supported version', async function () {
       const version = { major: 1, minor: 0, patch: 0 };
       sinon.stub(db.db, 'one').resolves(version);
-      await db.schemaCheck(false);
+      await db.initialize(false);
     });
     it('fails low version', async function () {
       const version = { major: 0, minor: 0, patch: 0 };
       sinon.stub(db.db, 'one').resolves(version);
       try {
-        await db.schemaCheck(false);
+        await db.initialize(false);
         assert.fail(noExpectedException);
       } catch (e) {
         assert(e instanceof DBErrors.MigrationNeeded);
@@ -175,7 +215,7 @@ describe('DatabasePostgres', function () {
       const version = { major: 100, minor: 100, patch: 100 };
       sinon.stub(db.db, 'one').resolves(version);
       try {
-        await db.schemaCheck(false);
+        await db.initialize(false);
         assert.fail(noExpectedException);
       } catch (e) {
         assert(e instanceof DBErrors.MigrationNeeded);
@@ -183,12 +223,34 @@ describe('DatabasePostgres', function () {
     });
     it('covers migration', async function() {
       sinon.stub(db.db, 'oneOrNone').resolves({});
-      sinon.stub(db.db, 'multiResult');
-      sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.max);
+      sinon.stub(db.db, 'multiResult').resolves({});
+      sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.min);
       sinon.stub(db.db, 'one').resolves(db.schemaVersionsSupported.max);
-      await db.schemaCheck();
+      await db.initialize();
     });
-  }); // schemaCheck
+    it('covers migration failure', async function() {
+      const expected = new Error('oh no');
+      sinon.stub(db.db, 'oneOrNone').resolves({});
+      sinon.stub(db.db, 'multiResult').rejects(expected);
+      sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.min);
+      sinon.stub(db.db, 'one').resolves(db.schemaVersionsSupported.max);
+      try {
+        await db.initialize();
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+    it('covers listener', async function() {
+      db.listener = {
+        start: sinon.stub(),
+      };
+      const version = { major: 1, minor: 0, patch: 0 };
+      sinon.stub(db.db, 'one').resolves(version);
+      await db.initialize(false);
+      assert(db.listener.start.called);
+    });
+  }); // initialize
 
   describe('healthCheck', function () {
     beforeEach(function () {
@@ -228,6 +290,9 @@ describe('DatabasePostgres', function () {
   }); // _queryFileHelper
 
   describe('_closeConnection', function () {
+    after(function () {
+      delete db.listener;
+    });
     it('success', async function () {
       sinon.stub(db._pgp, 'end');
       await db._closeConnection();
@@ -243,6 +308,14 @@ describe('DatabasePostgres', function () {
         assert.deepStrictEqual(e, expected);
       }
     });
+    it('covers listener', async function () {
+      db.listener = {
+        stop: sinon.stub(),
+      };
+      sinon.stub(db._pgp, 'end');
+      await db._closeConnection();
+      assert(db._pgp.end.called);
+    });
   }); // _closeConnection
 
   describe('_purgeTables', function () {
@@ -268,6 +341,84 @@ describe('DatabasePostgres', function () {
     });
   }); // _purgeTables
 
+  describe('_topicChanged', function () {
+    beforeEach(function () {
+      db.cache = new Map();
+      sinon.stub(db.cache, 'delete');
+    });
+    after(function () {
+      delete db.cache;
+    });
+    it('covers', function () {
+      db._topicChanged('topic-id');
+      assert(db.cache.delete.called);
+    });
+    it('ignores ping', function () {
+      db._topicChanged('ping');
+      assert(!db.cache.delete.called);
+    });
+  }); // _topicChanged
+
+  describe('_listenerEstablished', function () {
+    it('creates cache', function () {
+      delete db.cache;
+      db._listenerEstablished();
+      assert(db.cache instanceof Map);
+    });
+  }); // _listenerEstablished
+
+  describe('_listenerLost', function () {
+    it('removes cache', function () {
+      db.cache = new Map();
+      db._listenerLost();
+      assert(!db.cache);
+    });
+  }); // _listenerLost
+
+  describe('_cacheGet', function () {
+    let key;
+    beforeEach(function () {
+      key = 'key';
+    });
+    it('nothing if no cache', function () {
+      delete db.cache;
+      const result = db._cacheGet(key);
+      assert.strictEqual(result, undefined);
+    });
+    it('nothing if no entry', function () {
+      db.cache = new Map();
+      const result = db._cacheGet(key);
+      assert.strictEqual(result, undefined);
+    });
+    it('returns cached entry', function () {
+      db.cache = new Map();
+      const expected = {
+        foo: 'bar',
+      };
+      db._cacheSet(key, expected);
+      const result = db._cacheGet(key);
+      assert.deepStrictEqual(result, expected);
+    });
+  }); // _cacheGet
+
+  describe('_cacheSet', function () {
+    let key;
+    beforeEach(function () {
+      key = 'key';
+    });
+    it('covers no cache', function () {
+      delete db.cache;
+      db._cacheSet(key, 'data');
+    });
+    it('covers cache', function () {
+      db.cache = new Map();
+      const expected = 'blah';
+      db._cacheSet(key, expected);
+      const result = db._cacheGet(key);
+      assert.deepStrictEqual(result, expected);
+    });
+  }); // _cacheSet
+
   describe('context', function () {
     it('covers', async function () {
       await db.context(common.nop);
@@ -335,10 +486,11 @@ describe('DatabasePostgres', 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 = {
@@ -347,7 +499,7 @@ describe('DatabasePostgres', function () {
         duration: 22,
       };
       sinon.stub(db.db, 'result').resolves(dbResult);
-      await db.authenticationUpsert(dbCtx, identifier, credential);
+      await db.authenticationUpsert(dbCtx, identifier, credential, otpKey);
     });
     it('failure', async function() {
       credential = undefined;
@@ -358,7 +510,7 @@ describe('DatabasePostgres', function () {
       };
       sinon.stub(db.db, 'result').resolves(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);
@@ -366,6 +518,68 @@ describe('DatabasePostgres', function () {
     });
   }); // authenticationUpsert
 
+  describe('authenticationUpdateCredential', function () {
+    let identifier, credential;
+    beforeEach(function () {
+      identifier = 'username';
+    });
+    it('success', async function () {
+      const dbResult = {
+        rowCount: 1,
+        rows: undefined,
+        duration: 22,
+      };
+      sinon.stub(db.db, 'result').resolves(dbResult);
+      await db.authenticationUpdateCredential(dbCtx, identifier, credential);
+    });
+    it('failure', async function() {
+      credential = undefined;
+      const dbResult = {
+        rowCount: 0,
+        rows: undefined,
+        duration: 22,
+      };
+      sinon.stub(db.db, 'result').resolves(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 = {
+        rowCount: 1,
+        rows: undefined,
+        duration: 22,
+      };
+      sinon.stub(db.db, 'result').resolves(dbResult);
+      await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
+    });
+    it('failure', async function() {
+      const dbResult = {
+        rowCount: 0,
+        rows: undefined,
+        duration: 22,
+      };
+      sinon.stub(db.db, 'result').resolves(dbResult);
+      try {
+        await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // authenticationUpdateOTPKey
+
   describe('subscriptionsByTopicId', function () {
     it('success', async function () {
       const expected = [];
@@ -415,7 +629,7 @@ describe('DatabasePostgres', function () {
         changes: 1,
         lastInsertRowid: undefined,
         duration: 10,
-      }
+      };
       sinon.stub(db.db, 'result').resolves(dbResult);
       const result = await db.subscriptionDelete(dbCtx, callback, topicId);
       assert.deepStrictEqual(result, expected);
@@ -432,6 +646,34 @@ describe('DatabasePostgres', function () {
     });
   }); // subscriptionDelete
 
+  describe('subscriptionDeleteExpired', function () {
+    it('success', async function () {
+      const dbResult = {
+        rowCount: 1,
+        rows: [],
+        duration: 10,
+      };
+      const expected = {
+        changes: 1,
+        lastInsertRowid: undefined,
+        duration: 10,
+      };
+      sinon.stub(db.db, 'result').resolves(dbResult);
+      const result = await db.subscriptionDeleteExpired(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function() {
+      const expected = new Error();
+      sinon.stub(db.db, 'result').rejects(expected);
+      try {
+        await db.subscriptionDeleteExpired(dbCtx, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  });
+
   describe('subscriptionDeliveryClaim', function () {
     it('success', async function() {
       const dbResult = [
@@ -448,7 +690,7 @@ describe('DatabasePostgres', function () {
       const expected = new Error();
       sinon.stub(db.db, 'manyOrNone').throws(expected);
       try {
-        await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant );
+        await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
         assert.fail(noExpectedException);
       } catch (e) {
         assert.deepStrictEqual(e, expected);
@@ -467,7 +709,7 @@ describe('DatabasePostgres', function () {
         changes: 1,
         lastInsertRowid: 'c2e254c5-aa6e-4a8f-b1a1-e474b07392bb',
         duration: 11,
-      }
+      };
       sinon.stub(db.db, 'result').resolves(dbResult);
       const result = await db.subscriptionDeliveryClaimById(dbCtx, subscriptionId, claimTimeoutSeconds, claimant);
       assert.deepStrictEqual(result, expected);
@@ -489,12 +731,16 @@ describe('DatabasePostgres', function () {
   }); // subscriptionDeliveryClaimById
 
   describe('subscriptionDeliveryComplete', function () {
+    let topicContentUpdated;
+    before(function () {
+      topicContentUpdated = new Date();
+    });
     it('success', async function() {
       const dbResult = {
         rowCount: 1,
       };
       sinon.stub(db.db, 'result').resolves(dbResult);
-      await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+      await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
     });
     it('failure', async function () {
       const dbResult = {
@@ -502,7 +748,7 @@ describe('DatabasePostgres', function () {
       };
       sinon.stub(db.db, 'result').onCall(0).resolves(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);
@@ -517,7 +763,7 @@ describe('DatabasePostgres', function () {
       };
       sinon.stub(db.db, 'result').onCall(0).resolves(dbResult0).onCall(1).resolves(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);
@@ -839,7 +1085,7 @@ describe('DatabasePostgres', function () {
         rowCount: 1,
         rows: [],
         duration: 10,
-      }
+      };
       const expected = {
         changes: 1,
         lastInsertRowid: undefined,
@@ -861,7 +1107,7 @@ describe('DatabasePostgres', function () {
         rowCount: 1,
         rows: [],
         duration: 10,
-      }
+      };
       const expected = {
         changes: 1,
         lastInsertRowid: undefined,
@@ -883,7 +1129,7 @@ describe('DatabasePostgres', function () {
         rowCount: 0,
         rows: [],
         duration: 10,
-      }
+      };
       sinon.stub(db.db, 'one').resolves(dbOne);
       sinon.stub(db.db, 'result').onCall(0).resolves(dbResult0).onCall(1).resolves(dbResult1);
       try {
@@ -904,7 +1150,7 @@ describe('DatabasePostgres', function () {
         rowCount: 0,
         rows: [],
         duration: 10,
-      }
+      };
       sinon.stub(db.db, 'one').resolves(dbOne);
       sinon.stub(db.db, 'result').onCall(0).resolves(dbResult0).onCall(1).resolves(dbResult1);
       try {
@@ -1006,11 +1252,17 @@ describe('DatabasePostgres', function () {
 
   describe('topicGetByUrl', function () {
     it('success', async function() {
-      const expected = [];
+      const expected = { id: topicId };
       sinon.stub(db.db, 'oneOrNone').resolves(expected);
       const result = await db.topicGetByUrl(dbCtx, topicUrl);
       assert.deepStrictEqual(result, expected);
     });
+    it('success, no default', async function() {
+      const expected = { id: topicId };
+      sinon.stub(db.db, 'oneOrNone').resolves(expected);
+      const result = await db.topicGetByUrl(dbCtx, topicUrl, false);
+      assert.deepStrictEqual(result, expected);
+    });
     it('failure', async function () {
       const expected = new Error();
       sinon.stub(db.db, 'oneOrNone').throws(expected);
@@ -1024,8 +1276,15 @@ describe('DatabasePostgres', function () {
   }); // topicGetByUrl
 
   describe('topicGetContentById', function () {
+    let topic;
+    beforeEach(function () {
+      delete db.cache;
+      topic = {
+        id: topicId,
+      };
+    });
     it('success', async function() {
-      const expected = { id: topicId };
+      const expected = topic;
       sinon.stub(db.db, 'oneOrNone').resolves(expected);
       const result = await db.topicGetContentById(dbCtx, topicId);
       assert.deepStrictEqual(result, expected);
@@ -1046,8 +1305,103 @@ describe('DatabasePostgres', function () {
         assert.deepStrictEqual(e, expected);
       }
     });
+    it('caches success', async function () {
+      db.cache = new Map();
+      const expected = topic;
+      sinon.stub(db.db, 'oneOrNone').resolves(expected);
+      const result = await db.topicGetContentById(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers cached entry', async function() {
+      let result;
+      db.cache = new Map();
+      const expected = topic;
+      sinon.stub(db.db, 'oneOrNone').resolves(expected);
+      result = await db.topicGetContentById(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+      result = await db.topicGetContentById(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
   }); // topicGetContentById
 
+  describe('topicPendingDelete', function () {
+    beforeEach(function () {
+      sinon.stub(db.db, 'one');
+      sinon.stub(db.db, 'result');
+    });
+    it('success', async function () {
+      db.db.one.onCall(0).resolves({
+        id: topicId,
+        isDeleted: true,
+      }).onCall(1).resolves({
+        count: 0,
+      });
+      const dbResult = {
+        rowCount: 1,
+        rows: [],
+        duration: 10,
+      };
+      db.db.result.resolves(dbResult);
+      await db.topicPendingDelete(dbCtx, topicId);
+      assert(db.db.result.called);
+    });
+    it('does not delete non-deleted topic', async function () {
+      db.db.one.onCall(0).resolves({
+        id: topicId,
+        isDeleted: false,
+      }).onCall(1).resolves({
+        count: 0,
+      });
+      await db.topicPendingDelete(dbCtx, topicId);
+      assert(!db.db.result.called);
+    });
+    it('does not delete topic with active subscriptions', async function () {
+      db.db.one.onCall(0).resolves({
+        id: topicId,
+        isDeleted: true,
+      }).onCall(1).resolves({
+        count: 10,
+      });
+      await db.topicPendingDelete(dbCtx, topicId);
+      assert(!db.db.result.called);
+    });
+    it('covers no deletion', async function () {
+      db.db.one.onCall(0).resolves({
+        id: topicId,
+        isDeleted: true,
+      }).onCall(1).resolves({
+        count: 0,
+      });
+      const dbResult = {
+        rowCount: 0,
+        rows: [],
+        duration: 10,
+      };
+      db.db.result.resolves(dbResult);
+      try {
+        await db.topicPendingDelete(dbCtx, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  });
+
+  describe('topicPublishHistory', function () {
+    beforeEach(function () {
+      sinon.stub(db.db, 'manyOrNone');
+    });
+    it('success', async function () {
+      db.db.manyOrNone.returns([
+        { daysAgo: 1, contentUpdates: 1 },
+        { daysAgo: 3, contentUpdates: 2 },
+      ]);
+      const result = await 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 () {
@@ -1118,6 +1472,7 @@ describe('DatabasePostgres', function () {
         contentType: 'text/plain',
         contentHash: 'abc123',
       };
+      sinon.stub(db.db, 'result');
     });
     it('success', async function() {
       const dbResult = {
@@ -1130,7 +1485,7 @@ describe('DatabasePostgres', function () {
         lastInsertRowid: undefined,
         duration: 10,
       };
-      sinon.stub(db.db, 'result').resolves(dbResult);
+      db.db.result.resolves(dbResult);
       const result = await db.topicSetContent(dbCtx, data);
       assert.deepStrictEqual(result, expected);
     });
@@ -1140,7 +1495,28 @@ describe('DatabasePostgres', function () {
         rows: [],
         duration: 10,
       };
-      sinon.stub(db.db, 'result').resolves(dbResult);
+      db.db.result.resolves(dbResult);
+      try {
+        await db.topicSetContent(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('failure 2', async function () {
+      const dbResultSuccess = {
+        rowCount: 1,
+        rows: [],
+        duration: 10,
+      };
+      const dbResultFail = {
+        rowCount: 0,
+        rows: [],
+        duration: 10,
+      };
+      db.db.result
+        .onCall(0).resolves(dbResultSuccess)
+        .onCall(1).resolves(dbResultFail);
       try {
         await db.topicSetContent(dbCtx, data);
         assert.fail(noExpectedException);
@@ -1185,7 +1561,7 @@ describe('DatabasePostgres', function () {
       }
     });
 
-  });
+  }); // topicUpdate
 
   describe('verificationClaim', function () {
     it('success', async function() {
@@ -1453,7 +1829,7 @@ describe('DatabasePostgres', function () {
         rowCount: 0,
         rows: [],
         duration: 10,
-      }
+      };
       sinon.stub(db.db, 'result').resolves(dbResult);
       try {
         await db.verificationUpdate(dbCtx, verificationId, data);
@@ -1479,7 +1855,7 @@ describe('DatabasePostgres', function () {
         rowCount: 1,
         rows: [],
         duration: 10,
-      }
+      };
       sinon.stub(db.db, 'result').resolves(dbResult);
       await db.verificationValidated(dbCtx, verificationId);
     });
@@ -1488,7 +1864,7 @@ describe('DatabasePostgres', function () {
         rowCount: 0,
         rows: [],
         duration: 10,
-      }
+      };
       sinon.stub(db.db, 'result').resolves(dbResult);
       try {
         await db.verificationValidated(dbCtx, verificationId);