diff --git a/lib/db/mysql/index.js b/lib/db/mysql/index.js index 07915e2b6..fcabeae28 100644 --- a/lib/db/mysql/index.js +++ b/lib/db/mysql/index.js @@ -68,7 +68,6 @@ function checkDbPatchLevel(patcher) { patcher.readDbPatchLevel(function(err) { if (err) { - logger.error('checkDbPatchLevel', err); return d.reject(err); } // We are only guaranteed to run correctly if we're at the current @@ -77,7 +76,6 @@ function checkDbPatchLevel(patcher) { if (patcher.currentPatchLevel !== patch.level) { if (patcher.currentPatchLevel !== patch.level + 1) { err = 'unexpected db patch level: ' + patcher.currentPatchLevel; - logger.error('checkDbPatchLevel', err); return d.reject(new Error(err)); } } @@ -105,6 +103,9 @@ MysqlStore.connect = function mysqlConnect(options) { return checkDbPatchLevel(patcher); }).then(function() { return P.promisify(patcher.end, patcher)(); + }, function(error) { + logger.error('checkDbPatchLevel', error); + patcher.end(process.exit.bind(process, 1)); }).then(function() { return new MysqlStore(options); }); diff --git a/package.json b/package.json index dd247d2c8..d4bc47e82 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,9 @@ "load-grunt-tasks": "^3.1.0", "mocha-text-cov": "^0.1.0", "nock": "^1.2.1", + "proxyquire": "^1.6.0", "read": "^1.0.5", + "sinon": "^1.15.4", "time-grunt": "^1.1.0" } } diff --git a/test/db.js b/test/db/index.js similarity index 99% rename from test/db.js rename to test/db/index.js index 958ffe3eb..ca854d728 100644 --- a/test/db.js +++ b/test/db/index.js @@ -8,8 +8,8 @@ const assert = require('insist'); const buf = require('buf').hex; const hex = require('buf').to.hex; -const db = require('../lib/db'); -const config = require('../lib/config'); +const db = require('../../lib/db'); +const config = require('../../lib/config'); /*global describe,it,before*/ diff --git a/test/db/mysql.js b/test/db/mysql.js new file mode 100644 index 000000000..da10fcd98 --- /dev/null +++ b/test/db/mysql.js @@ -0,0 +1,200 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const assert = require('insist'); +const sinon = require('sinon'); +const mocks = require('../lib/mocks'); + +const modulePath = '../../lib/db/mysql'; + +var instances = {}; +var dependencies = mocks.require([ + { path: 'buf' }, + { path: 'mysql' }, + { path: 'mysql-patcher', ctor: function() { return instances.patcher; } }, + { path: '../../config' }, + { path: '../../encrypt' }, + { path: '../../logging', ctor: function() { return instances.logger; } }, + { path: '../../scope' }, + { path: '../../unique', ctor: function() { return instances.scope; } }, + { path: './patch' } +], modulePath, __dirname); + +function nop() { +} + +function callback(cb) { + cb(); +} + +process.setMaxListeners(0); + +describe('db/mysql:', function() { + var sandbox, mysql; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + sandbox.stub(dependencies['../../config'], 'get', function() { + return 'mock config.get result'; + }); + instances.logger = { + info: nop, + debug: nop, + warn: nop, + error: nop, + verbose: nop + }; + Object.keys(instances.logger).forEach(function(methodName) { + sandbox.spy(instances.logger, methodName); + }); + + mocks.register(dependencies, modulePath, __dirname); + + mysql = require(modulePath); + }); + + afterEach(function() { + sandbox.restore(); + mocks.deregister(); + }); + + it('exports a connect function', function() { + assert.equal(typeof mysql.connect, 'function'); + }); + + describe('connect:', function() { + beforeEach(function() { + instances.patcher = { + connect: nop, + patch: nop, + readDbPatchLevel: nop, + end: nop, + currentPatchLevel: dependencies['./patch'].level + }; + sandbox.stub(instances.patcher, 'connect', callback); + sandbox.stub(instances.patcher, 'patch', nop); + sandbox.stub(instances.patcher, 'end', callback); + sandbox.stub(process, 'exit', nop); + }); + + describe('readDbPatchLevel succeeds:', function() { + beforeEach(function() { + sandbox.stub(instances.patcher, 'readDbPatchLevel', function(callback) { + callback(); + }); + }); + + describe('db patch level is okay:', function() { + var result; + + beforeEach(function() { + return mysql.connect({}).then(function(r) { + result = r; + }); + }); + + it('called patcher.connect', function() { + assert.equal(instances.patcher.connect.callCount, 1); + var args = instances.patcher.connect.getCall(0).args; + assert.equal(args.length, 1); + assert.equal(typeof args[0], 'function'); + assert.equal(args[0].length, 2); + }); + + it('did not call patcher.patch', function() { + assert.equal(instances.patcher.patch.callCount, 0); + }); + + it('called patcher.readDbPatchLevel', function() { + assert.equal(instances.patcher.readDbPatchLevel.callCount, 1); + var args = instances.patcher.readDbPatchLevel.getCall(0).args; + assert.equal(args.length, 1); + assert.equal(typeof args[0], 'function'); + assert.equal(args[0].length, 1); + }); + + it('called patcher.end', function() { + assert.equal(instances.patcher.end.callCount, 1); + var args = instances.patcher.end.getCall(0).args; + assert.equal(args.length, 1); + assert.equal(typeof args[0], 'function'); + assert.equal(args[0].length, 2); + }); + + it('did not call logger.error', function() { + assert.equal(instances.logger.error.callCount, 0); + }); + + it('did not call process.exit', function() { + assert.equal(process.exit.callCount, 0); + }); + + it('returned an object', function () { + assert.equal(typeof result, 'object'); + assert.notEqual(result, null); + }); + }); + + describe('db patch level is bad:', function() { + beforeEach(function() { + instances.patcher.currentPatchLevel += 2; + return mysql.connect({}); + }); + + afterEach(function() { + instances.patcher.currentPatchLevel -= 2; + }); + + it('called patcher.end', function() { + assert.equal(instances.patcher.end.callCount, 1); + }); + + it('called logger.error', function() { + assert.equal(instances.logger.error.callCount, 1); + var args = instances.logger.error.getCall(0).args; + assert.equal(args.length, 2); + assert.equal(args[0], 'checkDbPatchLevel'); + assert(args[1] instanceof Error); + assert.equal(args[1].message, 'unexpected db patch level: ' + (dependencies['./patch'].level + 2)); + }); + + it('called process.exit', function() { + assert.equal(process.exit.callCount, 1); + var args = process.exit.getCall(0).args; + assert.equal(args.length, 1); + assert.equal(args[0], 1); + }); + }); + }); + + describe('readDbPatchLevel fails:', function() { + beforeEach(function() { + sandbox.stub(instances.patcher, 'readDbPatchLevel', function(callback) { + callback('foo'); + }); + return mysql.connect({}); + }); + + it('called patcher.end', function() { + assert.equal(instances.patcher.end.callCount, 1); + }); + + it('called logger.error', function() { + assert.equal(instances.logger.error.callCount, 1); + var args = instances.logger.error.getCall(0).args; + assert.equal(args[0], 'checkDbPatchLevel'); + assert.equal(args[1], 'foo'); + }); + + it('called process.exit', function() { + assert.equal(process.exit.callCount, 1); + var args = process.exit.getCall(0).args; + assert.equal(args.length, 1); + assert.equal(args[0], 1); + }); + }); + }); +}); + diff --git a/test/lib/mocks.js b/test/lib/mocks.js new file mode 100644 index 000000000..fbc42c497 --- /dev/null +++ b/test/lib/mocks.js @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const path = require('path'); +const proxyquire = require('proxyquire'); +const m = require('module'); + +var moduleCache; + +module.exports = { + require: requireDependencies, + register: registerDependencies, + deregister: deregisterDependencies +}; + +// `mocks.require` +// +// Require dependencies using the same path that is specifed in the module +// under test. +// +// Returns an object containing dependencies keyed by their path. +// +// Expects three arguments; `dependencies`, `modulePath` and `basePath`. +// +// dependencies: Array of { path, ctor } items. +// path: The dependency path, as specified in the module +// under test. +// ctor: Optional. If the dependency is a constructor for +// an instance that you wish to mock, set this to a +// function that returns your mock instance. +// modulePath: The relative path to the module under test. +// basePath: The base path, i.e. __dirname for the test itself. +function requireDependencies(dependencies, modulePath, basePath) { + var result = {}; + + dependencies.forEach(function (dependency) { + result[dependency.path] = requireDependency(dependency, modulePath, basePath); + }); + + return result; +} + +function requireDependency(dependency, modulePath, basePath) { + if (typeof dependency.ctor === 'function') { + return dependency.ctor; + } + + var localPath = dependency.path; + + if (localPath[0] === '.') { + localPath = path.relative( + basePath, + path.resolve(basePath, modulePath, localPath) + ); + } + + return require(localPath); +} + +// `mocks.register` +// +// Register mock dependencies, fixing paths as we go so that it works +// with the blanket coverage tool (which rewrites require paths in the +// instrumented code). You should call this function inside beforeEach. +// +// Expects three arguments; `dependencies`, `modulePath` and `basePath`. +// +// dependencies: An object, where keys are dependency paths and values +// are mock objects. This argument is typically the return +// value from `mocks.require`, modified by sinon for your +// tests. +// modulePath: The relative path to the module under test. +// basePath: The base path, i.e. __dirname for the test itself. +function registerDependencies(dependencies, modulePath, basePath) { + var instrumentedDependencies = {}; + + clearModuleCache(); + + Object.keys(dependencies).forEach(function(dependencyPath) { + var instrumentedPath = getInstrumentedPath(dependencyPath, modulePath, basePath); + instrumentedDependencies[instrumentedPath] = dependencies[dependencyPath]; + }); + + proxyquire(modulePath, instrumentedDependencies); +} + +function clearModuleCache() { + moduleCache = m._cache; + m._cache = {}; +} + +function getInstrumentedPath(dependencyPath, modulePath, basePath) { + if (dependencyPath[0] !== '.') { + return dependencyPath; + } + + return path.resolve(basePath, modulePath) + '/' + dependencyPath; +} + +// `mocks.deregister` +// +// Deregister mock dependencies. You should call this function +// inside afterEach. +function deregisterDependencies() { + if (moduleCache) { + m._cache = moduleCache; + moduleCache = null; + } +} +