Skip to content

Commit 46fffa0

Browse files
authored
feat(runtime): add CORS headers to OPTIONS requests to assets (#141)
1 parent ec26ac5 commit 46fffa0

File tree

4 files changed

+100
-11
lines changed

4 files changed

+100
-11
lines changed

__tests__/runtime/__snapshots__/integration.test.ts.snap

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`Function integration tests basic-twiml.js should match snapshot 1`] = `
3+
exports[`with an express app Assets integration tests hello.js should match snapshot 1`] = `
4+
Object {
5+
"body": Object {},
6+
"headers": Object {
7+
"accept-ranges": "bytes",
8+
"cache-control": "public, max-age=0",
9+
"connection": "close",
10+
"content-type": "application/javascript; charset=UTF-8",
11+
"x-powered-by": "Express",
12+
},
13+
"statusCode": 200,
14+
"text": "alert('Hello world!');
15+
",
16+
"type": "application/javascript",
17+
}
18+
`;
19+
20+
exports[`with an express app Function integration tests basic-twiml.js should match snapshot 1`] = `
421
Object {
522
"body": Object {},
623
"headers": Object {

__tests__/runtime/integration.test.ts

+70-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createServer } from '../../src/runtime/server';
1010
const TEST_DIR = resolve(__dirname, '../../fixtures');
1111

1212
const TEST_FUNCTIONS_DIR = resolve(TEST_DIR, 'functions');
13+
const TEST_ASSETS_DIR = resolve(TEST_DIR, 'assets');
1314
const TEST_ENV = {};
1415

1516
const availableFunctions = readdirSync(TEST_FUNCTIONS_DIR).map(
@@ -19,6 +20,11 @@ const availableFunctions = readdirSync(TEST_FUNCTIONS_DIR).map(
1920
return { name, url, path };
2021
}
2122
);
23+
const availableAssets = readdirSync(TEST_ASSETS_DIR).map((name: string) => {
24+
const path = resolve(TEST_ASSETS_DIR, name);
25+
const url = `/${name}`;
26+
return { name, url, path };
27+
});
2228

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

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

51-
describe('Function integration tests', () => {
58+
describe('with an express app', () => {
5259
let app: Express;
5360

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

62-
for (const testFnCode of availableFunctions) {
63-
test(`${testFnCode.name} should match snapshot`, async () => {
64-
const response = await request(app).get(testFnCode.url);
65-
if (response.status === 500) {
66-
expect(response.text).toMatch(/Error/);
67-
} else {
69+
describe('Function integration tests', () => {
70+
for (const testFnCode of availableFunctions) {
71+
test(`${testFnCode.name} should match snapshot`, async () => {
72+
const response = await request(app).get(testFnCode.url);
73+
if (response.status === 500) {
74+
expect(response.text).toMatch(/Error/);
75+
} else {
76+
const result = responseToSnapshotJson(response as InternalResponse);
77+
expect(result).toMatchSnapshot();
78+
}
79+
});
80+
}
81+
});
82+
83+
describe('Assets integration tests', () => {
84+
for (const testAsset of availableAssets) {
85+
test(`${testAsset.name} should match snapshot`, async () => {
86+
const response = await request(app).get(testAsset.url);
6887
const result = responseToSnapshotJson(response as InternalResponse);
6988
expect(result).toMatchSnapshot();
70-
}
71-
});
72-
}
89+
});
90+
91+
test(`OPTIONS request to ${testAsset.name} should return CORS headers and no body`, async () => {
92+
const response = (await request(app).options(
93+
testAsset.url
94+
)) as InternalResponse;
95+
expect(response.headers['access-control-allow-origin']).toEqual('*');
96+
expect(response.headers['access-control-allow-headers']).toEqual(
97+
'Accept, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since'
98+
);
99+
expect(response.headers['access-control-allow-methods']).toEqual(
100+
'GET, POST, OPTIONS'
101+
);
102+
expect(response.headers['access-control-expose-headers']).toEqual(
103+
'ETag'
104+
);
105+
expect(response.headers['access-control-max-age']).toEqual('86400');
106+
expect(response.headers['access-control-allow-credentials']).toEqual(
107+
'true'
108+
);
109+
expect(response.text).toEqual('');
110+
});
111+
112+
test(`GET request to ${testAsset.name} should not return CORS headers`, async () => {
113+
const response = (await request(app).get(
114+
testAsset.url
115+
)) as InternalResponse;
116+
expect(response.headers['access-control-allow-origin']).toBeUndefined();
117+
expect(
118+
response.headers['access-control-allow-headers']
119+
).toBeUndefined();
120+
expect(
121+
response.headers['access-control-allow-methods']
122+
).toBeUndefined();
123+
expect(
124+
response.headers['access-control-expose-headers']
125+
).toBeUndefined();
126+
expect(response.headers['access-control-max-age']).toBeUndefined();
127+
expect(
128+
response.headers['access-control-allow-credentials']
129+
).toBeUndefined();
130+
});
131+
}
132+
});
73133
});

fixtures/assets/hello.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
alert('Hello world!');

src/runtime/server.ts

+11
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,17 @@ export async function createServer(
177177
if (routeInfo.filePath) {
178178
if (routeInfo.access === 'private') {
179179
res.status(403).send('This asset has been marked as private');
180+
} else if (req.method === 'OPTIONS') {
181+
res.set({
182+
'access-control-allow-origin': '*',
183+
'access-control-allow-headers':
184+
'Accept, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since',
185+
'access-control-allow-methods': 'GET, POST, OPTIONS',
186+
'access-control-expose-headers': 'ETag',
187+
'access-control-max-age': '86400',
188+
'access-control-allow-credentials': true,
189+
});
190+
res.status(200).end();
180191
} else {
181192
res.sendFile(routeInfo.filePath);
182193
}

0 commit comments

Comments
 (0)