import fs from 'fs'; import should from 'should'; import clone from './clone'; import { diffKeys, randomNumber, randomString } from './helpers'; import nodeCache from './index'; const { stringify } = JSON; const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')); const localCache = new nodeCache({ stdTTL: 0 }); const localCacheNoClone = new nodeCache({ stdTTL: 0, useClones: false, checkperiod: 0, }); const localCacheMaxKeys = new nodeCache({ maxKeys: 2 }); const localCacheTTL = new nodeCache({ stdTTL: 0.3, checkperiod: 0 }); const localCacheNoDelete = new nodeCache({ stdTTL: 0.3, checkperiod: 0, deleteOnExpire: false, }); const localCacheMset = new nodeCache({ stdTTL: 0 }); const indexOf = [].indexOf; const BENCH = {}; localCache._killCheckPeriod(); let state = {}; describe(`\`${pkg.name}@${pkg.version}\` on \`node@${process.version}\``, () => { afterAll(() => { let txt = `Benchmark node@${process.version}:`; for (const type in BENCH) { const ops = BENCH[type]; txt += `\n - ${type}: ${ops.toFixed(1)} ops/s`; } console.log(txt); }); describe('base', () => { beforeAll(() => { localCache.flushAll(); state = { start: clone(localCache.getStats()), value: randomString(100), value2: randomString(100), value3: randomString(100), key: randomString(10), obj: { a: 1, b: { x: 2, y: 3 }, }, otp: randomString(10), }; }); it('set key', () => { const res = localCache.set(state.key, state.value, 0); true.should.eql(res); (1).should.eql(localCache.getStats().keys - state.start.keys); }); it('get key', () => { const res = localCache.get(state.key); state.value.should.eql(res); }); it('get key names', () => { const res = localCache.keys(); [state.key].should.eql(res); }); it('has key', () => { const res = localCache.has(state.key); res.should.eql(true); }); it('does not have key', () => { const res = localCache.has('non existing key'); res.should.eql(false); }); it('delete an undefined key', () => { const count = localCache.del('xxx'); (0).should.eql(count); }); it('take key', () => { let res = localCache.has('otp'); res.should.eql(false); res = localCache.take('otp'); should.not.exist(res); res = localCache.set('otp', state.otp, 0); true.should.eql(res); res = localCache.has('otp'); res.should.eql(true); res = localCache.has('otp'); res.should.eql(true); const otp = localCache.take('otp'); otp.should.eql(state.otp); res = localCache.has('otp'); res.should.eql(false); res = localCache.set('otp', 'some other value'); true.should.eql(res); const otp2 = localCache.take('otp'); otp2.should.eql('some other value'); res = localCache.has('otp'); res.should.eql(false); }); it('take key with falsy values', () => { let res = localCache.has('otp'); res.should.eql(false); res = localCache.set('otp', 0); true.should.eql(res); const otp = localCache.take('otp'); otp.should.eql(0); res = localCache.has('otp'); res.should.eql(false); }); it('update key (and get it to check if the update worked)', () => { let res = localCache.set(state.key, state.value2, 0); true.should.eql(res); res = localCache.get(state.key); state.value2.should.eql(res); (1).should.eql(localCache.getStats().keys - state.start.keys); }); it('delete the defined key', () => { localCache.once('del', (key, val) => { state.key.should.eql(key); state.value2.should.eql(val); }); const count = localCache.del(state.key); (1).should.eql(count); (0).should.eql(localCache.getStats().keys - state.start.keys); }); it('delete multiple keys (after setting them)', () => { const keys = ['multiA', 'multiB', 'multiC']; keys.forEach((key) => { const res = localCache.set(key, state.value3); true.should.eql(res); }); keys.forEach((key) => { const res = localCache.get(key); state.value3.should.eql(res); }); let count = localCache.del(keys.slice(0, 2)); (2).should.eql(count); keys.slice(0, 2).forEach((key) => { const res = localCache.get(key); should(res).be.undefined(); }); let res = localCache.get(keys[2]); state.value3.should.eql(res); count = localCache.del(keys[2]); (1).should.eql(count); res = localCache.get(keys[2]); should(res).be.undefined(); count = localCache.del(keys); (0).should.eql(count); }); it('set a key to 0', () => { const res = localCache.set('zero', 0); true.should.eql(res); }); it('get previously set key', () => { const res = localCache.get('zero'); (0).should.eql(res); }); it('set a key to an object clone', () => { const res = localCache.set('clone', state.obj); true.should.eql(res); }); it('get cloned object', () => { const res = localCache.get('clone'); state.obj.should.not.equal(res); state.obj.should.eql(res); res.b.y = 42; const res2 = localCache.get('clone'); state.obj.should.eql(res2); }); it('test promise storage (fulfill before adding to cache)', () => new Promise((resolve) => { const deferred_value = 'Some deferred value'; const p = new Promise((fulfill) => fulfill(deferred_value)); p.then((value) => { deferred_value.should.eql(value); }); localCache.set('promise', p); const q = localCache.get('promise'); q.then(() => resolve()); })); it('test promise storage (fulfill after adding to cache)', () => new Promise((resolve) => { const deferred_value = 'Some deferred value'; let called = 0; const callStub = () => { called++; if (called === 2) resolve(); }; const p = new Promise((fulfill) => setTimeout(() => fulfill(deferred_value), 250), ); p.then((value) => { deferred_value.should.eql(value); callStub(); }); localCache.set('promise', p); const q = localCache.get('promise'); q.then((value) => { deferred_value.should.eql(value); callStub(); }); })); it('test es6 map', () => { const key = randomString(10); const map = new Map([ ['firstkey', 'firstvalue'], ['2ndkey', '2ndvalue'], ['thirdkey', 'thirdvalue'], ]); localCache.set(key, map); map.set('fourthkey', 'fourthvalue'); const cached_map = localCache.get(key); should(cached_map.get('2ndkey')).eql('2ndvalue'); should(cached_map.get('fourthkey')).be.undefined(); }); it('test `useClones = true` with an Object', () => { const key = randomString(10); const value = { a: 123, b: 456 }; const c = 789; localCache.set(key, value); value.a = c; value.should.not.be.eql(localCache.get(key)); }); it('test `useClones = false` with an Object', () => { const key = randomString(10); const value = { a: 123, b: 456 }; const c = 789; localCacheNoClone.set(key, value); value.a = c; should(value === localCacheNoClone.get(key)).be.true(); }); }); describe('max key amount', () => { beforeAll(() => { state = { key1: randomString(10), key2: randomString(10), key3: randomString(10), value1: randomString(10), value2: randomString(10), value3: randomString(10), }; }); it('exceed max key size', () => { const setKey = localCacheMaxKeys.set(state.key1, state.value1, 0); true.should.eql(setKey); const setKey2 = localCacheMaxKeys.set(state.key2, state.value2, 0); true.should.eql(setKey2); (() => localCacheMaxKeys.set(state.key3, state.value3, 0)).should.throw({ name: 'ECACHEFULL', message: 'Cache max keys amount exceeded', }); }); it('remove a key and set another one', () => { const del = localCacheMaxKeys.del(state.key1); (1).should.eql(del); const setKey3 = localCacheMaxKeys.set(state.key3, state.value3, 0); true.should.eql(setKey3); }); }); describe('correct and incorrect key types', () => { describe('number', () => { beforeAll(() => { state = { keys: [], val: randomString(20) }; for (let j = 1; j <= 10; j++) { state.keys.push(randomNumber(100000)); } }); it('set', () => { state.keys.forEach((key) => { const res = localCache.set(key, state.val); true.should.eql(res); }); }); it('get', () => { const res = localCache.get(state.keys[0]); state.val.should.eql(res); }); it('mget', () => { const res = localCache.mget(state.keys.slice(0, 2)); const prediction = {}; prediction[state.keys[0]] = state.val; prediction[state.keys[1]] = state.val; prediction.should.eql(res); }); it('del single', () => { const count = localCache.del(state.keys[0]); (1).should.eql(count); }); it('del multi', () => { const count = localCache.del(state.keys.slice(1, 3)); (2).should.eql(count); }); it('ttl', () => new Promise((resolve) => { const success = localCache.ttl(state.keys[3], 0.3); true.should.eql(success); let res = localCache.get(state.keys[3]); state.val.should.eql(res); setTimeout(() => { res = localCache.get(state.keys[3]); should.not.exist(res); resolve(); }, 400); })); it('getTtl', () => { const now = Date.now(); const success = localCache.ttl(state.keys[4], 0.5); true.should.eql(success); const ttl = localCache.getTtl(state.keys[4]); (ttl - now > 485 && ttl - now < 510).should.eql(true); }); afterAll(() => { localCache.flushAll(false); }); }); describe('string', () => { beforeAll(() => { state = { keys: [], val: randomString(20) }; for (let j = 1; j <= 10; j++) { state.keys.push(randomString(10)); } }); it('set', () => { state.keys.forEach((key) => { const res = localCache.set(key, state.val); true.should.eql(res); }); }); it('get', () => { const res = localCache.get(state.keys[0]); state.val.should.eql(res); }); it('mget', () => { const res = localCache.mget(state.keys.slice(0, 2)); const prediction = {}; prediction[state.keys[0]] = state.val; prediction[state.keys[1]] = state.val; prediction.should.eql(res); }); it('del single', () => { const count = localCache.del(state.keys[0]); (1).should.eql(count); }); it('del multi', () => { const count = localCache.del(state.keys.slice(1, 3)); (2).should.eql(count); }); it('ttl', () => new Promise((resolve) => { const success = localCache.ttl(state.keys[3], 0.3); true.should.eql(success); let res = localCache.get(state.keys[3]); state.val.should.eql(res); setTimeout(() => { res = localCache.get(state.keys[3]); should.not.exist(res); resolve(); }, 400); })); it('getTtl', () => { const now = Date.now(); const success = localCache.ttl(state.keys[4], 0.5); true.should.eql(success); const ttl = localCache.getTtl(state.keys[4]); (ttl - now > 485 && ttl - now < 510).should.eql(true); }); }); describe('boolean - invalid type', () => { beforeAll(() => { state = { keys: [true, false], val: randomString(20) }; }); it('set sync-style', () => { (() => localCache.set(state.keys[0], state.val)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `boolean`', }); }); it('get sync-style', () => { (() => localCache.get(state.keys[0])).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `boolean`', }); }); it('mget sync-style', () => { (() => localCache.mget(state.keys)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `boolean`', }); }); it('del single sync-style', () => { (() => localCache.del(state.keys[0])).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `boolean`', }); }); it('del multi sync-style', () => { (() => localCache.del(state.keys)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `boolean`', }); }); it('ttl sync-style', () => { (() => localCache.ttl(state.keys[0], 10)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `boolean`', }); }); it('getTtl sync-style', () => { (() => localCache.getTtl(state.keys[0])).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `boolean`', }); }); }); describe('object - invalid type', () => { beforeAll(() => { state = { keys: [{ a: 1 }, { b: 2 }], val: randomString(20) }; }); it('set sync-style', () => { (() => localCache.set(state.keys[0], state.val)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `object`', }); }); it('get sync-style', () => { (() => localCache.get(state.keys[0])).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `object`', }); }); it('mget sync-style', () => { (() => localCache.mget(state.keys)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `object`', }); }); it('del single sync-style', () => { (() => localCache.del(state.keys[0])).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `object`', }); }); it('del multi sync-style', () => { (() => localCache.del(state.keys)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `object`', }); }); it('ttl sync-style', () => { (() => localCache.ttl(state.keys[0], 10)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `object`', }); }); it('getTtl sync-style', () => { (() => localCache.getTtl(state.keys[0])).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `object`', }); }); }); }); describe('flush', () => { beforeAll(() => { state = { n: 0, count: 100, startKeys: localCache.getStats().keys, keys: [], val: randomString(20), }; }); it('set keys', () => { for (let j = 1; j <= state.count; j++) { const key = randomString(7); state.keys.push(key); } state.keys.forEach((key) => { localCache.set(key); state.n++; }); state.count.should.eql(state.n); (state.startKeys + state.count).should.eql(localCache.getStats().keys); }); it('flush keys', () => { localCache.flushAll(false); (0).should.eql(localCache.getStats().keys); (0).should.eql(localCache.data.size); }); }); describe('flushStats', () => { let cache = null; beforeAll(() => { cache = new nodeCache(); }); it('set cache and flush stats value', () => { const key = randomString(10); const value = randomString(10); const res = cache.set(key, value); true.should.eql(res); (1).should.eql(cache.getStats().keys); cache.flushStats(); (0).should.eql(cache.getStats().keys); cache.get(key); (1).should.eql(cache.getStats().hits); cache.get(randomString(10)); (1).should.eql(cache.getStats().misses); }); }); describe('delete', () => { beforeAll(() => { state.n = 0; }); beforeAll(() => { state = { n: 0, count: 100000, keys: [], val: randomString(20), }; for (let j = 1; j <= state.count; j++) { const key = randomString(7); state.keys.push(key); localCache.set(key, state.val); } }); it( 'delete all previously set keys', () => { for (let i = 0; i < state.count; i++) { (1).should.eql(localCache.del(state.keys[i])); state.n++; } state.n.should.eql(state.count); localCache.getStats().keys.should.eql(0); }, 0, ); it( 'delete keys again; should not delete anything', () => { for (let i = 0; i < state.count; i++) { (0).should.eql(localCache.del(state.keys[i])); state.n++; } state.n.should.eql(state.count * 2); localCache.getStats().keys.should.eql(0); }, 0, ); }); describe('stats', () => { beforeAll(() => { state = { n: 0, start: clone(localCache.getStats()), count: 5, keylength: 7, valuelength: 50, keys: [], values: [], }; for (let j = 1; j <= state.count * 2; j++) { const key = randomString(state.keylength); const value = randomString(state.valuelength); state.keys.push(key); state.values.push(value); true.should.eql(localCache.set(key, value, 0)); state.n++; } }); it('get and remove `count` elements', () => { for (let i = 1; i <= state.count; i++) { state.values[i].should.eql(localCache.get(state.keys[i])); state.n++; } for (let i = 1; i <= state.count; i++) { (1).should.eql(localCache.del(state.keys[i])); state.n++; } const after = localCache.getStats(); const diff = diffKeys(after, state.start); diff.hits.should.eql(5); diff.keys.should.eql(5); diff.ksize.should.eql(state.count * state.keylength); diff.vsize.should.eql(state.count * state.valuelength); }); it('generate `count` misses', () => { for (let i = 1; i <= state.count; i++) { should(localCache.get('xxxx')).be.undefined(); state.n++; } const after = localCache.getStats(); const diff = diffKeys(after, state.start); diff.misses.should.eql(5); }); it('check successful runs', () => { state.n.should.eql(5 * state.count); }); }); describe('multi', () => { beforeAll(() => { state = { n: 0, count: 100, startKeys: localCache.getStats().keys, value: randomString(20), keys: [], }; for (let j = 1; j <= state.count; j++) { const key = randomString(7); state.keys.push(key); } state.keys.forEach((key) => { localCache.set(key, state.value, 0); state.n++; }); }); it('generate a sub-list of keys', () => { state.getKeys = state.keys.splice(50, 5); }); it('generate prediction', () => { state.prediction = {}; state.getKeys.forEach((key) => { state.prediction[key] = state.value; }); }); it('try to mget with a single key', () => { (() => localCache.mget(state.getKeys[0])).should.throw({ name: 'EKEYSTYPE', message: 'The keys argument has to be an array.', }); state.n++; }); it('mget the sub-list', () => { state.prediction.should.eql(localCache.mget(state.getKeys)); state.n++; }); it('delete keys in the sub-list', () => { state.getKeys.length.should.eql(localCache.del(state.getKeys)); state.n++; }); it('try to mget the sub-list again', () => { ({}).should.eql(localCache.mget(state.getKeys)); state.n++; }); it('check successful runs', () => { state.n.should.eql(state.count + 4); }); }); describe('ttl', () => { beforeAll(() => { state = { n: 0, val: randomString(20), key1: `k1_${randomString(20)}`, key2: `k2_${randomString(20)}`, key3: `k3_${randomString(20)}`, key4: `k4_${randomString(20)}`, key5: `k5_${randomString(20)}`, key6: `k6_${randomString(20)}`, now: Date.now(), }; state.keys = [state.key1, state.key2, state.key3, state.key4, state.key5]; }); describe('has validates expired ttl', () => { it('set a key with ttl', () => { true.should.eql(localCacheTTL.set(state.key6, state.val, 0.7)); }); it('check this key immediately', () => { true.should.eql(localCacheTTL.has(state.key6)); }); it('before it times out', () => new Promise((resolve) => { setTimeout(() => { state.n++; const res = localCacheTTL.has(state.key6); res.should.eql(true); state.val.should.eql(localCacheTTL.get(state.key6)); resolve(); }, 20); })); it('and after it timed out', () => new Promise((resolve) => { setTimeout(() => { const res = localCacheTTL.has(state.key6); res.should.eql(false); state.n++; should(localCacheTTL.get(state.key6)).be.undefined(); resolve(); }, 800); })); }); it('set a key with ttl', () => { const res = localCache.set(state.key1, state.val, 0.7); true.should.eql(res); const ts = localCache.getTtl(state.key1); if (state.now < ts && ts < state.now + 300) { throw new Error('Invalid timestamp'); } }); it('check this key immediately', () => { state.val.should.eql(localCache.get(state.key1)); }); it('before it times out', () => new Promise((resolve) => { setTimeout(() => { state.n++; const res = localCache.has(state.key1); res.should.eql(true); state.val.should.eql(localCache.get(state.key1)); resolve(); }, 20); })); it('and after it timed out', () => new Promise((resolve) => { setTimeout(() => { const res = localCache.has(state.key1); res.should.eql(false); const ts = localCache.getTtl(state.key1); should.not.exist(ts); state.n++; should(localCache.get(state.key1)).be.undefined(); resolve(); }, 700); })); it('set another key with ttl', () => { const res = localCache.set(state.key2, state.val, 0.5); true.should.eql(res); }); it('check this key immediately', () => { const res = localCache.get(state.key2); state.val.should.eql(res); }); it('before it times out', () => new Promise((resolve) => { setTimeout(() => { state.n++; state.val.should.eql(localCache.get(state.key2)); resolve(); }, 20); })); it('and after it timed out, too', () => new Promise((resolve) => { setTimeout(() => { const ts = localCache.getTtl(state.key2); should.not.exist(ts); state.n++; should(localCache.get(state.key2)).be.undefined(); resolve(); }, 500); })); describe('test the automatic check', () => { let innerState = null; beforeAll( () => new Promise((resolve) => { setTimeout(() => { innerState = { startKeys: localCache.getStats().keys, key: 'autotest', val: randomString(20), }; resolve(); }, 1000); }), ); it('set a key with ttl', () => { localCache.once('set', (key) => { innerState.key.should.eql(key); }); true.should.eql(localCache.set(innerState.key, innerState.val, 0.5)); (innerState.startKeys + 1).should.eql(localCache.getStats().keys); (0).should.eql(localCache.listeners('set').length); }); it("and check it's existence", () => { innerState.val.should.eql(localCache.get(innerState.key)); }); it("wait for 'expired' event", () => new Promise((resolve) => { localCache.once('expired', (key, val) => { innerState.key.should.eql(key); (indexOf.call(state.keys, key) < 0).should.eql(true); should(localCache.data.get(key)).be.undefined(); resolve(); }); setTimeout(() => { localCache._checkData(false); }, 550); })); }); describe('more ttl tests', () => { it('set a third key with ttl', () => { true.should.eql(localCache.set(state.key3, state.val, 100)); }); it('check it immediately', () => { state.val.should.eql(localCache.get(state.key3)); }); it('set ttl to the invalid key', () => { false.should.eql(localCache.ttl(`${state.key3}false`, 0.3)); }); it('set ttl to the correct key', () => { true.should.eql(localCache.ttl(state.key3, 0.3)); }); it('check if the key still exists', () => { const res = localCache.get(state.key3); state.val.should.eql(res); }); it('wait until ttl has ended and check if the key was deleted', () => new Promise((resolve) => { setTimeout(() => { const res = localCache.get(state.key3); should(res).be.undefined(); should(localCache.data.get(state.key3)).be.undefined(); resolve(); }, 500); })); it('set a key with ttl = 100s (default: infinite), reset its ttl to default and check if it still exists', () => { const res = localCache.set(state.key4, state.val, 100); true.should.eql(res); state.val.should.eql(localCache.get(state.key4)); false.should.eql(localCache.ttl(`${state.key4}false`)); true.should.eql(localCache.ttl(state.key4)); const res2 = localCache.get(state.key4); state.val.should.eql(res2); }); it('set a key with ttl = 100s (default: 0.3s), reset its ttl to default, check if it still exists, and wait for its timeout', () => new Promise((resolve) => { true.should.eql(localCacheTTL.set(state.key5, state.val, 100)); state.val.should.eql(localCacheTTL.get(state.key5)); false.should.eql(localCacheTTL.ttl(`${state.key5}false`)); true.should.eql(localCacheTTL.ttl(state.key5)); state.val.should.eql(localCacheTTL.get(state.key5)); setTimeout(() => { const res = localCacheTTL.get(state.key5); should.not.exist(res); localCacheTTL._checkData(false); should(localCacheTTL.data.get(state.key5)).be.undefined(); resolve(); }, 350); })); it('set a key with a cache initialized with no automatic delete on expire', () => new Promise((resolve) => { localCacheNoDelete.set(state.key1, state.val); setTimeout(() => { const res = localCacheNoDelete.get(state.key1); should(res).eql(state.val); resolve(); }, 500); })); it( 'test issue #78 with expire event not fired', () => new Promise((resolve) => { const localCacheTTL2 = new nodeCache({ stdTTL: 1, checkperiod: 0.5, }); let expCount = 0; const expkeys = ['ext78_test:a', 'ext78_test:b']; localCacheTTL2.set(expkeys[0], expkeys[0], 2); localCacheTTL2.set(expkeys[1], expkeys[1], 3); localCacheTTL2.on('expired', (key, value) => { key.should.eql(expkeys[expCount]); value.should.eql(expkeys[expCount]); expCount++; }); setTimeout(() => { expCount.should.eql(2); localCacheTTL2.close(); resolve(); }, 5000); }), 6000, ); }); }); describe('clone', () => { it('a function', () => new Promise((resolve) => { const key = randomString(10); const value = () => { resolve(); }; localCache.set(key, value); const fn = localCache.get(key); fn(); })); it('a regex', () => { const key = randomString(10); const regex = /\b\w{4}\b/g; const match = 'king'; const noMatch = 'bla'; true.should.eql(regex.test(match)); false.should.eql(regex.test(noMatch)); localCache.set(key, regex); const cachedRegex = localCache.get(key); true.should.eql(cachedRegex.test(match)); false.should.eql(cachedRegex.test(noMatch)); }); }); describe('mset', () => { beforeAll(() => { state = { keyValueSet: [ { key: randomString(10), val: randomString(10) }, { key: randomString(10), val: randomString(10) }, ], }; }); it('mset an array of key value pairs', () => { const res = localCacheMset.mset(state.keyValueSet); true.should.eql(res); (2).should.eql(localCacheMset.getStats().keys); }); it('mset - integer key', () => { localCacheMset.flushAll(); state.keyValueSet[0].key = randomNumber(10); const res = localCacheMset.mset(state.keyValueSet); true.should.eql(res); (2).should.eql(localCacheMset.getStats().keys); }); it('mset - boolean key throw error', () => { localCacheMset.flushAll(); state.keyValueSet[0].key = true; (() => localCacheMset.mset(state.keyValueSet)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `boolean`', }); }); it('mset - object key throw error', () => { localCacheMset.flushAll(); state.keyValueSet[0].key = { a: 1 }; (() => localCacheMset.mset(state.keyValueSet)).should.throw({ name: 'EKEYTYPE', message: 'The key argument has to be of type `string` or `number`. Found: `object`', }); }); it('mset - ttl type error check', () => { localCacheMset.flushAll(); state.keyValueSet[0].ttl = { a: 1 }; (() => localCacheMset.mset(state.keyValueSet)).should.throw({ name: 'ETTLTYPE', message: 'The ttl argument has to be a number.', }); }); }); describe('fetch', () => { beforeEach(() => { localCache.flushAll(); state = { func: () => 'foo' }; }); describe('when value is type of Function', () => { it('execute it and fetch returned value', () => { 'foo'.should.eql(localCache.fetch('key', 100, state.func)); }); }); describe('when value is not a function', () => { it('return the value itself', () => { 'bar'.should.eql(localCache.fetch('key', 100, 'bar')); }); }); describe('cache hit', () => { it('return cached value', () => { localCache.set('key', 'bar', 100); 'bar'.should.eql(localCache.fetch('key', 100, state.func)); }); }); describe('cache miss', () => { it('write given value to cache and return it', () => { 'foo'.should.eql(localCache.fetch('key', 100, state.func)); 'foo'.should.eql(localCache.get('key')); }); }); describe('when ttl is omitted', () => { it('swap ttl and value', () => { 'foo'.should.eql(localCache.fetch('key', state.func)); }); }); }); describe('Issues', () => { describe('#151 - cannot set null', () => { let cache = null; beforeAll(() => { cache = new nodeCache(); }); it('set the value `null` - this should not throw or otherwise fail', () => { cache.set('test', null); }); it('should also return `null`', () => { should(cache.get('test')).be.null(); }); }); describe('#197 - ReferenceError: Buffer is not defined', () => { let cache = null; const globalBuffer = global.Buffer; beforeAll(() => { global.Buffer = undefined; cache = new nodeCache(); }); it('should not throw when setting a key of type `object` when `Buffer` is not available', () => { should(Buffer).be.undefined(); cache.set('foo', {}); }); afterAll(() => { global.Buffer = globalBuffer; should(Buffer).eql(globalBuffer); }); }); describe('#263 - forceString never works', () => { let cache = null; beforeAll(() => { cache = new nodeCache({ forceString: true }); }); it('set the value `null` - this should transform into a string', () => { cache.set('test', null); should(cache.get('test')).eql('null'); }); it("set the value `{ hello: 'World' }` - this should transform into a string", () => { cache.set('test', { hello: 'World' }); should(cache.get('test')).eql('{"hello":"World"}'); }); }); }); describe('regression', () => { it('get("toString") on a fresh cache returns undefined', () => { const cache = new nodeCache(); should(cache.get('toString')).be.undefined(); should(cache.get('hasOwnProperty')).be.undefined(); should(cache.get('__proto__')).be.undefined(); }); it('mget emits "miss" with the single missing key (not the full keys array)', () => { const cache = new nodeCache(); cache.set('present', 1); const missed: unknown[] = []; cache.on('miss', (k) => missed.push(k)); cache.mget(['present', 'absent-a', 'absent-b']); missed.should.eql(['absent-a', 'absent-b']); }); it('ttl(key, 0) deletes the key', () => { const cache = new nodeCache({ stdTTL: 0 }); cache.set('k', 'v', 100); cache.has('k').should.be.true(); cache.ttl('k', 0).should.be.true(); cache.has('k').should.be.false(); }); it('take on a missing key does not emit "take"', () => { const cache = new nodeCache(); let emitted = false; cache.on('take', () => { emitted = true; }); const res = cache.take('missing'); should(res).be.undefined(); emitted.should.be.false(); }); it('mset off-by-one: maxKeys=N permits exactly N keys', () => { const cache = new nodeCache({ maxKeys: 3 }); cache.mset([ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 }, ]); cache.getStats().keys.should.eql(3); }); it('set(key, undefined) round-trips as undefined', () => { const cache = new nodeCache(); cache.set('u', undefined); cache.has('u').should.be.true(); should(cache.get('u')).be.undefined(); }); it('set/ttl reject NaN ttl with ETTLTYPE', () => { const cache = new nodeCache(); (() => cache.set('k', 'v', Number.NaN)).should.throw({ name: 'ETTLTYPE', }); }); it('fetchAsync deduplicates concurrent misses', async () => { const cache = new nodeCache(); let calls = 0; const fn = async () => { calls++; await new Promise((r) => setTimeout(r, 20)); return 'hello'; }; const [a, b, c] = await Promise.all([ cache.fetchAsync('shared', 100, fn), cache.fetchAsync('shared', 100, fn), cache.fetchAsync('shared', 100, fn), ]); a.should.eql('hello'); b.should.eql('hello'); c.should.eql('hello'); calls.should.eql(1); cache.get('shared').should.eql('hello'); }); }); });