redis-mock
Version:
Redis client mock object for unit testing
418 lines (374 loc) • 10.9 kB
JavaScript
const helpers = require("../helpers.js");
const Item = require("./item.js");
const mockCallback = helpers.noOpCallback;
const validKeyType = function(mockInstance, key, callback) {
return helpers.validKeyType(mockInstance, key, 'list', callback);
};
const initKey = function(mockInstance, key) {
return helpers.initKey(mockInstance, key, Item.createList);
};
/**
* Llen
*/
exports.llen = function (key, callback) {
const length = this.storage[key] ? this.storage[key].value.length : 0;
helpers.callCallback(callback, null, length);
};
const push = function (fn, args) {
const len = args.length;
if (len < 2) {
return;
}
var mockInstance = args[0];
var key = args[1];
var callback = helpers.parseCallback(args);
if (typeof callback === 'undefined') {
callback = mockCallback;
}
if (!validKeyType(mockInstance, key, callback)) {
return;
}
// init key
initKey(mockInstance, key);
// parse only the values from the args;
var values = [];
for (var i=2, val; i < len; i++) {
val = args[i];
if ('function' === typeof val) {
break;
}
values.push(val);
}
fn.call(mockInstance.storage[key], values);
var length = mockInstance.storage[key].value.length;
pushListWatcher.pushed(key);
helpers.callCallback(callback, null, length);
};
/**
* Lpush
*/
exports.lpush = function (...args) {
push(Item._list.prototype.lpush, [this].concat(args));
};
/**
* Rpush
*/
exports.rpush = function (...args) {
push(Item._list.prototype.rpush, [this].concat(args));
};
const pushx = function (fn, mockInstance, key, value, callback) {
var length = 0;
if (mockInstance.storage[key]) {
if (mockInstance.storage[key].type !== "list") {
return helpers.callCallback(callback,
new Error("ERR Operation against a key holding the wrong kind of value"));
}
fn.call(mockInstance.storage[key], [value]);
length = mockInstance.storage[key].value.length;
pushListWatcher.pushed(key);
}
helpers.callCallback(callback, null, length);
};
/**
* Rpushx
*/
exports.rpushx = function (key, value, callback) {
pushx(Item._list.prototype.rpush, this, key, value, callback);
};
/**
* Lpushx
*/
exports.lpushx = function (key, value, callback) {
pushx(Item._list.prototype.lpush, this, key, value, callback);
};
const pop = function (fn, mockInstance, key, callback) {
var val = null;
if (mockInstance.storage[key] && mockInstance.storage[key].type !== "list") {
return helpers.callCallback(callback,
new Error("ERR Operation against a key holding the wrong kind of value"));
}
if (mockInstance.storage[key] && mockInstance.storage[key].value.length > 0) {
val = fn.call(mockInstance.storage[key]);
}
helpers.callCallback(callback, null, val);
};
/**
* Lpop
*/
exports.lpop = function (key, callback) {
pop.call(this, Item._list.prototype.lpop, this, key, callback);
};
/**
* Rpop
*/
exports.rpop = function (key, callback) {
pop.call(this, Item._list.prototype.rpop, this, key, callback);
};
/**
* Rpoplpush
*/
exports.rpoplpush = function(sourceKey, destinationKey, callback) {
pop.call(this, Item._list.prototype.rpop, this, sourceKey, (err, reply) => {
if (err) {
return helpers.callCallback(callback, err, null);
}
if (reply === null || reply === undefined) {
return helpers.callCallback(callback, null);
}
push(Item._list.prototype.lpush, [
this,
destinationKey,
reply,
(err) => {
if (err) {
return helpers.callCallback(callback, err, null);
}
return helpers.callCallback(callback, null, reply);
}
]);
});
};
/**
* Listen to all the list identified by keys and set a timeout if timeout != 0
*/
const listenToPushOnLists = function (mockInstance, keys, timeout, callback) {
var listenedTo = [];
var expire = null;
var listener = (key) => {
// We remove all the other listeners.
pushListWatcher.removeListeners(listenedTo, listener);
if (expire) {
clearTimeout(expire);
}
callback(key);
};
for (var i = 0; i < keys.length; i++) {
listenedTo.push(keys[i]);
pushListWatcher.suscribe(keys[i], listener);
}
if (timeout > 0) {
expire = setTimeout(function () {
pushListWatcher.removeListeners(listenedTo, listener);
callback(null);
}, timeout * 1000);
if (expire.unref) {
expire.unref();
}
}
};
/**
* Helper function to build blpop and brpop
*/
const bpop = function (fn, mockInstance, keys, timeout, callback) {
var val = null;
// Look if any element can be returned
for (var i = 0; i < keys.length; i++) {
if (mockInstance.storage[keys[i]] && mockInstance.storage[keys[i]].value.length > 0) {
var key = keys[i];
val = fn.call(mockInstance.storage[key]);
helpers.callCallback(callback, null, [key, val]);
return;
}
}
// We listen to all the list we asked for
listenToPushOnLists(mockInstance, keys, timeout, (key) => {
if (key !== null) {
val = fn.call(mockInstance.storage[key]);
helpers.callCallback(callback, null, [key, val]);
} else {
helpers.callCallback(callback, null, null);
}
});
};
/**
* BLpop
*/
exports.blpop = function (keys, timeout, callback) {
bpop.call(this, Item._list.prototype.lpop, this, keys, timeout, callback);
};
/**
* BRpop
*/
exports.brpop = function (keys, timeout, callback) {
bpop.call(this, Item._list.prototype.rpop, this, keys, timeout, callback);
};
/**
* Lindex
*/
exports.lindex = function (key, index, callback) {
let val = null;
if (this.storage[key]) {
if (this.storage[key].type !== "list") {
return helpers.callCallback(callback,
new Error("ERR Operation against a key holding the wrong kind of value"));
}
if (index < 0 && -this.storage[key].value.length <= index) {
val = this.storage[key].value[this.storage[key].value.length + index];
} else if (this.storage[key].value.length > index) {
val = this.storage[key].value[index];
}
}
helpers.callCallback(callback, null, val);
};
/**
* Lrange
*/
exports.lrange = function (key, startIndex, stopIndex, callback) {
var val = [];
var index1 = startIndex;
var index2 = stopIndex;
if (this.storage[key]) {
if (this.storage[key].type !== "list") {
return helpers.callCallback(callback,
new Error("ERR Operation against a key holding the wrong kind of value"));
}
index1 = index1 >= 0 ? index1 : Math.max(this.storage[key].value.length + index1, 0);
index2 = index2 >= 0 ? index2 : Math.max(this.storage[key].value.length + index2, 0);
val = this.storage[key].value.slice(index1, index2 + 1);
}
helpers.callCallback(callback, null, val);
};
/**
* Lrem
*/
exports.lrem = function (key, count, value, callback) {
var removedCount = 0;
if (this.storage[key]) {
if (this.storage[key].type !== "list") {
return helpers.callCallback(callback,
new Error("ERR Operation against a key holding the wrong kind of value"));
}
var list = this.storage[key].value;
var strValue = Item._stringify(value);
var filteredList = [];
if (count > 0) {
// count > 0: Remove elements equal to value moving from head to tail
for (var i = 0; i < list.length; ++i) {
if (list[i] === strValue && count > 0) {
--count;
++removedCount;
} else {
filteredList.push(list[i]);
}
}
} else if (count < 0) {
// count < 0: Remove elements equal to value moving from tail to head.
for (i = list.length; i > 0; --i) {
if (list[i-1] === strValue && count < 0) {
++count;
++removedCount;
} else {
filteredList.unshift(list[i-1]);
}
}
} else {
// count = 0: Remove all elements equal to value.
for (i = 0; i < list.length; ++i) {
if (list[i] === strValue) {
++removedCount;
} else {
filteredList.push(list[i]);
}
}
}
this.storage[key].value = filteredList;
}
helpers.callCallback(callback, null, removedCount);
};
/**
* Lset
*/
exports.lset = function (key, index, value, callback) {
var res = "OK";
var len = -1;
if (!this.storage[key]) {
return helpers.callCallback(callback,
new Error("ERR no such key"));
}
if (this.storage[key].type !== "list") {
return helpers.callCallback(callback,
new Error("ERR Operation against a key holding the wrong kind of value"));
}
len = this.storage[key].value.length;
if (len <= index || -len > index) {
return helpers.callCallback(callback,
new Error("ERR index out of range"));
}
if (index < 0) {
this.storage[key].value[len + index] = Item._stringify(value);
} else {
this.storage[key].value[index] = Item._stringify(value);
}
helpers.callCallback(callback, null, res);
};
/**
* ltrim
*/
exports.ltrim = function(key, start, end, callback) {
var res = "OK";
var len = -1;
if (!this.storage[key]) {
return helpers.callCallback(callback, null, res);
}
if (this.storage[key].type !== "list") {
return helpers.callCallback(callback,
new Error("WRONGTYPE Operation against a key holding the wrong kind of value"));
}
len = this.storage[key].value.length;
if (start < 0) {
start = len + start;
}
if (end < 0) {
end = len + end;
}
if (end >= len) {
end = len - 1;
}
if (start >= len || start > end) {
// trim whole list
delete this.storage[key];
} else {
this.storage[key].value = this.storage[key].value.slice(start, end + 1);
}
helpers.callCallback(callback, null, res);
};
/**
* Used to follow a list depending on its key (used by blpop and brpop mainly)
*/
const PushListWatcher = function () {
this.listeners = {};
};
/**
* Watch for the next push in the list key
*/
PushListWatcher.prototype.suscribe = function (key, listener) {
if (this.listeners[key]) {
this.listeners[key].push(listener);
} else {
this.listeners[key] = [listener];
}
};
/**
* Calls the first listener which was waiting for an element
* to call when we push to a list
*/
PushListWatcher.prototype.pushed = function (key) {
if (this.listeners[key] && this.listeners[key].length > 0) {
var listener = this.listeners[key].shift();
listener(key);
}
};
/**
* Remove all the listener from all the keys it was listening to
*/
PushListWatcher.prototype.removeListeners = function (listenedTo, listener) {
for (var i = 0; i < listenedTo.length; i++) {
for (var j = 0; j < this.listeners[listenedTo[i]].length; j++) {
if (this.listeners[listenedTo[i]][j] === listener) {
this.listeners[listenedTo[i]].splice(j, 1);
j = this.listeners[listenedTo[i]];
}
}
}
};
const pushListWatcher = new PushListWatcher();