Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
fix(server): exit if db patch level is wrong
Browse files Browse the repository at this point in the history
  • Loading branch information
philbooth committed Jul 23, 2015
1 parent 4b8a0a7 commit 78d6382
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 4 deletions.
5 changes: 3 additions & 2 deletions lib/db/mysql/index.js
Expand Up @@ -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
Expand All @@ -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));
}
}
Expand Down Expand Up @@ -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);
});
Expand Down
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -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"
}
}
4 changes: 2 additions & 2 deletions test/db.js → test/db/index.js
Expand Up @@ -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*/

Expand Down
200 changes: 200 additions & 0 deletions 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);
});
});
});
});

111 changes: 111 additions & 0 deletions 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;
}
}

0 comments on commit 78d6382

Please sign in to comment.