1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-10 11:43:01 +03:00
Files
node-redis/test/node_redis.spec.js
Ruben Bridgewater a857829a36 Improve error handling
Arguments are now passed to an command error in case they exist
An error is only emitted if that very same error is not already handled in a callback
2016-04-29 04:10:23 +02:00

1139 lines
58 KiB
JavaScript

'use strict';
var assert = require('assert');
var config = require('./lib/config');
var helper = require('./helper');
var utils = require('../lib/utils');
var fork = require('child_process').fork;
var redis = config.redis;
describe('The node_redis client', function () {
helper.allTests(function (parser, ip, args) {
describe('using ' + parser + ' and ' + ip, function () {
var client;
afterEach(function () {
client.end(true);
});
describe('when connected', function () {
beforeEach(function (done) {
client = redis.createClient.apply(null, args);
client.once('connect', function () {
client.flushdb(done);
});
});
describe('duplicate', function () {
it('check if all options got copied properly', function (done) {
client.selected_db = 2;
var client2 = client.duplicate();
assert.strictEqual(client.connectionId + 1, client2.connection_id);
assert.strictEqual(client2.selected_db, 2);
assert(client.connected);
assert(!client2.connected);
for (var elem in client.options) {
if (client.options.hasOwnProperty(elem)) {
assert.strictEqual(client2.options[elem], client.options[elem]);
}
}
client2.on('ready', function () {
client2.end(true);
done();
});
});
it('check if all new options replaced the old ones', function (done) {
var client2 = client.duplicate({
no_ready_check: true
});
assert(client.connected);
assert(!client2.connected);
assert.strictEqual(client.options.no_ready_check, undefined);
assert.strictEqual(client2.options.no_ready_check, true);
assert.notDeepEqual(client.options, client2.options);
for (var elem in client.options) {
if (client.options.hasOwnProperty(elem)) {
if (elem !== 'no_ready_check') {
assert.strictEqual(client2.options[elem], client.options[elem]);
}
}
}
client2.on('ready', function () {
client2.end(true);
done();
});
});
it('works with a callback', function (done) {
client.duplicate(function (err, client) {
assert(!err);
assert.strictEqual(client.ready, true);
client.quit(done);
});
});
it('works with a callback and errors out', function (done) {
client.duplicate({
port: '9999'
}, function (err, client) {
assert.strictEqual(err.code, 'ECONNREFUSED');
done(client);
});
});
it('works with a promises', function () {
return client.duplicateAsync().then(function (client) {
assert.strictEqual(client.ready, true);
return client.quitAsync();
});
});
it('works with a promises and errors', function () {
return client.duplicateAsync({
port: 9999
}).catch(function (err) {
assert.strictEqual(err.code, 'ECONNREFUSED');
});
});
});
describe('big data', function () {
// Check if the fast mode for big strings is working correct
it('safe strings that are bigger than 30000 characters', function (done) {
var str = 'foo ಠ_ಠ bar ';
while (str.length < 111111) {
str += str;
}
client.set('foo', str);
client.get('foo', function (err, res) {
assert.strictEqual(res, str);
done();
});
});
it('safe strings that are bigger than 30000 characters with multi', function (done) {
var str = 'foo ಠ_ಠ bar ';
while (str.length < 111111) {
str += str;
}
var called = false;
var temp = client.write_buffers.bind(client);
assert(client.fire_strings);
client.write_buffers = function (data) {
called = true;
// To increase write performance for strings the value is converted to a buffer
assert(!client.fire_strings);
temp(data);
};
client.multi().set('foo', str).get('foo', function (err, res) {
assert.strictEqual(res, str);
}).exec(function (err, res) {
assert(called);
assert.strictEqual(res[1], str);
done();
});
assert(client.fire_strings);
});
});
describe('send_command', function () {
it('omitting args should be fine', function (done) {
client.server_info = {};
client.send_command('info');
client.send_command('ping', function (err, res) {
assert.strictEqual(res, 'PONG');
// Check if the previous info command used the internal individual info command
assert.notDeepEqual(client.server_info, {});
client.server_info = {};
});
client.send_command('info', null, undefined);
client.send_command('ping', null, function (err, res) {
assert.strictEqual(res, 'PONG');
// Check if the previous info command used the internal individual info command
assert.notDeepEqual(client.server_info, {});
client.server_info = {};
});
client.send_command('info', undefined, undefined);
client.send_command('ping', function (err, res) {
assert.strictEqual(res, 'PONG');
// Check if the previous info command used the internal individual info command
assert.notDeepEqual(client.server_info, {});
client.server_info = {};
});
client.send_command('info', undefined, function (err, res) {
assert(/redis_version/.test(res));
// The individual info command should also be called by using send_command
// console.log(info, client.server_info);
assert.notDeepEqual(client.server_info, {});
done();
});
});
it('using multi with send_command should work as individual command instead of using the internal multi', function (done) {
// This is necessary to keep backwards compatibility and it is the only way to handle multis as you want in node_redis
client.send_command('multi');
client.send_command('set', ['foo', 'bar'], helper.isString('QUEUED'));
client.get('foo');
client.exec(function (err, res) { // exec is not manipulated if not fired by the individual multi command
// As the multi command is handled individually by the user he also has to handle the return value
assert.strictEqual(res[0].toString(), 'OK');
assert.strictEqual(res[1].toString(), 'bar');
done();
});
});
it('multi should be handled special', function (done) {
client.send_command('multi', undefined, helper.isString('OK'));
var args = ['test', 'bla'];
client.send_command('set', args, helper.isString('QUEUED'));
assert.deepEqual(args, ['test', 'bla']); // Check args manipulation
client.get('test', helper.isString('QUEUED'));
client.exec(function (err, res) {
// As the multi command is handled individually by the user he also has to handle the return value
assert.strictEqual(res[0].toString(), 'OK');
assert.strictEqual(res[1].toString(), 'bla');
done();
});
});
it('using another type as cb should throw', function () {
try {
client.send_command('set', ['test', 'bla'], [true]);
throw new Error('failed');
} catch (err) {
assert.strictEqual(err.message, 'Wrong input type "Array" for callback function');
}
try {
client.send_command('set', ['test', 'bla'], null);
throw new Error('failed');
} catch (err) {
assert.strictEqual(err.message, 'Wrong input type "null" for callback function');
}
});
it('command argument has to be of type string', function () {
try {
client.send_command(true, ['test', 'bla'], function () {});
throw new Error('failed');
} catch (err) {
assert.strictEqual(err.message, 'Wrong input type "Boolean" for command name');
}
try {
client.send_command(undefined, ['test', 'bla'], function () {});
throw new Error('failed');
} catch (err) {
assert.strictEqual(err.message, 'Wrong input type "undefined" for command name');
}
try {
client.send_command(null, ['test', 'bla'], function () {});
throw new Error('failed');
} catch (err) {
assert.strictEqual(err.message, 'Wrong input type "null" for command name');
}
});
it('args may only be of type Array or undefined', function () {
try {
client.send_command('info', 123);
throw new Error('failed');
} catch (err) {
assert.strictEqual(err.message, 'Wrong input type "Number" for args');
}
});
it('passing a callback as args and as callback should throw', function () {
try {
client.send_command('info', function a () {}, function b () {});
throw new Error('failed');
} catch (err) {
assert.strictEqual(err.message, 'Wrong input type "Function" for args');
}
});
it('multi should be handled special', function (done) {
client.send_command('multi', undefined, helper.isString('OK'));
var args = ['test', 'bla'];
client.send_command('set', args, helper.isString('QUEUED'));
assert.deepEqual(args, ['test', 'bla']); // Check args manipulation
client.get('test', helper.isString('QUEUED'));
client.exec(function (err, res) {
// As the multi command is handled individually by the user he also has to handle the return value
assert.strictEqual(res[0].toString(), 'OK');
assert.strictEqual(res[1].toString(), 'bla');
done();
});
});
it('the args array may contain a arbitrary number of arguments', function (done) {
client.send_command('mset', ['foo', 1, 'bar', 2, 'baz', 3], helper.isString('OK'));
client.mget(['foo', 'bar', 'baz'], function (err, res) {
// As the multi command is handled individually by the user he also has to handle the return value
assert.strictEqual(res[0].toString(), '1');
assert.strictEqual(res[1].toString(), '2');
assert.strictEqual(res[2].toString(), '3');
done();
});
});
it('send_command with callback as args', function (done) {
client.send_command('abcdef', function (err, res) {
assert.strictEqual(err.message, "ERR unknown command 'abcdef'");
done();
});
});
});
describe('retry_unfulfilled_commands', function () {
it('should retry all commands instead of returning an error if a command did not yet return after a connection loss', function (done) {
var bclient = redis.createClient({
parser: parser,
retry_unfulfilled_commands: true
});
bclient.blpop('blocking list 2', 5, function (err, value) {
assert.strictEqual(value[0], 'blocking list 2');
assert.strictEqual(value[1], 'initial value');
done(err);
});
bclient.once('ready', function () {
setTimeout(function () {
bclient.stream.destroy();
client.rpush('blocking list 2', 'initial value', helper.isNumber(1));
}, 100);
});
});
});
describe('.end', function () {
it('used without flush / flush set to false', function (done) {
var finished = false;
var end = helper.callFuncAfter(function () {
if (!finished) {
done(new Error('failed'));
}
}, 20);
var cb = function (err, res) {
assert(/The connection has already been closed/.test(err.message));
end();
};
for (var i = 0; i < 20; i++) {
if (i === 10) {
client.end();
}
client.set('foo', 'bar', cb);
}
client.on('warning', function () {}); // Ignore deprecation message
setTimeout(function () {
finished = true;
done();
}, 25);
});
it('used with flush set to true', function (done) {
var end = helper.callFuncAfter(function () {
done();
}, 20);
var cb = function (err, res) {
assert(/The connection has already been closed./.test(err.message));
end();
};
for (var i = 0; i < 20; i++) {
if (i === 10) {
client.end(true);
client.stream.write('foo'); // Trigger an error on the closed stream that we ignore
}
client.set('foo', 'bar', cb);
}
});
});
describe('commands after using .quit should fail', function () {
it('return an error in the callback', function (done) {
if (helper.redisProcess().spawnFailed()) this.skip();
// TODO: Investigate why this test is failing hard and killing mocha if using '/tmp/redis.sock'.
// Seems like something is wrong with nyc while passing a socket connection to create client!
client = redis.createClient();
client.quit(function () {
client.get('foo', function (err, res) {
assert.strictEqual(err.message, 'Stream connection ended and running command aborted. It might have been processed.');
assert.strictEqual(client.offline_queue.length, 0);
done();
});
});
});
it('return an error in the callback version two', function (done) {
if (helper.redisProcess().spawnFailed()) this.skip();
client.quit();
setTimeout(function () {
client.get('foo', function (err, res) {
assert.strictEqual(err.message, 'GET can\'t be processed. The connection has already been closed.');
assert.strictEqual(err.command, 'GET');
assert.strictEqual(client.offline_queue.length, 0);
done();
});
}, 50);
});
it('emit an error', function (done) {
if (helper.redisProcess().spawnFailed()) this.skip();
client.quit();
client.on('error', function (err) {
assert.strictEqual(err.message, 'SET can\'t be processed. The connection has already been closed.');
assert.strictEqual(err.command, 'SET');
assert.strictEqual(client.offline_queue_length, 0);
done();
});
setTimeout(function () {
client.set('foo', 'bar');
}, 50);
});
});
describe('when redis closes unexpectedly', function () {
it('reconnects and can retrieve the pre-existing data', function (done) {
client.on('reconnecting', function on_recon (params) {
client.on('connect', function on_connect () {
var end = helper.callFuncAfter(function () {
client.removeListener('connect', on_connect);
client.removeListener('reconnecting', on_recon);
assert.strictEqual(client.server_info.db0.keys, 2);
assert.strictEqual(Object.keys(client.server_info.db0).length, 3);
done();
}, 4);
client.get('recon 1', helper.isString('one', end));
client.get('recon 1', helper.isString('one', end));
client.get('recon 2', helper.isString('two', end));
client.get('recon 2', helper.isString('two', end));
});
});
client.set('recon 1', 'one');
client.set('recon 2', 'two', function (err, res) {
// Do not do this in normal programs. This is to simulate the server closing on us.
// For orderly shutdown in normal programs, do client.quit()
client.stream.destroy();
});
});
it('reconnects properly when monitoring', function (done) {
client.on('reconnecting', function on_recon (params) {
client.on('ready', function on_ready () {
assert.strictEqual(client.monitoring, true, 'monitoring after reconnect');
client.removeListener('ready', on_ready);
client.removeListener('reconnecting', on_recon);
done();
});
});
assert.strictEqual(client.monitoring, false, 'monitoring off at start');
client.set('recon 1', 'one');
client.monitor(function (err, res) {
assert.strictEqual(client.monitoring, true, 'monitoring on after monitor()');
client.set('recon 2', 'two', function (err, res) {
// Do not do this in normal programs. This is to simulate the server closing on us.
// For orderly shutdown in normal programs, do client.quit()
client.stream.destroy();
});
});
});
describe("and it's subscribed to a channel", function () {
// "Connection in subscriber mode, only subscriber commands may be used"
it('reconnects, unsubscribes, and can retrieve the pre-existing data', function (done) {
client.on('ready', function on_connect () {
client.unsubscribe(helper.isNotError());
client.on('unsubscribe', function (channel, count) {
// we should now be out of subscriber mode.
assert.strictEqual(channel, 'recon channel');
assert.strictEqual(count, 0);
client.set('foo', 'bar', helper.isString('OK', done));
});
});
client.set('recon 1', 'one');
client.subscribe('recon channel', function (err, res) {
// Do not do this in normal programs. This is to simulate the server closing on us.
// For orderly shutdown in normal programs, do client.quit()
client.stream.destroy();
});
});
it('reconnects, unsubscribes, and can retrieve the pre-existing data of a explicit channel', function (done) {
client.on('ready', function on_connect () {
client.unsubscribe('recon channel', helper.isNotError());
client.on('unsubscribe', function (channel, count) {
// we should now be out of subscriber mode.
assert.strictEqual(channel, 'recon channel');
assert.strictEqual(count, 0);
client.set('foo', 'bar', helper.isString('OK', done));
});
});
client.set('recon 1', 'one');
client.subscribe('recon channel', function (err, res) {
// Do not do this in normal programs. This is to simulate the server closing on us.
// For orderly shutdown in normal programs, do client.quit()
client.stream.destroy();
});
});
});
describe('domain', function () {
it('allows client to be executed from within domain', function (done) {
var domain = require('domain').create();
domain.run(function () {
client.set('domain', 'value', function (err, res) {
assert.ok(process.domain);
throw new Error('ohhhh noooo');
});
});
// this is the expected and desired behavior
domain.on('error', function (err) {
assert.strictEqual(err.message, 'ohhhh noooo');
domain.exit();
done();
});
});
it('catches all errors from within the domain', function (done) {
var domain = require('domain').create();
domain.run(function () {
// Trigger an error within the domain
client.end(true);
client.set('domain', 'value');
});
domain.on('error', function (err) {
assert.strictEqual(err.message, 'SET can\'t be processed. The connection has already been closed.');
domain.exit();
done();
});
});
});
});
describe('monitor', function () {
it('monitors commands on all redis clients and works in the correct order', function (done) {
var monitorClient = redis.createClient.apply(null, args);
var responses = [];
var end = helper.callFuncAfter(done, 5);
monitorClient.set('foo', 'bar');
monitorClient.flushdb();
monitorClient.monitor(function (err, res) {
assert.strictEqual(res, 'OK');
client.mget('some', 'keys', 'foo', 'bar');
client.set('json', JSON.stringify({
foo: '123',
bar: 'sdflkdfsjk',
another: false
}));
monitorClient.get('baz', function (err, res) {
assert.strictEqual(res, null);
end(err);
});
monitorClient.set('foo', 'bar" "s are " " good!"', function (err, res) {
assert.strictEqual(res, 'OK');
end(err);
});
monitorClient.mget('foo', 'baz', function (err, res) {
assert.strictEqual(res[0], 'bar" "s are " " good!"');
assert.strictEqual(res[1], null);
end(err);
});
monitorClient.subscribe('foo', 'baz', function (err, res) {
// The return value might change in v.3
// assert.strictEqual(res, 'baz');
// TODO: Fix the return value of subscribe calls
end(err);
});
});
monitorClient.on('monitor', function (time, args, rawOutput) {
responses.push(args);
assert(utils.monitor_regex.test(rawOutput), rawOutput);
if (responses.length === 6) {
assert.deepEqual(responses[0], ['mget', 'some', 'keys', 'foo', 'bar']);
assert.deepEqual(responses[1], ['set', 'json', '{"foo":"123","bar":"sdflkdfsjk","another":false}']);
assert.deepEqual(responses[2], ['get', 'baz']);
assert.deepEqual(responses[3], ['set', 'foo', 'bar" "s are " " good!"']);
assert.deepEqual(responses[4], ['mget', 'foo', 'baz']);
assert.deepEqual(responses[5], ['subscribe', 'foo', 'baz']);
monitorClient.quit(end);
}
});
});
it('monitors returns strings in the rawOutput even with return_buffers activated', function (done) {
var monitorClient = redis.createClient({
return_buffers: true
});
monitorClient.MONITOR(function (err, res) {
assert.strictEqual(res.inspect(), new Buffer('OK').inspect());
client.mget('hello', new Buffer('world'));
});
monitorClient.on('monitor', function (time, args, rawOutput) {
assert.strictEqual(typeof rawOutput, 'string');
assert(utils.monitor_regex.test(rawOutput), rawOutput);
assert.deepEqual(args, ['mget', 'hello', 'world']);
// Quit immediatly ends monitoring mode and therefore does not stream back the quit command
monitorClient.quit(done);
});
});
it('monitors reconnects properly and works with the offline queue', function (done) {
var i = 0;
client.MONITOR(helper.isString('OK'));
client.mget('hello', 'world');
client.on('monitor', function (time, args, rawOutput) {
assert(utils.monitor_regex.test(rawOutput), rawOutput);
assert.deepEqual(args, ['mget', 'hello', 'world']);
if (i++ === 2) {
// End after two reconnects
return done();
}
client.stream.destroy();
client.mget('hello', 'world');
});
});
it('monitors reconnects properly and works with the offline queue in a batch statement', function (done) {
var i = 0;
var multi = client.batch();
multi.MONITOR(helper.isString('OK'));
multi.mget('hello', 'world');
multi.exec(function (err, res) {
assert.deepEqual(res, ['OK', [null, null]]);
});
client.on('monitor', function (time, args, rawOutput) {
assert(utils.monitor_regex.test(rawOutput), rawOutput);
assert.deepEqual(args, ['mget', 'hello', 'world']);
if (i++ === 2) {
// End after two reconnects
return done();
}
client.stream.destroy();
client.mget('hello', 'world');
});
});
it('monitor does not activate if the command could not be processed properly', function (done) {
client.MONITOR(function (err, res) {
assert.strictEqual(err.code, 'UNCERTAIN_STATE');
});
client.on('error', function (err) {}); // Ignore error here
client.stream.destroy();
client.on('monitor', function (time, args, rawOutput) {
done(new Error('failed')); // Should not be activated
});
client.on('reconnecting', function () {
client.get('foo', function (err, res) {
assert(!err);
assert.strictEqual(client.monitoring, false);
setTimeout(done, 10); // The monitor command might be returned a tiny bit later
});
});
});
it('monitors works in combination with the pub sub mode and the offline queue', function (done) {
var responses = [];
var pub = redis.createClient();
pub.on('ready', function () {
client.MONITOR(function (err, res) {
assert.strictEqual(res, 'OK');
pub.get('foo', helper.isNull());
});
client.subscribe('/foo', '/bar');
client.unsubscribe('/bar');
setTimeout(function () {
client.stream.destroy();
client.once('ready', function () {
pub.publish('/foo', 'hello world');
});
client.set('foo', 'bar', helper.isError());
client.subscribe('baz');
client.unsubscribe('baz');
}, 150);
var called = false;
client.on('monitor', function (time, args, rawOutput) {
responses.push(args);
assert(utils.monitor_regex.test(rawOutput), rawOutput);
if (responses.length === 7) {
assert.deepEqual(responses[0], ['subscribe', '/foo', '/bar']);
assert.deepEqual(responses[1], ['unsubscribe', '/bar']);
assert.deepEqual(responses[2], ['get', 'foo']);
assert.deepEqual(responses[3], ['subscribe', '/foo']);
assert.deepEqual(responses[4], ['subscribe', 'baz']);
assert.deepEqual(responses[5], ['unsubscribe', 'baz']);
assert.deepEqual(responses[6], ['publish', '/foo', 'hello world']);
// The publish is called right after the reconnect and the monitor is called before the message is emitted.
// Therefore we have to wait till the next tick
process.nextTick(function () {
assert(called);
client.quit(done);
pub.end(false);
});
}
});
client.on('message', function (channel, msg) {
assert.strictEqual(channel, '/foo');
assert.strictEqual(msg, 'hello world');
called = true;
});
});
});
});
describe('idle', function () {
it('emits idle as soon as there are no outstanding commands', function (done) {
var end = helper.callFuncAfter(done, 2);
client.on('warning', function (msg) {
assert.strictEqual(
msg,
'The idle event listener is deprecated and will likely be removed in v.3.0.0.\n' +
'If you rely on this feature please open a new ticket in node_redis with your use case'
);
end();
});
client.on('idle', function onIdle () {
client.removeListener('idle', onIdle);
client.get('foo', helper.isString('bar', end));
});
client.set('foo', 'bar');
});
});
describe('utf8', function () {
it('handles utf-8 keys', function (done) {
var utf8_sample = 'ಠ_ಠ';
client.set(['utf8test', utf8_sample], helper.isString('OK'));
client.get(['utf8test'], function (err, obj) {
assert.strictEqual(utf8_sample, obj);
done(err);
});
});
});
});
describe('unref', function () {
it('exits subprocess as soon as final command is processed', function (done) {
this.timeout(12000);
var args = config.HOST[ip] ? [config.HOST[ip], config.PORT] : [ip];
var external = fork('./test/lib/unref.js', args);
var id = setTimeout(function () {
external.kill();
done(Error('unref subprocess timed out'));
}, 8000);
external.on('close', function (code) {
clearTimeout(id);
assert.strictEqual(code, 0);
done();
});
});
});
describe('execution order / fire query while loading', function () {
it('keep execution order for commands that may fire while redis is still loading', function (done) {
client = redis.createClient.apply(null, args);
var fired = false;
client.set('foo', 'bar', function (err, res) {
assert(fired === false);
done();
});
client.info(function (err, res) {
fired = true;
});
});
// TODO: consider allowing loading commands in v.3
// it('should fire early', function (done) {
// client = redis.createClient.apply(null, args);
// var fired = false;
// client.info(function (err, res) {
// fired = true;
// });
// client.set('foo', 'bar', function (err, res) {
// assert(fired);
// done();
// });
// assert.strictEqual(client.offline_queue.length, 1);
// assert.strictEqual(client.command_queue.length, 1);
// client.on('connect', function () {
// assert.strictEqual(client.offline_queue.length, 1);
// assert.strictEqual(client.command_queue.length, 1);
// });
// client.on('ready', function () {
// assert.strictEqual(client.offline_queue.length, 0);
// });
// });
});
describe('socket_nodelay', function () {
describe('true', function () {
var args = config.configureClient(parser, ip, {
socket_nodelay: true
});
it("fires client.on('ready')", function (done) {
client = redis.createClient.apply(null, args);
client.on('ready', function () {
assert.strictEqual(true, client.options.socket_nodelay);
client.quit(done);
});
});
it('client is functional', function (done) {
client = redis.createClient.apply(null, args);
client.on('ready', function () {
assert.strictEqual(true, client.options.socket_nodelay);
client.set(['set key 1', 'set val'], helper.isString('OK'));
client.set(['set key 2', 'set val'], helper.isString('OK'));
client.get(['set key 1'], helper.isString('set val'));
client.get(['set key 2'], helper.isString('set val'));
client.quit(done);
});
});
});
describe('false', function () {
var args = config.configureClient(parser, ip, {
socket_nodelay: false
});
it("fires client.on('ready')", function (done) {
client = redis.createClient.apply(null, args);
client.on('ready', function () {
assert.strictEqual(false, client.options.socket_nodelay);
client.quit(done);
});
});
it('client is functional', function (done) {
client = redis.createClient.apply(null, args);
client.on('ready', function () {
assert.strictEqual(false, client.options.socket_nodelay);
client.set(['set key 1', 'set val'], helper.isString('OK'));
client.set(['set key 2', 'set val'], helper.isString('OK'));
client.get(['set key 1'], helper.isString('set val'));
client.get(['set key 2'], helper.isString('set val'));
client.quit(done);
});
});
});
describe('defaults to true', function () {
it("fires client.on('ready')", function (done) {
client = redis.createClient.apply(null, args);
client.on('ready', function () {
assert.strictEqual(true, client.options.socket_nodelay);
client.quit(done);
});
});
it('client is functional', function (done) {
client = redis.createClient.apply(null, args);
client.on('ready', function () {
assert.strictEqual(true, client.options.socket_nodelay);
client.set(['set key 1', 'set val'], helper.isString('OK'));
client.set(['set key 2', 'set val'], helper.isString('OK'));
client.get(['set key 1'], helper.isString('set val'));
client.get(['set key 2'], helper.isString('set val'));
client.quit(done);
});
});
});
});
describe('retry_max_delay', function () {
it('sets upper bound on how long client waits before reconnecting', function (done) {
var time;
var timeout = process.platform !== 'win32' ? 20 : 100;
client = redis.createClient.apply(null, config.configureClient(parser, ip, {
retry_max_delay: 1 // ms
}));
client.on('ready', function () {
if (this.times_connected === 1) {
this.stream.end();
time = Date.now();
} else {
done();
}
});
client.on('reconnecting', function () {
time = Date.now() - time;
assert(time < timeout, 'The reconnect should not have taken longer than ' + timeout + ' but it took ' + time);
});
client.on('error', function (err) {
// This is rare but it might be triggered.
// So let's have a robust test
assert.strictEqual(err.code, 'ECONNRESET');
});
});
});
describe('protocol error', function () {
it('should gracefully recover and only fail on the already send commands', function (done) {
client = redis.createClient.apply(null, args);
client.on('error', function (err) {
assert.strictEqual(err.message, 'Protocol error, got "a" as reply type byte');
// After the hard failure work properly again. The set should have been processed properly too
client.get('foo', function (err, res) {
assert.strictEqual(res, 'bar');
done();
});
});
client.once('ready', function () {
client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.message, 'Protocol error, got "a" as reply type byte');
});
// Fail the set answer. Has no corresponding command obj and will therefore land in the error handler and set
client.reply_parser.execute(new Buffer('a*1\r*1\r$1`zasd\r\na'));
});
});
});
describe('enable_offline_queue', function () {
describe('true', function () {
it('should emit drain if offline queue is flushed and nothing to buffer', function (done) {
client = redis.createClient({
parser: parser,
no_ready_check: true
});
var end = helper.callFuncAfter(done, 3);
client.set('foo', 'bar');
client.get('foo', end);
client.on('warning', function (msg) {
assert.strictEqual(
msg,
'The drain event listener is deprecated and will be removed in v.3.0.0.\n' +
'If you want to keep on listening to this event please listen to the stream drain event directly.'
);
end();
});
client.on('drain', function () {
assert(client.offline_queue.length === 0);
end();
});
});
it('does not return an error and enqueues operation', function (done) {
client = redis.createClient(9999, null, {
max_attempts: 0,
parser: parser
});
var finished = false;
client.on('error', function (e) {
// ignore, b/c expecting a "can't connect" error
});
setTimeout(function () {
client.set('foo', 'bar', function (err, result) {
if (!finished) done(err);
assert.strictEqual(err.message, "The command can't be processed. The connection has already been closed.");
});
setTimeout(function () {
assert.strictEqual(client.offline_queue.length, 1);
finished = true;
done();
}, 25);
}, 50);
});
it('enqueues operation and keep the queue while trying to reconnect', function (done) {
client = redis.createClient(9999, null, {
max_attempts: 4,
parser: parser
});
var i = 0;
client.on('error', function (err) {
if (err.message === 'Redis connection in broken state: maximum connection attempts exceeded.') {
assert(i, 3);
assert.strictEqual(client.offline_queue.length, 0);
done();
} else {
assert.equal(err.code, 'ECONNREFUSED');
assert.equal(err.errno, 'ECONNREFUSED');
assert.equal(err.syscall, 'connect');
}
});
client.on('reconnecting', function (params) {
i++;
assert.equal(params.attempt, i);
assert.strictEqual(params.times_connected, 0);
assert(params.error instanceof Error);
assert(typeof params.total_retry_time === 'number');
assert.strictEqual(client.offline_queue.length, 2);
});
// Should work with either a callback or without
client.set('baz', 13);
client.set('foo', 'bar', function (err, result) {
assert(i, 3);
assert(err);
assert.strictEqual(client.offline_queue.length, 0);
});
});
it('flushes the command queue if connection is lost', function (done) {
client = redis.createClient({
parser: parser
});
client.once('ready', function () {
var multi = client.multi();
multi.config('bar');
var cb = function (err, reply) {
assert.equal(err.code, 'UNCERTAIN_STATE');
};
for (var i = 0; i < 12; i += 3) {
client.set('foo' + i, 'bar' + i);
multi.set('foo' + (i + 1), 'bar' + (i + 1), cb);
multi.set('foo' + (i + 2), 'bar' + (i + 2));
}
multi.exec();
assert.equal(client.command_queue_length, 15);
helper.killConnection(client);
});
var end = helper.callFuncAfter(done, 3);
client.on('error', function (err) {
if (err.command === 'EXEC') {
assert.strictEqual(client.command_queue.length, 0);
assert.strictEqual(err.errors.length, 9);
assert.strictEqual(err.errors[1].command, 'SET');
assert.deepEqual(err.errors[1].args, ['foo1', 'bar1']);
end();
} else if (err.code === 'UNCERTAIN_STATE') {
assert.strictEqual(client.command_queue.length, 0);
assert.strictEqual(err.errors.length, 4);
assert.strictEqual(err.errors[0].command, 'SET');
assert.deepEqual(err.errors[0].args, ['foo0', 'bar0']);
end();
} else {
assert.equal(err.code, 'ECONNREFUSED');
assert.equal(err.errno, 'ECONNREFUSED');
assert.equal(err.syscall, 'connect');
end();
}
});
});
});
describe('false', function () {
it('stream not writable', function (done) {
client = redis.createClient({
parser: parser,
enable_offline_queue: false
});
client.on('ready', function () {
client.stream.destroy();
client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.message, "SET can't be processed. Stream not writeable.");
done();
});
});
});
it('emit an error and does not enqueues operation', function (done) {
client = redis.createClient(9999, null, {
parser: parser,
max_attempts: 0,
enable_offline_queue: false
});
var end = helper.callFuncAfter(done, 3);
client.on('error', function (err) {
assert(/offline queue is deactivated|ECONNREFUSED/.test(err.message));
assert.equal(client.command_queue.length, 0);
end();
});
client.set('foo', 'bar');
assert.doesNotThrow(function () {
client.set('foo', 'bar', function (err) {
// should callback with an error
assert.ok(err);
setTimeout(end, 50);
});
});
});
it('flushes the command queue if connection is lost', function (done) {
client = redis.createClient({
parser: parser,
max_attempts: 2,
enable_offline_queue: false
});
client.once('ready', function () {
var multi = client.multi();
multi.config('bar');
var cb = function (err, reply) {
assert.equal(err.code, 'UNCERTAIN_STATE');
};
for (var i = 0; i < 12; i += 3) {
client.set('foo' + i, 'bar' + i);
multi.set('foo' + (i + 1), 'bar' + (i + 1), cb);
multi.set('foo' + (i + 2), 'bar' + (i + 2));
}
multi.exec();
assert.equal(client.command_queue.length, 15);
helper.killConnection(client);
});
var end = helper.callFuncAfter(done, 3);
client.on('error', function (err) {
if (err.command === 'EXEC') {
assert.equal(client.command_queue.length, 0);
assert.equal(err.errors.length, 9);
end();
} else if (err.code === 'UNCERTAIN_STATE') {
assert.equal(client.command_queue.length, 0);
assert.equal(err.errors.length, 4);
end();
} else {
assert.equal(err.code, 'ECONNREFUSED');
assert.equal(err.errno, 'ECONNREFUSED');
assert.equal(err.syscall, 'connect');
end();
}
});
});
});
});
});
});
});