app-backend: added static assets middleware + header constants

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-12-29 16:52:59 +01:00
parent 3a7911ece8
commit 7261b76935
4 changed files with 186 additions and 0 deletions
@@ -0,0 +1,109 @@
/*
* 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 express from 'express';
import request from 'supertest';
import { createStaticAssetsStoreMiddleware } from './createStaticAssetsStoreMiddleware';
import { StaticAssetsStore } from './StaticAssetsStore';
const mockStore = {
getAsset: jest.fn(),
} as unknown as jest.Mocked<StaticAssetsStore>;
describe('createStaticAssetsStoreMiddleware', () => {
const app = express();
app.use(createStaticAssetsStoreMiddleware(mockStore));
app.use((_req, res) => {
res.status(404).end('Not Found');
});
beforeEach(() => {
jest.resetAllMocks();
});
it('should respond with an asset', async () => {
const now = new Date();
mockStore.getAsset.mockResolvedValueOnce({
path: 'foo.js',
lastModifiedAt: now,
content: Buffer.from('foo'),
});
const res = await request(app).get('/foo.js');
expect(res.status).toBe(200);
expect(res.text).toBe('foo');
expect(res.get('Content-Type')).toBe(
'application/javascript; charset=utf-8',
);
expect(res.get('Content-Length')).toBe('3');
expect(res.get('Cache-Control')).toBe('public, max-age=1209600');
expect(res.get('Last-Modified')).toBe(now.toUTCString());
expect(mockStore.getAsset).toHaveBeenCalledTimes(1);
expect(mockStore.getAsset).toHaveBeenCalledWith('foo.js');
});
mockStore.getAsset.mockResolvedValueOnce(undefined);
it('should respond with not found', async () => {
const res = await request(app).get('/foo.js');
expect(res.status).toBe(404);
expect(res.text).toBe('Not Found');
expect(mockStore.getAsset).toHaveBeenCalledTimes(1);
expect(mockStore.getAsset).toHaveBeenCalledWith('foo.js');
});
it('should handle other content type', async () => {
mockStore.getAsset.mockResolvedValueOnce({
path: 'foo.css',
lastModifiedAt: new Date(),
content: Buffer.from('foo'),
});
const res = await request(app).get('/foo.css');
expect(res.status).toBe(200);
expect(res.text).toBe('foo');
expect(res.get('Content-Type')).toBe('text/css; charset=utf-8');
expect(res.get('Content-Length')).toBe('3');
expect(mockStore.getAsset).toHaveBeenCalledTimes(1);
expect(mockStore.getAsset).toHaveBeenLastCalledWith('foo.css');
});
it('should handle unknown content types', async () => {
mockStore.getAsset.mockResolvedValueOnce({
path: 'foo.notavalidextension',
lastModifiedAt: new Date(),
content: Buffer.from('foo'),
});
const res = await request(app).get('/foo.notavalidextension');
expect(res.status).toBe(200);
expect(res.body).toEqual(Buffer.from('foo'));
expect(res.get('Content-Type')).toBe('application/octet-stream');
expect(res.get('Content-Length')).toBe('3');
expect(mockStore.getAsset).toHaveBeenCalledTimes(1);
expect(mockStore.getAsset).toHaveBeenLastCalledWith(
'foo.notavalidextension',
);
});
});
@@ -0,0 +1,58 @@
/*
* 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 { extname } from 'path';
import { RequestHandler } from 'express';
import { StaticAssetsStore } from './StaticAssetsStore';
import { CACHE_CONTROL_MAX_CACHE } from '../headers';
export function createStaticAssetsStoreMiddleware(
store: StaticAssetsStore,
): RequestHandler {
return (req, res, next) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
next();
return;
}
Promise.resolve(
(async () => {
// Drop leading slashes from the incoming path
const path = req.path.startsWith('/') ? req.path.slice(1) : req.path;
const asset = await store.getAsset(path);
if (!asset) {
next();
return;
}
// Set the Content-Type header, falling back to octet-stream
const ext = extname(asset.path);
if (ext) {
res.type(ext);
} else {
res.type('bin');
}
// Same as our express.static override
res.setHeader('Cache-Control', CACHE_CONTROL_MAX_CACHE);
res.setHeader('Last-Modified', asset.lastModifiedAt.toUTCString());
res.send(asset.content);
})(),
).catch(next);
};
}
@@ -17,3 +17,4 @@
export { StaticAssetsStore } from './StaticAssetsStore';
export type { StaticAsset, StaticAssetInput } from './types';
export { findStaticAssets } from './findStaticAssets';
export { createStaticAssetsStoreMiddleware } from './createStaticAssetsStoreMiddleware';
+18
View File
@@ -0,0 +1,18 @@
/*
* 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.
*/
export const CACHE_CONTROL_NO_CACHE = 'no-store, max-age=0';
export const CACHE_CONTROL_MAX_CACHE = 'public, max-age=1209600'; // 14 days