diff --git a/__tests__/runtime/__snapshots__/integration.test.ts.snap b/__tests__/runtime/__snapshots__/integration.test.ts.snap index c418bbad..98499d1d 100644 --- a/__tests__/runtime/__snapshots__/integration.test.ts.snap +++ b/__tests__/runtime/__snapshots__/integration.test.ts.snap @@ -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 { diff --git a/__tests__/runtime/integration.test.ts b/__tests__/runtime/integration.test.ts index 2e804fb6..55c86bd9 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; @@ -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 @@ -48,7 +55,7 @@ function responseToSnapshotJson(response: InternalResponse) { }; } -describe('Function integration tests', () => { +describe('with an express app', () => { let app: Express; beforeAll(async () => { @@ -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(); + }); + } + }); }); 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..198d3615 100644 --- a/src/runtime/server.ts +++ b/src/runtime/server.ts @@ -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); }