app-backend: added static assets middleware + header constants
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user