diff --git a/README.md b/README.md index 43fc135..fb3f684 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/addCommand.js b/lib/addCommand.js new file mode 100644 index 0000000..97ea8da --- /dev/null +++ b/lib/addCommand.js @@ -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; diff --git a/lib/client/redis-client.js b/lib/client/redis-client.js index a6952f0..2bc9298 100644 --- a/lib/client/redis-client.js +++ b/lib/client/redis-client.js @@ -724,3 +724,8 @@ types.getMethods(RedisClient).public() }); module.exports = RedisClient; + +/** + * @private + * */ +module.exports.$_parser = parseArguments; diff --git a/lib/index.js b/lib/index.js index 9f79fee..45692c0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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, @@ -14,5 +15,7 @@ module.exports = { RedisClient, Multi, - createClient + createClient, + addCommand, + addMockCommand }; diff --git a/test/client/addCommand.test.js b/test/client/addCommand.test.js new file mode 100644 index 0000000..c1975bf --- /dev/null +++ b/test/client/addCommand.test.js @@ -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(); + }); + }); + }); + }); + +});