From 2b513a868563ca3a332969c83837233c9ad60278 Mon Sep 17 00:00:00 2001 From: antoinechalifour Date: Sat, 31 Aug 2019 14:09:08 +0200 Subject: [PATCH] feat: add support for disabling caching in configuration --- README.md | 41 +++- package.json | 1 + src/cli.ts | 14 ++ src/configuration.ts | 10 + src/domain/entity/DisableCachePattern.ts | 4 + src/domain/entity/index.ts | 1 + src/domain/usecase/RespondToRequest.test.ts | 225 ++++++++++++++++++++ src/domain/usecase/RespondToRequest.ts | 22 +- src/index.ts | 1 + tests/integration/utils.ts | 1 + 10 files changed, 313 insertions(+), 7 deletions(-) create mode 100644 src/domain/entity/DisableCachePattern.ts diff --git a/README.md b/README.md index 7cb1d31..7b1d005 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,41 @@ _Note: `npx` is a command that comes with `npm` when installing Node and enables The following options are supported: -| Option | Description | Example | Default value | -| ------------------- | ------------------------------------------------------------- | --------------------- | -------------- | -| targetUrl | The API base URL | http://localhost:4000 | None | -| port | The port used to launch Memento | 9876 | 3344 | -| cacheDirectory | The cache directory used for storing responses | memento-integration | .memento-cache | -| useRealResponseTime | Whether Memento should respond using the actual response time | true | false | +| Option | Description | Example | Default value | +| ---------------------- | ------------------------------------------------------------- | ------------------------------------------------------ | -------------- | +| targetUrl | The API base URL | http://localhost:4000 | None | +| port | The port used to launch Memento | 9876 | 3344 | +| cacheDirectory | The cache directory used for storing responses | memento-integration | .memento-cache | +| useRealResponseTime | Whether Memento should respond using the actual response time | true | false | +| disableCachingPatterns | An array of patterns usd to ignore caching certain requests | [{ method: 'post', urlPattern: '/pokemon/*/sprites' }] | [] | + +### Option: disableCachingPatterns + +You may use `disableCachingPatterns` in your configuration to tell Memento to ignore caching responses based on the request method and URL. As an example, if you wish to not cache routes likes `/pokemon/mew/abilities` and `pokemon/ditto/abilities`, you may use the following configuration : + +``` +{ + // ... your configuration + "disableCachingPatterns": [{ + "method": "GET", + "urlPattern": "/pokemon/*/abilities" + }] +} +``` + +The [minimatch](https://www.npmjs.com/package/minimatch) package is used for comparing glob patterns and the actual url. You may use a tool like [globtester](http://www.globtester.com) to test your configurations. + +### Recipe: ignore caching all POST requests + +``` +{ + // ... your configuration + "disableCachingPatterns": [{ + "method": "post", + "urlPattern": "**" + }] +} +``` ## Examples diff --git a/package.json b/package.json index b1c2593..93f9f6b 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "fs-extra": "8.1.0", "koa": "2.8.1", "koa-bodyparser": "4.2.1", + "minimatch": "^3.0.4", "object-hash": "1.3.1", "text-table": "0.2.0", "vorpal": "1.12.0" diff --git a/src/cli.ts b/src/cli.ts index baa2b5f..922ff65 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,6 +14,7 @@ import { GetRequestDetails, SetResponseTime, } from './domain/usecase'; +import { DisableCachePattern } from './domain/entity'; import { getRequestDirectory } from './utils/path'; interface CreateCliOptions { @@ -23,6 +24,9 @@ interface CreateCliOptions { export function createCli({ container }: CreateCliOptions) { const targetUrl = container.resolve('targetUrl'); const cacheDirectory = container.resolve('cacheDirectory'); + const disableCachingPatterns = container.resolve( + 'disableCachingPatterns' + ); const clearAllRequestsUseCase = container.resolve( 'clearAllRequestsUseCase' ); @@ -210,6 +214,16 @@ export function createCli({ container }: CreateCliOptions) { console.log(chalk`Using Memento {yellow ${appVersion}}`); console.log(chalk`Request will be forwarded to {yellow ${targetUrl}}`); console.log(chalk`Cache directory is set to {yellow ${cacheDirectory}}`); + + if (disableCachingPatterns.length) { + console.log(chalk`Caching will be disabled for the following patterns:`); + + disableCachingPatterns.forEach(option => { + console.log( + chalk`\t- {green ${option.method}} {yellow ${option.urlPattern}}` + ); + }); + } console.log(chalk`Type {green help} to get available commands`); return vorpal; diff --git a/src/configuration.ts b/src/configuration.ts index c6b424f..1597cb9 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -34,6 +34,16 @@ export const configuration = { useRealResponseTime: getUseRealResponseTime( cosmicConfiguration.config.useRealResponseTime ), + disableCachingPatterns: + cosmicConfiguration.config.disableCachingPatterns || [], }; assert(configuration.targetUrl, 'targetUrl option is required'); + +configuration.disableCachingPatterns.forEach((option: any) => { + assert(option.method, 'Invalid disableCachingPatterns: method is required'); + assert( + option.urlPattern, + 'Invalid disableCachingPatterns: urlPattern is required' + ); +}); diff --git a/src/domain/entity/DisableCachePattern.ts b/src/domain/entity/DisableCachePattern.ts new file mode 100644 index 0000000..8c769cb --- /dev/null +++ b/src/domain/entity/DisableCachePattern.ts @@ -0,0 +1,4 @@ +export interface DisableCachePattern { + method: string; + urlPattern: string; +} diff --git a/src/domain/entity/index.ts b/src/domain/entity/index.ts index 0289fca..9e8a380 100644 --- a/src/domain/entity/index.ts +++ b/src/domain/entity/index.ts @@ -2,3 +2,4 @@ export * from './Request'; export * from './Response'; export * from './Headers'; export * from './Method'; +export * from './DisableCachePattern'; diff --git a/src/domain/usecase/RespondToRequest.test.ts b/src/domain/usecase/RespondToRequest.test.ts index fa81b46..d1b936c 100644 --- a/src/domain/usecase/RespondToRequest.test.ts +++ b/src/domain/usecase/RespondToRequest.test.ts @@ -37,6 +37,7 @@ describe('when the response is in the cache', () => { requestRepository, networkService, useRealResponseTime: false, + disableCachingPatterns: [], }); const method = 'GET'; const url = '/beers/1'; @@ -56,6 +57,7 @@ describe('when the response is in the cache', () => { requestRepository, networkService, useRealResponseTime: true, + disableCachingPatterns: [], }); const method = 'GET'; const url = '/beers/1'; @@ -107,6 +109,12 @@ describe('when no response is in the cache', () => { requestRepository, networkService, useRealResponseTime: true, + disableCachingPatterns: [ + { + method: 'POST', + urlPattern: '/beers/1', + }, + ], }); const method = 'GET'; const url = '/beers/1'; @@ -148,3 +156,220 @@ describe('when no response is in the cache', () => { ); }); }); + +describe('when caching is disabled for the method and url', () => { + beforeEach(() => { + (networkService.executeRequest as jest.Mock).mockResolvedValue( + new Response( + 200, + { 'cache-control': 'something' }, + Buffer.from('some body'), + 66 + ) + ); + }); + + it('should not cache the response (1 matching pattern)', async () => { + // Given + const useCase = new RespondToRequest({ + networkService, + requestRepository, + useRealResponseTime: false, + disableCachingPatterns: [ + { + method: 'post', + urlPattern: '/pokemon/ditto', + }, + ], + }); + const method = 'POST'; + const url = '/pokemon/ditto'; + + // When + const response = await useCase.execute(method, url, {}, ''); + + //Then + expect(requestRepository.persistResponseForRequest).not.toHaveBeenCalled(); + expect(requestRepository.getResponseByRequestId).not.toHaveBeenCalled(); + expect(response).toEqual( + new Response( + 200, + { 'cache-control': 'something' }, + Buffer.from('some body'), + 66 + ) + ); + }); + + it('should not cache the response (3 patterns / 1 matching pattern)', async () => { + // Given + const useCase = new RespondToRequest({ + networkService, + requestRepository, + useRealResponseTime: false, + disableCachingPatterns: [ + { + method: 'POST', + urlPattern: '/pokemon/ditto', + }, + { + method: 'get', + urlPattern: '/pokemon/ditto', + }, + { + method: 'post', + urlPattern: '/pokemon/ditto?format=true', + }, + ], + }); + const method = 'POST'; + const url = '/pokemon/ditto'; + + // When + const response = await useCase.execute(method, url, {}, ''); + + //Then + expect(requestRepository.persistResponseForRequest).not.toHaveBeenCalled(); + expect(requestRepository.getResponseByRequestId).not.toHaveBeenCalled(); + expect(response).toEqual( + new Response( + 200, + { 'cache-control': 'something' }, + Buffer.from('some body'), + 66 + ) + ); + }); + + it('should not cache the response (glob style)', async () => { + // Given + const useCase = new RespondToRequest({ + networkService, + requestRepository, + useRealResponseTime: false, + disableCachingPatterns: [ + { + method: 'post', + urlPattern: '/pokemon/ditto*', + }, + ], + }); + const method = 'POST'; + const url = '/pokemon/ditto?format=true'; + + // When + const response = await useCase.execute(method, url, {}, ''); + + //Then + expect(requestRepository.persistResponseForRequest).not.toHaveBeenCalled(); + expect(requestRepository.getResponseByRequestId).not.toHaveBeenCalled(); + expect(response).toEqual( + new Response( + 200, + { 'cache-control': 'something' }, + Buffer.from('some body'), + 66 + ) + ); + }); + + it('should not cache the response (glob style)', async () => { + // Given + const useCase = new RespondToRequest({ + networkService, + requestRepository, + useRealResponseTime: false, + disableCachingPatterns: [ + { + method: 'get', + urlPattern: '/pokemon/mew', + }, + ], + }); + const method = 'GET'; + const url = '/pokemon/mew/'; + + // When + const response = await useCase.execute(method, url, {}, ''); + + //Then + expect(requestRepository.persistResponseForRequest).not.toHaveBeenCalled(); + expect(requestRepository.getResponseByRequestId).not.toHaveBeenCalled(); + expect(response).toEqual( + new Response( + 200, + { 'cache-control': 'something' }, + Buffer.from('some body'), + 66 + ) + ); + }); + + it('should not cache the response (nested route)', async () => { + // Given + const useCase = new RespondToRequest({ + networkService, + requestRepository, + useRealResponseTime: false, + disableCachingPatterns: [ + { + method: 'get', + urlPattern: '/pokemon/mew/**/*', + }, + ], + }); + const method = 'GET'; + const url = '/pokemon/mew/abilities/2/stats'; + + // When + const response = await useCase.execute(method, url, {}, ''); + + //Then + expect(requestRepository.persistResponseForRequest).not.toHaveBeenCalled(); + expect(requestRepository.getResponseByRequestId).not.toHaveBeenCalled(); + expect(response).toEqual( + new Response( + 200, + { 'cache-control': 'something' }, + Buffer.from('some body'), + 66 + ) + ); + }); + + it('should not cache the response (nested route)', async () => { + // Given + const useCase = new RespondToRequest({ + networkService, + requestRepository, + useRealResponseTime: false, + disableCachingPatterns: [ + { + method: 'get', + urlPattern: '/pokemon/*/sprites/**', + }, + { + method: 'post', + urlPattern: '/pokemon/*/sprites/**', + }, + ], + }); + const method = 'GET'; + const url = '/pokemon/mew/sprites/2/back'; + + // When + const response = await useCase.execute(method, url, {}, ''); + + //Then + expect(requestRepository.persistResponseForRequest).not.toHaveBeenCalled(); + expect(requestRepository.getResponseByRequestId).not.toHaveBeenCalled(); + expect(response).toEqual( + new Response( + 200, + { 'cache-control': 'something' }, + Buffer.from('some body'), + 66 + ) + ); + }); +}); diff --git a/src/domain/usecase/RespondToRequest.ts b/src/domain/usecase/RespondToRequest.ts index 37f13c4..0e70c48 100644 --- a/src/domain/usecase/RespondToRequest.ts +++ b/src/domain/usecase/RespondToRequest.ts @@ -1,5 +1,7 @@ +import minimatch from 'minimatch'; + import { wait } from '../../utils/timers'; -import { Method, Response, Request } from '../entity'; +import { Method, Response, Request, DisableCachePattern } from '../entity'; import { RequestRepository } from '../repository'; import { NetworkService } from '../service'; @@ -7,6 +9,7 @@ interface Dependencies { requestRepository: RequestRepository; networkService: NetworkService; useRealResponseTime: boolean; + disableCachingPatterns: DisableCachePattern[]; } export interface Headers { @@ -17,15 +20,18 @@ export class RespondToRequest { private requestRepository: RequestRepository; private networkService: NetworkService; private useRealResponseTime: boolean; + private disableCachingPatterns: DisableCachePattern[]; public constructor({ requestRepository, networkService, useRealResponseTime, + disableCachingPatterns, }: Dependencies) { this.requestRepository = requestRepository; this.networkService = networkService; this.useRealResponseTime = useRealResponseTime; + this.disableCachingPatterns = disableCachingPatterns; } public async execute( @@ -36,6 +42,10 @@ export class RespondToRequest { ): Promise { const request = new Request(method, url, headers, body); + if (this.shouldIgnoreCaching(method, url)) { + return this.networkService.executeRequest(request); + } + const cachedResponse = await this.requestRepository.getResponseByRequestId( request.id ); @@ -55,4 +65,14 @@ export class RespondToRequest { return response; } + + private shouldIgnoreCaching(method: Method, url: string) { + return this.disableCachingPatterns.some(disableCacheParams => { + const methodMatch = + method.toLowerCase() === disableCacheParams.method.toLowerCase(); + const globMatch = minimatch(url, disableCacheParams.urlPattern); + + return methodMatch && globMatch; + }); + } } diff --git a/src/index.ts b/src/index.ts index c80f1ef..eb65049 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ container.register({ cacheDirectory: asValue(configuration.cacheDirectory), appVersion: asValue(version), useRealResponseTime: asValue(configuration.useRealResponseTime), + disableCachingPatterns: asValue(configuration.disableCachingPatterns), // Use cases respondToRequestUseCase: asClass(RespondToRequest), diff --git a/tests/integration/utils.ts b/tests/integration/utils.ts index 66e0e91..d285818 100644 --- a/tests/integration/utils.ts +++ b/tests/integration/utils.ts @@ -16,6 +16,7 @@ export function getTestApplication() { // Constants targetUrl: asValue(targetUrl), useRealResponseTime: asValue(false), + disableCachingPatterns: asValue([]), // Use cases respondToRequestUseCase: asClass(RespondToRequest),