Skip to content

Commit

Permalink
Merge pull request #40 from denbon05/req-with-query
Browse files Browse the repository at this point in the history
Add more tests
  • Loading branch information
denbon05 authored Apr 19, 2024
2 parents 1cc5558 + d99e26e commit 8fe7f13
Show file tree
Hide file tree
Showing 15 changed files with 7,163 additions and 3,433 deletions.
22 changes: 2 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,35 +53,17 @@ axios.get(url);

## API

### Options (default)

```ts
{
ttlInMinutes?: 5,
disableCache?: false;
statusesToCache?: [200],
methodsToCache?: ['GET'],
excludeRoutes?: [];
}
```

### On fastify instance

<p><b>app.lcache</b> available inside your app</p>

```ts
interface CachedResponse<T> {
payload: T;
headers?: { [key: string]: string | number | string[] };
statusCode?: number;
}

interface IStorage {
// Get cached data
get<T>(key: string): CachedResponse<T>;
get(key: string): any;

// Set data to cache
set<T>(key: string, value: CachedResponse<T>): void;
set(key: string, value: any): void;

// Check if data exists in cache
has(key: string): boolean;
Expand Down
8 changes: 8 additions & 0 deletions __tests__/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export const getApp = (options: ICacheOptions = {}) => {
reply.send('pong');
});

app.post('/ping', async (_req, reply) => {
reply.send('pong');
});

app.delete('/ping', async (_req, reply) => {
reply.send('pong');
});

app.get('/json', async (_req, reply) => {
reply.send({ hello: 'world' });
});
Expand Down
180 changes: 175 additions & 5 deletions __tests__/lcache.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { TLL_CHECK_INTERVAL_MS } from '@/constants';
import '@/types/fastify';
import type { RequestMethod } from '@/types/lcache';
import type { FastifyInstance } from 'fastify';
import { getApp } from './helpers';

Expand All @@ -13,11 +15,40 @@ describe('Caching with default options', () => {
await app.close();
});

test('Plugin exists on app instance', () => {
test('Plugin should exist on app instance', () => {
expect(app.hasDecorator('lcache')).toBeTruthy();
});

test('Cache is working', async () => {
test('Should be possible to put/retrieve values from app instance', async () => {
const strKey = 'str';
const expectedStrValue = 'data1';
const objKey = 'obj';
const expectedObjValue = {
name: 'Bob',
age: 23,
};
app.lcache.set(strKey, expectedStrValue);
app.lcache.set(objKey, expectedObjValue);

const actualStrValue = app.lcache.get(strKey);
expect(actualStrValue).toEqual(expectedStrValue);

const actualObjValue = app.lcache.get(objKey);
expect(actualObjValue).toEqual(actualObjValue);
});

test('Should be possible to overwrite data by key', async () => {
const key = 'str';
const value1 = 'data1';
const value2 = NaN;

app.lcache.set(key, value1);
app.lcache.set(key, value2);

expect(app.lcache.get(key)).toStrictEqual(value2);
});

test('Cache should work', async () => {
const spyGet = jest.spyOn(app.lcache, 'get');
const spySet = jest.spyOn(app.lcache, 'set');
const getPing = async () =>
Expand Down Expand Up @@ -51,6 +82,26 @@ describe('Caching with default options', () => {
expect(res2.headers['content-type']).toBe(res1.headers['content-type']);
expect(spyGet).toHaveBeenCalledTimes(1);
});

test('Response should be cached separately for different query', async () => {
const spyGet = jest.spyOn(app.lcache, 'get');
const spySet = jest.spyOn(app.lcache, 'set');
const res1 = await app.inject({
method: 'GET',
path: '/date',
query: 'asd',
});

const res2 = await app.inject({
method: 'GET',
path: '/date',
query: 'sdf',
});

expect(res1.body).not.toBe(res2.body);
expect(spyGet).not.toHaveBeenCalled();
expect(spySet).toHaveBeenCalledTimes(2);
});
});

describe('Caching with custom options', () => {
Expand All @@ -68,21 +119,21 @@ describe('Caching with custom options', () => {
await app.close();
});

test('Response should be cached separately for different payload', async () => {
test('Response should be cached separately for different body', async () => {
const spyGet = jest.spyOn(app.lcache, 'get');
const spySet = jest.spyOn(app.lcache, 'set');
const res1 = await app.inject({
method: 'POST',
path: '/post',
payload: {
body: {
data: 'first-payload',
},
});

const res2 = await app.inject({
method: 'POST',
path: '/post',
payload: {
body: {
data: 'second-payload',
},
});
Expand Down Expand Up @@ -129,4 +180,123 @@ describe('Caching with custom options', () => {
expect(res1.body).not.toBe(res2.body);
expect(spySet).not.toHaveBeenCalled();
});

test('Cache reset should work', async () => {
const spySet = jest.spyOn(app.lcache, 'set');
const getPing = async () =>
app.inject({
method: 'GET',
path: '/ping',
});

const postPing = async () =>
app.inject({
method: 'POST',
path: '/ping',
});

const dataKey1 = 'someKey1';
const dataValue1 = 'someValue1';
const dataKey2 = 'someKey2';
const dataValue2 = [1, 2, 3, 4];
const dataKey3 = 'someKey3';
const dataValue3 = 'someValue3';

// add data to the cache via request
await getPing();
await postPing();
// manually set some values
app.lcache.set(dataKey1, dataValue1);
app.lcache.set(dataKey2, dataValue2);
app.lcache.set(dataKey3, dataValue3);

expect(spySet).toBeCalledTimes(5); // 1 post + 1 get requests + 3 manual set
// remove specific key
app.lcache.reset(dataKey1);
app.lcache.reset([dataKey3]); // also check array

expect(app.lcache.get(dataKey1)).toBeFalsy();
expect(app.lcache.get(dataKey1)).toBeUndefined();
expect(app.lcache.get(dataKey3)).toBeUndefined();
// expect not specified data is still in the cache
expect(app.lcache.get(dataKey2)).toStrictEqual(dataValue2);

// prune all cached data
app.lcache.reset();
expect(app.lcache.has(dataKey2)).toBeFalsy();
expect(app.lcache.get(dataKey2)).toBeUndefined();

// check cached data via request
const spyGet = jest.spyOn(app.lcache, 'get');
await getPing();
await postPing();
// lcache should place data again to the cache and not try to get it
expect(spyGet).not.toHaveBeenCalled();
});
});

describe('Disabled lcache plugin', () => {
let app: FastifyInstance;
const methodsToCache: RequestMethod[] = ['GET', 'POST', 'DELETE'];

beforeEach(async () => {
app = await getApp({
excludeRoutes: [],
statusesToCache: [200, 201],
methodsToCache,
disableCache: true,
});
});

afterEach(async () => {
await app.close();
});

test.each(methodsToCache)(
"Shouldn't cache %s method regardless plugin config",
async (httpMethod) => {
const spySet = jest.spyOn(app.lcache, 'set');

await app.inject({
method: httpMethod,
path: '/ping',
payload: {
data: '456',
},
});

expect(spySet).not.toHaveBeenCalled();
}
);
});

describe('Interval cleanup', () => {
// convert to minutes for lcache usage
const ttlInMinutes = TLL_CHECK_INTERVAL_MS / 60000;
let app: FastifyInstance;

beforeEach(async () => {
app = await getApp({
ttlInMinutes,
});
});

afterEach(async () => {
await app.close();
jest.clearAllTimers();
});

test('Cached data should be removed after ttl', async () => {
const key = 'someKey';
const value = 'someValue';

app.lcache.set(key, value);
// wait doubled ttl time
await new Promise((resolve) => {
setTimeout(resolve, TLL_CHECK_INTERVAL_MS * 2);
});

expect(app.lcache.get(key)).toBeUndefined();
expect(app.lcache.has(key)).toBeFalsy();
});
});
9 changes: 2 additions & 7 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import type { Config } from 'jest';
import type { JestConfigWithTsJest } from 'ts-jest';

const jestConfig: Config = {
const jestConfig: JestConfigWithTsJest = {
rootDir: '.',
testEnvironment: 'node',
transform: { '^.+\\.ts?$': 'ts-jest' },
globals: {
'ts-jest': {
diagnostics: false,
},
},
moduleFileExtensions: ['js', 'ts', 'd.ts'],
testRegex: '.*\\.test\\.ts$',
modulePathIgnorePatterns: ['<rootDir>/__tests__/helpers/'],
Expand Down
8 changes: 8 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* eslint-disable import/prefer-default-export */

import type { RequestMethod } from './types/lcache';

export const TTL_IN_MINUTES = 5;
export const STATUSES_TO_CACHE = [200];
export const METHODS_TO_CACHE: RequestMethod[] = ['GET'];
export const TLL_CHECK_INTERVAL_MS = 1000;
4 changes: 2 additions & 2 deletions lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { FastifyRequest } from 'fastify';
import type {
RequestMethod,
ICacheOptions,
ICachePluginOptions,
RequestMethod,
} from './types/lcache';

const getMilliseconds = (min: number): number => min * 60000;
Expand All @@ -15,7 +15,7 @@ export const formatOptions = (opts: ICacheOptions): ICachePluginOptions => ({
ttl: getMilliseconds(opts.ttlInMinutes),
});

export const shouldBeCached = (
export const checkShouldBeCached = (
opts: ICachePluginOptions,
request: FastifyRequest,
statusCode: number
Expand Down
30 changes: 14 additions & 16 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
import type { FastifyInstance, FastifyPluginCallback } from 'fastify';
import fp from 'fastify-plugin';
import type { ICacheOptions } from '@/types/lcache';
import { formatOptions, shouldBeCached } from '@/helpers';
import { checkShouldBeCached, formatOptions } from '@/helpers';
import MapStorage from '@/storage/Map';
import type { ICacheOptions } from '@/types/lcache';
import { buildCacheKey } from '@/utils';
import type { FastifyInstance, FastifyPluginCallback } from 'fastify';
import fp from 'fastify-plugin';
import * as constants from '@/constants';

const defaultOpts: ICacheOptions = {
ttlInMinutes: 5,
statusesToCache: [200],
methodsToCache: ['GET'],
ttlInMinutes: constants.TTL_IN_MINUTES,
statusesToCache: constants.STATUSES_TO_CACHE,
methodsToCache: constants.METHODS_TO_CACHE,
ttlCheckIntervalMs: constants.TLL_CHECK_INTERVAL_MS,
};

const cache: FastifyPluginCallback<ICacheOptions> = (
instance: FastifyInstance,
opts: ICacheOptions,
_next
) => {
if (opts.disableCache) {
_next();
return;
}

const storageOpts = formatOptions({ ...defaultOpts, ...opts });

const storage = new MapStorage(storageOpts);

instance.addHook('onSend', async (request, reply, payload) => {
const cacheKey = buildCacheKey(request);

if (
const shouldValueBeCached =
!storage.has(cacheKey) &&
shouldBeCached(storageOpts, request, reply.statusCode)
) {
checkShouldBeCached(storageOpts, request, reply.statusCode) &&
!opts.disableCache;

if (shouldValueBeCached) {
storage.set(cacheKey, {
payload,
headers: reply.getHeaders(),
Expand Down
Loading

0 comments on commit 8fe7f13

Please sign in to comment.