Permalink
Browse files

fix(server): exit if db patch level is wrong

  • Loading branch information...
1 parent 4b8a0a7 commit 78d6382980a8ce1e6adbcb2af5825f643cbcbccd @philbooth philbooth committed Jul 16, 2015
Showing with 318 additions and 4 deletions.
  1. +3 −2 lib/db/mysql/index.js
  2. +2 −0 package.json
  3. +2 −2 test/{db.js → db/index.js}
  4. +200 −0 test/db/mysql.js
  5. +111 −0 test/lib/mocks.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);
});
View
@@ -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"
}
}
@@ -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*/
View
@@ -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);
+ });
+ });
+ });
+});
+
View
@@ -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;
+ }
+}
+

0 comments on commit 78d6382

Please sign in to comment.