Add tests for cache middleware. Do not cache non-200 responses.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-07-24 22:27:18 +02:00
committed by Eric Peterson
parent a16dce0433
commit 74ccd66e26
2 changed files with 146 additions and 2 deletions
@@ -0,0 +1,142 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { createCacheMiddleware, TechDocsCache } from '.';
/**
* Mocks cached HTTP response.
*/
const getMockHttpResponseFor = (content: string): Buffer => {
return Buffer.concat([
Buffer.from(`HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Sat, 1 Jul 2021 12:00:00 GMT
Date: Sat, 1 Jul 2021 12:00:00 GMT
Connection: close
Content-Length: ${content.length}\n\n`),
Buffer.from(content),
]);
};
/**
* Wait for the socket to close. Works because, above, we set connection: close
*/
const waitForSocketClose = () => new Promise(resolve => setTimeout(resolve, 0));
describe('createCacheMiddleware', () => {
let cache: jest.Mocked<TechDocsCache>;
let app: express.Express;
beforeEach(async () => {
cache = ({
get: jest.fn().mockResolvedValue(undefined),
set: jest.fn().mockResolvedValue(undefined),
invalidate: jest.fn().mockResolvedValue(undefined),
invalidateMultiple: jest.fn().mockResolvedValue(undefined),
} as unknown) as jest.Mocked<TechDocsCache>;
const router = await createCacheMiddleware({
logger: getVoidLogger(),
cache,
});
app = express().use(router);
app.use((req, res, next) => {
// By default, send cacheable content.
if (req.path !== '/api/static/docs/error.png') {
res.send('default-response');
} else {
next(new Error());
}
});
});
describe('invalidate', () => {
it('responds with 400 when no objects are provided', async () => {
const response = await request(app).post('/cache/invalidate');
expect(response.status).toBe(400);
});
it('responds with 500 if invalidation throws', async () => {
cache.invalidateMultiple.mockRejectedValueOnce(new Error());
const response = await request(app)
.post('/cache/invalidate')
.set('Content-Type', 'application/json')
.send({
objects: ['one/index.html', 'two/index.html'],
});
expect(response.status).toBe(500);
});
it('responds with 204 if invalidation succeeds', async () => {
const expectedObjects = ['one/index.html', 'two/index.html'];
cache.invalidateMultiple.mockResolvedValue([]);
await request(app)
.post('/cache/invalidate')
.set('Content-Type', 'application/json')
.send({
objects: expectedObjects,
})
.expect(204);
expect(cache.invalidateMultiple).toHaveBeenCalledWith(expectedObjects);
});
});
describe('middleware', () => {
it('does not apply to non-static/docs paths', async () => {
await request(app)
.get('/api/static/not-docs')
.expect(200, 'default-response');
expect(cache.set).not.toHaveBeenCalled();
});
it('responds with cached response', async () => {
cache.get.mockResolvedValueOnce(getMockHttpResponseFor('xyz'));
await request(app).get('/api/static/docs/foo.html').expect(200, 'xyz');
await waitForSocketClose();
expect(cache.set).not.toHaveBeenCalled();
});
it('sets cache when content is cacheable', async () => {
const expectedPath = 'default/api/xyz/index.html';
await request(app)
.get(`/api/static/docs/${expectedPath}`)
.expect(200, 'default-response');
await waitForSocketClose();
expect(cache.set).toHaveBeenCalled();
const [actualPath, actualBuffer] = (cache.set as jest.Mock).mock.calls[0];
expect(actualPath).toBe(expectedPath);
expect(actualBuffer.toString()).toContain('default-response');
});
it('does not set cache on error', async () => {
await request(app).get('/api/static/docs/error.png').expect(500);
await waitForSocketClose();
expect(cache.set).not.toHaveBeenCalled();
});
});
});
+4 -2
View File
@@ -114,8 +114,10 @@ export const createCacheMiddleware = ({
// When a socket is closed, if there were no errors and the data written
// over the socket should be cached, cache it!
socket.on('close', hadError => {
if (writeToCache && !hadError) {
cache.set(reqPath, Buffer.concat(chunks));
const content = Buffer.concat(chunks);
const head = content.toString('utf8', 0, 12);
if (writeToCache && !hadError && head === 'HTTP/1.1 200') {
cache.set(reqPath, content);
}
});