diff --git a/server/managers/ApiCacheManager b/server/managers/ApiCacheManager index 882b9b61..b311af53 100644 --- a/server/managers/ApiCacheManager +++ b/server/managers/ApiCacheManager @@ -3,12 +3,16 @@ const Logger = require('../Logger') const Database = require('../Database') class ApiCacheManager { - constructor(options = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => item.length }) { - this.options = options + + defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => item.length } + defaultTtlOptions = { ttl: 30 * 60 * 1000 } + + constructor(cache = new LRUCache(this.defaultCacheOptions), ttlOptions = this.defaultTtlOptions) { + this.cache = cache + this.ttlOptions = ttlOptions } init(database = Database) { - this.cache = new LRUCache(this.options) let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy'] hooks.forEach(hook => database.sequelize.addHook(hook, (model) => this.clear(model, hook))) } @@ -23,23 +27,22 @@ class ApiCacheManager { const key = { user: req.user.username, url: req.url } const stringifiedKey = JSON.stringify(key) Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`) - Logger.debug(`[ApiCacheManager] Cache key: ${stringifiedKey}`) const cached = this.cache.get(stringifiedKey) if (cached) { Logger.debug(`[ApiCacheManager] Cache hit: ${stringifiedKey}`) res.send(cached) return } - res.sendResponse = res.send + res.originalSend = res.send res.send = (body) => { Logger.debug(`[ApiCacheManager] Cache miss: ${stringifiedKey}`) if (key.url.search(/^\/libraries\/.*?\/personalized/) !== -1) { - Logger.debug(`[ApiCacheManager] Caching personalized with 30 minues TTL`) - this.cache.set(stringifiedKey, body, { ttl: 30 * 60 * 1000 }) + Logger.debug(`[ApiCacheManager] Caching with ${this.ttlOptions.ttl} ms TTL`) + this.cache.set(stringifiedKey, body, this.ttlOptions) } else { this.cache.set(stringifiedKey, body) } - res.sendResponse(body) + res.originalSend(body) } next() } diff --git a/test/server/managers/ApiCacheManager.test.js b/test/server/managers/ApiCacheManager.test.js new file mode 100644 index 00000000..2cfc0dfc --- /dev/null +++ b/test/server/managers/ApiCacheManager.test.js @@ -0,0 +1,85 @@ +// Import dependencies and modules for testing +const { expect } = require('chai') +const sinon = require('sinon') +const ApiCacheManager = require('../../../server/managers/ApiCacheManager') + +describe('ApiCacheManager', () => { + let cache + let req + let res + let next + let manager + + beforeEach(() => { + cache = { get: sinon.stub(), set: sinon.spy() } + req = { user: { username: 'testUser' }, url: '/test-url' } + res = { send: sinon.spy() } + next = sinon.spy() + }) + + describe('middleware', () => { + it('should send cached data if available', () => { + // Arrange + const cachedData = { data: 'cached data' } + cache.get.returns(cachedData) + const key = JSON.stringify({ user: req.user.username, url: req.url }) + manager = new ApiCacheManager(cache) + + // Act + manager.middleware(req, res, next) + + // Assert + expect(cache.get.calledOnce).to.be.true + expect(cache.get.calledWith(key)).to.be.true + expect(res.send.calledOnce).to.be.true + expect(res.send.calledWith(cachedData)).to.be.true + expect(res.originalSend).to.be.undefined + expect(next.called).to.be.false + expect(cache.set.called).to.be.false + }) + + it('should cache and send response if data is not cached', () => { + // Arrange + cache.get.returns(null) + const responseData = { data: 'response data' } + const key = JSON.stringify({ user: req.user.username, url: req.url }) + manager = new ApiCacheManager(cache) + + // Act + manager.middleware(req, res, next) + res.send(responseData) + + // Assert + expect(cache.get.calledOnce).to.be.true + expect(cache.get.calledWith(key)).to.be.true + expect(next.calledOnce).to.be.true + expect(cache.set.calledOnce).to.be.true + expect(cache.set.calledWith(key, responseData)).to.be.true + expect(res.originalSend.calledOnce).to.be.true + expect(res.originalSend.calledWith(responseData)).to.be.true + }) + + it('should cache personalized response with 30 minutes TTL', () => { + // Arrange + cache.get.returns(null) + const responseData = { data: 'personalized data' } + req.url = '/libraries/id/personalized' + const key = JSON.stringify({ user: req.user.username, url: req.url }) + const ttlOptions = { ttl: 30 * 60 * 1000 } + manager = new ApiCacheManager(cache, ttlOptions) + + // Act + manager.middleware(req, res, next) + res.send(responseData) + + // Assert + expect(cache.get.calledOnce).to.be.true + expect(cache.get.calledWith(key)).to.be.true + expect(next.calledOnce).to.be.true + expect(cache.set.calledOnce).to.be.true + expect(cache.set.calledWith(key, responseData, ttlOptions)).to.be.true + expect(res.originalSend.calledOnce).to.be.true + expect(res.originalSend.calledWith(responseData)).to.be.true + }) + }) +}) \ No newline at end of file