UNPKG

@gis-ag/oniyi-http-plugin-cache-redis

Version:

Plugin responsible for caching responses into redis db

629 lines (538 loc) 17.7 kB
// 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(); }); }); });