Skip to content

feat(runtime): add CORS headers to OPTIONS requests to assets #141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion __tests__/runtime/__snapshots__/integration.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Function integration tests basic-twiml.js should match snapshot 1`] = `
exports[`with an express app Assets integration tests hello.js should match snapshot 1`] = `
Object {
"body": Object {},
"headers": Object {
"accept-ranges": "bytes",
"cache-control": "public, max-age=0",
"connection": "close",
"content-type": "application/javascript; charset=UTF-8",
"x-powered-by": "Express",
},
"statusCode": 200,
"text": "alert('Hello world!');
",
"type": "application/javascript",
}
`;

exports[`with an express app Function integration tests basic-twiml.js should match snapshot 1`] = `
Object {
"body": Object {},
"headers": Object {
Expand Down
80 changes: 70 additions & 10 deletions __tests__/runtime/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createServer } from '../../src/runtime/server';
const TEST_DIR = resolve(__dirname, '../../fixtures');

const TEST_FUNCTIONS_DIR = resolve(TEST_DIR, 'functions');
const TEST_ASSETS_DIR = resolve(TEST_DIR, 'assets');
const TEST_ENV = {};

const availableFunctions = readdirSync(TEST_FUNCTIONS_DIR).map(
Expand All @@ -19,6 +20,11 @@ const availableFunctions = readdirSync(TEST_FUNCTIONS_DIR).map(
return { name, url, path };
}
);
const availableAssets = readdirSync(TEST_ASSETS_DIR).map((name: string) => {
const path = resolve(TEST_ASSETS_DIR, name);
const url = `/${name}`;
return { name, url, path };
});

type InternalResponse = request.Response & {
statusCode: number;
Expand All @@ -30,6 +36,7 @@ type InternalResponse = request.Response & {
function responseToSnapshotJson(response: InternalResponse) {
let { statusCode, type, body, text, headers } = response;
delete headers['date'];
delete headers['last-modified'];

if (text && text.startsWith('Error')) {
// stack traces are different in every environment
Expand All @@ -48,7 +55,7 @@ function responseToSnapshotJson(response: InternalResponse) {
};
}

describe('Function integration tests', () => {
describe('with an express app', () => {
let app: Express;

beforeAll(async () => {
Expand All @@ -59,15 +66,68 @@ describe('Function integration tests', () => {
} as StartCliConfig);
});

for (const testFnCode of availableFunctions) {
test(`${testFnCode.name} should match snapshot`, async () => {
const response = await request(app).get(testFnCode.url);
if (response.status === 500) {
expect(response.text).toMatch(/Error/);
} else {
describe('Function integration tests', () => {
for (const testFnCode of availableFunctions) {
test(`${testFnCode.name} should match snapshot`, async () => {
const response = await request(app).get(testFnCode.url);
if (response.status === 500) {
expect(response.text).toMatch(/Error/);
} else {
const result = responseToSnapshotJson(response as InternalResponse);
expect(result).toMatchSnapshot();
}
});
}
});

describe('Assets integration tests', () => {
for (const testAsset of availableAssets) {
test(`${testAsset.name} should match snapshot`, async () => {
const response = await request(app).get(testAsset.url);
const result = responseToSnapshotJson(response as InternalResponse);
expect(result).toMatchSnapshot();
}
});
}
});

test(`OPTIONS request to ${testAsset.name} should return CORS headers and no body`, async () => {
const response = (await request(app).options(
testAsset.url
)) as InternalResponse;
expect(response.headers['access-control-allow-origin']).toEqual('*');
expect(response.headers['access-control-allow-headers']).toEqual(
'Accept, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since'
);
expect(response.headers['access-control-allow-methods']).toEqual(
'GET, POST, OPTIONS'
);
expect(response.headers['access-control-expose-headers']).toEqual(
'ETag'
);
expect(response.headers['access-control-max-age']).toEqual('86400');
expect(response.headers['access-control-allow-credentials']).toEqual(
'true'
);
expect(response.text).toEqual('');
});

test(`GET request to ${testAsset.name} should not return CORS headers`, async () => {
const response = (await request(app).get(
testAsset.url
)) as InternalResponse;
expect(response.headers['access-control-allow-origin']).toBeUndefined();
expect(
response.headers['access-control-allow-headers']
).toBeUndefined();
expect(
response.headers['access-control-allow-methods']
).toBeUndefined();
expect(
response.headers['access-control-expose-headers']
).toBeUndefined();
expect(response.headers['access-control-max-age']).toBeUndefined();
expect(
response.headers['access-control-allow-credentials']
).toBeUndefined();
});
}
});
});
1 change: 1 addition & 0 deletions fixtures/assets/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
alert('Hello world!');
11 changes: 11 additions & 0 deletions src/runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,17 @@ export async function createServer(
if (routeInfo.filePath) {
if (routeInfo.access === 'private') {
res.status(403).send('This asset has been marked as private');
} else if (req.method === 'OPTIONS') {
res.set({
'access-control-allow-origin': '*',
'access-control-allow-headers':
'Accept, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since',
'access-control-allow-methods': 'GET, POST, OPTIONS',
'access-control-expose-headers': 'ETag',
'access-control-max-age': '86400',
'access-control-allow-credentials': true,
});
res.status(200).end();
} else {
res.sendFile(routeInfo.filePath);
}
Expand Down