UNPKG

@binwus-org/redis-mock

Version:

Redis client mock object for unit testing

738 lines (604 loc) 23.2 kB
const events = require("events"), parsers = require("./args/argParsers"), helpers = require("../helpers"), pubsub = require("./pubsub.js"), multi = require("./multi"), types = require("../utils/types"), { getRedisMock } = require("../utils/redis-mock-factory"); /** * @deprecated use {@link parsers} instead */ const parseArguments = function(args, options) { // eslint-disable-line complexity var arr, len = args.length, callback, i = 0; if (Array.isArray(args[0])) { // arg0 = [hash, k1, v1, k2, v2,] // arg1 = callback arr = args[0]; callback = args[1]; } else if (Array.isArray(args[1])) { // arg0 = hash // arg1 = [k1, v1, k2, v2,] // arg2 = callback if (len === 3) { callback = args[2]; } len = args[1].length; arr = new Array(len + 1); arr[0] = args[0]; for (; i < len; i += 1) { arr[i + 1] = args[1][i]; } } else if (typeof args[1] === 'object' && (args.length === 2 || args.length === 3 && (typeof args[2] === 'function' || typeof args[2] === 'undefined'))) { // arg0 = hash // arg1 = {k1: v1, k2: v2,} // arg2 = callback arr = [args[0]]; if(options && options.valueIsString) { arr.push(String(args[1])); } else if(options && options.valueIsBuffer) { arr.push(args[1]); } else { for (var field in args[1]) { arr.push(field, args[1][field]); } } callback = args[2]; } else { // arg0 = hash // arg1..N-1 = k1,v1,k2,v2,...N-1 // argN = callback len = args.length; // The later should not be the average use case if (len !== 0 && (typeof args[len - 1] === 'function' || typeof args[len - 1] === 'undefined')) { len--; callback = args[len]; } arr = new Array(len); for (; i < len; i += 1) { arr[i] = args[i]; } } if (callback) { arr.push(callback); } return arr; }; const splitUserCallbackOutOfArguments = (args) => { let userCallback; if (typeof args[args.length - 1] === 'function') { userCallback = args[args.length - 1]; args = args.splice(0, args.length - 1); } else { userCallback = helpers.noOpCallback; } return { userCallback, parsableArgs: args.length === 1 && Array.isArray(args[0]) ? args[0] : args }; }; const exec = (parser, args, cb) => { const { userCallback, parsableArgs } = splitUserCallbackOutOfArguments(args); try { const parsed = parser.parse(parsableArgs); // if the server mock function just returns a value instead of calling callback, // then wrap it into a callback here if (cb.length === 1) { const result = cb(parsed); // eslint-disable-line callback-return return userCallback(null, result); } return cb(parsed, userCallback); } catch (err) { return helpers.callCallback(userCallback, err); } }; class RedisClient extends events.EventEmitter { constructor(options, stream, redisMock) { super(); this.options = options; this.stream = stream; this.connected = false; this.ready = false; this.pub_sub_mode = false; this._redisMock = redisMock || getRedisMock(options); this._redisMock.on('message', (ch, msg) => this._message(ch, msg)); // Pub/sub subscriptions this.subscriptions = {}; this.psubscriptions = {}; this.subscriptionsListeners = {}; process.nextTick(() => { this.connected = true; this.ready = true; this.emit("connect"); this.emit("ready"); }); this._selectedDbIndex = options.db || 0; } get _selectedDb() { return this._redisMock && this._redisMock.select(this._selectedDbIndex); } /** * We always listen for 'message', even if this is not a subscription client. * We will only act on it, however, if the channel is in this.subscriptions, which is populated through subscribe * @private */ _message(ch, msg) { if (ch in this.subscriptions && this.subscriptions[ch] === true) { this.emit('message', ch, msg); } // Emit the message to ALL matching subscriptions Object.keys(this.psubscriptions).forEach((key) => { if(this.psubscriptions[key].test(ch)) { this.emit('pmessage', key, ch, msg); return true; } return false; }); } duplicate(_options, callback) { const duplicate = new RedisClient(this.options, this.stream, this._redisMock); if (typeof callback !== 'undefined') { return callback(null, duplicate); } else { return duplicate; } } connect() { process.nextTick(() => { this.connected = true; this.ready = true; this.emit("connect"); this.emit("ready"); }); } quit(callback) { // Remove all subscriptions (pub/sub) this.subscriptions = {}; this.subscriptionsListeners = {}; this.connected = false; this.ready = false; //Remove listener from this._redisMock to avoid 'too many subscribers errors' this._redisMock.removeListener('message', (ch, msg) => this._message(ch, msg)); // TODO: Anything else we need to clear? process.nextTick(() => { this.emit("end"); if (callback) { return callback(); } }); } end() { return this.quit(); } } /** * Publish / subscribe / unsubscribe */ RedisClient.prototype.subscribe = pubsub.subscribe; RedisClient.prototype.psubscribe = pubsub.psubscribe; RedisClient.prototype.unsubscribe = pubsub.unsubscribe; RedisClient.prototype.punsubscribe = pubsub.punsubscribe; RedisClient.prototype.publish = function (channel, msg, callback) { pubsub.publish.call(this, this._redisMock, channel, msg); process.nextTick(() => { if (callback) { return callback(); } }); }; /** * multi */ RedisClient.prototype.multi = RedisClient.prototype.batch = function(commands) { return multi.multi(this, commands, false); }; RedisClient.prototype.batch = function (commands) { return multi.multi(this, commands, true); }; /** * Keys function */ const getKeysVarArgs = function (args) { var keys = []; var hasCallback = typeof(args[args.length - 1]) === 'function'; for (var i = 0; i < (hasCallback ? args.length - 1 : args.length); i++) { keys.push(args[i]); } var callback = hasCallback ? args[args.length - 1] : undefined; return {keys: keys, callback: callback}; }; RedisClient.prototype.del = RedisClient.prototype.DEL = function (keys, callback) { this._selectedDb.del(keys, callback); }; RedisClient.prototype.exists = RedisClient.prototype.EXISTS = function (keys, callback) { const args = getKeysVarArgs(arguments); keys = args.keys; callback = args.callback; this._selectedDb.exists(keys, callback); }; RedisClient.prototype.type = RedisClient.prototype.TYPE = function(key, callback) { this._selectedDb.type(key, callback); }; RedisClient.prototype.expire = RedisClient.prototype.EXPIRE = function (key, seconds, callback) { this._selectedDb.expire(key, seconds, callback); }; RedisClient.prototype.pexpire = RedisClient.prototype.PEXPIRE = function (key, ms, callback) { this._selectedDb.pexpire(key, ms, callback); }; RedisClient.prototype.expireat = RedisClient.prototype.EXPIREAT = function (key, seconds, callback) { this._selectedDb.expireat(key, seconds, callback); }; RedisClient.prototype.pexpireat = RedisClient.prototype.PEXPIREAT = function (key, ms, callback) { this._selectedDb.pexpireat(key, ms, callback); }; RedisClient.prototype.persist = RedisClient.prototype.PERSIST = function (key, callback) { this._selectedDb.persist(key, callback); }; RedisClient.prototype.ttl = RedisClient.prototype.TTL = function (key, callback) { this._selectedDb.ttl(key, callback); }; RedisClient.prototype.pttl = RedisClient.prototype.PTTL = function (key, callback) { this._selectedDb.pttl(key, callback); }; RedisClient.prototype.keys = RedisClient.prototype.KEYS = function (pattern, callback) { this._selectedDb.keys(pattern, callback); }; /** * SCAN cursor [MATCH pattern] [COUNT count] [TYPE type] */ RedisClient.prototype.scan = RedisClient.prototype.SCAN = function (...userArgs) { exec(parsers.scan, userArgs, (args, cb) => { //TODO: add support for TYPE this._selectedDb.scan(args.default.cursor, args.named.match, args.named.count, cb); }); }; RedisClient.prototype.rename = RedisClient.prototype.RENAME = function (key, newKey, callback) { this._selectedDb.rename(key, newKey, callback); }; RedisClient.prototype.renamenx = RedisClient.prototype.RENAMENX = function (key, newKey, callback) { this._selectedDb.renamenx(key, newKey, callback); }; RedisClient.prototype.dbsize = RedisClient.prototype.DBSIZE = function (callback) { this._selectedDb.dbsize(callback); }; RedisClient.prototype.incr = RedisClient.prototype.INCR = function (key, callback) { this._selectedDb.incr(key, callback); }; RedisClient.prototype.incrby = RedisClient.prototype.INCRBY = function (key, value, callback) { this._selectedDb.incrby(key, value, callback); }; RedisClient.prototype.incrbyfloat = RedisClient.prototype.INCRBYFLOAT = function (key, value, callback) { this._selectedDb.incrbyfloat(key, value, callback); }; RedisClient.prototype.decr = RedisClient.prototype.DECR = function (key, callback) { this._selectedDb.decr(key, callback); }; RedisClient.prototype.decrby = RedisClient.prototype.DECRBY = function (key, value, callback) { this._selectedDb.decrby(key, value, callback); }; RedisClient.prototype.get = RedisClient.prototype.GET = function (key, callback) { this._selectedDb.get(key, callback); }; RedisClient.prototype.getset = RedisClient.prototype.GETSET = function (key, value, callback) { this._selectedDb.getset(key, value, callback); }; /** * SET key value [EX seconds|PX milliseconds|KEEPTTL] [NX|XX] [GET] * * Sets key value pair * * EX - expiration time in seconds * PX - expiration time in milliseconds * KEEPTTL - Retain the time to live associated with the key * NX - Only set the key if it does not already exist * XX - Only set the key if it already exist * GET - Return the old value stored at key, or nil when key did not exist */ RedisClient.prototype.set = RedisClient.prototype.SET = function (...userArgs) { exec(parsers.set, userArgs, (args, cb) => this._selectedDb.set(args.default.key, args.default.value, cb, Object.assign({}, args.flags, args.named)) ); }; RedisClient.prototype.append = RedisClient.prototype.APPEND = function(...userArgs) { exec(parsers.append, userArgs, (args, cb) => this._selectedDb.append(args.default.key, args.default.value, cb) ); }; RedisClient.prototype.ping = RedisClient.prototype.PING = function (callback) { this._selectedDb.ping(callback); }; RedisClient.prototype.setex = RedisClient.prototype.SETEX = function (key, seconds, value, callback) { this._selectedDb.set(key, value, () => { this._selectedDb.expire(key, seconds, (err, result) => { helpers.callCallback(callback, err, "OK"); }); }); }; RedisClient.prototype.setnx = RedisClient.prototype.SETNX = function (key, value, callback) { this._selectedDb.setnx(key, value, callback); }; RedisClient.prototype.mget = RedisClient.prototype.MGET = function (...args) { this._selectedDb.mget(...args); }; RedisClient.prototype.mset = RedisClient.prototype.MSET = function (...args) { this._selectedDb.mset(false, ...args); }; RedisClient.prototype.msetnx = RedisClient.prototype.MSETNX = function (...args) { this._selectedDb.mset(true, ...args); }; RedisClient.prototype.hget = RedisClient.prototype.HGET = function (hash, key, callback) { this._selectedDb.hget(...parseArguments(arguments)); }; RedisClient.prototype.hexists = RedisClient.prototype.HEXISTS = function (hash, key, callback) { this._selectedDb.hexists(...parseArguments(arguments)); }; RedisClient.prototype.hdel = RedisClient.prototype.HDEL = function (hash, key, callback) { this._selectedDb.hdel(...parseArguments(arguments)); }; RedisClient.prototype.hset = RedisClient.prototype.HSET = function (hash, key, value, callback) { this._selectedDb.hset(...parseArguments(arguments)); }; RedisClient.prototype.hincrby = RedisClient.prototype.HINCRBY = function (hash, key, increment, callback) { this._selectedDb.hincrby(...parseArguments(arguments)); }; RedisClient.prototype.hincrbyfloat = RedisClient.prototype.HINCRBYFLOAT = function (hash, key, increment, callback) { this._selectedDb.hincrbyfloat(...parseArguments(arguments)); }; RedisClient.prototype.hsetnx = RedisClient.prototype.HSETNX = function (hash, key, value, callback) { this._selectedDb.hsetnx(...parseArguments(arguments)); }; RedisClient.prototype.hlen = RedisClient.prototype.HLEN = function (hash, callback) { this._selectedDb.hlen(...parseArguments(arguments)); }; RedisClient.prototype.hkeys = RedisClient.prototype.HKEYS = function (hash, callback) { this._selectedDb.hkeys(...parseArguments(arguments)); }; RedisClient.prototype.hvals = RedisClient.prototype.HVALS = function (hash, callback) { this._selectedDb.hvals(...parseArguments(arguments)); }; RedisClient.prototype.hmset = RedisClient.prototype.HMSET = function () { this._selectedDb.hmset(...parseArguments(arguments)); }; RedisClient.prototype.hmget = RedisClient.prototype.HMGET = function () { this._selectedDb.hmget(...parseArguments(arguments)); }; RedisClient.prototype.hgetall = RedisClient.prototype.HGETALL = function (hash, callback) { this._selectedDb.hgetall(...parseArguments(arguments)); }; RedisClient.prototype.hscan = RedisClient.prototype.HSCAN = function () { const args = parseArguments(arguments); const hash = args[0]; const index = args[1] || 0; let match = '*'; let count = 10; if(args.length > 0) { for (let i = 0; i < args.length; i++) { if(typeof args[i] === 'string' && args[i].toLowerCase() === "match") { match = args[i+1]; } else if(typeof args[i] === 'string' && args[i].toLowerCase() === "count") { count = args[i+1]; } } } const callback = args.pop(); this._selectedDb.hscan(hash, index, match, count, callback); }; /** * List functions */ RedisClient.prototype.llen = RedisClient.prototype.LLEN = function (key, callback) { this._selectedDb.llen(key, callback); }; RedisClient.prototype.lpush = RedisClient.prototype.LPUSH = function () { const args = parseArguments(arguments); this._selectedDb.lpush(...args); }; RedisClient.prototype.rpush = RedisClient.prototype.RPUSH = function () { const args = parseArguments(arguments); this._selectedDb.rpush(...args); }; RedisClient.prototype.lpushx = RedisClient.prototype.LPUSHX = function (key, value, callback) { this._selectedDb.lpushx(key, value, callback); }; RedisClient.prototype.rpushx = RedisClient.prototype.RPUSHX = function (key, value, callback) { this._selectedDb.rpushx(key, value, callback); }; RedisClient.prototype.lpop = RedisClient.prototype.LPOP = function (key, callback) { this._selectedDb.lpop(key, callback); }; RedisClient.prototype.rpop = RedisClient.prototype.RPOP = function (key, callback) { this._selectedDb.rpop(key, callback); }; RedisClient.prototype.rpoplpush = RedisClient.prototype.RPOPLPUSH = function (sourceKey, destinationKey, callback) { this._selectedDb.rpoplpush(sourceKey, destinationKey, callback); }; RedisClient.prototype._bpop = function (fn, key, timeout, callback) { const keys = []; const hasCallback = typeof(arguments[arguments.length - 1]) === "function"; for (let i = 1; i < (hasCallback ? arguments.length - 2 : arguments.length - 1); i++) { keys.push(arguments[i]); } if (hasCallback) { fn(keys, arguments[arguments.length - 2], arguments[arguments.length - 1]); } else { fn(keys, arguments[arguments.length - 1]); } }; RedisClient.prototype.blpop = RedisClient.prototype.BLPOP = function (key, timeout, callback) { this._bpop((...args) => this._selectedDb.blpop(...args), ...arguments); }; RedisClient.prototype.brpop = RedisClient.prototype.BRPOP = function (key, timeout, callback) { this._bpop((...args) => this._selectedDb.brpop(...args), ...arguments); }; RedisClient.prototype.lindex = RedisClient.prototype.LINDEX = function (key, index, callback) { this._selectedDb.lindex(key, index, callback); }; RedisClient.prototype.lrange = RedisClient.prototype.LRANGE = function (key, index1, index2, callback) { this._selectedDb.lrange(key, index1, index2, callback); }; RedisClient.prototype.lrem = RedisClient.prototype.LREM = function (key, index, value, callback) { this._selectedDb.lrem(key, index, value, callback); }; RedisClient.prototype.lset = RedisClient.prototype.LSET = function (key, index, value, callback) { this._selectedDb.lset(key, index, value, callback); }; RedisClient.prototype.ltrim = RedisClient.prototype.LTRIM = function (key, start, end, callback) { this._selectedDb.ltrim(key, start, end, callback); }; RedisClient.prototype.sadd = RedisClient.prototype.SADD = function () { this._selectedDb.sadd(...parseArguments(arguments)); }; RedisClient.prototype.srem = RedisClient.prototype.SREM = function () { this._selectedDb.srem(...parseArguments(arguments)); }; RedisClient.prototype.smembers = RedisClient.prototype.SMEMBERS = function (key, callback) { this._selectedDb.smembers(key, callback); }; RedisClient.prototype.scard = RedisClient.prototype.SCARD = function (key, callback) { this._selectedDb.scard(key, callback); }; RedisClient.prototype.sismember = RedisClient.prototype.SISMEMBER = function (key, member, callback) { this._selectedDb.sismember(key, member, callback); }; RedisClient.prototype.smove = RedisClient.prototype.SMOVE = function (source, destination, member, callback) { this._selectedDb.smove(source, destination, member, callback); }; RedisClient.prototype.srandmember = RedisClient.prototype.SRANDMEMBER = function (key, count, callback) { this._selectedDb.srandmember(key, count, callback); }; RedisClient.prototype.sscan = RedisClient.prototype.SSCAN = function(...userArgs) { exec(parsers.sscan, userArgs, (args, cb) => { this._selectedDb.sscan(args.default.key, args.default.cursor, args.named.match, args.named.count, cb); }); }; /** * SortedSet functions *** NOT IMPLEMENTED *** ZLEXCOUNT key min max ZRANGEBYLEX key min max [LIMIT offset count] ZREVRANGEBYLEX key max min [LIMIT offset count] ZREMRANGEBYLEX key min max ZSCAN key cursor [MATCH pattern] [COUNT count] *** PARTIALLY IMPLEMENTED *** ZINTERSTORE - needs [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] ZUNIONSTORE - needs [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] */ RedisClient.prototype.zadd = RedisClient.prototype.ZADD = function (...userArgs) { exec(parsers.zadd, userArgs, (args, cb) => { this._selectedDb.zadd(args.default.key, args.flags, args.multiple, cb); }); }; RedisClient.prototype.zcard = RedisClient.prototype.ZCARD = function () { const args = parseArguments(arguments); this._selectedDb.zcard(...args); }; RedisClient.prototype.zcount = RedisClient.prototype.ZCOUNT = function () { const args = parseArguments(arguments); this._selectedDb.zcount(...args); }; RedisClient.prototype.zincrby = RedisClient.prototype.ZINCRBY = function () { const args = parseArguments(arguments); this._selectedDb.zincrby(...args); }; RedisClient.prototype.zrange = RedisClient.prototype.ZRANGE = function () { const args = parseArguments(arguments); this._selectedDb.zrange(...args); }; RedisClient.prototype.zrangebyscore = RedisClient.prototype.ZRANGEBYSCORE = function () { const args = parseArguments(arguments); this._selectedDb.zrangebyscore(...args); }; RedisClient.prototype.zrank = RedisClient.prototype.ZRANK = function () { const args = parseArguments(arguments); this._selectedDb.zrank(...args); }; RedisClient.prototype.zrem = RedisClient.prototype.ZREM = function () { const args = parseArguments(arguments); this._selectedDb.zrem(...args); }; RedisClient.prototype.zremrangebyrank = RedisClient.prototype.ZREMRANGEBYRANK = function () { const args = parseArguments(arguments); this._selectedDb.zremrangebyrank(...args); }; RedisClient.prototype.zremrangebyscore = RedisClient.prototype.ZREMRANGEBYSCORE = function () { const args = parseArguments(arguments); this._selectedDb.zremrangebyscore(...args); }; RedisClient.prototype.zrevrange = RedisClient.prototype.ZREVRANGE = function () { const args = parseArguments(arguments); this._selectedDb.zrevrange(...args); }; RedisClient.prototype.zrevrangebyscore = RedisClient.prototype.ZREVRANGEBYSCORE = function () { const args = parseArguments(arguments); this._selectedDb.zrevrangebyscore(...args); }; RedisClient.prototype.zrevrank = RedisClient.prototype.ZREVRANK = function () { const args = parseArguments(arguments); this._selectedDb.zrevrank(...args); }; RedisClient.prototype.zunionstore = RedisClient.prototype.ZUNIONSTORE = function() { const args = parseArguments(arguments); this._selectedDb.zunionstore(...args); }; RedisClient.prototype.zinterstore = RedisClient.prototype.ZINTERSTORE = function() { const args = parseArguments(arguments); this._selectedDb.zinterstore(...args); }; RedisClient.prototype.zscore = RedisClient.prototype.ZSCORE = function () { const args = parseArguments(arguments); this._selectedDb.zscore(...args); }; /** * Other commands (Lua scripts) */ RedisClient.prototype.send_command = RedisClient.prototype.SEND_COMMAND = function (callback) { if (typeof(arguments[arguments.length - 1]) === 'function') { arguments[arguments.length - 1](); } }; RedisClient.prototype.select = function (databaseIndex, callback) { const defaultMaxIndex = helpers.getMaxDatabaseCount() - 1; if (!isNaN(databaseIndex) && (databaseIndex <= defaultMaxIndex)) { this._selectedDbIndex = databaseIndex; return helpers.callCallback(callback, null, "OK"); } else { return helpers.callCallback(callback, new Error('ERR invalid DB index'), null); } }; RedisClient.prototype.flushdb = RedisClient.prototype.FLUSHDB = function (callback) { this._selectedDb.flushdb(callback); }; RedisClient.prototype.time = RedisClient.prototype.TIME = function (callback) { this._selectedDb.time(callback); }; RedisClient.prototype.flushall = RedisClient.prototype.FLUSHALL = function (callback) { helpers.callCallback(callback, null, this._redisMock.flushall(callback)); }; RedisClient.prototype.auth = RedisClient.prototype.AUTH = function (password, callback) { helpers.callCallback(callback, null, this._redisMock.auth(password)); }; RedisClient.prototype.script = RedisClient.prototype.SCRIPT = function (...userArgs) { exec(parsers.script, userArgs, (args) => { const scriptArgs = args.multiple.map((arg) => arg.scriptArgs); return this._selectedDb.script(scriptArgs[0], ...scriptArgs.slice(1)); }); }; /** * Mirror all commands for Multi */ types.getMethods(RedisClient).public() .skip('duplicate', 'quit', 'end') .forEach((methodName) => { multi.Multi.prototype[methodName] = function (...args) { this._command(methodName, args); //Return this for chaining return this; }; }); module.exports = RedisClient;