forked from redwoodjs/redwood
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CORS fixes for 1.1.0+ (redwoodjs#5429)
* Make cors mapping testable * Just supply the cors object instead of doing a builder * Add origins in config too * Fix import * Start adding tests for createGraphqlHandler * Try with latest canary build * Cleanup mocked lambda event * Downgrade undici fetch | Set yoga cors to false if no redwood cors options supplied (still to be implemented in yoga) * Downgrade cross-undici-fetch everywhere * Bump yoga to 2.5.0
- Loading branch information
Showing
10 changed files
with
392 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
import type { APIGatewayProxyEvent, Context } from 'aws-lambda' | ||
|
||
import { createLogger } from '@redwoodjs/api/logger' | ||
|
||
import { createGraphQLHandler } from '../functions/graphql' | ||
|
||
jest.mock('../makeMergedSchema/makeMergedSchema', () => { | ||
const { makeExecutableSchema } = require('@graphql-tools/schema') | ||
// Return executable schema | ||
return { | ||
makeMergedSchema: () => | ||
makeExecutableSchema({ | ||
typeDefs: /* GraphQL */ ` | ||
type Query { | ||
me: User! | ||
} | ||
type Query { | ||
forbiddenUser: User! | ||
getUser(id: Int!): User! | ||
} | ||
type User { | ||
id: ID! | ||
name: String! | ||
} | ||
`, | ||
resolvers: { | ||
Query: { | ||
me: () => { | ||
return { _id: 1, firstName: 'Ba', lastName: 'Zinga' } | ||
}, | ||
forbiddenUser: () => { | ||
throw Error('You are forbidden') | ||
}, | ||
getUser: (id) => { | ||
return { id, firstName: 'Ba', lastName: 'Zinga' } | ||
}, | ||
}, | ||
User: { | ||
id: (u) => u._id, | ||
name: (u) => `${u.firstName} ${u.lastName}`, | ||
}, | ||
}, | ||
}), | ||
} | ||
}) | ||
|
||
jest.mock('../directives/makeDirectives', () => { | ||
return { | ||
makeDirectivesForPlugin: () => [], | ||
} | ||
}) | ||
|
||
const mockLambdaEvent = ({ | ||
headers, | ||
body = null, | ||
httpMethod, | ||
...others | ||
}): APIGatewayProxyEvent => { | ||
return { | ||
headers, | ||
body, | ||
httpMethod, | ||
multiValueQueryStringParameters: null, | ||
isBase64Encoded: false, | ||
multiValueHeaders: {}, | ||
path: '', | ||
pathParameters: null, | ||
stageVariables: null, | ||
queryStringParameters: null, | ||
requestContext: null, | ||
resource: null, | ||
...others, | ||
} | ||
} | ||
|
||
describe('CORS', () => { | ||
it('Returns the origin correctly when configured in handler', async () => { | ||
const handler = createGraphQLHandler({ | ||
loggerConfig: { logger: createLogger({}), options: {} }, | ||
sdls: {}, | ||
directives: {}, | ||
services: {}, | ||
cors: { | ||
origin: 'https://web.redwoodjs.com', | ||
}, | ||
onException: () => {}, | ||
}) | ||
|
||
const mockedEvent = mockLambdaEvent({ | ||
headers: { | ||
origin: 'https://redwoodjs.com', | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ query: '{ me { id, name } }' }), | ||
httpMethod: 'POST', | ||
}) | ||
|
||
const response = await handler(mockedEvent, {} as Context) | ||
|
||
expect(response.statusCode).toBe(200) | ||
expect(response.multiValueHeaders['access-control-allow-origin']).toEqual([ | ||
'https://web.redwoodjs.com', | ||
]) | ||
}) | ||
|
||
it('Returns requestOrigin if cors origin set to true', async () => { | ||
const handler = createGraphQLHandler({ | ||
loggerConfig: { logger: createLogger({}), options: {} }, | ||
sdls: {}, | ||
directives: {}, | ||
services: {}, | ||
cors: { | ||
origin: true, | ||
}, | ||
onException: () => {}, | ||
}) | ||
|
||
const mockedEvent = mockLambdaEvent({ | ||
headers: { | ||
origin: 'https://someothersite.newjsframework.com', | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ query: '{ me { id, name } }' }), | ||
httpMethod: 'POST', | ||
}) | ||
|
||
const response = await handler(mockedEvent, {} as Context) | ||
|
||
expect(response.statusCode).toBe(200) | ||
expect(response.multiValueHeaders['access-control-allow-origin']).toEqual([ | ||
'https://someothersite.newjsframework.com', | ||
]) | ||
}) | ||
|
||
it('Returns the origin for OPTIONS requests', async () => { | ||
const handler = createGraphQLHandler({ | ||
loggerConfig: { logger: createLogger({}), options: {} }, | ||
sdls: {}, | ||
directives: {}, | ||
services: {}, | ||
cors: { | ||
origin: 'https://mycrossdomainsite.co.uk', | ||
}, | ||
onException: () => {}, | ||
}) | ||
|
||
const mockedEvent = mockLambdaEvent({ | ||
headers: { | ||
origin: 'https://someothersite.newjsframework.com', | ||
'Content-Type': 'application/json', | ||
}, | ||
httpMethod: 'OPTIONS', | ||
}) | ||
|
||
const response = await handler(mockedEvent, {} as Context) | ||
|
||
expect(response.statusCode).toBe(204) | ||
expect(response.multiValueHeaders['access-control-allow-origin']).toEqual([ | ||
'https://mycrossdomainsite.co.uk', | ||
]) | ||
}) | ||
|
||
it('Does not return cross origin headers if option not specified', async () => { | ||
const handler = createGraphQLHandler({ | ||
loggerConfig: { logger: createLogger({}), options: {} }, | ||
sdls: {}, | ||
directives: {}, | ||
services: {}, | ||
onException: () => {}, | ||
}) | ||
|
||
const mockedEvent = mockLambdaEvent({ | ||
headers: { | ||
origin: 'https://someothersite.newjsframework.com', | ||
'Content-Type': 'application/json', | ||
}, | ||
httpMethod: 'OPTIONS', | ||
}) | ||
|
||
const response = await handler(mockedEvent, {} as Context) | ||
|
||
expect(response.statusCode).toBe(204) | ||
const responeHeaderKeys = Object.keys(response.multiValueHeaders) | ||
|
||
expect(responeHeaderKeys).not.toContain('access-control-allow-origin') | ||
expect(responeHeaderKeys).not.toContain('access-control-allow-credentials') | ||
}) | ||
|
||
it('Returns the requestOrigin when moore than one origin supplied in config', async () => { | ||
const handler = createGraphQLHandler({ | ||
loggerConfig: { logger: createLogger({}), options: {} }, | ||
sdls: {}, | ||
directives: {}, | ||
services: {}, | ||
cors: { | ||
origin: ['https://site1.one', 'https://site2.two'], | ||
}, | ||
onException: () => {}, | ||
}) | ||
|
||
const mockedEvent: APIGatewayProxyEvent = mockLambdaEvent({ | ||
headers: { | ||
origin: 'https://site2.two', | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ query: '{ me { id, name } }' }), | ||
httpMethod: 'POST', | ||
}) | ||
|
||
const response = await handler(mockedEvent, {} as Context) | ||
|
||
expect(response.statusCode).toBe(200) | ||
expect(response.multiValueHeaders['access-control-allow-origin']).toEqual([ | ||
'https://site2.two', | ||
]) | ||
}) | ||
}) |
100 changes: 100 additions & 0 deletions
100
packages/graphql-server/src/__tests__/mapRwCorsToYoga.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { mapRwCorsOptionsToYoga } from '../cors' | ||
|
||
/** Yoga CORS Options looks like | ||
* | ||
* export interface CORSOptions { | ||
origin?: string[]; | ||
methods?: string[]; | ||
allowedHeaders?: string[]; | ||
exposedHeaders?: string[]; | ||
credentials?: boolean; | ||
maxAge?: number; | ||
} | ||
* | ||
*/ | ||
describe('mapRwCorsOptionsToYoga', () => { | ||
it('Handles single endpoint, headers and method', () => { | ||
const output = mapRwCorsOptionsToYoga({ | ||
origin: 'http://localhost:8910', | ||
allowedHeaders: 'X-Bazinga', | ||
methods: 'PATCH', | ||
credentials: true, | ||
}) | ||
|
||
expect(output).toEqual({ | ||
credentials: true, | ||
allowedHeaders: ['X-Bazinga'], | ||
methods: ['PATCH'], | ||
origin: ['http://localhost:8910'], | ||
}) | ||
}) | ||
|
||
it('Handles options as an array', () => { | ||
const output = mapRwCorsOptionsToYoga({ | ||
origin: ['http://localhost:8910'], | ||
credentials: false, | ||
allowedHeaders: ['X-Bazinga', 'X-Kittens', 'Authorization'], | ||
methods: ['PATCH', 'PUT', 'POST'], | ||
}) | ||
|
||
expect(output).toEqual({ | ||
origin: ['http://localhost:8910'], | ||
methods: ['PATCH', 'PUT', 'POST'], | ||
allowedHeaders: ['X-Bazinga', 'X-Kittens', 'Authorization'], | ||
}) | ||
}) | ||
|
||
it('Handles multiple endpoints', () => { | ||
const output = mapRwCorsOptionsToYoga({ | ||
origin: ['https://bazinga.com', 'https://softkitty.mew'], | ||
credentials: true, | ||
allowedHeaders: ['X-Bazinga', 'X-Kittens', 'Authorization'], | ||
methods: ['PATCH', 'PUT', 'POST'], | ||
}) | ||
|
||
expect(output).toEqual({ | ||
credentials: true, | ||
origin: ['https://bazinga.com', 'https://softkitty.mew'], | ||
methods: ['PATCH', 'PUT', 'POST'], | ||
allowedHeaders: ['X-Bazinga', 'X-Kittens', 'Authorization'], | ||
}) | ||
}) | ||
|
||
it('Returns the request origin, if cors origin is set to true', () => { | ||
const output = mapRwCorsOptionsToYoga( | ||
{ | ||
origin: true, | ||
credentials: true, | ||
allowedHeaders: ['Auth-Provider', 'X-Kittens', 'Authorization'], | ||
methods: ['DELETE'], | ||
}, | ||
'https://myapiside.redwood.com' // <-- this is the Request.headers.origin | ||
) | ||
|
||
expect(output).toEqual({ | ||
credentials: true, | ||
origin: ['https://myapiside.redwood.com'], | ||
methods: ['DELETE'], | ||
allowedHeaders: ['Auth-Provider', 'X-Kittens', 'Authorization'], | ||
}) | ||
}) | ||
|
||
it('Returns the *, if cors origin is set to true AND no request origin supplied', () => { | ||
const output = mapRwCorsOptionsToYoga( | ||
{ | ||
origin: true, | ||
credentials: true, | ||
allowedHeaders: ['Auth-Provider', 'X-Kittens', 'Authorization'], | ||
methods: ['DELETE'], | ||
}, | ||
undefined | ||
) | ||
|
||
expect(output).toEqual({ | ||
credentials: true, | ||
origin: ['*'], | ||
methods: ['DELETE'], | ||
allowedHeaders: ['Auth-Provider', 'X-Kittens', 'Authorization'], | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.