Add tests for cache middleware. Do not cache non-200 responses.
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
committed by
Eric Peterson
parent
a16dce0433
commit
74ccd66e26
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user