@gis-ag/oniyi-http-plugin-cache-redis
Version:
Plugin responsible for caching responses into redis db
629 lines (538 loc) • 17.7 kB
JavaScript
// node core modules
// 3rd party modules
const test = require('ava');
const _ = require('lodash');
// internal modules
const { mock } = require('./fixtures/http-mocking');
const { initContext, queryModes, redisClient } = require('./fixtures/utils');
let tempBody;
test.before(() => mock());
/* eslint-disable no-console */
test.after(() => {
redisClient.flushdb((err, succeeded) => {
if (err) {
console.log(err);
return;
}
const prefixLog = 'oniyi-http-plugin-redis-cache:tests';
console.log(`${prefixLog}: Removing keys saved by tests for db: [${redisClient.selected_db}]`);
console.log(`${prefixLog}: Redis flushdb status: [${succeeded}]`);
});
});
/* eslint-enable no-console */
test.beforeEach(initContext);
test.cb('requestPhase: cached data not found/loaded', (t) => {
const { client, requestOptionsJson } = t.context;
// since this is our initial request, there should be no cached data
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.true(_.isUndefined(response.fromCache));
t.end();
}).catch(err => console.log(err));
});
test.cb('requestPhase: storing public data with authenticated user provided', (t) => {
const { client, requestOptionsJson, mockedUser } = t.context;
_.assign(requestOptionsJson, {
user: mockedUser,
});
// since this is our initial request, there should be no cached data
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.true(_.isUndefined(response.fromCache));
t.end();
});
});
test.cb('responsePhase: handler skipped, caching aborted', (t) => {
const { client, requestOptionsJson, eventName } = t.context;
_.assign(requestOptionsJson, {
phasesToSkip: {
responsePhases: ['cache'],
},
qs: {
mode: queryModes.abortCaching,
},
});
client.makeRequest(requestOptionsJson, () => {
// since we provide skipping of "cache" response phase, caching will never happen
// and on second request, we should not receive response from cache
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.true(_.isUndefined(response.fromCache));
t.false(response.eventNames().includes(eventName), "there shouldn't be a registered listener for [addToCache]");
t.end();
});
});
});
test.cb('requestPhase: handler skipped, cached data not loaded', (t) => {
const { client, requestOptionsJson, eventName } = t.context;
_.assign(requestOptionsJson, {
phasesToSkip: {
requestPhases: ['cache'],
},
});
client.makeRequest(requestOptionsJson, (err, response, body) => {
t.ifError(err);
t.true(response.eventNames().includes(eventName), 'there should be a registered listener for [addToCache]');
response.emit(eventName, { data: body });
// even thought the response body got cached, since we marked request phase for skipping,
// we should not receive any cached data
client.makeRequest(requestOptionsJson, (errSecond, responseSecond) => {
t.ifError(errSecond);
t.true(_.isUndefined(responseSecond.fromCache));
t.end();
});
});
});
// use after caching data, max-age=0
test.cb('requestPhase: requestOptions marked as non-retrievable, cached data not loaded', (t) => {
const { client, requestOptionsJson } = t.context;
_.merge(requestOptionsJson, {
headers: {
'cache-control': 'public, max-age=0',
},
});
// even though we might have data in cache, this request non-retreivable validator
// need to make sure that response does not come from the cache
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.true(_.isUndefined(response.fromCache));
t.end();
});
});
test.cb('responsePhase: response marked as non-storable, caching aborted', (t) => {
const { client, requestOptionsJson, eventName } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.publicMaxAgeZero,
},
});
// since one of the response validators failed(since max-age=0), we can see that
// our event was not registered at all.
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.false(response.eventNames().includes(eventName), "there shouldn't be registered listener for [addToCache]");
t.end();
});
});
test.cb('response phase: caching should be successful when max-age is set', (t) => {
const { client, requestOptionsJson } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.publicMaxAgeTen,
},
plugins: {
cache: {
delayCaching: false,
},
},
});
client.makeRequest(requestOptionsJson, () => {
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.true(response.fromCache);
t.true(response.headers['cache-control'].includes('max-age=10'));
t.end();
});
});
});
test.cb('response phase: remove data from the cache', (t) => {
const { client, requestOptionsJson } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.publicMaxAgeThirty,
},
plugins: {
cache: {
delayCaching: false,
},
},
});
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
response.emit('removeFromCache');
t.end();
});
});
// cache-control private, no private hash
test.cb(
'response phase: requested storing data into private cache, but no privateHashedId available. Caching aborted',
(t) => {
const { client, requestOptionsJson } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.privateMaxAgeTen,
},
});
client.makeRequest(requestOptionsJson, () => {
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.true(_.isUndefined(response.fromCache));
t.true(response.headers['cache-control'].includes('max-age=10'));
t.end();
});
});
}
);
// cache-control private, private hash available, max-age set
test.cb('response phase: storing data into private cache, out-of-reach for non-authenticated request ', (t) => {
const { client, requestOptionsJson, mockedUser } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.privateMaxAgeTen,
},
plugins: {
cache: {
delayCaching: false,
},
},
});
const privateRequestOptions = _.merge(
{
user: mockedUser,
},
requestOptionsJson
);
client.makeRequest(privateRequestOptions, () => {
// authenticated request should receive private cached data
client.makeRequest(privateRequestOptions, (err, response) => {
t.ifError(err);
t.true(response.fromCache);
t.true(response.headers['cache-control'].includes('max-age=10'));
t.end();
});
// since we have stored only private data, the non-authenticated request
// should not receive a private cached data.
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.true(_.isUndefined(response.fromCache));
t.end();
});
});
});
// cache-control private, private hash available, ttl not set, "expires" set
test.cb('response phase: storing data into private cache, using "expires" option', (t) => {
const { requestOptionsJson, mockedUser, client } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.privateExpiresSet,
},
plugins: {
cache: {
delayCaching: false,
ttl: 0,
},
},
user: mockedUser,
});
client.makeRequest(requestOptionsJson, () => {
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.true(response.fromCache);
t.end();
});
});
});
// cache-control private, private hash available, no expiration params
test.cb('response phase: storing data into a private cache when all expiration params have failed', (t) => {
const { requestOptionsJson, client } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.publicNoExpirationTime,
},
plugins: {
cache: {
delayCaching: false,
ttl: 0,
},
},
});
client.makeRequest(requestOptionsJson, () => {
client.makeRequest(requestOptionsJson, (err, response) => {
t.ifError(err);
t.true(response.fromCache);
t.end();
});
});
});
// cache-control private, private hash available, storing by registering an event
test.cb('response phase: storing data into a private cache by using "delayed" mechanism', (t) => {
const {
client, requestOptionsJson, eventName, mockedUser,
} = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.privateMaxAgeTwenty,
},
});
const privateRequestOptions = _.merge({ user: mockedUser }, requestOptionsJson);
client.makeRequest(privateRequestOptions, (err, response, body) => {
t.ifError(err);
t.true(response.eventNames().includes(eventName), 'there should be a registered listener for [addToCache]');
response.emit(eventName, { data: body });
// here we make a new http request with "user" object provided
// since it is also private data that we are trying to access,
// it can be found
client.makeRequest(privateRequestOptions, (errSecond, cachedResponse, cachedBody) => {
t.ifError(errSecond);
t.true(cachedResponse.fromCache);
t.deepEqual(body, cachedBody);
});
// same options, no user/authorization. We should not get response from the cache
client.makeRequest(requestOptionsJson, (errSecond, responseSecond) => {
t.ifError(errSecond);
t.true(_.isUndefined(responseSecond.fromCache));
t.end();
});
});
});
test.cb('request phase: loading public cached data which was stored as simple string', (t) => {
const { client, requestOptions } = t.context;
_.assign(requestOptions, {
qs: {
mode: queryModes.publicSMaxAgeTwenty,
},
});
client.makeRequest(requestOptions, () => {
client.makeRequest(requestOptions, (err, responseBody, cachedBody) => {
t.ifError(err);
t.true(responseBody.fromCache);
t.true(_.isString(cachedBody) && cachedBody.includes('some xml data'));
t.end();
});
});
});
test.cb(
'request phase: self-update of request options when E-Tag / Last-Modified are provided, received http status 200',
(t) => {
// eslint-disable-line max-len
const { client, requestOptionsJson } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.setETagLastMod,
},
plugins: {
cache: {
ttl: 60,
delayCaching: false,
},
},
});
client.makeRequest(requestOptionsJson, () => {
// even though the response got cached, we got a status code 200 from the server
// which means that cached version is stale, and we need to re-cache it.
// as a result, we got completely fresh response
client.makeRequest(requestOptionsJson, (err, response, body) => {
t.ifError(err);
const { request: { headers } } = response;
['if-none-match', 'if-modified-since'].forEach((headerProp) => {
t.true(headerProp in headers, `[${headerProp}] should be a member of response.request headers`);
// original request options should not be mutated
t.false(
headerProp in requestOptionsJson.headers,
`[${headerProp}] should NOT be a member of request headers`
);
});
t.true(_.isUndefined(response.fromCache));
tempBody = _.assign({}, body);
t.end();
});
});
}
);
test.cb('request phase: loading public cached data when server responds with httpStatus 304', (t) => {
const { client, requestOptionsJson } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.setETagLastMod,
},
plugins: {
cache: {
delayCaching: false,
},
},
});
client.makeRequest(requestOptionsJson, (err, resp, body) => {
t.ifError(err);
t.true(resp.fromCache);
// tempBody received in previous test (statusCode = 200) should not be different
// from the response body that we receive when statusCode = 304.
t.deepEqual(tempBody, body);
t.end();
});
});
test.cb('request phase: retrieving "raw" data from cache instead of JSON obj when JSON string is not valid', (t) => {
const { client, requestOptionsJson } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.badJSON,
},
json: true,
plugins: {
cache: {
delayCaching: false,
},
},
});
client.makeRequest(requestOptionsJson, () => {
client.makeRequest(requestOptionsJson, (err, resp, body) => {
t.ifError(err);
t.true(resp.fromCache);
t.true(body.includes('bad json syntax'));
t.end();
});
});
});
test.cb('response phase: non-valid status code received while re-validating cached data, caching aborted', (t) => {
const { client, requestOptionsJson } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.unsupportedStatusCode,
},
plugins: {
cache: {
delayCaching: false,
},
},
});
client.makeRequest(requestOptionsJson, () => {
// response should not be cached, since we received statusCode=401 while re-validating cached data
client.makeRequest(requestOptionsJson, (err, response, body) => {
t.ifError(err);
t.true(_.isUndefined(response.fromCache));
t.true(_.isEmpty(body));
t.end();
});
});
});
test.cb('response phase: using "delayed" caching mechanism', (t) => {
const { client, requestOptionsJson, eventName } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.delayed,
},
});
client.makeRequest(requestOptionsJson, (originalError, originalResponse, originalBody) => {
t.ifError(originalError);
const updatedBody = _.assign({ some: 'update' }, originalBody);
t.true(originalResponse.eventNames().includes('addToCache'));
originalResponse.emit(eventName, { data: updatedBody, storeMultiResponse: true });
client.makeRequest(requestOptionsJson, (err, response, body) => {
t.ifError(err);
t.true(response.fromCache);
t.deepEqual(body, updatedBody);
t.end();
});
});
});
test.cb('response phase: using "delayed" caching mechanism, no-cache/must-revalidate ignored', (t) => {
const { client, requestOptionsJson, eventName } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.delayedWithMustRevalidate,
},
json: true,
});
client.makeRequest(requestOptionsJson, (originalError, originalResponse, originalBody) => {
t.ifError(originalError);
const updatedBody = _.assign({ some: 'update' }, originalBody);
t.true(originalResponse.eventNames().includes('addToCache'));
originalResponse.emit(eventName, { data: updatedBody, storeMultiResponse: true });
client.makeRequest(requestOptionsJson, (err, response, body) => {
t.ifError(err);
t.true(response.fromCache);
t.true(_.isUndefined(response.headers.storeMultiResponse));
t.deepEqual(body, updatedBody);
t.end();
});
});
});
test.cb('error validation when building a redis client with provided invalid "unixSocket" path', (t) => {
const {
create, requestOptions, httpClientParams, initCachePlugin,
} = t.context;
const unixSocket = './wrong/path';
const pluginOptions = {
ttl: 2,
redis: {
unixSocket,
},
};
const plugin = initCachePlugin(pluginOptions);
const client = create(httpClientParams).use(plugin);
const callback = (err, response, body) => {
t.ifError(err);
t.is(body, 'Bad Request');
t.is(response.statusCode, 400);
t.end();
};
_.assign(requestOptions, {
qs: {
mode: queryModes.errorResponse,
},
callback,
});
client.makeRequest(requestOptions);
});
test.cb('validators should be skipped per request', (t) => {
const { client, requestOptionsJson } = t.context;
_.assign(requestOptionsJson, {
qs: {
mode: queryModes.publicMaxAgeZeroToSkip,
},
plugins: {
cache: {
delayCaching: false,
validatorsToSkip: {
requestValidators: ['maxAgeZero'],
responseValidators: ['maxAgeZero'],
},
},
},
});
client.makeRequest(requestOptionsJson, (originalError) => {
t.ifError(originalError);
client.makeRequest(requestOptionsJson, (error, response) => {
t.ifError(error);
t.true(response.fromCache);
t.end();
});
});
});
test.cb('validators should be skipped while initiating a plugin', (t) => {
const {
create, requestOptionsJson, httpClientParams, initCachePlugin, mockedUser,
} = t.context;
const pluginOptions = {
ttl: 60 * 60,
validatorsToSkip: {
requestValidators: ['maxAgeZero'],
responseValidators: ['maxAgeZero'],
},
hostConfig: {
'cache-plugin': {
storePrivate: true,
},
},
};
const plugin = initCachePlugin(pluginOptions);
const client = create(httpClientParams).use(plugin);
_.assign(requestOptionsJson, {
user: mockedUser,
qs: {
mode: queryModes.privateMaxAgeZeroToSkip,
},
plugins: {
cache: {
delayCaching: false,
},
},
});
client.makeRequest(requestOptionsJson, (originalError) => {
t.ifError(originalError);
client.makeRequest(requestOptionsJson, (error, response) => {
t.ifError(error);
t.true(response.fromCache);
t.end();
});
});
});