Repository: basho/riak-nodejs-client (js)

Issue:
Support load balancers [JIRA: CLIENTS-829]
`RiakNode` should take an option indicating that it is really a load balancer and not a Riak node. If this option is set, then the node will never be marked offline.

Pull request description:
READY: Add externalLoadBalancer option to RiakNode
Resolves #151 (CLIENTS-57) (CLIENTS-829)

Patch:
diff --git a/lib/core/riakcluster.js b/lib/core/riakcluster.js
index e6a625a..e66eb74 100644
--- a/lib/core/riakcluster.js
+++ b/lib/core/riakcluster.js
@@ -8,6 +8,7 @@ var util = require('util');
 
 var RiakNode = require ('./riaknode');
 var RoundRobinNodeManager = require('./roundrobinnodemanager');
+var utils = require('./utils');
 
 /**
  * @module Core
@@ -15,12 +16,12 @@ var RoundRobinNodeManager = require('./roundrobinnodemanager');
 
 /**
  * Provides the RiakCluster class and its Builder.
- * 
- * Instances of this class maintain a set of {{#crossLink "RiakNode"}}{{/crossLink}} objects and 
+ *
+ * Instances of this class maintain a set of {{#crossLink "RiakNode"}}{{/crossLink}} objects and
  * executes commands on them.
- * 
+ *
  * __options__ is an object with the following defaults:
- * 
+ *
  *     {
  *       nodes: [defaultRiakNode],
  *       executionAttempts: 3,
@@ -28,16 +29,16 @@ var RoundRobinNodeManager = require('./roundrobinnodemanager');
  *       queueCommands: false,
  *       queueMaxDepth: unlimited
  *     }
- *     
- * The __defaultRiakNode__ is a RiakNode connected to 127.0.0.1:8087 
- * 
- * As a convenience a builder class is provided. 
- * 
+ *
+ * The __defaultRiakNode__ is a RiakNode connected to 127.0.0.1:8087
+ *
+ * As a convenience a builder class is provided.
+ *
  *     var nodeTemplate = new RiakNode.Builder().withMinConnections(10);
  *     var nodeAddys = ['192.168.1.1', '192.168.1.2'];
  *     var arrayOfNodes = RiakNode.buildNodes(nodeAddys, nodeTemplate);
  *     var myCluster = new RiakCluster.Builder().withRiakNodes(arrayOfNodes).build();
- *     
+ *
  * See {{#crossLink "RiakCluster.Builder"}}RiakCluster.Builder{{/crossLink}}
  *
  * @class RiakCluster
@@ -78,9 +79,8 @@ function RiakCluster(options) {
     // this.setMaxListeners(1);
 
     this._stateCheck = function(allowedStates) {
-        if (allowedStates.indexOf(this.state) === -1) {
-            throw new Error('RiakCluster: Illegal State; required: ' + allowedStates + ' current: ' + this.state);
-        }
+        return utils.stateCheck('[RiakCluster]', this.state,
+            allowedStates, stateNames);
     };
 }
 
@@ -153,7 +153,7 @@ function makeStopNodeFunc(node) {
     var f = function (async_cb) {
         node.stop(function (err, rslt) {
             if (err) {
-                logger.error('[RiakCluster] error stopping node (%s:%d):', 
+                logger.error('[RiakCluster] error stopping node (%s:%d):',
                     node.remoteAddress, node.remotePort, err);
             }
             if (async_cb) {
@@ -169,7 +169,7 @@ RiakCluster.prototype._startNode = function (node, callback) {
     node.on(EVT_RC, this._onRetryCommand.bind(this));
     node.start(function (err, rslt) {
         if (err) {
-            logger.error('[RiakCluster] error starting node (%s:%d):', 
+            logger.error('[RiakCluster] error starting node (%s:%d):',
                 node.remoteAddress, node.remotePort, err);
         }
         if (callback) {
@@ -179,7 +179,7 @@ RiakCluster.prototype._startNode = function (node, callback) {
 };
 
 RiakCluster.prototype._shutdown = function (callback) {
-    logger.debug('[RiakCluster] checking to see if nodes are shut down.'); 
+    logger.debug('[RiakCluster] checking to see if nodes are shut down.');
 
     var allStopped = true;
     for (var i = 0; i < this.nodes.length; i++) {
@@ -195,7 +195,7 @@ RiakCluster.prototype._shutdown = function (callback) {
         this.state = State.SHUTDOWN;
         logger.debug('[RiakCluster] cluster shut down.');
         if (this._commandQueue.length) {
-            logger.warn('[RiakCluster] There were %d commands in the queue at shutdown', 
+            logger.warn('[RiakCluster] There were %d commands in the queue at shutdown',
                 this._commandQueue.length);
         }
         this.emit(EVT_SC, this.state);
@@ -211,7 +211,7 @@ RiakCluster.prototype._shutdown = function (callback) {
 
 /**
  * Execute a command on this RiakCluster.
- * 
+ *
  * Selects a RiakNode from the cluster and executes the command on it.
  * @method execute
  * @param {Object} riakCommand - the command to execute.
@@ -372,27 +372,37 @@ RiakCluster.prototype._onRetryCommand = function(command, lastNode) {
 
 /**
  * The state of this cluster.
- * 
+ *
  * If listening for stateChange events, a numeric value will be sent that
  * can be compared to:
- * 
+ *
  *     RiakCluster.State.CREATED
  *     RiakCluster.State.RUNNING
  *     RiakCluster.State.SHUTTING_DOWN
  *     RiakCluster.State.SHUTDOWN
- * 
+ *
  * See: {{#crossLink "RiakCluster/stateChange:event"}}stateChange{{/crossLink}}
- * 
+ *
  * @property State
  * @type {Object}
  * @static
  * @final
  */
