You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
731 lines
25 KiB
731 lines
25 KiB
// TODO: These are really a mix of unit and integration tests.
|
|
|
|
var assert = require('assert');
|
|
var async = require('async');
|
|
var sinon = require('sinon');
|
|
var support = require('./support');
|
|
var checkErr = support.checkErr;
|
|
var caching = require('../index').caching;
|
|
var memoryStore = require('../lib/stores/memory');
|
|
|
|
var methods = {
|
|
getWidget: function(name, cb) {
|
|
process.nextTick(function() {
|
|
cb(null, {name: name});
|
|
});
|
|
}
|
|
};
|
|
|
|
describe("caching", function() {
|
|
var cache;
|
|
var key;
|
|
var defaultTtl = 1;
|
|
var name;
|
|
var value;
|
|
|
|
describe("get() and set()", function() {
|
|
['memory'].forEach(function(store) {
|
|
context("using " + store + " store", function() {
|
|
beforeEach(function() {
|
|
cache = caching({store: store});
|
|
key = support.random.string(20);
|
|
value = support.random.string();
|
|
});
|
|
|
|
it("lets us set and get data in cache", function(done) {
|
|
cache.set(key, value, {ttl: defaultTtl}, function(err) {
|
|
checkErr(err);
|
|
|
|
cache.get(key, function(err, result) {
|
|
checkErr(err);
|
|
assert.equal(result, value);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it("lets us set and get data without a callback", function(done) {
|
|
cache.set(key, value, {ttl: defaultTtl});
|
|
|
|
setTimeout(function() {
|
|
var result = cache.get(key);
|
|
assert.equal(result, value);
|
|
done();
|
|
}, 20);
|
|
});
|
|
|
|
it("lets us set and get data without options object or callback", function(done) {
|
|
cache.set(key, value);
|
|
|
|
setTimeout(function() {
|
|
var result = cache.get(key);
|
|
assert.equal(result, value);
|
|
done();
|
|
}, 20);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("del()", function() {
|
|
['memory'].forEach(function(store) {
|
|
context("using " + store + " store", function() {
|
|
beforeEach(function(done) {
|
|
cache = caching({store: store});
|
|
key = support.random.string(20);
|
|
value = support.random.string();
|
|
cache.set(key, value, {ttl: defaultTtl}, function(err) {
|
|
checkErr(err);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("deletes data from cache", function(done) {
|
|
cache.get(key, function(err, result) {
|
|
assert.equal(result, value);
|
|
|
|
cache.del(key, function(err) {
|
|
checkErr(err);
|
|
|
|
cache.get(key, function(err, result) {
|
|
assert.ok(!result);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it("lets us delete data without a callback", function(done) {
|
|
cache.get(key, function(err, result) {
|
|
assert.equal(result, value);
|
|
|
|
cache.del(key);
|
|
|
|
setTimeout(function() {
|
|
cache.get(key, function(err, result) {
|
|
assert.ok(!result);
|
|
done();
|
|
});
|
|
}, 20);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("reset()", function() {
|
|
var key2;
|
|
var value2;
|
|
|
|
beforeEach(function(done) {
|
|
cache = caching({store: 'memory'});
|
|
key = support.random.string(20);
|
|
value = support.random.string();
|
|
cache.set(key, value, function(err) {
|
|
checkErr(err);
|
|
|
|
key2 = support.random.string(20);
|
|
value2 = support.random.string();
|
|
|
|
cache.set(key2, value2, done);
|
|
});
|
|
});
|
|
|
|
it("clears the cache", function(done) {
|
|
cache.reset(function(err) {
|
|
checkErr(err);
|
|
|
|
cache.get(key, function(err, result) {
|
|
assert.ok(!result);
|
|
|
|
cache.get(key2, function(err, result) {
|
|
assert.ok(!result);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it("lets us clear the cache without a callback (memory store only)", function(done) {
|
|
cache.reset();
|
|
setTimeout(function() {
|
|
cache.get(key, function(err, result) {
|
|
assert.ok(!result);
|
|
|
|
cache.get(key2, function(err, result) {
|
|
assert.ok(!result);
|
|
done();
|
|
});
|
|
});
|
|
}, 10);
|
|
});
|
|
|
|
context("when store has no del() method", function() {
|
|
var fakeStore;
|
|
|
|
beforeEach(function() {
|
|
fakeStore = {
|
|
get: function() {},
|
|
set: function() {},
|
|
};
|
|
});
|
|
|
|
it("it doesn't throw an error", function() {
|
|
assert.doesNotThrow(function() {
|
|
caching({store: fakeStore});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("setex()", function() {
|
|
var fakeStore;
|
|
|
|
beforeEach(function() {
|
|
fakeStore = {
|
|
get: function() {},
|
|
set: function() {},
|
|
del: function() {},
|
|
setex: function() {}
|
|
};
|
|
|
|
sinon.stub(fakeStore, 'setex');
|
|
|
|
cache = caching({store: fakeStore});
|
|
});
|
|
|
|
it("passes the params to the underlying store's setex() method", function() {
|
|
cache.setex('foo', 'bar', 'blah');
|
|
assert.ok(fakeStore.setex.calledWith('foo', 'bar', 'blah'));
|
|
});
|
|
});
|
|
|
|
describe("ttl()", function() {
|
|
var fakeStore;
|
|
|
|
beforeEach(function() {
|
|
fakeStore = {
|
|
get: function() {},
|
|
set: function() {},
|
|
del: function() {},
|
|
ttl: function() {}
|
|
};
|
|
|
|
sinon.stub(fakeStore, 'ttl');
|
|
|
|
cache = caching({store: fakeStore});
|
|
});
|
|
|
|
it("passes the params to the underlying store's ttl() method", function() {
|
|
cache.ttl('foo');
|
|
assert.ok(fakeStore.ttl.calledWith('foo'));
|
|
});
|
|
});
|
|
|
|
describe("keys()", function() {
|
|
var keyCount;
|
|
var savedKeys = [];
|
|
|
|
beforeEach(function(done) {
|
|
keyCount = 10;
|
|
var processed = 0;
|
|
|
|
cache = caching({store: 'memory'});
|
|
|
|
function isDone() {
|
|
return processed === keyCount;
|
|
}
|
|
|
|
async.until(isDone, function(cb) {
|
|
processed += 1;
|
|
key = support.random.string(20);
|
|
savedKeys.push(key);
|
|
value = support.random.string();
|
|
cache.set(key, value, cb);
|
|
}, done);
|
|
});
|
|
|
|
it("calls back with all keys in cache", function(done) {
|
|
cache.keys(function(err, keys) {
|
|
checkErr(err);
|
|
assert.deepEqual(keys.sort, savedKeys.sort);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("lets us get the keys without a callback (memory store only)", function() {
|
|
var keys = cache.keys();
|
|
assert.deepEqual(keys.sort, savedKeys.sort);
|
|
});
|
|
});
|
|
|
|
describe("wrap()", function() {
|
|
describe("using memory (lru-cache) store", function() {
|
|
var memoryStoreStub;
|
|
var opts;
|
|
|
|
beforeEach(function() {
|
|
opts = {ttl: 0.1};
|
|
memoryStoreStub = memoryStore.create(opts);
|
|
|
|
sinon.stub(memoryStore, 'create').returns(memoryStoreStub);
|
|
|
|
cache = caching({store: 'memory', ttl: opts.ttl, ignoreCacheErrors: false});
|
|
key = support.random.string(20);
|
|
name = support.random.string();
|
|
});
|
|
|
|
afterEach(function() {
|
|
memoryStore.create.restore();
|
|
});
|
|
|
|
context("calls back with the result of the wrapped function", function() {
|
|
beforeEach(function() {
|
|
sinon.spy(memoryStoreStub, 'set');
|
|
});
|
|
|
|
afterEach(function() {
|
|
memoryStoreStub.set.restore();
|
|
});
|
|
|
|
it("when a ttl is passed in", function(done) {
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, cb);
|
|
}, opts, function(err, widget) {
|
|
checkErr(err);
|
|
assert.deepEqual(widget, {name: name});
|
|
sinon.assert.calledWith(memoryStoreStub.set, key, {name: name}, opts);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("when a ttl is not passed in", function(done) {
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, cb);
|
|
}, function(err, widget) {
|
|
checkErr(err);
|
|
assert.deepEqual(widget, {name: name});
|
|
sinon.assert.calledWith(memoryStoreStub.set, key, {name: name}, {});
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
context("when result is already cached", function() {
|
|
function getCachedWidget(name, cb) {
|
|
cache.wrap(key, function(cacheCb) {
|
|
methods.getWidget(name, cacheCb);
|
|
}, opts, cb);
|
|
}
|
|
|
|
beforeEach(function(done) {
|
|
getCachedWidget(name, function(err, widget) {
|
|
checkErr(err);
|
|
assert.ok(widget);
|
|
|
|
memoryStoreStub.get(key, function(err, result) {
|
|
checkErr(err);
|
|
assert.ok(result);
|
|
|
|
sinon.spy(memoryStoreStub, 'get');
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
afterEach(function() {
|
|
memoryStoreStub.get.restore();
|
|
});
|
|
|
|
it("retrieves data from cache", function(done) {
|
|
var funcCalled = false;
|
|
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, function(err, result) {
|
|
funcCalled = true;
|
|
cb(err, result);
|
|
});
|
|
}, function(err, widget) {
|
|
checkErr(err);
|
|
assert.deepEqual(widget, {name: name});
|
|
assert.ok(memoryStoreStub.get.calledWith(key));
|
|
assert.ok(!funcCalled);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
var falseyValues = [false, null, 0];
|
|
|
|
falseyValues.forEach(function(falseyValue) {
|
|
context("when cached value is `" + falseyValue + "`", function() {
|
|
function getFalseyValue(cb) {
|
|
process.nextTick(function() {
|
|
cb(null, falseyValue);
|
|
});
|
|
}
|
|
|
|
function getCachedFalseyValue(cb) {
|
|
cache.wrap(key, function(cacheCb) {
|
|
getFalseyValue(cacheCb);
|
|
}, cb);
|
|
}
|
|
|
|
beforeEach(function(done) {
|
|
getCachedFalseyValue(function(err, result) {
|
|
checkErr(err);
|
|
assert.strictEqual(result, falseyValue);
|
|
|
|
memoryStoreStub.get(key, function(err, result) {
|
|
checkErr(err);
|
|
assert.strictEqual(result, falseyValue);
|
|
|
|
sinon.spy(memoryStoreStub, 'get');
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
afterEach(function() {
|
|
memoryStoreStub.get.restore();
|
|
});
|
|
|
|
it("retrieves data from cache", function(done) {
|
|
getCachedFalseyValue(function(err, value) {
|
|
checkErr(err);
|
|
assert.strictEqual(value, falseyValue);
|
|
assert.ok(memoryStoreStub.get.calledWith(key));
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
context("when we pass in an isCacheableValue function to the caching constructor", function() {
|
|
var testCallbacks = {
|
|
isCacheableValue: function(value) {
|
|
return value !== 'do_not_store_this' && value !== undefined;
|
|
}
|
|
};
|
|
|
|
function getValue(name, cb) {
|
|
process.nextTick(function() {
|
|
if (name === 'foo') {
|
|
cb(null, 'store_this');
|
|
} else {
|
|
cb(null, 'do_not_store_this');
|
|
}
|
|
});
|
|
}
|
|
|
|
function getCachedValue(name, cb) {
|
|
cache.wrap(key, function(cacheCb) {
|
|
getValue(name, function(err, result) {
|
|
cacheCb(err, result);
|
|
});
|
|
}, {ttl: defaultTtl}, cb);
|
|
}
|
|
|
|
beforeEach(function() {
|
|
sinon.spy(testCallbacks, 'isCacheableValue');
|
|
cache = caching({store: 'memory', isCacheableValue: testCallbacks.isCacheableValue});
|
|
sinon.spy(memoryStoreStub, 'set');
|
|
});
|
|
|
|
afterEach(function() {
|
|
memoryStoreStub.set.restore();
|
|
testCallbacks.isCacheableValue.restore();
|
|
});
|
|
|
|
it("stores allowed values", function(done) {
|
|
var name = 'foo';
|
|
|
|
getCachedValue(name, function(err) {
|
|
checkErr(err);
|
|
assert.ok(memoryStoreStub.set.called);
|
|
assert.ok(testCallbacks.isCacheableValue.called);
|
|
|
|
getCachedValue(name, function(err) {
|
|
checkErr(err);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it("does not store non-allowed values", function(done) {
|
|
var name = 'bar';
|
|
|
|
getCachedValue(name, function(err) {
|
|
checkErr(err);
|
|
assert.ok(memoryStoreStub.set.notCalled);
|
|
assert.ok(testCallbacks.isCacheableValue.called);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it("lets us make nested calls", function(done) {
|
|
function getCachedWidget(name, cb) {
|
|
cache.wrap(key, function(cacheCb) {
|
|
methods.getWidget(name, cacheCb);
|
|
}, cb);
|
|
}
|
|
|
|
getCachedWidget(name, function(err, widget) {
|
|
checkErr(err);
|
|
assert.equal(widget.name, name);
|
|
|
|
getCachedWidget(name, function(err, widget) {
|
|
checkErr(err);
|
|
assert.equal(widget.name, name);
|
|
|
|
getCachedWidget(name, function(err, widget) {
|
|
checkErr(err);
|
|
assert.equal(widget.name, name);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it("expires cached result after ttl seconds", function(done) {
|
|
var ttl = 0.1;
|
|
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, cb);
|
|
}, {ttl: ttl}, function(err, widget) {
|
|
checkErr(err);
|
|
assert.deepEqual(widget, {name: name});
|
|
|
|
memoryStoreStub.get(key, function(err, result) {
|
|
checkErr(err);
|
|
assert.ok(result);
|
|
|
|
var funcCalled = false;
|
|
|
|
setTimeout(function() {
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, function(err, result) {
|
|
funcCalled = true;
|
|
cb(err, result);
|
|
});
|
|
}, function(err, widget) {
|
|
checkErr(err);
|
|
assert.ok(funcCalled);
|
|
assert.deepEqual(widget, {name: name});
|
|
done();
|
|
});
|
|
}, (ttl * 1000 + 10));
|
|
});
|
|
});
|
|
});
|
|
|
|
context("when an error is thrown in the work function", function() {
|
|
var fakeError;
|
|
|
|
beforeEach(function() {
|
|
fakeError = new Error(support.random.string());
|
|
});
|
|
|
|
it("bubbles up that error", function(done) {
|
|
cache.wrap(key, function() {
|
|
throw fakeError;
|
|
}, function(err) {
|
|
assert.equal(err, fakeError);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
context("when store.get() calls back with an error", function() {
|
|
context("and ignoreCacheErrors is not set (default is false)", function() {
|
|
it("bubbles up that error", function(done) {
|
|
var fakeError = new Error(support.random.string());
|
|
|
|
sinon.stub(memoryStoreStub, 'get', function(key, options, cb) {
|
|
cb(fakeError);
|
|
});
|
|
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, cb);
|
|
}, function(err) {
|
|
assert.equal(err, fakeError);
|
|
memoryStoreStub.get.restore();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
context("and ignoreCacheErrors is set to true", function() {
|
|
it("does not bubble up that error", function(done) {
|
|
cache = caching({store: 'memory', ttl: defaultTtl, ignoreCacheErrors: true});
|
|
|
|
var fakeError = new Error(support.random.string());
|
|
|
|
sinon.stub(memoryStoreStub, 'get', function(key, options, cb) {
|
|
cb(fakeError);
|
|
});
|
|
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, cb);
|
|
}, function(err) {
|
|
assert.equal(err, null);
|
|
memoryStoreStub.get.restore();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
context("when store.set() calls back with an error", function() {
|
|
context("and ignoreCacheErrors is not set", function() {
|
|
it("bubbles up that error", function(done) {
|
|
var fakeError = new Error(support.random.string());
|
|
|
|
sinon.stub(memoryStoreStub, 'set', function(key, val, ttl, cb) {
|
|
cb(fakeError);
|
|
});
|
|
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, cb);
|
|
}, function(err) {
|
|
assert.equal(err, fakeError);
|
|
memoryStoreStub.set.restore();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
context("and ignoreCacheErrors is set to true", function() {
|
|
it("does not bubbles up that error", function(done) {
|
|
cache = caching({store: 'memory', ttl: defaultTtl, ignoreCacheErrors: true});
|
|
var fakeError = new Error(support.random.string());
|
|
|
|
sinon.stub(memoryStoreStub, 'set').yields(fakeError);
|
|
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, cb);
|
|
}, function(err) {
|
|
assert.equal(err, null);
|
|
memoryStoreStub.set.restore();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
context("when wrapped function calls back with an error", function() {
|
|
it("calls back with that error", function(done) {
|
|
var fakeError = new Error(support.random.string());
|
|
sinon.stub(methods, 'getWidget', function(name, cb) {
|
|
cb(fakeError, {name: name});
|
|
});
|
|
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, cb);
|
|
}, function(err, widget) {
|
|
methods.getWidget.restore();
|
|
assert.equal(err, fakeError);
|
|
assert.ok(!widget);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("when called multiple times in parallel with same key", function() {
|
|
var construct;
|
|
|
|
beforeEach(function() {
|
|
cache = caching({
|
|
store: 'memory',
|
|
max: 50,
|
|
ttl: 5 * 60
|
|
});
|
|
|
|
construct = sinon.spy(function(val, cb) {
|
|
var timeout = support.random.number(100);
|
|
setTimeout(function() {
|
|
cb(null, 'value');
|
|
}, timeout);
|
|
});
|
|
});
|
|
|
|
it("calls the wrapped function once", function(done) {
|
|
var values = [];
|
|
for (var i = 0; i < 2; i++) {
|
|
values.push(i);
|
|
}
|
|
|
|
async.each(values, function(val, next) {
|
|
cache.wrap('key', function(cb) {
|
|
construct(val, cb);
|
|
}, function(err, result) {
|
|
assert.equal(result, 'value');
|
|
next(err);
|
|
});
|
|
}, function(err) {
|
|
checkErr(err);
|
|
assert.equal(construct.callCount, 1);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("instantiating with no store passed in", function() {
|
|
it("defaults to 'memory' store", function() {
|
|
var cache = caching();
|
|
assert.equal(cache.store.name, 'memory');
|
|
});
|
|
});
|
|
|
|
describe("instantiating with custom store", function() {
|
|
it("allows us to pass in our own store object", function(done) {
|
|
var store = memoryStore.create({ttl: defaultTtl});
|
|
cache = caching({store: store});
|
|
cache.set(key, value, function(err) {
|
|
checkErr(err);
|
|
cache.get(key, function(err, result) {
|
|
assert.equal(result, value);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it("allows us to pass in a module (uninstantiated)", function(done) {
|
|
var store = memoryStore;
|
|
cache = caching({store: store});
|
|
cache.set(key, value, {ttl: defaultTtl}, function(err) {
|
|
checkErr(err);
|
|
cache.get(key, function(err, result) {
|
|
assert.equal(result, value);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("overloading with custom store", function() {
|
|
it("allows us to override isCacheableValue", function(done) {
|
|
var store = memoryStore.create({ttl: defaultTtl});
|
|
var onlyOne = true;
|
|
store.isCacheableValue = function(result) {
|
|
if (onlyOne) {
|
|
onlyOne = false;
|
|
done();
|
|
}
|
|
return result !== undefined;
|
|
};
|
|
cache = caching({store: store});
|
|
cache.wrap(key, function(cb) {
|
|
methods.getWidget(name, cb);
|
|
}, function(err, widget) {
|
|
checkErr(err);
|
|
assert.deepEqual(widget, {name: name});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|