From 0ec1ad3950acc34e20178ecf76fa38131fc68566 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Tue, 9 Jun 2020 12:01:33 +1000 Subject: [PATCH 1/3] Adds CORS headers to OPTIONS requests to assets. --- .../__snapshots__/integration.test.ts.snap | 20 ++++- __tests__/runtime/integration.test.ts | 78 ++++++++++++++++--- fixtures/assets/hello.js | 1 + src/runtime/server.ts | 11 +++ 4 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 fixtures/assets/hello.js diff --git a/__tests__/runtime/__snapshots__/integration.test.ts.snap b/__tests__/runtime/__snapshots__/integration.test.ts.snap index c418bbad..1f0e41a3 100644 --- a/__tests__/runtime/__snapshots__/integration.test.ts.snap +++ b/__tests__/runtime/__snapshots__/integration.test.ts.snap @@ -1,6 +1,24 @@ // 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", + "last-modified": "Tue, 09 Jun 2020 01:11:06 GMT", + "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 { diff --git a/__tests__/runtime/integration.test.ts b/__tests__/runtime/integration.test.ts index 2e804fb6..9313410c 100644 --- a/__tests__/runtime/integration.test.ts +++ b/__tests__/runtime/integration.test.ts @@ -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( @@ -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; @@ -48,7 +54,7 @@ function responseToSnapshotJson(response: InternalResponse) { }; } -describe('Function integration tests', () => { +describe('with an express app', () => { let app: Express; beforeAll(async () => { @@ -59,15 +65,67 @@ 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`, 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' + ); + }); + + 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(); + }); + } + }); }); diff --git a/fixtures/assets/hello.js b/fixtures/assets/hello.js new file mode 100644 index 00000000..1d156df3 --- /dev/null +++ b/fixtures/assets/hello.js @@ -0,0 +1 @@ +alert('Hello world!'); diff --git a/src/runtime/server.ts b/src/runtime/server.ts index 83563421..53fe8f75 100644 --- a/src/runtime/server.ts +++ b/src/runtime/server.ts @@ -178,6 +178,17 @@ export async function createServer( 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.sendFile(routeInfo.filePath); } } else { From 21980c6b1e9e3edeaaa65a9249e75fbbd6d04289 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Tue, 9 Jun 2020 13:29:31 +1000 Subject: [PATCH 2/3] Removes last-modified from request snapshot --- __tests__/runtime/__snapshots__/integration.test.ts.snap | 1 - __tests__/runtime/integration.test.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/runtime/__snapshots__/integration.test.ts.snap b/__tests__/runtime/__snapshots__/integration.test.ts.snap index 1f0e41a3..98499d1d 100644 --- a/__tests__/runtime/__snapshots__/integration.test.ts.snap +++ b/__tests__/runtime/__snapshots__/integration.test.ts.snap @@ -8,7 +8,6 @@ Object { "cache-control": "public, max-age=0", "connection": "close", "content-type": "application/javascript; charset=UTF-8", - "last-modified": "Tue, 09 Jun 2020 01:11:06 GMT", "x-powered-by": "Express", }, "statusCode": 200, diff --git a/__tests__/runtime/integration.test.ts b/__tests__/runtime/integration.test.ts index 9313410c..6f3a7fb5 100644 --- a/__tests__/runtime/integration.test.ts +++ b/__tests__/runtime/integration.test.ts @@ -36,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 From 65579f789763bf50cecf27cc96e72366396a9ec1 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Tue, 16 Jun 2020 10:31:58 +1000 Subject: [PATCH 3/3] Don't return body for OPTIONS request to assets. --- __tests__/runtime/integration.test.ts | 3 ++- src/runtime/server.ts | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/__tests__/runtime/integration.test.ts b/__tests__/runtime/integration.test.ts index 6f3a7fb5..55c86bd9 100644 --- a/__tests__/runtime/integration.test.ts +++ b/__tests__/runtime/integration.test.ts @@ -88,7 +88,7 @@ describe('with an express app', () => { expect(result).toMatchSnapshot(); }); - test(`OPTIONS request to ${testAsset.name} should return CORS headers`, async () => { + test(`OPTIONS request to ${testAsset.name} should return CORS headers and no body`, async () => { const response = (await request(app).options( testAsset.url )) as InternalResponse; @@ -106,6 +106,7 @@ describe('with an express app', () => { 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 () => { diff --git a/src/runtime/server.ts b/src/runtime/server.ts index 53fe8f75..198d3615 100644 --- a/src/runtime/server.ts +++ b/src/runtime/server.ts @@ -177,18 +177,18 @@ 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 { - 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.sendFile(routeInfo.filePath); } } else {