@lepauloricardo/sequelize-simple-cache
Version:
A simple, transparent, client-side, in-memory cache for Sequelize (Fork de funny-bytes/sequelize-simple-cache)
587 lines (544 loc) • 22.5 kB
JavaScript
const sinon = require('sinon');
const { Op, fn } = require('sequelize');
const crypto = require('crypto');
const SequelizeSimpleCache = require('../..');
require('../test-helper');
describe('SequelizeSimpleCache', () => {
let stubConsoleDebug;
let stubDateNow;
let nowOffset = 0;
beforeEach(() => {
stubConsoleDebug = sinon.stub(console, 'debug');
nowOffset = 0; // reset
const now = Date.now();
stubDateNow = sinon.stub(Date, 'now').callsFake(() => now + nowOffset);
});
afterEach(() => {
stubConsoleDebug.restore(); // eslint-disable-line no-console
stubDateNow.restore();
});
it('should create cache without crashing / no args', () => {
expect(() => new SequelizeSimpleCache()).to.not.throw();
});
it('should create cache without crashing / empty args / 1', () => {
expect(() => new SequelizeSimpleCache({}, {})).to.not.throw();
});
it('should create cache without crashing / empty args / 2', () => {
expect(() => new SequelizeSimpleCache({}, { ops: false })).to.not.throw();
});
it('should create cache without crashing / dummy model', () => {
expect(() => new SequelizeSimpleCache({ User: {} }, { ops: false })).to.not.throw();
});
it('should generate unique hashes for Sequelize queries with ES6 symbols and functions', () => {
const queries = [{
where: {
config: '07d54b5c-78d0-4315-9ffc-581a4afa6f6d',
startDate: { [Op.lte]: fn('NOW') },
},
order: [['majorVersion', 'DESC'], ['minorVersion', 'DESC'], ['patchVersion', 'DESC']],
}, {
where: {
config: '07d54b5c-78d0-4315-9ffc-581a4afa6f6d',
startDate: { [Op.lte]: fn('NOW-XXX') },
},
order: [['majorVersion', 'DESC'], ['minorVersion', 'DESC'], ['patchVersion', 'DESC']],
}, {
where: {
config: '07d54b5c-78d0-4315-9ffc-581a4afa6f6d',
startDate: {},
},
order: [['majorVersion', 'DESC'], ['minorVersion', 'DESC'], ['patchVersion', 'DESC']],
}];
const hashes = new Set();
const hashes2 = new Set();
queries.forEach((q) => hashes.add(crypto.createHash('md5').update(SequelizeSimpleCache.key(q)).digest('hex')));
queries.forEach((q) => hashes2.add(crypto.createHash('md5').update(SequelizeSimpleCache.key(q)).digest('hex')));
const union = new Set([...hashes, ...hashes2]);
expect(hashes.size).to.be.equal(queries.length);
expect(hashes2.size).to.be.equal(queries.length);
expect(union.size).to.be.equal(queries.length);
});
it('should create decorations on model / cached', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
expect(User.noCache).to.be.a('function');
expect(User.clearCache).to.be.a('function');
expect(User.clearCacheAll).to.be.a('function');
});
it('should create decorations on model / not cached', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ }, { ops: false });
const User = cache.init(model);
expect(User.noCache).to.be.a('function');
expect(User.clearCache).to.be.a('function');
expect(User.clearCacheAll).to.be.a('function');
});
it('should cache result and call database only once', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
const result2 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledOnce).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
});
it('should cache result and clear cache completely (via cache)', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
cache.clear();
const result2 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
});
it('should cache result and clear cache by model (via cache)', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const model2 = {
name: 'Page',
findOne: sinon.stub().resolves({ foo: true }),
};
const cache = new SequelizeSimpleCache({ User: {}, Page: {} }, { ops: false });
const User = cache.init(model);
const Page = cache.init(model2);
const result1 = await User.findOne({ where: { username: 'fred' } });
const result2 = await Page.findOne({ where: { foo: true } });
expect(cache.size()).to.be.equal(2);
cache.clear('User');
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ foo: true });
expect(cache.size()).to.be.equal(1);
});
it('should cache result and clear cache by model (via cache) / unknown model name', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const model2 = {
name: 'Page',
findOne: sinon.stub().resolves({ foo: true }),
};
const cache = new SequelizeSimpleCache({ User: {}, Page: {} }, { ops: false });
const User = cache.init(model);
const Page = cache.init(model2);
const result1 = await User.findOne({ where: { username: 'fred' } });
const result2 = await Page.findOne({ where: { foo: true } });
expect(cache.size()).to.be.equal(2);
cache.clear('Foo');
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ foo: true });
expect(cache.size()).to.be.equal(2);
});
it('should cache result and clear cache by model (via model)', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
User.clearCache();
const result2 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
});
it('should cache result and clear cache completely (via model)', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
User.clearCacheAll();
const result2 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
});
it('should cache but expire after ttl', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: { ttl: 1 } }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
const result2 = await User.findOne({ where: { username: 'fred' } });
nowOffset = 1200;
const result3 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
expect(result3).to.be.deep.equal({ username: 'fred' });
});
it('should cache forever', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: { ttl: false } }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
nowOffset = 999999999;
const result2 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledOnce).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
});
it('should not cache a value of `null`', async () => {
const stub = sinon.stub().resolves(null);
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
const result2 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.equal(null);
expect(result2).to.be.equal(null);
});
it('should not cache a value of `undefined`', async () => {
const stub = sinon.stub().resolves(undefined);
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
const result2 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.equal(undefined);
expect(result2).to.be.equal(undefined);
});
it('should cache if an additional method was configured', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findFoo: stub,
};
const cache = new SequelizeSimpleCache({ User: { methods: ['findFoo'] } }, { ops: false });
const User = cache.init(model);
const result1 = await User.findFoo({ where: { username: 'fred' } });
const result2 = await User.findFoo({ where: { username: 'fred' } });
expect(stub.calledOnce).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
});
it('should not cache if a method was de-configured', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
findFoo: async () => {},
};
const cache = new SequelizeSimpleCache({ User: { methods: ['findFoo'] } }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
const result2 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
});
it('should pass on error if db call is rejected', async () => {
const stub = sinon.stub().rejects(new Error('foo'));
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
try {
await User.findOne({ where: { username: 'fred' } });
throw (new Error('bar'));
} catch (err) {
expect(stub.calledOnce).to.be.true;
expect(err).to.have.property('message').to.be.equal('foo');
}
});
it('should bypass cache if model is not configured to be cached', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ Foo: {} }, { ops: false });
const User = cache.init(model); // TODO: should this issue a warning?
const result1 = await User.findOne({ where: { username: 'fred' } });
const result2 = await User.findOne({ where: { username: 'fred' } });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
});
it('should bypass cache if query with transaction', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' }, transaction: true });
const result2 = await User.findOne({ where: { username: 'fred' }, transaction: true });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
});
it('should bypass cache on model (via model decoration)', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const result1 = await User.findOne({ where: { username: 'fred' } });
const result2 = await User.findOne({ where: { username: 'fred' } });
const result3 = await User.noCache().findOne({ where: { username: 'fred' } });
expect(stub.calledTwice).to.be.true;
expect(result1).to.be.deep.equal({ username: 'fred' });
expect(result2).to.be.deep.equal({ username: 'fred' });
expect(result3).to.be.deep.equal({ username: 'fred' });
});
it('should bypass unknown function / cached', async () => {
const stub = sinon.stub().resolves({ foo: true });
const model = {
name: 'User',
foo: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const result = await User.foo();
expect(stub.calledOnce).to.be.true;
expect(result).to.be.deep.equal({ foo: true });
});
it('should bypass unknown function / not cached', async () => {
const stub = sinon.stub().resolves({ foo: true });
const model = {
name: 'User',
foo: stub,
};
const cache = new SequelizeSimpleCache({ }, { ops: false });
const User = cache.init(model);
const result = await User.foo();
expect(stub.calledOnce).to.be.true;
expect(result).to.be.deep.equal({ foo: true });
});
it('should print debug output if debug=true', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { debug: true, ops: false });
cache.init(model);
expect(stubConsoleDebug.called).to.be.true;
});
it('should not print debug output if debug=false', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
cache.init(model);
expect(stubConsoleDebug.called).to.be.false;
});
it('should print ops output if ops>0', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { debug: false, ops: 1 });
cache.init(model);
await new Promise((resolve) => {
setTimeout(() => resolve(), 1200);
});
clearInterval(cache.heart);
expect(stubConsoleDebug.called).to.be.true;
});
it('should not print ops output if ops=false', async () => {
const stub = sinon.stub().resolves({ username: 'fred' });
const model = {
name: 'User',
findOne: stub,
};
const cache = new SequelizeSimpleCache({ User: {} }, { debug: false, ops: false });
cache.init(model);
expect(stubConsoleDebug.called).to.be.false;
});
it('should work to stub model using Sinon in unit tests / cached / restore pattern 1', async () => {
const model = {
name: 'User',
findOne: async () => ({ username: 'fred' }),
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
const stub = sinon.stub(User, 'findOne').resolves({ username: 'foo' });
const result1 = await User.findOne({ where: { username: 'foo' } });
const result2 = await User.findOne({ where: { username: 'foo' } });
expect(result1).to.be.deep.equal({ username: 'foo' });
expect(result2).to.be.deep.equal({ username: 'foo' });
expect(stub.calledOnce).to.be.true;
stub.restore();
});
it('should work to stub model using Sinon in unit tests / not cached / restore pattern 1', async () => {
const model = {
name: 'User',
findOne: async () => ({ username: 'fred' }),
};
const cache = new SequelizeSimpleCache({ }, { ops: false });
const User = cache.init(model);
const stub = sinon.stub(User, 'findOne').resolves({ username: 'foo' });
const result1 = await User.findOne({ where: { username: 'foo' } });
const result2 = await User.findOne({ where: { username: 'foo' } });
expect(result1).to.be.deep.equal({ username: 'foo' });
expect(result2).to.be.deep.equal({ username: 'foo' });
expect(stub.calledTwice).to.be.true;
stub.restore();
});
it('should work to stub model using Sinon in unit tests / cached / restore pattern 2', async () => {
const model = {
name: 'User',
findOne: async () => ({ username: 'fred' }),
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
sinon.stub(User, 'findOne').resolves({ username: 'foo' });
const result1 = await User.findOne({ where: { username: 'foo' } });
const result2 = await User.findOne({ where: { username: 'foo' } });
expect(result1).to.be.deep.equal({ username: 'foo' });
expect(result2).to.be.deep.equal({ username: 'foo' });
expect(User.findOne.calledOnce).to.be.true;
User.findOne.restore();
});
it('should work to stub model using Sinon in unit tests / not cached / restore pattern 2', async () => {
const model = {
name: 'User',
findOne: async () => ({ username: 'fred' }),
};
const cache = new SequelizeSimpleCache({ }, { ops: false });
const User = cache.init(model);
sinon.stub(User, 'findOne').resolves({ username: 'foo' });
const result1 = await User.findOne({ where: { username: 'foo' } });
const result2 = await User.findOne({ where: { username: 'foo' } });
expect(result1).to.be.deep.equal({ username: 'foo' });
expect(result2).to.be.deep.equal({ username: 'foo' });
expect(User.findOne.calledTwice).to.be.true;
User.findOne.restore();
});
it('should throw error if model is wrongly mocked', async () => {
const model = {
name: 'User',
findOne: async () => ({ username: 'fred' }),
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
sinon.stub(User, 'findOne').returns({ username: 'foo' }); // should be `resolves`
User.findOne({ where: { username: 'foo' } })
.should.be.rejectedWith(Error, 'User.findOne() did not return a promise but should');
});
it('should ensure limit is not exceeded', async () => {
const model = {
name: 'User',
findOne: async () => ({ username: 'fred' }),
};
const cache = new SequelizeSimpleCache({ User: { limit: 3 } }, { ops: false });
const User = cache.init(model);
await User.findOne({ where: { username: 'john' } });
await User.findOne({ where: { username: 'jim' } });
await User.findOne({ where: { username: 'bob' } });
expect(cache.size()).to.be.equal(3);
await User.findOne({ where: { username: 'ron' } });
expect(cache.size()).to.be.equal(3);
});
it('should automatically clear cache on create (default)', async () => {
const stub = sinon.stub().resolves([{ username: 'fred' }]);
const model = {
name: 'User',
findAll: stub,
create: () => {},
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
await User.findAll();
User.create({ username: 'jim-bob' });
await User.findAll();
expect(stub.calledTwice).to.be.true;
});
it('should automatically clear cache on update (default)', async () => {
const stub = sinon.stub().resolves([{ username: 'fred' }]);
const model = {
name: 'User',
findAll: stub,
update: () => {},
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
await User.findAll();
User.update();
await User.findAll();
expect(stub.calledTwice).to.be.true;
});
it('should automatically clear cache on bulkCreate (default)', async () => {
const stub = sinon.stub().resolves([{ username: 'fred' }]);
const model = {
name: 'User',
findAll: stub,
bulkCreate: () => {},
};
const cache = new SequelizeSimpleCache({ User: {} }, { ops: false });
const User = cache.init(model);
await User.findAll();
User.bulkCreate();
await User.findAll();
expect(stub.calledTwice).to.be.true;
});
it('should not automatically clear cache on update if clearOnUpdate=false', async () => {
const stub = sinon.stub().resolves([{ username: 'fred' }]);
const model = {
name: 'User',
findAll: stub,
update: () => {},
};
const cache = new SequelizeSimpleCache({ User: { clearOnUpdate: false } }, { ops: false });
const User = cache.init(model);
await User.findAll();
User.update();
await User.findAll();
expect(stub.calledOnce).to.be.true;
});
});