-var State = Object.freeze({ CREATED : 0, 
-                            RUNNING : 1,
-                            QUEUEING: 2, 
-                            SHUTTING_DOWN : 3, 
-                            SHUTDOWN : 4});
+var State = Object.freeze({
+    CREATED : 0,
+    RUNNING : 1,
+    QUEUEING: 2,
+    SHUTTING_DOWN : 3,
+    SHUTDOWN : 4
+});
+
+var stateNames = Object.freeze({
+    0 : 'CREATED',
+    1 : 'RUNNING',
+    2 : 'QUEUEING',
+    3 : 'SHUTTING_DOWN',
+    4 : 'SHUTDOWN'
+});
 
 var defaultRiakNode = new RiakNode();
 var defaultExecutionAttempts = 4;
@@ -435,12 +445,12 @@ var EVT_RC = 'retryCommand';
 
 /**
  * A Builder for constructing RiakCluster instances.
- * 
+ *
  * Rather than having to manually construct the __options__ and instantiating
  * a RiakCluster directly, this builder may be used.
- * 
+ *
  * var newCluster = new RiakCluster.Builder().withRiakNodes([node1, node2]).build();
- * 
+ *
  * @class RiakCluster.Builder
  * @constructor
  */
@@ -458,15 +468,15 @@ Builder.prototype = {
     withRiakNodes : function(nodes) {
         for (var i = 0; i < nodes.length; i++) {
             if (nodes[i] instanceof RiakNode) {
-                this.nodes.push(nodes[i]); 
+                this.nodes.push(nodes[i]);
             }
         }
         return this;
     },
     /**
      * Set the number of times a command will be attempted.
-     * In the case of a command failing for any reason, it will be retried 
-     * on a different node. 
+     * In the case of a command failing for any reason, it will be retried
+     * on a different node.
      * @method withExecutionAttempts
      * @param {Number} numAttempts - the number of times to attempt a command (__default:__ 3)
      * @return {RiakCluster.Builder}
@@ -477,7 +487,7 @@ Builder.prototype = {
     },
     /**
      * Set the NodeManager for this cluster.
-     * 
+     *
      * If not provided the {{#crossLink "RoundRobinNodeManager"}}{{/crossLink}} is
      * used.
      * @method withNodeManager
@@ -490,16 +500,16 @@ Builder.prototype = {
     },
     /**
      * Set whether to queue commands or not if no RiakNodes are available.
-     * 
+     *
      * If all nodes are down (health checking) or maxConnections are in use on
-     * all nodes, the default behavior is to fail commands when submitted. 
-     * 
+     * all nodes, the default behavior is to fail commands when submitted.
+     *
      * Setting this option causes the the RiakCluster to queue additional commands
-     * (FIFO) then send them when nodes/connections become available. 
-     * 
-     * If maxDepth is supplied the queue is bounded and additional commands 
+     * (FIFO) then send them when nodes/connections become available.
+     *
+     * If maxDepth is supplied the queue is bounded and additional commands
      * attempting to be queued will be failed. The default is an unbounded queue.
-     * 
+     *
      * @method withQueueCommands
      * @param {Number} [maxDepth=unlimited] the maximum number of commands to queue. Default is unlimited.
      * @param {Number} [submitInterval=500] The duration in milliseconds between queue submission attempts. Default is 500.
diff --git a/lib/core/riaknode.js b/lib/core/riaknode.js
index 6ae2897..6d5476f 100644
--- a/lib/core/riaknode.js
+++ b/lib/core/riaknode.js
@@ -57,6 +57,7 @@ var nid = 0;
  * @param {Number} [options.requestTimeout=5000] Set the timeout used when executing commands.
  * @param {Object} [options.auth] Set the authentication information for connections made by this node.
  * @param {Boolean} [options.cork] Use 'cork' on all sockets. Default is true.
+ * @param {Boolean} [options.externalLoadBalancer] This RiakNode object connects to a load balancer. Default is false.
  * @param {String} options.auth.user Riak username.
  * @param {String} [options.auth.password] Riak password. Not required if using user cert.
  * @param {String|Buffer} [options.auth.pfx] A string or buffer holding the PFX or PKCS12 encoded private key, certificate and CA certificates.
@@ -91,6 +92,7 @@ function RiakNode(options) {
         self.state = State.CREATED;
         self.auth = options.auth;
         self.cork = options.cork;
+        self.externalLoadBalancer = options.externalLoadBalancer;
         self.healthCheck = options.healthCheck;
     });
 
@@ -103,7 +105,7 @@ function RiakNode(options) {
 
     // Note: useful for debugging event issues
     // this.setMaxListeners(1);
-    
+
     // private data
     var currentNumConnections = 0;
     var available = [];
@@ -288,15 +290,26 @@ function RiakNode(options) {
     this._doHealthCheck = function () {
         switch (this.state) {
             case State.HEALTH_CHECKING:
-                logger.debug("%s already in health check", this.name);
+                if (this.externalLoadBalancer) {
+                    logger.error('%s already in health check, external load balancer:',
+                        this.name, this.externalLoadBalancer);
+                } else {
+                    logger.debug("%s already in health check", this.name);
+                }
                 break;
             case State.RUNNING:
-                this.state = State.HEALTH_CHECKING;
-                this.emit(EVT_SC, this, this.state);
-                hcb.backoff();
+                if (this.externalLoadBalancer) {
+                    logger.warn('%s would have health checked, external load balancer:',
+                        this.name, this.externalLoadBalancer);
+                } else {
+                    this.state = State.HEALTH_CHECKING;
+                    this.emit(EVT_SC, this, this.state);
+                    hcb.backoff();
+                }
                 break;
             default:
-                logger.warn("%s health check attempted in state: %d", this.state);
+                logger.warn('%s health check attempted in state:',
+                    this.name, stateNames[this.state]);
         }
     };
 
@@ -394,9 +407,8 @@ function RiakNode(options) {
     };
 
     this._stateCheck = function (allowedStates) {
-        if (allowedStates.indexOf(this.state) === -1) {
-            throw new Error("RiakNode: Illegal State; required: " + allowedStates + " current: " + this.state);
-        }
+        return utils.stateCheck(this.name, this.state,
+            allowedStates, stateNames);
     };
 }
 
@@ -472,7 +484,7 @@ RiakNode.prototype.execute = function (command) {
                     // connection is established
                     if (self._executeAllowed() && newConn.execute(command)) {
                         self.executeCount++;
-                        logger.debug("%s (%d) executed '%s'",
+                        logger.debug("%s (%d) executed command: '%s'",
                             self.name, self.executeCount, command.name);
                     }
                 }, function (err) {
@@ -495,7 +507,8 @@ RiakNode.prototype.execute = function (command) {
                 this.name, command.name, this.executeCount);
             if (conn.execute(command)) {
                 this.executeCount++;
-                logger.debug("%s executed (%d)", this.name, this.executeCount);
+                logger.debug("%s (%d) executed command: '%s'",
+                    this.name, this.executeCount, command.name);
             }
             // NB: returning true is the only option since
             // executing a command is async
@@ -590,31 +603,44 @@ RiakNode.prototype._maybeRetryCommand = function (command, errfunc) {
  * @static
  * @final
  */
-var State = Object.freeze({ CREATED : 0,
-                            RUNNING : 1,
-                            HEALTH_CHECKING : 2,
-                            SHUTTING_DOWN : 3,
-                            SHUTDOWN : 4});
-
-var defaultRemoteAddress = '127.0.0.1';
-var defaultRemotePort = 8087;
-var defaultMinConnections = 1;
-var defaultMaxConnections = 256;
-var defaultIdleTimeout = 10000;
-var defaultConnectionTimeout = 3000;
-var defaultRequestTimeout = 5000;
-var defaultHealthCheck = new Ping(function (){});
+var State = Object.freeze({
+    CREATED : 0,
+    RUNNING : 1,
+    HEALTH_CHECKING : 2,
+    SHUTTING_DOWN : 3,
+    SHUTDOWN : 4
+});
+
+var stateNames = Object.freeze({
+    0 : 'CREATED',
+    1 : 'RUNNING',
+    2 : 'HEALTH_CHECKING',
+    3 : 'SHUTTING_DOWN',
+    4 : 'SHUTDOWN'
+});
+
+var consts = Object.freeze({
+    DefaultRemoteAddress : '127.0.0.1',
+    DefaultRemotePort : 8087,
+    DefaultMinConnections : 1,
+    DefaultMaxConnections : 256,
+    DefaultIdleTimeout : 10000,
+    DefaultConnectionTimeout : 3000,
+    DefaultRequestTimeout : 5000,
+    DefaultHealthCheck : new Ping(function (){})
+});
 
 var schema = Joi.object().keys({
-    remoteAddress: Joi.string().default(defaultRemoteAddress),
-    remotePort: Joi.number().min(1).default(defaultRemotePort),
-    minConnections: Joi.number().min(0).default(defaultMinConnections),
-    maxConnections: Joi.number().min(0).default(defaultMaxConnections),
-    idleTimeout: Joi.number().min(1000).default(defaultIdleTimeout),
-    connectionTimeout: Joi.number().min(1).default(defaultConnectionTimeout),
-    requestTimeout: Joi.number().min(1).default(defaultRequestTimeout),
-    healthCheck: Joi.object().default(defaultHealthCheck),
+    remoteAddress: Joi.string().default(consts.DefaultRemoteAddress),
+    remotePort: Joi.number().min(1).default(consts.DefaultRemotePort),
+    minConnections: Joi.number().min(0).default(consts.DefaultMinConnections),
+    maxConnections: Joi.number().min(0).default(consts.DefaultMaxConnections),
+    idleTimeout: Joi.number().min(1000).default(consts.DefaultIdleTimeout),
+    connectionTimeout: Joi.number().min(1).default(consts.DefaultConnectionTimeout),
+    requestTimeout: Joi.number().min(1).default(consts.DefaultRequestTimeout),
+    healthCheck: Joi.object().default(consts.DefaultHealthCheck),
     cork: Joi.boolean().default(true),
+    externalLoadBalancer: Joi.boolean().default(false),
     auth: Joi.object().optional().keys({
         user: Joi.string().required(),
         password: Joi.string().allow(''),
@@ -661,7 +687,6 @@ var EVT_RC = 'retryCommand';
 function Builder() {}
 
 Builder.prototype = {
-
     /**
      * Set the remote address for the RiakNode.
      * @method withRemoteAddress
@@ -672,7 +697,6 @@ Builder.prototype = {
         this.remoteAddress = address;
         return this;
     },
-
     /**
      * Set the remote port for this RiakNode.
      * @method withRemotePort
@@ -683,7 +707,6 @@ Builder.prototype = {
         this.remotePort = port;
         return this;
     },
-
     /**
      * Set the minimum number of active connections to maintain.
      * These connections are exempt from the idle timeout.
@@ -695,7 +718,6 @@ Builder.prototype = {
         this.minConnections = minConnections;
         return this;
     },
-
     /**
      * Set the maximum number of connections allowed.
      * @method withMaxConnections
@@ -706,7 +728,6 @@ Builder.prototype = {
         this.maxConnections = maxConnections;
         return this;
     },
-
     /**
      * Set the idle timeout used to reap inactive connections.
      * Any connection that has been idle for this amount of time
@@ -720,7 +741,6 @@ Builder.prototype = {
         this.idleTimeout = idleTimeout;
         return this;
     },
-
     /**
      * Set the connection timeout used when making new connections.
      * @method withConnectionTimeout
@@ -731,7 +751,6 @@ Builder.prototype = {
         this.connectionTimeout = connectionTimeout;
         return this;
     },
-
     /**
      * Set the request timeout used when executing commands.
      * @method withRequestTimeout
@@ -742,7 +761,6 @@ Builder.prototype = {
         this.requestTimeout = requestTimeout;
         return this;
     },
-
     /**
      * Set whether to use the cork/uncork socket functions.
      *
@@ -754,7 +772,17 @@ Builder.prototype = {
         this.cork = cork;
         return this;
     },
-
+    /**
+     * Set whether this RiakNode connects to an external load balancer.
+     *
+     * @method withExternalLoadBalancer
+     * @param {Boolean} [externalLoadBalancer=true] connects to external load balancer. Default is false.
+     * @chainable
+     */
+    withExternalLoadBalancer : function (externalLoadBalancer) {
+        this.externalLoadBalancer = externalLoadBalancer;
+        return this;
+    },
     /**
      * Set the authentication information for connections made by this node.
      * @method withAuth
@@ -773,7 +801,6 @@ Builder.prototype = {
         this.auth = auth;
         return this;
     },
-
     /**
      * Set the command to be used for a health check.
      *
@@ -789,7 +816,6 @@ Builder.prototype = {
         this.healthCheck = healthCheck;
         return this;
     },
-
     /**
      * Builds a RiakNode instance.
      * @method build
@@ -839,3 +865,4 @@ module.exports = RiakNode;
 module.exports.buildNodes = buildNodes;
 module.exports.Builder = Builder;
 module.exports.State = State;
+module.exports.consts = consts;
diff --git a/lib/core/utils.js b/lib/core/utils.js
index 9366746..de3d69a 100644
--- a/lib/core/utils.js
+++ b/lib/core/utils.js
@@ -54,4 +54,16 @@ function handleRiakResponse(d, onError, onSuccess) {
     }
 }
 
+function stateCheck(name, state, allowedStates, stateNames) {
+    if (allowedStates.indexOf(state) === -1) {
+        var sn = allowedStates.map(function (val, idx, ary) {
+            return stateNames[val];
+        });
+        var msg = util.format('%s illegal state! required: %s current: %s',
+            name, sn, stateNames[state]);
+        throw new Error(msg);
+    }
+}
+
 module.exports.handleRiakResponse = handleRiakResponse;
+module.exports.stateCheck = stateCheck;


Test patch:
diff --git a/test/integration/core/riaknode.js b/test/integration/core/riaknode.js
index fa94283..07dee02 100644
--- a/test/integration/core/riaknode.js
+++ b/test/integration/core/riaknode.js
@@ -65,11 +65,81 @@ describe('integration-core-riaknode', function() {
 
                 node.start(scb);
             };
-            
+
             server.listen({ host: '127.0.0.1', port: port }, lcb);
         });
     });
 
+    describe('load-balancer', function() {
+        it('does-not-health-check', function(done) {
+            var port = Test.getPort();
+            var i = 0;
+            var server = net.createServer(function(socket) {
+                socket.on('data' , function(data) {
+                    i++;
+                    logger.debug('[t/i/c/rn] i: %d', i);
+                    if (i % 2 === 0) {
+                        socket.destroy();
+                    } else {
+                        var header = new Buffer(5);
+                        header.writeUInt8(2, 4);
+                        header.writeInt32BE(1, 0);
+                        socket.write(header);
+                    }
+                });
+            });
+
+            server.listen(port, '127.0.0.1', function () {
+                var node = new RiakNode.Builder()
+                        .withRemotePort(port)
+                        .withMinConnections(1)
+                        .withMaxConnections(1)
+                        .withExternalLoadBalancer(true)
+                        .build();
+
+                var sawHealthCheck = false;
+
+                function ping() {
+                    logger.debug('[t/i/c/rn] executing ping, i: %d', i);
+                    var ping = new Ping(pingCb);
+                    node.execute(ping);
+                }
+
+                var pingCb = function(err, resp) {
+                    if (err) {
+                        logger.debug('[t/i/c/rn] ping err:', err);
+                    }
+                    if (i < 10) {
+                        setTimeout(ping, 125);
+                    } else {
+                        node.removeAllListeners();
+                        node.stop(function (err, rslt) {
+                            assert(!err);
+                            assert.strictEqual(sawHealthCheck, false);
+                            assert.equal(rslt, RiakNode.State.SHUTDOWN);
+                            server.close(function () {
+                                done();
+                            });
+                        });
+                    }
+                };
+
+                var stateCb = function(node, state) {
+                    if (state === RiakNode.State.HEALTH_CHECKING) {
+                        sawHealthCheck = true;
+                    }
+                };
+
+                node.start(function (err, n) {
+                    assert(Object.is(node, n));
+                    assert(!err, err);
+                    node.on('stateChange', stateCb);
+                    ping();
+                });
+            });
+        });
+    });
+
     describe('health-check', function() {
         it('recovers-using-default-check', function(done) {
             var socketClosed = false;
@@ -92,7 +162,7 @@ describe('integration-core-riaknode', function() {
                     });
                 }
             });
-            
+
             server.listen(port, '127.0.0.1', function () {
                 var node = new RiakNode.Builder()
                         .withRemotePort(port)
@@ -125,7 +195,7 @@ describe('integration-core-riaknode', function() {
                         });
                     }
                 };
-                
+
                 node.start(function (err, n) {
                     assert(Object.is(node, n));
                     assert(!err, err);
@@ -138,7 +208,7 @@ describe('integration-core-riaknode', function() {
                 });
             });
         });
-        
+
         it('should recover using StoreValue as a check', function(done) {
             var port = Test.getPort();
             var connects = 0;
@@ -156,7 +226,7 @@ describe('integration-core-riaknode', function() {
                     });
                 }
             });
-            
+
             server.listen(port, '127.0.0.1', function () {
                 var storeCheck = new StoreValue({bucket: 'b', value: 'v'}, function(){});
                 var node = new RiakNode.Builder()
@@ -177,7 +247,7 @@ describe('integration-core-riaknode', function() {
                         default:
                             break;
                     }
-                    
+
                     if (heathChecking && healthChecked) {
                         node.removeAllListeners();
                         node.stop(function (err, rslt) {
@@ -189,7 +259,7 @@ describe('integration-core-riaknode', function() {
                         });
                     }
                 };
-                
+
                 node.start(function (err, n) {
                     assert(Object.is(node, n));
                     assert(!err, err);
diff --git a/test/unit/core/riaknode.js b/test/unit/core/riaknode.js
index 65a820e..ef03b5b 100644
--- a/test/unit/core/riaknode.js
+++ b/test/unit/core/riaknode.js
@@ -6,9 +6,32 @@ var joi = require('joi');
 var fs = require('fs');
 
 describe('RiakNode', function() {
-    describe('auth-validation', function() {
+    describe('builder', function() {
+        it('uses-default-values', function(done) {
+            var b = new RiakNode.Builder();
+            var n = b.build();
+            assert.strictEqual(n.remoteAddress, RiakNode.consts.DefaultRemoteAddress);
+            assert.strictEqual(n.remotePort, RiakNode.consts.DefaultRemotePort);
+            assert.strictEqual(n.minConnections, RiakNode.consts.DefaultMinConnections);
+            assert.strictEqual(n.maxConnections, RiakNode.consts.DefaultMaxConnections);
+            assert.strictEqual(n.idleTimeout, RiakNode.consts.DefaultIdleTimeout);
+            assert.strictEqual(n.requestTimeout, RiakNode.consts.DefaultRequestTimeout);
+            assert.strictEqual(n.healthCheck, RiakNode.consts.DefaultHealthCheck);
+            assert.strictEqual(n.cork, true);
+            assert.strictEqual(n.externalLoadBalancer, false);
+            done();
+        });
 
-        it('should require user', function(done) {
+        it('can-specify-load-balancer', function(done) {
+            var b = new RiakNode.Builder();
+            b.withExternalLoadBalancer(true);
+            var n = b.build();
+            assert.strictEqual(n.externalLoadBalancer, true);
+            done();
+        });
+    });
+    describe('auth-validation', function() {
+        it('requires-user', function(done) {
             var options = {
                 auth: {
                 }
@@ -25,7 +48,7 @@ describe('RiakNode', function() {
             done();
         });
 
-        it('should OK user with empty password specified', function(done) {
+        it('user-and-empty-password-is-ok', function(done) {
             // This shouldn't throw because it is allowed to have an empty password
             var options = {
                 auth: {
@@ -41,7 +64,7 @@ describe('RiakNode', function() {
             done();
         });
 
-        it('should OK user and password', function(done) {
+        it('user-and-password-is-ok', function(done) {
             // This shouldn't throw because user + password is a valid auth combo
             var options = {
                 auth: {
@@ -57,7 +80,7 @@ describe('RiakNode', function() {
             done();
         });
 
-        it('should disallow user with just cert or just key', function(done) {
+        it('user-with-just-cert-or-key-is-not-ok', function(done) {
             // This should throw because *both* cert and key are required
             var options = {
                 auth: {
@@ -92,7 +115,7 @@ describe('RiakNode', function() {
             done();
         });
 
-        it('should OK user with cert & key', function(done) {
+        it('user-with-cert-and-key-is-ok', function(done) {
             // This should not throw because *both* cert and key are provided
             var options = {
                 auth: {
@@ -109,7 +132,7 @@ describe('RiakNode', function() {
             done();
         });
 
-        it('should OK user with cert & key read from a file', function(done) {
+        it('user-with-cert-and-key-from-files-is-ok', function(done) {
             // This should not throw because *both* cert and key are provided
             var options = {
                 auth: {
@@ -127,7 +150,7 @@ describe('RiakNode', function() {
             done();
         });
 
-        it('should OK user and pfx', function(done) {
+        it('user-and-pfx-is-ok', function(done) {
             // This should not throw because pfx files contain both public and private keys
             var options = {
                 auth: {
@@ -143,7 +166,7 @@ describe('RiakNode', function() {
             done();
         });
 
-        it('should OK user and pfx read from file', function(done) {
+        it('user-and-pfx-from-file-is-ok', function(done) {
             // This should not throw because pfx files contain both public and private keys
             var options = {
                 auth: {
@@ -160,7 +183,7 @@ describe('RiakNode', function() {
             done();
         });
 
-        it('should disallow password and pfx, or password and cert/key', function(done) {
+        it('disallows-password-with-certs', function(done) {
             var options = {
                 auth: {
                     user: 'riaktest',
diff --git a/test/unit/core/utils.js b/test/unit/core/utils.js
new file mode 100644
index 0000000..fc43334
--- /dev/null
+++ b/test/unit/core/utils.js
@@ -0,0 +1,42 @@
+'use strict';
+
+var assert = require('assert');
+var logger = require('winston');
+
+var utils = require('../../../lib/core/utils');
+
+var state = Object.freeze({
+    CREATED : 0,
+    RUNNING : 1,
+    HEALTH_CHECKING : 2,
+    SHUTTING_DOWN : 3,
+    SHUTDOWN : 4
+});
+
+var stateNames = Object.freeze({
+    0 : 'CREATED',
+    1 : 'RUNNING',
+    2 : 'HEALTH_CHECKING',
+    3 : 'SHUTTING_DOWN',
+    4 : 'SHUTDOWN'
+});
+
+describe('core-utils', function() {
+    it('state-check-failure', function(done) {
+        assert.throws(
+            function () {
+                utils.stateCheck('FRAZZLE',
+                    state.RUNNING, [state.CREATED, state.SHUTDOWN], stateNames);
+            },
+            function (err) {
+                logger.debug('[t/u/c/utils] saw err:', err.message);
+                return err instanceof Error &&
+                    /RUNNING/.test(err) &&
+                    /CREATED/.test(err) &&
+                    /SHUTDOWN/.test(err);
+            },
+            'unexpected error!'
+        );
+        done();
+    });
+});