Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Support for custom commands #200

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ $ npm test:valid

You will need to have a running instance of `redis` on you machine and our tests use flushdb a lot so make sure you don't have anything important on it.


# Roadmap
redis-mock is work in progress, feel free to report an issue

Expand Down
96 changes: 96 additions & 0 deletions lib/addCommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
*
* Custom commands
*
* Related to @yeahoffline/redis-mock/issue#163
*
* Built almost identical to addCommand in the original
* but the command held back from being added to the prototype
* unless it is called by addCommand
* */

const Client = require('./client/redis-client');
const multi = require("./client/multi");

/**
* Hold all the commands here as they will only populate
* the prototype when called by 'addCommand'
* */
const commands = {};

/**
* @typedef MockCommandCallback
*
* @property {RedisClient} client
* @property {Array} args
* @property {Function} callback
* */

/**
*
* Add global command to the clients either singular or
* passing a map object with the key as the name, and the
* callback as the value
*
* @param {string|Object} command
* @param {MockCommandCallback} [callback]
* */
const addMockCommand = function (command, callback) {
if (typeof command === 'object') {
return Object.keys(command).forEach((cmd) => addMockCommand(cmd, command[cmd]));
}

if (commands[command]) {
throw new Error(`Command [${command}] already registered`);
}

commands[command] = callback;
};

const addCommand = function (command) {
// Some rare Redis commands use special characters in their command name
// Convert those to a underscore to prevent using invalid function names
const commandName = command.replace(/(?:^([0-9])|[^a-zA-Z0-9_$])/g, '_$1');

const callback = commands[command];

if (!callback) {
process.emitWarning(`Command [${command}] has not been registered with mock, returning`);
return;
}

if (!Client.prototype[command]) {
Client.prototype[command.toUpperCase()] = Client.prototype[command] = function () {
// Should make a customer parser to handle this and not have to mess
// with preexisting exports??
const args = Client.$_parser(arguments);
let cb;
if (typeof args[args.length - 1] === 'function') {
cb = args.pop();
}

return callback(this, args, cb);
};

// Alias special function names (e.g. JSON.SET becomes JSON_SET and json_set)
if (commandName !== command) {
Client.prototype[commandName.toUpperCase()] = Client.prototype[commandName] = Client.prototype[command];
}
}

if (!multi.Multi.prototype[command]) {
multi.Multi.prototype[command.toUpperCase()] = multi.Multi.prototype[command] = function (...args) {
this._command(command, args);
//Return this for chaining
return this;
};

// Alias special function names (e.g. JSON.SET becomes JSON_SET and json_set)
if (commandName !== command) {
multi.Multi.prototype[commandName.toUpperCase()] = multi.Multi.prototype[commandName] = multi.Multi.prototype[command];
}
}
};

module.exports.addCommand = addCommand;
module.exports.addMockCommand = addMockCommand;
5 changes: 5 additions & 0 deletions lib/client/redis-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -724,3 +724,8 @@ types.getMethods(RedisClient).public()
});

module.exports = RedisClient;

/**
* @private
* */
module.exports.$_parser = parseArguments;
7 changes: 5 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use strict';

const {Multi} = require("./client/multi");
const { Multi } = require("./client/multi");
const RedisClient = require('./client/redis-client');
const errors = require('./errors');
const createClient = require('./client/createClient');
const { addCommand, addMockCommand } = require('./addCommand');

module.exports = {
AbortError: errors.AbortError,
Expand All @@ -14,5 +15,7 @@ module.exports = {

RedisClient,
Multi,
createClient
createClient,
addCommand,
addMockCommand
};
165 changes: 165 additions & 0 deletions test/client/addCommand.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// TODO: Clean these up

const should = require('should');
const helpers = require('../helpers');
const redismock = require("../../lib");

// Since this is purely for mocking plugins
if (process.env.VALID_TESTS) {
return;
}

/* eslint-disable-next-line */
const noop = () => {};

// Clean the db after each test
afterEach(function (done) {
var r = helpers.createClient();
r.flushdb(function () {
r.end(true);
done();
});
});

describe('addMockCommand()', function () {

var Redis = redismock.RedisClient;

it('should exist', function () {
should.exist(redismock.addMockCommand);
});

it('should not populate the prototype', function () {
redismock.addMockCommand('gh163.addMockCommand', noop);
should.not.exist(Redis.prototype.gh163_addMockCommand);
});

});

describe('addCommand()', function () {

describe('adding command', function () {

var Redis = redismock.RedisClient;
var Multi = redismock.Multi;

it('should exist', function () {
should.exist(redismock.addCommand);
});

it('should populate the prototype', function () {
redismock.addMockCommand('gh163.addCommand', noop);
redismock.addCommand('gh163.addCommand');

should.exist(Redis.prototype.gh163_addCommand);
});

it('should convert special characters in functions names to lowercase', function () {
const command = 'gh163.addCommand.convert';

redismock.addMockCommand(command, noop);
redismock.addCommand(command);

should.exist(Redis.prototype[command]);
should.exist(Redis.prototype[command.toUpperCase()]);
should.exist(Redis.prototype.gh163_addCommand_convert);
should.exist(Redis.prototype.GH163_ADDCOMMAND_CONVERT);
});

it('should add to multi', function () {
const command = 'gh163.addCommand.multi';

redismock.addMockCommand(command, noop);
redismock.addCommand(command);

should.exist(Multi.prototype[command]);
});
});

describe('using new command', function () {

before(function () {
// Better functionality but will work
//
// The way the mock works just need a little workaround for multi
redismock.addMockCommand('json.set', (client, args, callback) => {
if (client instanceof redismock.Multi) {
client = client._client;
}

client.set(args[0], JSON.stringify(args[2]), callback);
});

redismock.addMockCommand('json.get', (client, args, callback) => {
client.get(args[0], callback);
});

redismock.addCommand('json.set');
redismock.addCommand('json.get');
});

describe('client', function () {

let r;

beforeEach(function () {
r = redismock.createClient();
});

afterEach(function(done) {
r.flushall();
r.quit(done);
});

it('should set value via working command', function (done) {
const value = {
hello: 'world'
};

r.json_set('foo', '.', value, function (err, result) {
result.should.eql('OK');

r.json_get('foo', function (err, result) {
JSON.parse(result).should.deepEqual(value);
done();
});
});
});
});

describe('multi', function () {

let r;

beforeEach(function () {
r = redismock.createClient();
});

afterEach(function(done) {
r.flushall();
r.quit(done);
});

it('should set the value with a ttl', function (done) {
const value = {
hello: 'world'
};

const multi = r.multi();

multi.json_set('key', 'path', JSON.stringify(value))
.expire('key', 60)
.ttl('key')
.exec((err, results) => {
should(err).not.be.ok();
should(results[0]).equal('OK');
should(results[1]).equal(1);
(results[2] <= 60).should.be.true();

done();
});
});
});
});

});