Initial release
[websub-hub] / test / src / db / sqlite.js
diff --git a/test/src/db/sqlite.js b/test/src/db/sqlite.js
new file mode 100644 (file)
index 0000000..310e87a
--- /dev/null
@@ -0,0 +1,1504 @@
+/* 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 sinon = require('sinon');
+const DBStub = require('../../stub-db');
+const stubLogger = require('../../stub-logger');
+const DB = require('../../../src/db/sqlite');
+const DBErrors = require('../../../src/db/errors');
+const common = require('../../../src/common');
+const Config = require('../../../config');
+
+const noExpectedException = 'did not receive expected exception';
+
+describe('DatabaseSQLite', function () {
+  let db, options;
+  let dbCtx, claimant, claimTimeoutSeconds, callback, subscriptionId, topicId, verificationId;
+  let topicUrl, leaseSeconds, secret, httpRemoteAddr, httpFrom, retryDelays, wanted;
+  before(function () {
+    options = new Config('test');
+    options.db.connectionString = 'sqlite://:memory:';
+    db = new DB(stubLogger, options);
+  });
+  beforeEach(function () {
+    stubLogger._reset();
+    dbCtx = db.db;
+    claimant = '19af19b8-6be3-4a6f-8946-65f5f1ccc5d7';
+    claimTimeoutSeconds = 300;
+    subscriptionId = 'fbaf8f19-ed9c-4a21-89ae-98b7005e3bf6';
+    topicUrl = 'https://example.com/blog';
+    callback = 'https://example.com/callback?id=123';
+    topicId = 'c59d4bda-10ad-41d9-99df-4ce8bc331424';
+    verificationId = '55cd7748-d2d5-11eb-b355-0025905f714a';
+    retryDelays = [60];
+    leaseSeconds = 86400;
+    secret = 'secret';
+    httpRemoteAddr = '127.0.0.1';
+    httpFrom = 'user@example.com';
+    wanted = 5;
+  });
+  afterEach(function () {
+    sinon.restore();
+  });
+
+  // Ensure all interface methods are implemented
+  describe('Implementation', function () {
+    it('implements interface', async function () {
+      const results = await Promise.allSettled(DBStub._implementation.map(async (fn) => {
+        try {
+          // eslint-disable-next-line security/detect-object-injection
+          await db[fn](db.db);
+        } catch (e) {
+          assert(!(e instanceof DBErrors.NotImplemented), `${fn} not implemented`);
+        }
+      }));
+      const failures = results.filter((x) => x.status === 'rejected');
+      assert(!failures.length, failures.map((x) => {
+        x = x.reason.toString();
+        return x.slice(x.indexOf(': '));
+      }));
+    });
+  }); // Implementation
+
+  describe('_currentSchema', function () {
+    it('covers', async function () {
+      const version = { major: 1, minor: 0, patch: 0 };
+      sinon.stub(db.db, 'prepare').returns({
+        get: () => version,
+      });
+      const result = await db._currentSchema();
+      assert.deepStrictEqual(result, version);
+    });
+  }); // _currentSchema
+
+  describe('_closeConnection', function () {
+    it('success', async function () {
+      sinon.stub(db.db, 'close');
+      await db._closeConnection();
+      assert(db.db.close.called);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.db, 'close').throws(expected);
+      try {
+        await db._closeConnection();
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // _closeConnection
+
+  describe('_purgeTables', function () {
+    beforeEach(function () {
+      sinon.stub(db.db, 'prepare').returns({
+        run: sinon.stub(),
+      });
+    });
+    it('covers not really', async function () {
+      await db._purgeTables(false);
+      assert(!db.db.prepare.called);
+    });
+    it('success', async function () {
+      await db._purgeTables(true);
+      assert(db.db.prepare.called);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      db.db.prepare.restore();
+      sinon.stub(db.db, 'prepare').throws(expected);
+      try {
+        await db._purgeTables(true);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // _purgeTables
+
+  describe('_optimize', function () {
+    let origOAC;
+    beforeEach(function () {
+      origOAC = db.optimizeAfterChanges;
+      sinon.stub(db.statement._optimize, 'all');
+      sinon.stub(db.db, 'pragma');
+    });
+    this.afterEach(function () {
+      db.optimizeAfterChanges = origOAC;
+    });
+    it('covers', async function () {
+      db.optimizeAfterChanges = 10;
+      db.changesSinceLastOptimize = BigInt(20);
+      await db._optimize();
+      assert(db.db.pragma.called);
+    });
+    it('covers none', async function () {
+      db.optimizeAfterChanges = 0;
+      await db._optimize();
+      assert(!db.db.pragma.called);
+    });
+    it('covers not enough changes', async function () {
+      db.optimizeAfterChanges = 10;
+      db.changesSinceLastOptimize = BigInt(5);
+      await db._optimize();
+      assert(!db.db.pragma.called);
+    });
+  }); // _optimize
+
+  describe('_deOphidiate', function () {
+    it('covers non-array', function () {
+      const obj = {
+        'snake_case': 1,
+      };
+      const expected = {
+        snakeCase: 1,
+      };
+      const result = DB._deOphidiate(obj);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers array', function () {
+      const rows = [
+        {
+          'snek_field': 'foo',
+        },
+        {
+          'snek_field': 'bar',
+        },
+      ];
+      const expected = [
+        {
+          snekField: 'foo',
+        },
+        {
+          snekField: 'bar',
+        },
+      ];
+      const result = DB._deOphidiate(rows);
+      assert.deepStrictEqual(result, expected);
+    });
+  }); // _deOphidiate
+
+  describe('_topicDataToNative', function () {
+    it('covers', function () {
+      const now = new Date();
+      const nowEpoch = now.getTime() / 1000;
+      const topic = {
+        isActive: 1,
+        isDeleted: 0,
+        created: nowEpoch,
+        lastPublish: nowEpoch,
+        contentFetchNextAttempt: nowEpoch,
+        contentUpdated: nowEpoch,
+        url: 'https://example.com/',
+      };
+      const expected = {
+        isActive: true,
+        isDeleted: false,
+        created: now,
+        lastPublish: now,
+        contentFetchNextAttempt: now,
+        contentUpdated: now,
+        url: topic.url,
+      }
+      const result = DB._topicDataToNative(topic);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers empty', function () {
+      const topic = undefined;
+      const result = DB._topicDataToNative(topic);
+      assert.deepStrictEqual(result, topic);
+    });
+  }); // _topicDataToNative
+
+  describe('healthCheck', function () {
+    let origDb;
+    beforeEach(function () {
+      origDb = db.db;
+    });
+    afterEach(function () {
+      db.db = origDb;
+    });
+    it('covers', function () {
+      db.healthCheck();
+    });
+    it('covers failure', function () {
+      db.db = { open: false };
+      try {
+        db.healthCheck();
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // healthCheck
+
+  describe('context', function () {
+    it('covers', async function () {
+      await db.context(common.nop);
+    });
+  }); // context
+
+  describe('transaction', function () {
+    it('covers', async function () {
+      await db.transaction(db.db, common.nop);
+    });
+    it('covers no context', async function () {
+      await db.transaction(undefined, common.nop);
+    });
+  }); // transaction
+
+  describe('authenticationSuccess', function () {
+    let identifier;
+    beforeEach(function () {
+      identifier = 'username';
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.authenticationSuccess, 'run').returns(dbResult);
+      await db.authenticationSuccess(dbCtx, identifier);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.authenticationSuccess, 'run').returns(dbResult);
+      try {
+        await db.authenticationSuccess(dbCtx, identifier);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // authenticationSuccess
+
+  describe('authenticationGet', function () {
+    let identifier, credential;
+    beforeEach(function () {
+      identifier = 'username';
+      credential = '$z$foo';
+    });
+    it('success', async function() {
+      const expected = {
+        identifier,
+        credential,
+      };
+      sinon.stub(db.statement.authenticationGet, 'get').returns(expected);
+      const result = await db.authenticationGet(dbCtx, identifier);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.authenticationGet, 'get').throws(expected);
+      try {
+        await db.authenticationGet(dbCtx, identifier);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // authenticationGet
+
+  describe('authenticationUpsert', 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.authenticationUpsert, 'run').returns(dbResult);
+      await db.authenticationUpsert(dbCtx, identifier, credential);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult);
+      try {
+        await db.authenticationUpsert(dbCtx, identifier, credential);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // authenticationUpsert
+
+  describe('subscriptionsByTopicId', function () {
+    it('success', async function () {
+      const expected = { count: 3 };
+      sinon.stub(db.statement.subscriptionsByTopicId, 'all').returns(expected);
+      const result = await db.subscriptionsByTopicId(dbCtx, topicUrl);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.subscriptionsByTopicId, 'all').throws(expected);
+      try {
+        await db.subscriptionsByTopicId(dbCtx, topicUrl);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // subscriptionsByTopicId
+
+  describe('subscriptionCountByTopicUrl', function () {
+    it('success', async function () {
+      const expected = { count: 3 };
+      sinon.stub(db.statement.subscriptionCountByTopicUrl, 'get').returns(expected);
+      const result = await db.subscriptionCountByTopicUrl(dbCtx, topicUrl);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.subscriptionCountByTopicUrl, 'get').throws(expected);
+      try {
+        await db.subscriptionCountByTopicUrl(dbCtx, topicUrl);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // subscriptionCountByTopicUrl
+
+  describe('subscriptionDelete', function () {
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const expected = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.subscriptionDelete, 'run').returns(dbResult);
+      const result = await db.subscriptionDelete(dbCtx, callback, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.subscriptionDelete, 'run').returns(dbResult);
+      try {
+        await db.subscriptionDelete(dbCtx, callback, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // subscriptionDelete
+
+  describe('subscriptionDeliveryClaim', function () {
+    it('success', async function() {
+      const dbAllResult = [
+        {
+          id: 'c2e254c5-aa6e-4a8f-b1a1-e474b07392bb',
+        },
+      ];
+      const dbRunResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const expected = ['c2e254c5-aa6e-4a8f-b1a1-e474b07392bb'];
+      sinon.stub(db.statement.subscriptionDeliveryNeeded, 'all').returns(dbAllResult);
+      sinon.stub(db.statement.subscriptionDeliveryClaimById, 'run').returns(dbRunResult);
+      const result = await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbAllResult = [
+        {
+          id: 'c2e254c5-aa6e-4a8f-b1a1-e474b07392bb',
+        },
+      ];
+      const dbRunResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.subscriptionDeliveryNeeded, 'all').returns(dbAllResult);
+      sinon.stub(db.statement.subscriptionDeliveryClaimById, 'run').returns(dbRunResult);
+      try {
+        await db.subscriptionDeliveryClaim(dbCtx, wanted, claimTimeoutSeconds, claimant );
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // subscriptionDeliveryClaim
+
+  describe('subscriptionDeliveryClaimById', function () {
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.subscriptionDeliveryClaimById, 'run').returns(dbResult);
+      const result = await db.subscriptionDeliveryClaimById(dbCtx, subscriptionId, claimTimeoutSeconds, claimant);
+      assert.deepStrictEqual(result, dbResult);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.subscriptionDeliveryClaimById, 'run').returns(dbResult);
+      try {
+        await db.subscriptionDeliveryClaimById(dbCtx, subscriptionId, claimTimeoutSeconds, claimant);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // subscriptionDeliveryClaimById
+
+  describe('subscriptionDeliveryComplete', function () {
+    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);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+      };
+      sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult);
+      sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
+      try {
+        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('second failure', async function () {
+      const dbResult0 = {
+        changes: 1,
+      };
+      const dbResult1 = {
+        changes: 0,
+      };
+      sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult0);
+      sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult1);
+      try {
+        await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // subscriptionDeliveryComplete
+
+  describe('subscriptionDeliveryGone', function () {
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+      };
+      sinon.stub(db.statement.subscriptionDelete, 'run').returns(dbResult);
+      await db.subscriptionDeliveryGone(dbCtx, callback, topicId);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+      };
+      sinon.stub(db.statement.subscriptionDelete, 'run').returns(dbResult);
+      try {
+        await db.subscriptionDeliveryGone(dbCtx, callback, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // subscriptionDeliveryGone
+
+  describe('subscriptionDeliveryIncomplete', function () {
+    it('success', async function() {
+      const dbGet = { deliveryAttemptsSinceSuccess: 0 };
+      const dbResult = {
+        changes: 1,
+      };
+      sinon.stub(db.statement.subscriptionDeliveryAttempts, 'get').returns(dbGet);
+      sinon.stub(db.statement.subscriptionDeliveryFailure, 'run').returns(dbResult);
+      sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
+      await db.subscriptionDeliveryIncomplete(dbCtx, callback, topicId, retryDelays);
+    });
+    it('success covers default', async function() {
+      const dbGet = { deliveryAttemptsSinceSuccess: 0 };
+      const dbResult = {
+        changes: 1,
+      };
+      sinon.stub(db.statement.subscriptionDeliveryAttempts, 'get').returns(dbGet);
+      sinon.stub(db.statement.subscriptionDeliveryFailure, 'run').returns(dbResult);
+      sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
+      await db.subscriptionDeliveryIncomplete(dbCtx, callback, topicId);
+    });
+    it('failure', async function () {
+      const dbGet = { deliveryAttemptsSinceSuccess: 0 };
+      const dbResult = {
+        changes: 0,
+      };
+      sinon.stub(db.statement.subscriptionDeliveryAttempts, 'get').returns(dbGet);
+      sinon.stub(db.statement.subscriptionDeliveryFailure, 'run').returns(dbResult);
+      sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
+      try {
+        await db.subscriptionDeliveryIncomplete(dbCtx, callback, topicId, retryDelays);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('second failure', async function () {
+      const dbGet = { deliveryAttemptsSinceSuccess: 0 };
+      const dbResult0 = {
+        changes: 1,
+      };
+      const dbResult1 = {
+        changes: 0,
+      };
+      sinon.stub(db.statement.subscriptionDeliveryAttempts, 'get').returns(dbGet);
+      sinon.stub(db.statement.subscriptionDeliveryFailure, 'run').returns(dbResult0);
+      sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult1);
+      try {
+        await db.subscriptionDeliveryIncomplete(dbCtx, callback, topicId, retryDelays);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // subscriptionDeliveryIncomplete
+
+  describe('subscriptionGet', function () {
+    it('success', async function() {
+      const expected = {
+        id: subscriptionId,
+      };
+      sinon.stub(db.statement.subscriptionGet, 'get').returns(expected);
+      const result = await db.subscriptionGet(dbCtx, callback, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.subscriptionGet, 'get').throws(expected);
+      try {
+        await db.subscriptionGet(dbCtx, callback, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // subscriptionGet
+
+  describe('subscriptionGetById', function () {
+    it('success', async function() {
+      const expected = {
+        id: subscriptionId,
+      };
+      sinon.stub(db.statement.subscriptionGetById, 'get').returns(expected);
+      const result = await db.subscriptionGetById(dbCtx, subscriptionId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.subscriptionGetById, 'get').throws(expected);
+      try {
+        await db.subscriptionGetById(dbCtx, subscriptionId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // subscriptionGetById
+
+  describe('subscriptionUpdate', function () {
+    let data;
+    beforeEach(function () {
+      data = {
+        subscriptionId,
+        signatureAlgorithm: 'sha256',
+      };
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: subscriptionId,
+      };
+      sinon.stub(db.statement.subscriptionUpdate, 'run').returns(dbResult);
+      await db.subscriptionUpdate(dbCtx, data);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+      };
+      sinon.stub(db.statement.subscriptionUpdate, 'run').returns(dbResult);
+      try {
+        await db.subscriptionUpdate(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult, e);
+      }
+    });
+  }); // subscriptionUpdate
+
+  describe('subscriptionUpsert', function () {
+    let data;
+    beforeEach(function () {
+      data = {
+        callback,
+        topicId,
+        leaseSeconds,
+        secret,
+        httpRemoteAddr,
+        httpFrom,
+      };
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: subscriptionId,
+      };
+      const expected = {
+        changes: 1,
+        lastInsertRowid: subscriptionId,
+      };
+      sinon.stub(db.statement.subscriptionUpsert, 'run').returns(dbResult);
+      const result = await db.subscriptionUpsert(dbCtx, data);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+      };
+      sinon.stub(db.statement.subscriptionUpsert, 'run').returns(dbResult);
+      try {
+        await db.subscriptionUpsert(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // subscriptionUpsert
+
+  describe('topicDeleted', function () {
+    it('success', async function () {
+      sinon.stub(db.statement.topicDeleted, 'run').returns({ changes: 1 });
+      await db.topicDeleted(dbCtx, { topicId });
+    });
+    it('failure', async function () {
+      sinon.stub(db.statement.topicDeleted, 'run').returns({ changes: 0 });
+      try {
+        await db.topicDeleted(dbCtx, { topicId });
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // topicDeleted
+
+  describe('topicFetchClaim', function () {
+    it('success', async function() {
+      const dbAll = [{ id: topicId }];
+      const dbResult = {
+        changes: 1,
+      };
+      const expected = [topicId];
+      sinon.stub(db.statement.topicContentFetchNeeded, 'all').returns(dbAll);
+      sinon.stub(db.statement.topicContentFetchClaimById, 'run').returns(dbResult);
+      const result = await db.topicFetchClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbAll = [{ id: topicId }];
+      const dbResult = {
+        changes: 0,
+      };
+      sinon.stub(db.statement.topicContentFetchNeeded, 'all').returns(dbAll);
+      sinon.stub(db.statement.topicContentFetchClaimById, 'run').returns(dbResult);
+      try {
+        await db.topicFetchClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // topicFetchClaim
+
+  describe('topicFetchClaimById', function () {
+    it('success', async function() {
+      const expected = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicContentFetchClaimById, 'run').returns(expected);
+      const result = await db.topicFetchClaimById(dbCtx, topicId, claimTimeoutSeconds, claimant);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicContentFetchClaimById, 'run').returns(expected);
+      try {
+        await db.topicFetchClaimById(dbCtx, topicId, claimTimeoutSeconds, claimant);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // topicFetchClaimById
+
+  describe('topicFetchComplete', function () {
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicAttemptsReset, 'run').returns(dbResult);
+      sinon.stub(db.statement.topicContentFetchDone, 'run').returns(dbResult);
+      await db.topicFetchComplete(dbCtx, topicId);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicAttemptsReset, 'run').returns(dbResult);
+      sinon.stub(db.statement.topicContentFetchDone, 'run').returns(dbResult);
+      try {
+        await db.topicFetchComplete(dbCtx, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('second failure', async function () {
+      const dbResult0 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const dbResult1 = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicAttemptsReset, 'run').returns(dbResult0);
+      sinon.stub(db.statement.topicContentFetchDone, 'run').returns(dbResult1);
+      try {
+        await db.topicFetchComplete(dbCtx, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // topicFetchComplete
+
+  describe('topicFetchIncomplete', function () {
+    it('success', async function() {
+      const dbGet = { currentAttempt: 0 };
+      const dbResult0 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const dbResult1 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      }
+      const expected = {
+        changes: 1,
+        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);
+      const result = await db.topicFetchIncomplete(dbCtx, topicId, retryDelays);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers defaults', async function() {
+      const dbGet = { currentAttempt: 0 };
+      const dbResult0 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const dbResult1 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      }
+      const expected = {
+        changes: 1,
+        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);
+      const result = await db.topicFetchIncomplete(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbGet = { currentAttempt: 0 };
+      const dbResult0 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      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);
+      try {
+        await db.topicFetchIncomplete(dbCtx, topicId, retryDelays);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('second failure', async function () {
+      const dbGet = { currentAttempt: 0 };
+      const dbResult0 = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      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);
+      try {
+        await db.topicFetchIncomplete(dbCtx, topicId, retryDelays);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // topicFetchIncomplete
+
+  describe('topicFetchRequested', function () {
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const expected = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicContentFetchRequested, 'run').returns(dbResult);
+      const result = await db.topicFetchRequested(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicContentFetchRequested, 'run').returns(dbResult);
+      try {
+        await db.topicFetchRequested(dbCtx, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // topicFetchRequested
+
+  describe('topicGetAll', function () {
+    it('success', async function() {
+      const expected = [{ id: topicId }];
+      sinon.stub(db.statement.topicGetInfoAll, 'all').returns(expected);
+      const result = await db.topicGetAll(dbCtx);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers none', async function() {
+      const expected = undefined;
+      sinon.stub(db.statement.topicGetInfoAll, 'all').returns(expected);
+      const result = await db.topicGetAll(dbCtx);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.topicGetInfoAll, 'all').throws(expected);
+      try {
+        await db.topicGetAll(dbCtx);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // topicGetById
+
+  describe('topicGetById', function () {
+    it('success', async function() {
+      const expected = { id: topicId };
+      sinon.stub(db.statement.topicGetById, 'get').returns(expected);
+      const result = await db.topicGetById(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers no defaults', async function () {
+      const expected = { id: topicId };
+      sinon.stub(db.statement.topicGetById, 'get').returns(expected);
+      const result = await db.topicGetById(dbCtx, topicId, false);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers default', async function() {
+      const expected = undefined;
+      sinon.stub(db.statement.topicGetById, 'get').returns(expected);
+      const result = await db.topicGetById(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.topicGetById, 'get').throws(expected);
+      try {
+        await db.topicGetById(dbCtx, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // topicGetById
+
+  describe('topicGetByUrl', function () {
+    it('success', async function() {
+      const expected = [];
+      sinon.stub(db.statement.topicGetByUrl, 'get').returns(expected);
+      const result = await db.topicGetByUrl(dbCtx, topicUrl);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.topicGetByUrl, 'get').throws(expected);
+      try {
+        await db.topicGetByUrl(dbCtx, topicUrl);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // topicGetByUrl
+
+  describe('topicGetContentById', function () {
+    it('success', async function() {
+      const expected = { id: topicId };
+      sinon.stub(db.statement.topicGetContentById, 'get').returns(expected);
+      const result = await db.topicGetContentById(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers default', async function() {
+      const expected = undefined;
+      sinon.stub(db.statement.topicGetContentById, 'get').returns(expected);
+      const result = await db.topicGetContentById(dbCtx, topicId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.topicGetContentById, 'get').throws(expected);
+      try {
+        await db.topicGetContentById(dbCtx, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // topicGetContentById
+
+  describe('topicSet', function () {
+    let data;
+    beforeEach(function () {
+      data = {
+        url: topicUrl,
+      };
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: topicId,
+      };
+      const expected = {
+        changes: 1,
+        lastInsertRowid: topicId,
+      };
+      sinon.stub(db.statement.topicUpsert, 'run').returns(dbResult);
+      const result = await db.topicSet(dbCtx, data);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicUpsert, 'run').returns(dbResult);
+      try {
+        await db.topicSet(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('fails invalid value', async function () {
+      sinon.stub(db.statement.topicUpsert, 'run');
+      try {
+        data.leaseSecondsPreferred = -100;
+        await db.topicSet(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.DataValidation);
+      }
+      assert(!db.statement.topicUpsert.run.called);
+    });
+    it('fails invalid values', async function () {
+      sinon.stub(db.statement.topicUpsert, 'run');
+      try {
+        data.leaseSecondsPreferred = 10;
+        data.leaseSecondsMax = 100;
+        data.leaseSecondsMin = 50;
+        await db.topicSet(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.DataValidation);
+      }
+      assert(!db.statement.topicUpsert.run.called);
+    });
+  }); // topicSet
+
+  describe('topicSetContent', function () {
+    let data;
+    beforeEach(function () {
+      data = {
+        content: 'content',
+        contentType: 'text/plain',
+        contentHash: 'abc123',
+      };
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const expected = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicSetContent, 'run').returns(dbResult);
+      const result = await db.topicSetContent(dbCtx, data);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicSetContent, 'run').returns(dbResult);
+      try {
+        await db.topicSetContent(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // topicSetContent
+
+  describe('topicUpdate', function () {
+    let data;
+    beforeEach(function () {
+      data = {
+        topicId,
+        leaseSecondsPreferred: 9999,
+        leaseSecondsMax: 99999,
+        leaseSecondsMin: 999,
+        publisherValidationUrl: null,
+        contentHashAlgorithm: 'sha256',
+      };
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: topicId,
+      };
+      sinon.stub(db.statement.topicUpdate, 'run').returns(dbResult);
+      await db.topicUpdate(dbCtx, data);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.topicUpdate, 'run').returns(dbResult);
+      try {
+        await db.topicUpdate(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult, e);
+      }
+    });
+    it('fails invalid value', async function () {
+      sinon.stub(db.statement.topicUpdate, 'run');
+      try {
+        data.leaseSecondsPreferred = -100;
+        await db.topicUpdate(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.DataValidation, e);
+      }
+      assert(!db.statement.topicUpdate.run.called);
+    });
+    it('fails invalid values', async function () {
+      sinon.stub(db.statement.topicUpdate, 'run');
+      try {
+        data.leaseSecondsPreferred = 10;
+        data.leaseSecondsMax = 100;
+        data.leaseSecondsMin = 50;
+        await db.topicUpdate(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.DataValidation, e);
+      }
+      assert(!db.statement.topicUpdate.run.called);
+    });
+  }); // topicUpdate
+
+  describe('verificationClaim', function () {
+    it('success', async function() {
+      const dbAll = [{ id: verificationId }];
+      const dbRun = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const expected = [verificationId];
+      sinon.stub(db.statement.verificationNeeded, 'all').returns(dbAll);
+      sinon.stub(db.statement.verificationClaimById, 'run').returns(dbRun);
+      const result = await db.verificationClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbAll = [{ id: verificationId }];
+      const dbRun = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationNeeded, 'all').returns(dbAll);
+      sinon.stub(db.statement.verificationClaimById, 'run').returns(dbRun);
+      try {
+        await db.verificationClaim(dbCtx, wanted, claimTimeoutSeconds, claimant);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // verificationClaim
+
+  describe('verificationClaimById', function () {
+    it('success', async function() {
+      const dbRun = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationClaimById, 'run').returns(dbRun);
+      const result = await db.verificationClaimById(dbCtx, verificationId, claimTimeoutSeconds, claimant);
+      assert.deepStrictEqual(result, dbRun);
+    });
+    it('failure', async function () {
+      const dbRun = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationClaimById, 'run').returns(dbRun);
+      try {
+        await db.verificationClaimById(dbCtx, verificationId, claimTimeoutSeconds, claimant);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // verificationClaimById
+
+  describe('verificationComplete', function () {
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationScrub, 'run').returns(dbResult);
+      await db.verificationComplete(dbCtx, verificationId, callback, topicId);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationScrub, 'run').returns(dbResult);
+      try {
+        await db.verificationComplete(dbCtx, verificationId, callback, topicId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // verificationComplete
+
+  describe('verificationGetById', function () {
+    it('success', async function() {
+      const dbOneOrNone = { id: verificationId, isPublisherValidated: 1 };
+      const expected = { id: verificationId, isPublisherValidated: true };
+      sinon.stub(db.statement.verificationGetById, 'get').returns(dbOneOrNone);
+      const result = await db.verificationGetById(dbCtx, verificationId);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const expected = new Error();
+      sinon.stub(db.statement.verificationGetById, 'get').throws(expected);
+      try {
+        await db.verificationGetById(dbCtx, verificationId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert.deepStrictEqual(e, expected);
+      }
+    });
+  }); // verificationGetById
+
+  describe('verificationIncomplete', function () {
+    it('success', async function() {
+      const dbOne = { attempts: 0 };
+      const dbResult0 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const dbResult1 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationAttempts, 'get').returns(dbOne);
+      sinon.stub(db.statement.verificationAttemptsIncrement, 'run').returns(dbResult0);
+      sinon.stub(db.statement.verificationDone, 'run').returns(dbResult1);
+      await db.verificationIncomplete(dbCtx, verificationId, retryDelays);
+    });
+    it('covers defaults', async function() {
+      const dbOne = { attempts: 0 };
+      const dbResult0 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const dbResult1 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationAttempts, 'get').returns(dbOne);
+      sinon.stub(db.statement.verificationAttemptsIncrement, 'run').returns(dbResult0);
+      sinon.stub(db.statement.verificationDone, 'run').returns(dbResult1);
+      await db.verificationIncomplete(dbCtx, verificationId);
+    });
+    it('failure', async function () {
+      const dbOne = { attempts: 0 };
+      const dbResult0 = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      const dbResult1 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationAttempts, 'get').returns(dbOne);
+      sinon.stub(db.statement.verificationAttemptsIncrement, 'run').returns(dbResult0);
+      sinon.stub(db.statement.verificationDone, 'run').returns(dbResult1);
+      try {
+        await db.verificationIncomplete(dbCtx, verificationId, retryDelays);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('second failure', async function () {
+      const dbOne = { attempts: 0 };
+      const dbResult0 = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      const dbResult1 = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationAttempts, 'get').returns(dbOne);
+      sinon.stub(db.statement.verificationAttemptsIncrement, 'run').returns(dbResult0);
+      sinon.stub(db.statement.verificationDone, 'run').returns(dbResult1);
+      try {
+        await db.verificationIncomplete(dbCtx, verificationId, retryDelays);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // verificationIncomplete
+
+  describe('_verificationDataToEngine', function () {
+    it('covers no data', function () {
+      DB._verificationDataToEngine();
+    });
+    it('covers true', function () {
+      const data = {
+        isPublisherValidated: true,
+      };
+      DB._verificationDataToEngine(data);
+      assert.strictEqual(data.isPublisherValidated, 1);
+    });
+    it('covers false', function () {
+      const data = {
+        isPublisherValidated: false,
+      };
+      DB._verificationDataToEngine(data);
+      assert.strictEqual(data.isPublisherValidated, 0);
+    });
+  }) // _verificationDataToEngine
+
+  describe('verificationInsert', function () {
+    let verification;
+    beforeEach(function () {
+      verification = {
+        topicId,
+        callback,
+        mode: 'subscribe',
+        isPublisherValidated: true,
+        leaseSeconds: 86400,
+      };
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: verificationId,
+      };
+      const expected = verificationId;
+      sinon.stub(db.statement.verificationInsert, 'run').returns(dbResult);
+      const result = await db.verificationInsert(dbCtx, verification);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationInsert, 'run').returns(dbResult);
+      try {
+        await db.verificationInsert(dbCtx, verification);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+    it('fails validation', async function () {
+      delete verification.leaseSeconds;
+      try {
+        await db.verificationInsert(dbCtx, verification);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.DataValidation);
+      }
+    });
+  }); // verificationInsert
+
+  describe('verificationRelease', function () {
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationDone, 'run').returns(dbResult);
+      await db.verificationRelease(dbCtx, verificationId);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationDone, 'run').returns(dbResult);
+      try {
+        await db.verificationRelease(dbCtx, verificationId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // verificationRelease
+
+  describe('verificationUpdate', function () {
+    let data;
+    beforeEach(function () {
+      data = {
+        mode: 'subscribe',
+        isPublisherValidated: true,
+      };
+    });
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      };
+      sinon.stub(db.statement.verificationUpdate, 'run').returns(dbResult);
+      await db.verificationUpdate(dbCtx, verificationId, data);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      }
+      sinon.stub(db.statement.verificationUpdate, 'run').returns(dbResult);
+      try {
+        await db.verificationUpdate(dbCtx, verificationId, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult, e.name);
+      }
+    });
+    it('fails validation', async function () {
+      delete data.mode;
+      try {
+        await db.verificationUpdate(dbCtx, data);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.DataValidation);
+      }
+    });
+  }); // verificationUpdate
+
+  describe('verificationValidated', function () {
+    it('success', async function() {
+      const dbResult = {
+        changes: 1,
+        lastInsertRowid: undefined,
+      }
+      sinon.stub(db.statement.verificationValidate, 'run').returns(dbResult);
+      await db.verificationValidated(dbCtx, verificationId);
+    });
+    it('failure', async function () {
+      const dbResult = {
+        changes: 0,
+        lastInsertRowid: undefined,
+      }
+      sinon.stub(db.statement.verificationValidate, 'run').returns(dbResult);
+      try {
+        await db.verificationValidated(dbCtx, verificationId);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof DBErrors.UnexpectedResult);
+      }
+    });
+  }); // verificationValidated
+
+}); // DatabasePostgres