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 multiCaching = require('../index').multiCaching; var memoryStore = require('../lib/stores/memory'); var Promise = require('es6-promise').Promise; var methods = { getWidget: function(name, cb) { process.nextTick(function() { cb(null, {name: name}); }); }, getMultiWidget: function(names, cb) { process.nextTick(function() { cb(null, names.map(function(name) { return {name: name}; })); }); }, getTTL: function(data, store) { if (store === 'memory') { return 444; } return 777; }, }; describe("multiCaching", function() { var memoryCache; var memoryCache2; var memoryCache3; var multiCache; var key; var key2; var memoryTtl; var name; var name2; var defaultTtl; beforeEach(function() { memoryTtl = 0.1; defaultTtl = 5; memoryCache = caching({store: 'memory', ttl: memoryTtl, promiseDependency: Promise}); memoryCache2 = caching({store: 'memory', ttl: memoryTtl, promiseDependency: Promise}); memoryCache3 = caching({store: 'memory', ttl: memoryTtl, promiseDependency: Promise}); key = support.random.string(20); key2 = support.random.string(20); name = support.random.string(); name2 = support.random.string(); }); describe("get(), set(), del(), reset(), mget(), mset()", function() { var value; var value2; beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache2, memoryCache3]); key = support.random.string(20); key2 = support.random.string(20); value = support.random.string(); value2 = support.random.string(); }); describe("set()", function() { it("lets us set data in all caches", function(done) { multiCache.set(key, value, {ttl: defaultTtl}, function(err) { checkErr(err); memoryCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); }); }); it("lets us set data ('Object' type) in all caches", function(done) { value = {name: support.random.string()}; multiCache.set(key, value, function(err) { checkErr(err); memoryCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); }); }); it("lets us set data without a callback", function(done) { multiCache.set(key, value, {ttl: defaultTtl}); setTimeout(function() { multiCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); }); }, 20); }); it("lets us set data without an options param", function(done) { multiCache.set(key, value, function(err) { checkErr(err); multiCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); }); }); }); it("lets us set data without options or callback", function(done) { multiCache.set(key, value); setTimeout(function() { multiCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); }); }, 20); }); }); describe("mset()", function() { it("lets us set data in all caches", function(done) { multiCache.mset(key, value, key2, value2, {ttl: defaultTtl}, function(err) { checkErr(err); memoryCache.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache2.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache3.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); done(); }); }); }); }); }); it("lets us set data ('Object' type) in all caches", function(done) { value2 = {name: support.random.string()}; multiCache.mset(key, value, key2, value2, function(err) { checkErr(err); memoryCache.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache2.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache3.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); done(); }); }); }); }); }); it("lets us set data without a callback", function(done) { multiCache.mset(key, value, key2, value2, {ttl: defaultTtl}); setTimeout(function() { multiCache.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache2.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache3.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); done(); }); }); }); }); }, 20); }); it("lets us set data without an options param", function(done) { multiCache.mset(key, value, key2, value2, function(err) { checkErr(err); multiCache.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache2.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); memoryCache3.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); done(); }); }); }); }); }); }); it("lets us set data without options or callback", function(done) { multiCache.set(key, value); setTimeout(function() { multiCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, value); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); }); }, 20); }); }); describe("get()", function() { it("gets data from first cache that has it", function(done) { memoryCache3.set(key, value, function(err) { checkErr(err); multiCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); it("passes any options to underlying caches", function(done) { multiCache.set(key, value, function(err) { checkErr(err); sinon.spy(memoryCache.store, 'get'); var opts = {foo: 'bar'}; multiCache.get(key, opts, function(err, result) { checkErr(err); assert.equal(result, value); assert.ok(memoryCache.store.get.calledWith(key, opts)); memoryCache.store.get.restore(); done(); }); }); }); describe('using promises', function() { it('gets data from first cache that has it', function(done) { memoryCache3.set(key, value) .then(function() { return multiCache.get(key); }) .then(function(result) { assert.equal(result, value); }) .then(done); }); it("passes any options to underlying caches", function(done) { var opts = {foo: 'bar'}; multiCache.set(key, value) .then(function() { sinon.spy(memoryCache.store, 'get'); return multiCache.get(key, opts); }) .then(function(result) { assert.equal(result, value); assert.ok(memoryCache.store.get.calledWith(key, opts)); memoryCache.store.get.restore(); }) .then(done); }); }); }); describe("mget()", function() { it("gets data from first cache that has it (1)", function(done) { memoryCache3.mset(key, value, key2, value2, function(err) { checkErr(err); multiCache.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); done(); }); }); }); it("gets data from first cache that has it (2)", function(done) { var key3 = support.random.string(20); var value3 = support.random.string(); memoryCache.mset(key3, value3); memoryCache2.mset(key, value); memoryCache3.mset(key2, value2); setTimeout(function() { multiCache.mget(key, key2, key3, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); assert.equal(result[2], value3); done(); }); }, 20); }); it("gets data from first cache that has it (3)", function(done) { var key3 = support.random.string(20); memoryCache2.mset(key, value); setTimeout(function() { multiCache.mget(key2, key, key3, function(err, result) { checkErr(err); assert.equal(result[0], null); assert.equal(result[1], value); assert.equal(result[2], null); done(); }); }, 20); }); it("passes any options to underlying caches", function(done) { multiCache.mset(key, value, key2, value2, function(err) { checkErr(err); sinon.spy(memoryCache.store, 'mget'); var opts = {foo: 'bar'}; multiCache.mget(key, key2, opts, function(err, result) { checkErr(err); assert.equal(result[0], value); assert.equal(result[1], value2); assert.ok(memoryCache.store.mget.calledWith(key, key2, opts)); memoryCache.store.mget.restore(); done(); }); }); }); describe('using promises', function() { it('gets data from first cache that has it', function(done) { memoryCache3.mset(key, value, key2, value2) .then(function() { return multiCache.mget(key, key2); }) .then(function(result) { assert.equal(result[0], value); assert.equal(result[1], value2); }) .then(done); }); it("passes any options to underlying caches", function(done) { var opts = {foo: 'bar'}; multiCache.mset(key, value, key2, value2) .then(function() { sinon.spy(memoryCache.store, 'mget'); return multiCache.mget(key, key2, opts); }) .then(function(result) { assert.equal(result[0], value); assert.equal(result[1], value2); assert.ok(memoryCache.store.mget.calledWith(key, key2, opts)); memoryCache.store.mget.restore(); }) .then(done); }); }); }); describe("del()", function() { it("lets us delete data in all caches", function(done) { multiCache.set(key, value, function(err) { checkErr(err); multiCache.del(key, function(err) { checkErr(err); memoryCache.get(key, function(err, result) { assert.ok(!result); memoryCache2.get(key, function(err, result) { checkErr(err); assert.ok(!result); memoryCache3.get(key, function(err, result) { checkErr(err); assert.ok(!result); done(); }); }); }); }); }); }); it("lets us delete multiple keys in all caches (Args)", function(done) { multiCache.mset(key, value, key2, value2, function(err) { checkErr(err); multiCache.del(key, key2, function(err) { checkErr(err); memoryCache.mget(key, key2, function(err, result) { assert.ok(!result[0]); assert.ok(!result[1]); memoryCache2.mget(key, key2, function(err, result) { checkErr(err); assert.ok(!result[0]); assert.ok(!result[1]); memoryCache3.mget(key, key2, function(err, result) { checkErr(err); assert.ok(!result[0]); assert.ok(!result[1]); done(); }); }); }); }); }); }); it("lets us delete multiple keys in all caches (Array)", function(done) { multiCache.mset(key, value, key2, value2, function(err) { checkErr(err); multiCache.del([key, key2], function(err) { checkErr(err); memoryCache.mget(key, key2, function(err, result) { assert.ok(!result[0]); assert.ok(!result[1]); memoryCache2.mget(key, key2, function(err, result) { checkErr(err); assert.ok(!result[0]); assert.ok(!result[1]); memoryCache3.mget(key, key2, function(err, result) { checkErr(err); assert.ok(!result[0]); assert.ok(!result[1]); done(); }); }); }); }); }); }); it("lets us delete data without a callback", function(done) { multiCache.set(key, value, function(err) { checkErr(err); multiCache.del(key); setTimeout(function() { memoryCache.get(key, function(err, result) { checkErr(err); assert.ok(!result); memoryCache2.get(key, function(err, result) { checkErr(err); assert.ok(!result); memoryCache3.get(key, function(err, result) { checkErr(err); assert.ok(!result); done(); }); }); }); }, 10); }); }); }); describe("reset()", function() { it("resets all caches", function(done) { multiCache.set(key, value, function(err) { checkErr(err); multiCache.reset(function() { memoryCache.get(key, function(err, result) { checkErr(err); assert.ok(!result); memoryCache2.get(key, function(err, result) { checkErr(err); assert.ok(!result); memoryCache3.get(key, function(err, result) { checkErr(err); assert.ok(!result); done(); }); }); }); }); }); }); }); }); describe("getAndPassUp()", function() { var value; var key; describe("using a single cache store", function() { beforeEach(function() { multiCache = multiCaching([memoryCache3]); key = support.random.string(20); value = support.random.string(); }); it("gets data from first cache that has it", function(done) { memoryCache3.set(key, value, function(err) { checkErr(err); multiCache.getAndPassUp(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); it("gets data from first cache that has it using promises", function(done) { memoryCache3.set(key, value) .then(function() { return multiCache.getAndPassUp(key); }) .then(function(result) { assert.equal(result, value); done(); }); }); }); describe("when value is not found in any cache", function() { var response; beforeEach(function(done) { key = support.random.string(10); sinon.spy(memoryCache, 'set'); sinon.spy(memoryCache2, 'set'); sinon.spy(memoryCache3, 'set'); multiCache.getAndPassUp(key, function(err, result) { checkErr(err); response = result; done(); }); }); afterEach(function() { memoryCache.set.restore(); memoryCache2.set.restore(); memoryCache3.set.restore(); }); it("calls back with undefined", function() { assert.strictEqual(response, undefined); }); it("does not set anything in caches", function(done) { process.nextTick(function() { assert.ok(memoryCache.set.notCalled); assert.ok(memoryCache2.set.notCalled); assert.ok(memoryCache3.set.notCalled); done(); }); }); }); describe("using multi cache store", function() { beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache2, memoryCache3]); key = support.random.string(20); value = support.random.string(); }); it("checks to see if higher levels have item", function(done) { memoryCache3.set(key, value, function(err) { checkErr(err); multiCache.getAndPassUp(key, function(err, result) { checkErr(err); assert.equal(result, value); process.nextTick(function() { memoryCache.get(key, function(err, result) { assert.equal(result, value); checkErr(err); done(); }); }); }); }); }); it("checks to see if higher levels have item using promises", function(done) { memoryCache3.set(key, value) .then(function() { return multiCache.getAndPassUp(key); }) .then(function(result) { assert.equal(result, value); }) .then(function() { process.nextTick(function() { memoryCache.get(key) .then(function(fetchedValue) { assert.equal(fetchedValue, value); }); }); }) .then(done); }); context("when a cache store calls back with an error", function() { var fakeError; var memoryStoreStub; beforeEach(function() { memoryStoreStub = memoryStore.create({ttl: defaultTtl}); sinon.stub(memoryStore, 'create').returns(memoryStoreStub); memoryCache = caching({store: 'memory', ttl: defaultTtl}); multiCache = multiCaching([memoryCache]); fakeError = new Error(support.random.string()); sinon.stub(memoryStoreStub, 'get').yields(fakeError); }); afterEach(function() { memoryStore.create.restore(); }); it("bubbles up errors from caches", function(done) { multiCache.getAndPassUp(key, function(err) { assert.ok(memoryStoreStub.get.called); assert.equal(err, fakeError); done(); }); }); it("bubbles up errors from caches and reject promise", function(done) { multiCache.getAndPassUp(key) .catch(function(err) { assert.ok(memoryStoreStub.get.called); assert.equal(err, fakeError); done(); }); }); }); }); }); describe("wrap()", function() { describe("using a single cache store", function() { beforeEach(function() { multiCache = multiCaching([memoryCache3]); }); context("calls back with the result of a function", function() { beforeEach(function() { sinon.spy(memoryCache3.store, 'set'); sinon.spy(memoryCache3.store, 'mset'); }); afterEach(function() { memoryCache3.store.set.restore(); memoryCache3.store.mset.restore(); }); /** * Note: it's up to the underlying cache implementation to handle the ttl number. * We're just testing that the ttl gets passed to the underlying store. */ it('when a ttl number is passed in', function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, {ttl: defaultTtl}, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}, {ttl: defaultTtl}); done(); }); }); it('when a ttl option is passed in', function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, {ttl: defaultTtl}, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}, {ttl: defaultTtl}); done(); }); }); it('when no options are passed in', function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}); done(); }); }); it('when multiple keys are passed', function(done) { multiCache.wrap(key, key2, function(cb) { methods.getMultiWidget([name, name2], cb); }, function(err, widgets) { checkErr(err); assert.deepEqual(widgets[0], {name: name}); assert.deepEqual(widgets[1], {name: name2}); sinon.assert.calledWith(memoryCache3.store.mset, key, {name: name}, key2, {name: name2}); 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').yields(fakeError, {name: name}); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { methods.getWidget.restore(); assert.equal(err, fakeError); assert.ok(!widget); 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) { multiCache.wrap(key, function(cacheCb) { getFalseyValue(cacheCb); }, cb); } beforeEach(function(done) { multiCache = multiCaching([memoryCache3]); sinon.spy(memoryCache3.store, 'set'); getCachedFalseyValue(function(err, result) { checkErr(err); assert.strictEqual(result, falseyValue); memoryCache3.get(key, function(err, result) { checkErr(err); assert.strictEqual(result, falseyValue); sinon.spy(memoryCache3.store, 'get'); done(); }); }); }); afterEach(function() { memoryCache3.store.set.restore(); memoryCache3.store.get.restore(); }); it("sets data in and retrieves data from cache", function(done) { getCachedFalseyValue(function(err, value) { checkErr(err); assert.strictEqual(value, falseyValue); assert.ok(memoryCache3.store.set.calledWith(key)); assert.ok(memoryCache3.store.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) { multiCache.wrap(key, function(cacheCb) { getValue(name, function(err, result) { cacheCb(err, result); }); }, cb); } beforeEach(function() { sinon.spy(testCallbacks, 'isCacheableValue'); multiCache = multiCaching([memoryCache3], {isCacheableValue: testCallbacks.isCacheableValue}); sinon.spy(memoryCache3.store, 'set'); }); afterEach(function() { memoryCache3.store.set.restore(); testCallbacks.isCacheableValue.restore(); }); it("stores allowed values", function(done) { var name = 'foo'; getCachedValue(name, function(err) { checkErr(err); assert.ok(memoryCache3.store.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(memoryCache3.store.set.notCalled); done(); }); }); }); context("when an underlying store has its own isCacheableValue function", function() { var memoryCache4; var testCallbacks = { isCacheableValue: function(value) { var x = value !== 'do_not_store_this' && value !== undefined; return x; } }; 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) { multiCache.wrap(key, function(cacheCb) { getValue(name, function(err, result) { cacheCb(err, result); }); }, cb); } beforeEach(function() { sinon.spy(testCallbacks, 'isCacheableValue'); memoryCache4 = caching({ store: 'memory', ttl: memoryTtl }); // This simulates how node-cache-manager-redis sets its // isCacheableValue function: memoryCache4.store.isCacheableValue = testCallbacks.isCacheableValue; multiCache = multiCaching([memoryCache4]); sinon.spy(memoryCache4.store, 'set'); }); afterEach(function() { memoryCache4.store.set.restore(); testCallbacks.isCacheableValue.restore(); }); it("stores allowed values", function(done) { var name = 'foo'; getCachedValue(name, function(err) { checkErr(err); assert.ok(memoryCache4.store.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(memoryCache4.store.set.notCalled); done(); }); }); it("allows repeated calls for uncacheable value", function(done) { function getUndefined(name, cb) { multiCache.wrap(key, function(cacheCb) { process.nextTick(function() { cacheCb(null, undefined); }); }, cb); } var name = 'oof'; getUndefined(name, function(err, val) { checkErr(err); assert.ok(val === undefined); getUndefined(name, function(err, val) { checkErr(err); assert.ok(val === undefined); done(); }); }); }); it("allows simultaneous calls for uncacheable value", function(done) { var results = [true, undefined]; function getUndefined(name, cb) { multiCache.wrap(key, function(cacheCb) { process.nextTick(function() { cacheCb(null, results.pop()); }); }, cb); } var name = 'oof'; getUndefined(name, function(err, val) { checkErr(err); assert.ok(val === undefined); }); getUndefined(name, function(err, val) { checkErr(err); assert.ok(val === undefined); // ensure we didn't cache "undefined" getUndefined(name, function(err, val) { checkErr(err); assert.ok(val === true); done(); }); }); }); }); context("when wrapped function does not return results", function() { it("should not call 'setInMultipleCaches()'", function(done) { sinon.spy(memoryCache3.store, 'mset'); multiCache.wrap(key, key2, function(cb) { cb(null, [undefined, undefined]); }, function(err, widgets) { checkErr(err); assert.ok(widgets === undefined); sinon.assert.notCalled(memoryCache3.store.mset); memoryCache3.store.mset.restore(); done(); }); }); }); }); describe("using two cache stores", function() { beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache3]); }); it("calls back with the result of a function", function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); done(); }); }); it("sets value in all caches", function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache3.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); done(); }); }); }); }); it('sets the ttl value by context (store) with a function', function(done) { memoryCache3.store.name = 'redis'; sinon.spy(memoryCache.store, 'set'); sinon.spy(memoryCache3.store, 'set'); sinon.spy(methods, 'getTTL'); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, {ttl: methods.getTTL}, function(err) { checkErr(err); sinon.assert.calledTwice(methods.getTTL); sinon.assert.calledWith(methods.getTTL, [key, {name: name}], 'memory'); sinon.assert.calledWith(methods.getTTL, [key, {name: name}], 'redis'); sinon.assert.calledWith(memoryCache.store.set, key, {name: name}, {ttl: 444}); sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}, {ttl: 777}); methods.getTTL.restore(); memoryCache.store.set.restore(); memoryCache3.store.set.restore(); memoryCache3.store.name = 'memory'; done(); }); }); context("when value exists in first store but not second", function() { it("returns value from first store, does not set it in second", function(done) { memoryCache.set(key, {name: name}, function(err) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, null); done(); }); }); }); }); it("returns value from first store, does not set it in second (multiple keys)", function(done) { memoryCache.mset(key, {name: name}, key2, {name: name2}, function(err) { checkErr(err); multiCache.wrap(key, key2, function(cb) { methods.getMultiWidget([name, name2], cb); }, function(err, widgets) { checkErr(err); assert.deepEqual(widgets[0], {name: name}); assert.deepEqual(widgets[1], {name: name2}); memoryCache3.mget(key, key2, function(err, result) { checkErr(err); assert.equal(result[0], null); assert.equal(result[1], null); done(); }); }); }); }); }); context("when value exists in second store but not first", function() { it("returns value from second store, sets it in first store", function(done) { memoryCache3.set(key, {name: name}, function(err) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); done(); }); }); }); }); it("returns value from second store, sets it in first store (multiple keys)", function(done) { memoryCache3.mset(key, {name: name}, key2, {name: name2}, function(err) { checkErr(err); multiCache.wrap(key, key2, function(cb) { methods.getMultiWidget([name, name2], cb); }, function(err, widgets) { checkErr(err); assert.deepEqual(widgets[0], {name: name}); assert.deepEqual(widgets[1], {name: name2}); memoryCache.mget(key, key2, function(err, result) { checkErr(err); assert.deepEqual(result[0], {name: name}); assert.deepEqual(result[1], {name: name2}); done(); }); }); }); }); }); }); describe("using three cache stores", function() { beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache3, memoryCache2]); }); it("calls back with the result of a function", function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); done(); }); }); it("sets value in all caches", function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache2.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache3.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); done(); }); }); }); }); }); context("when value exists in first store only", function() { it("returns value from first store, does not set it in second or third", function(done) { memoryCache.set(key, {name: name}, function(err) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, null); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, null); done(); }); }); }); }); }); }); context("when value exists in second store only", function() { it("returns value from second store, sets it in first store, does not set third store", function(done) { memoryCache3.set(key, {name: name}, function(err) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, null); done(); }); }); }); }); }); }); context("when value exists in third store only", function() { it("returns value from third store, sets it in first and second stores", function(done) { memoryCache2.set(key, {name: name}, function(err) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache3.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); done(); }); }); }); }); }); }); it("lets us make nested calls", function(done) { function getCachedWidget(name, cb) { multiCache.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(); }); }); }); }); describe("passing multiple keys", function() { it("sets value in all caches", function(done) { multiCache.wrap(key, key2, function(cb) { methods.getMultiWidget([name, name2], cb); }, function(err, widgets) { checkErr(err); assert.deepEqual(widgets[0], {name: name}); assert.deepEqual(widgets[1], {name: name2}); memoryCache.mget(key, key2, function(err, result) { checkErr(err); assert.deepEqual(result[0], {name: name}); assert.deepEqual(result[1], {name: name2}); memoryCache2.mget(key, key2, function(err, result) { checkErr(err); assert.deepEqual(result[0], {name: name}); assert.deepEqual(result[1], {name: name2}); memoryCache3.mget(key, key2, function(err, result) { checkErr(err); assert.deepEqual(result[0], {name: name}); assert.deepEqual(result[1], {name: name2}); done(); }); }); }); }); }); it("should not throw error if one of the store does not have `mset() or mget()`", function(done) { var basicStore = { set: function() {}, get: function() {} }; sinon.spy(basicStore, 'set'); multiCache = multiCaching([memoryCache, {store: basicStore}]); multiCache.wrap(key, key2, function(cb) { methods.getMultiWidget([name, name2], cb); }, done); }); }); }); context("error handling", function() { var memoryStoreStub; var ttl; beforeEach(function() { ttl = 0.1; memoryStoreStub = memoryStore.create({ttl: ttl}); sinon.stub(memoryStore, 'create').returns(memoryStoreStub); memoryCache = caching({store: 'memory', ttl: ttl}); multiCache = multiCaching([memoryCache]); }); afterEach(function() { memoryStore.create.restore(); }); context("when an error is thrown in the work function", function() { var fakeError; beforeEach(function() { fakeError = new Error(support.random.string()); }); it("does not catch the error", function(done) { var originalExceptionHandler = process.listeners('uncaughtException').pop(); process.removeListener('uncaughtException', originalExceptionHandler); process.once('uncaughtException', function(err) { process.on('uncaughtException', originalExceptionHandler); assert.ok(err); done(); }); multiCache.wrap(key, function() { throw fakeError; }, function() { done(new Error('Should not have caught error')); }); }); }); context("when an error is thrown in the work function's callback", function() { var fakeError; beforeEach(function() { fakeError = new Error(support.random.string()); }); it("bubbles up that error", function(done) { multiCache.wrap(key, function(next) { next(); }, {ttl: ttl}, function() { // This test as-is doesn't prove a fix for #28 (https://github.com/BryanDonovan/node-cache-manager/issues/28) // but if you remove the try/catch, it shows that the undefined `waiting` array issue // is no longer present (the domain doesn't try to process the error in the callbackFiller). try { throw new Error('foo'); } catch (e) { assert.equal(e.message, 'foo'); } done(); }); }); it("bubbles up that error (multiple keys)", function(done) { multiCache.wrap(key, key2, function(next) { next(fakeError); }, {ttl: ttl}, function(err) { assert.equal(err, fakeError); done(); }); }); }); context("when store.get() calls back with an error", function() { it("bubbles up that error", function(done) { var fakeError = new Error(support.random.string()); sinon.stub(memoryStoreStub, 'get').yields(fakeError); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err) { assert.equal(err, fakeError); memoryStoreStub.get.restore(); done(); }); }); }); context("when store.set() calls back with an error", function() { it("bubbles up that error", function(done) { var fakeError = new Error(support.random.string()); sinon.stub(memoryStoreStub, 'set').yields(fakeError); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err) { assert.equal(err, fakeError); memoryStoreStub.set.restore(); done(); }); }); }); context("when store.mget() calls back with an error", function() { it("bubbles up that error", function(done) { var fakeError = new Error(support.random.string()); sinon.stub(memoryStoreStub, 'mget').yields(fakeError); multiCache.wrap(key, key2, function(cb) { methods.getMultiWidget([name, name2], cb); }, function(err) { assert.equal(err, fakeError); memoryStoreStub.mget.restore(); done(); }); }); }); }); describe("when called multiple times in parallel with same key", function() { var construct; var constructMulti; beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache3]); var firstTimeout = 110; var firstTimeoutUsed = false; function getTimeout() { if (firstTimeoutUsed) { support.random.number(100); } else { firstTimeoutUsed = true; return firstTimeout; } } construct = sinon.spy(function(val, cb) { var timeout = getTimeout(); setTimeout(function() { cb(null, 'value'); }, timeout); }); constructMulti = sinon.spy(function(val, cb) { var timeout = getTimeout(); setTimeout(function() { cb(null, ['value', 'value2']); }, timeout); }); }); it("calls the wrapped function once", function(done) { var values = []; for (var i = 0; i < 5; i++) { values.push(i); } async.each(values, function(val, next) { multiCache.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(); }); }); it("calls the multi wrapped function once", function(done) { var values = []; for (var i = 0; i < 5; i++) { values.push(i); } async.each(values, function(val, next) { multiCache.wrap('key', 'key2', function(cb) { constructMulti(val, cb); }, function(err, results) { assert.equal(results[0], 'value'); assert.equal(results[1], 'value2'); next(err); }); }, function(err) { checkErr(err); assert.equal(constructMulti.callCount, 1); done(); }); }); }); describe("using native promises", function() { beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache3]); }); it("should be able to chain with simple promise", function(done) { multiCache.wrap('key', function() { return 'OK'; }) .then(function(res) { assert.equal(res, 'OK'); done(); }); }); it("should be able to chain with cache function as a promise", function(done) { multiCache.wrap('key', function() { return new Promise(function(resolve) { resolve('OK'); }); }) .then(function(res) { assert.equal(res, 'OK'); done(); }); }); it("should be able to catch errors in cache function as a promise", function(done) { multiCache.wrap('key', function() { return new Promise(function(resolve, reject) { reject('NOK'); }); }) .then(function() { done(new Error('It should not call then since there is an error in the cache function!')); }) .catch(function() { done(); }); }); it("should be able to catch a throw in cache function as a promise", function(done) { multiCache.wrap('key', function() { throw 'NOK'; }) .then(function() { done(new Error('It should not call then since there is an error in the cache function!')); }) .catch(function() { done(); }); }); it("should be able to chain with non-cacheable value", function(done) { multiCache.wrap('key', function() { return; }) .then(function(res) { assert.equal(res, undefined); done(); }); }); }); }); context("when instantiated with a non-Array 'caches' arg", function() { it("throws an error", function() { assert.throws(function() { multiCaching({foo: 'bar'}); }, /multiCaching requires an array/); }); }); });