From 8ba2f19a3a569b1329a2693d8cb6dc8eabb5e080 Mon Sep 17 00:00:00 2001 From: Doug Moscrop Date: Thu, 20 Oct 2016 19:35:30 -0400 Subject: [PATCH 1/3] refactor apiGateway resources and methods --- .../deploy/compile/events/apiGateway/index.js | 11 +- .../compile/events/apiGateway/lib/cors.js | 75 + .../events/apiGateway/lib/deployment.js | 2 +- .../apiGateway/lib/method/authorization.js | 24 + .../apiGateway/lib/method/integration.js | 194 +++ .../events/apiGateway/lib/method/responses.js | 61 + .../compile/events/apiGateway/lib/methods.js | 665 +-------- .../events/apiGateway/lib/resources.js | 129 +- .../compile/events/apiGateway/lib/validate.js | 227 +++ .../compile/events/apiGateway/tests/all.js | 1 + .../compile/events/apiGateway/tests/cors.js | 126 ++ .../events/apiGateway/tests/deployment.js | 2 +- .../events/apiGateway/tests/methods.js | 1266 ++++++----------- .../events/apiGateway/tests/resources.js | 312 ++-- .../events/apiGateway/tests/validate.js | 810 ++++++++++- 15 files changed, 2237 insertions(+), 1668 deletions(-) create mode 100644 lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js create mode 100644 lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/authorization.js create mode 100644 lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js create mode 100644 lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/responses.js create mode 100644 lib/plugins/aws/deploy/compile/events/apiGateway/tests/cors.js diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/index.js b/lib/plugins/aws/deploy/compile/events/apiGateway/index.js index 5b0f2def9..a019e8937 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/index.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/index.js @@ -6,10 +6,14 @@ const validate = require('./lib/validate'); const compileRestApi = require('./lib/restApi'); const compileApiKeys = require('./lib/apiKeys'); const compileResources = require('./lib/resources'); +const compileCors = require('./lib/cors'); const compileMethods = require('./lib/methods'); const compileAuthorizers = require('./lib/authorizers'); const compileDeployment = require('./lib/deployment'); const compilePermissions = require('./lib/permissions'); +const getMethodAuthorization = require('./lib/method/authorization'); +const getMethodIntegration = require('./lib/method/integration'); +const getMethodResponses = require('./lib/method/responses'); class AwsCompileApigEvents { constructor(serverless, options) { @@ -23,10 +27,14 @@ class AwsCompileApigEvents { compileRestApi, compileApiKeys, compileResources, + compileCors, compileMethods, compileAuthorizers, compileDeployment, - compilePermissions + compilePermissions, + getMethodAuthorization, + getMethodIntegration, + getMethodResponses ); this.hooks = { @@ -40,6 +48,7 @@ class AwsCompileApigEvents { return BbPromise.bind(this) .then(this.compileRestApi) .then(this.compileResources) + .then(this.compileCors) .then(this.compileMethods) .then(this.compileAuthorizers) .then(this.compileDeployment) diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js new file mode 100644 index 000000000..3d0dbbf05 --- /dev/null +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js @@ -0,0 +1,75 @@ +'use strict'; + +const _ = require('lodash'); +const BbPromise = require('bluebird'); + +module.exports = { + + compileCors() { + _.forEach(this.validated.corsPreflight, (config, path) => { + const resourceName = this.getResourceName(path); + const resourceRef = this.getResourceId(path); + + const preflightHeaders = { + 'Access-Control-Allow-Origin': `'${config.origins.join(',')}'`, + 'Access-Control-Allow-Headers': `'${config.headers.join(',')}'`, + 'Access-Control-Allow-Methods': `'${config.methods.join(',')}'`, + }; + + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [`ApiGatewayMethod${resourceName}Options`]: { + Type: 'AWS::ApiGateway::Method', + Properties: { + AuthorizationType: 'NONE', + HttpMethod: 'OPTIONS', + MethodResponses: this.generateCorsMethodResponses(preflightHeaders), + RequestParameters: {}, + Integration: { + Type: 'MOCK', + RequestTemplates: { + 'application/json': '{statusCode:200}', + }, + IntegrationResponses: this.generateCorsIntegrationResponses(preflightHeaders), + }, + ResourceId: resourceRef, + RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, + }, + }, + }); + }); + + return BbPromise.resolve(); + }, + + generateCorsMethodResponses(preflightHeaders) { + const methodResponseHeaders = {}; + + _.forEach(preflightHeaders, (value, header) => { + methodResponseHeaders[`method.response.header.${header}`] = true; + }); + + return [ + { + StatusCode: '200', + ResponseParameters: methodResponseHeaders, + ResponseModels: {}, + }, + ]; + }, + + generateCorsIntegrationResponses(preflightHeaders) { + const responseParameters = _.mapKeys(preflightHeaders, + (value, header) => `method.response.header.${header}`); + + return [ + { + StatusCode: '200', + ResponseParameters: responseParameters, + ResponseTemplates: { + 'application/json': '', + }, + }, + ]; + }, + +}; diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/deployment.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/deployment.js index 1eb6c692a..0a2b4b05d 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/deployment.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/deployment.js @@ -14,7 +14,7 @@ module.exports = { RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, StageName: this.options.stage, }, - DependsOn: this.methodDependencies, + DependsOn: this.apiGatewayMethodLogicalIds, }, }); diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/authorization.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/authorization.js new file mode 100644 index 000000000..ac0a89687 --- /dev/null +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/authorization.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = { + getMethodAuthorization(http) { + if (http.authorizer) { + const normalizedAuthorizerName = http.authorizer.name[0].toUpperCase() + + http.authorizer.name.substr(1); + const authorizerLogicalId = `${normalizedAuthorizerName}ApiGatewayAuthorizer`; + + return { + Properties: { + AuthorizationType: 'CUSTOM', + AuthorizerId: { Ref: authorizerLogicalId }, + }, + DependsOn: authorizerLogicalId, + }; + } + return { + Properties: { + AuthorizationType: 'NONE', + }, + }; + }, +}; diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js new file mode 100644 index 000000000..f94269886 --- /dev/null +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js @@ -0,0 +1,194 @@ +'use strict'; + +const _ = require('lodash'); + +module.exports = { + getMethodIntegration(http, functionName) { + const normalizedFunctionName = functionName[0].toUpperCase() + + functionName.substr(1); + const integration = { + IntegrationHttpMethod: 'POST', + Type: http.integration, + Uri: { + 'Fn::Join': ['', + [ + 'arn:aws:apigateway:', + { Ref: 'AWS::Region' }, + ':lambda:path/2015-03-31/functions/', + { 'Fn::GetAtt': [`${normalizedFunctionName}LambdaFunction`, 'Arn'] }, + '/invocations', + ], + ], + }, + }; + + if (http.integration === 'AWS') { + _.assign(integration, { + PassthroughBehavior: http.request && http.request.passThrough, + RequestTemplates: this.getIntegrationRequestTemplates(http), + IntegrationResponses: this.getIntegrationResponses(http), + }); + } + + return { + Properties: { + Integration: integration, + }, + }; + }, + + getIntegrationResponses(http) { + const integrationResponses = []; + + if (http.response) { + const integrationResponseHeaders = []; + + if (http.cors) { + _.merge(integrationResponseHeaders, { + 'Access-Control-Allow-Origin': `'${http.cors.origins.join(',')}'`, + }); + } + + if (http.response.headers) { + _.merge(integrationResponseHeaders, http.response.headers); + } + + _.each(http.response.statusCodes, (config, statusCode) => { + const responseParameters = _.mapKeys(integrationResponseHeaders, + (value, header) => `method.response.header.${header}`); + + const integrationResponse = { + StatusCode: parseInt(statusCode, 10), + SelectionPattern: config.pattern || '', + ResponseParameters: responseParameters, + ResponseTemplates: {}, + }; + + if (config.headers) { + _.merge(integrationResponse.ResponseParameters, _.mapKeys(config.headers, + (value, header) => `method.response.header.${header}`)); + } + + if (http.response.template) { + _.merge(integrationResponse.ResponseTemplates, { + 'application/json': http.response.template, + }); + } + + if (config.template) { + const template = typeof config.template === 'string' ? + { 'application/json': config.template } + : config.template; + + _.merge(integrationResponse.ResponseTemplates, template); + } + + integrationResponses.push(integrationResponse); + }); + } + + return integrationResponses; + }, + + getIntegrationRequestTemplates(http) { + // default request templates + const integrationRequestTemplates = { + 'application/json': this.DEFAULT_JSON_REQUEST_TEMPLATE, + 'application/x-www-form-urlencoded': this.DEFAULT_FORM_URL_ENCODED_REQUEST_TEMPLATE, + }; + + // set custom request templates if provided + if (http.request && typeof http.request.template === 'object') { + _.assign(integrationRequestTemplates, http.request.template); + } + + return integrationRequestTemplates; + }, + + DEFAULT_JSON_REQUEST_TEMPLATE: ` + #define( $loop ) + { + #foreach($key in $map.keySet()) + "$util.escapeJavaScript($key)": + "$util.escapeJavaScript($map.get($key))" + #if( $foreach.hasNext ) , #end + #end + } + #end + + { + "body": $input.json("$"), + "method": "$context.httpMethod", + "principalId": "$context.authorizer.principalId", + "stage": "$context.stage", + + #set( $map = $input.params().header ) + "headers": $loop, + + #set( $map = $input.params().querystring ) + "query": $loop, + + #set( $map = $input.params().path ) + "path": $loop, + + #set( $map = $context.identity ) + "identity": $loop, + + #set( $map = $stageVariables ) + "stageVariables": $loop + } + `, + + DEFAULT_FORM_URL_ENCODED_REQUEST_TEMPLATE: ` + #define( $body ) + { + #foreach( $token in $input.path('$').split('&') ) + #set( $keyVal = $token.split('=') ) + #set( $keyValSize = $keyVal.size() ) + #if( $keyValSize >= 1 ) + #set( $key = $util.escapeJavaScript($util.urlDecode($keyVal[0])) ) + #if( $keyValSize >= 2 ) + #set( $val = $util.escapeJavaScript($util.urlDecode($keyVal[1])) ) + #else + #set( $val = '' ) + #end + "$key": "$val"#if($foreach.hasNext),#end + #end + #end + } + #end + + #define( $loop ) + { + #foreach($key in $map.keySet()) + "$util.escapeJavaScript($key)": + "$util.escapeJavaScript($map.get($key))" + #if( $foreach.hasNext ) , #end + #end + } + #end + + { + "body": $body, + "method": "$context.httpMethod", + "principalId": "$context.authorizer.principalId", + "stage": "$context.stage", + + #set( $map = $input.params().header ) + "headers": $loop, + + #set( $map = $input.params().querystring ) + "query": $loop, + + #set( $map = $input.params().path ) + "path": $loop, + + #set( $map = $context.identity ) + "identity": $loop, + + #set( $map = $stageVariables ) + "stageVariables": $loop + } + `, + +}; diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/responses.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/responses.js new file mode 100644 index 000000000..776306a01 --- /dev/null +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/responses.js @@ -0,0 +1,61 @@ +'use strict'; + +const _ = require('lodash'); + +module.exports = { + + getMethodResponses(http) { + const methodResponses = []; + + if (http.integration === 'AWS') { + if (http.response) { + const methodResponseHeaders = []; + + if (http.cors) { + _.merge(methodResponseHeaders, { + 'Access-Control-Allow-Origin': `'${http.cors.origins.join(',')}'`, + }); + } + + if (http.response.headers) { + _.merge(methodResponseHeaders, http.response.headers); + } + + _.each(http.response.statusCodes, (config, statusCode) => { + const methodResponse = { + ResponseParameters: {}, + ResponseModels: {}, + StatusCode: parseInt(statusCode, 10), + }; + + _.merge(methodResponse.ResponseParameters, + this.getMethodResponseHeaders(methodResponseHeaders)); + + if (config.headers) { + _.merge(methodResponse.ResponseParameters, + this.getMethodResponseHeaders(config.headers)); + } + + methodResponses.push(methodResponse); + }); + } + } + + return { + Properties: { + MethodResponses: methodResponses, + }, + }; + }, + + getMethodResponseHeaders(headers) { + const methodResponseHeaders = {}; + + Object.keys(headers).forEach(header => { + methodResponseHeaders[`method.response.header.${header}`] = true; + }); + + return methodResponseHeaders; + }, + +}; diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js index 0c019085f..dc13a3221 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js @@ -3,658 +3,47 @@ const BbPromise = require('bluebird'); const _ = require('lodash'); -const NOT_FOUND = -1; - module.exports = { + compileMethods() { - const corsPreflight = {}; + this.apiGatewayMethodLogicalIds = []; - const defaultStatusCodes = { - 200: { - pattern: '', - }, - 400: { - pattern: '.*\\[400\\].*', - }, - 401: { - pattern: '.*\\[401\\].*', - }, - 403: { - pattern: '.*\\[403\\].*', - }, - 404: { - pattern: '.*\\[404\\].*', - }, - 422: { - pattern: '.*\\[422\\].*', - }, - 500: { - pattern: '.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*', - }, - 502: { - pattern: '.*\\[502\\].*', - }, - 504: { - pattern: '.*\\[504\\].*', - }, - }; - /** - * Private helper functions - */ + this.validated.events.forEach((event) => { + const resourceId = this.getResourceId(event.http.path); + const resourceName = this.getResourceName(event.http.path); + const requestParameters = (event.http.request && event.http.request.parameters) || {}; - const generateMethodResponseHeaders = (headers) => { - const methodResponseHeaders = {}; - - Object.keys(headers).forEach(header => { - methodResponseHeaders[`method.response.header.${header}`] = true; - }); - - return methodResponseHeaders; - }; - - const generateIntegrationResponseHeaders = (headers) => { - const integrationResponseHeaders = {}; - - Object.keys(headers).forEach(header => { - integrationResponseHeaders[`method.response.header.${header}`] = headers[header]; - }); - - return integrationResponseHeaders; - }; - - const generateCorsPreflightConfig = (corsConfig, corsPreflightConfig, method) => { - const headers = [ - 'Content-Type', - 'X-Amz-Date', - 'Authorization', - 'X-Api-Key', - 'X-Amz-Security-Token', - ]; - - let newCorsPreflightConfig; - - const cors = { - origins: ['*'], - methods: ['OPTIONS'], - headers, + const template = { + Type: 'AWS::ApiGateway::Method', + Properties: { + HttpMethod: event.http.method.toUpperCase(), + RequestParameters: requestParameters, + ResourceId: resourceId, + RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, + }, }; - if (typeof corsConfig === 'object') { - Object.assign(cors, corsConfig); - - cors.methods = []; - if (cors.headers) { - if (!Array.isArray(cors.headers)) { - const errorMessage = [ - 'CORS header values must be provided as an array.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes - .Error(errorMessage); - } - } else { - cors.headers = headers; - } - - if (cors.methods.indexOf('OPTIONS') === NOT_FOUND) { - cors.methods.push('OPTIONS'); - } - - if (cors.methods.indexOf(method.toUpperCase()) === NOT_FOUND) { - cors.methods.push(method.toUpperCase()); - } - } else { - cors.methods.push(method.toUpperCase()); + if (event.http.private) { + template.Properties.ApiKeyRequired = true; } - if (corsPreflightConfig) { - cors.methods = _.union(cors.methods, corsPreflightConfig.methods); - cors.headers = _.union(cors.headers, corsPreflightConfig.headers); - cors.origins = _.union(cors.origins, corsPreflightConfig.origins); - newCorsPreflightConfig = _.merge(corsPreflightConfig, cors); - } else { - newCorsPreflightConfig = cors; - } + _.merge(template, + this.getMethodAuthorization(event.http), + this.getMethodIntegration(event.http, event.functionName), + this.getMethodResponses(event.http) + ); - return newCorsPreflightConfig; - }; + const methodName = event.http.method[0].toUpperCase() + + event.http.method.substr(1).toLowerCase(); + const methodLogicalId = `ApiGatewayMethod${resourceName}${methodName}`; - const hasDefaultStatusCode = (statusCodes) => - Object.keys(statusCodes).some((statusCode) => (statusCodes[statusCode].pattern === '')); + this.apiGatewayMethodLogicalIds.push(methodLogicalId); - const generateResponse = (responseConfig) => { - const response = { - methodResponses: [], - integrationResponses: [], - }; - - const statusCodes = {}; - Object.assign(statusCodes, responseConfig.statusCodes); - - if (!hasDefaultStatusCode(statusCodes)) { - _.merge(statusCodes, { 200: defaultStatusCodes['200'] }); - } - - Object.keys(statusCodes).forEach((statusCode) => { - const methodResponse = { - ResponseParameters: {}, - ResponseModels: {}, - StatusCode: parseInt(statusCode, 10), - }; - - const integrationResponse = { - StatusCode: parseInt(statusCode, 10), - SelectionPattern: statusCodes[statusCode].pattern || '', - ResponseParameters: {}, - ResponseTemplates: {}, - }; - - _.merge(methodResponse.ResponseParameters, - generateMethodResponseHeaders(responseConfig.methodResponseHeaders)); - if (statusCodes[statusCode].headers) { - _.merge(methodResponse.ResponseParameters, - generateMethodResponseHeaders(statusCodes[statusCode].headers)); - } - - _.merge(integrationResponse.ResponseParameters, - generateIntegrationResponseHeaders(responseConfig.integrationResponseHeaders)); - if (statusCodes[statusCode].headers) { - _.merge(integrationResponse.ResponseParameters, - generateIntegrationResponseHeaders(statusCodes[statusCode].headers)); - } - - if (responseConfig.integrationResponseTemplate) { - _.merge(integrationResponse.ResponseTemplates, { - 'application/json': responseConfig.integrationResponseTemplate, - }); - } - - if (statusCodes[statusCode].template) { - if (typeof statusCodes[statusCode].template === 'string') { - _.merge(integrationResponse.ResponseTemplates, { - 'application/json': statusCodes[statusCode].template, - }); - } else { - _.merge(integrationResponse.ResponseTemplates, statusCodes[statusCode].template); - } - } - - response.methodResponses.push(methodResponse); - response.integrationResponses.push(integrationResponse); - }); - - return response; - }; - - const hasRequestTemplate = (event) => { - // check if custom request configuration should be used - if (Boolean(event.http.request) === true) { - if (typeof event.http.request === 'object') { - // merge custom request templates if provided - if (Boolean(event.http.request.template) === true) { - if (typeof event.http.request.template === 'object') { - return true; - } - - const errorMessage = [ - 'Template config must be provided as an object.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - } else { - const errorMessage = [ - 'Request config must be provided as an object.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - } - - return false; - }; - - const hasRequestParameters = (event) => (event.http.request && event.http.request.parameters); - - const hasPassThroughRequest = (event) => { - const requestPassThroughBehaviors = [ - 'NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES', - ]; - - if (event.http.request && Boolean(event.http.request.passThrough) === true) { - if (requestPassThroughBehaviors.indexOf(event.http.request.passThrough) === -1) { - const errorMessage = [ - 'Request passThrough "', - event.http.request.passThrough, - '" is not one of ', - requestPassThroughBehaviors.join(', '), - ].join(''); - - throw new this.serverless.classes.Error(errorMessage); - } - - return true; - } - - return false; - }; - - const hasCors = (event) => (Boolean(event.http.cors) === true); - - const hasResponseTemplate = (event) => (event.http.response && event.http.response.template); - - const hasResponseHeaders = (event) => { - // check if custom response configuration should be used - if (Boolean(event.http.response) === true) { - if (typeof event.http.response === 'object') { - // prepare the headers if set - if (Boolean(event.http.response.headers) === true) { - if (typeof event.http.response.headers === 'object') { - return true; - } - - const errorMessage = [ - 'Response headers must be provided as an object.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - } else { - const errorMessage = [ - 'Response config must be provided as an object.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - } - - return false; - }; - - const configurePreflightMethods = (corsConfig, logicalIds) => { - const preflightMethods = {}; - - _.forOwn(corsConfig, (config, path) => { - const resourceLogicalId = logicalIds[path]; - - const preflightHeaders = { - 'Access-Control-Allow-Origin': `'${config.origins.join(',')}'`, - 'Access-Control-Allow-Headers': `'${config.headers.join(',')}'`, - 'Access-Control-Allow-Methods': `'${config.methods.join(',')}'`, - }; - - const preflightMethodResponse = generateMethodResponseHeaders(preflightHeaders); - const preflightIntegrationResponse = generateIntegrationResponseHeaders(preflightHeaders); - - const preflightTemplate = ` - { - "Type" : "AWS::ApiGateway::Method", - "Properties" : { - "AuthorizationType" : "NONE", - "HttpMethod" : "OPTIONS", - "MethodResponses" : [ - { - "ResponseModels" : {}, - "ResponseParameters" : ${JSON.stringify(preflightMethodResponse)}, - "StatusCode" : "200" - } - ], - "RequestParameters" : {}, - "Integration" : { - "Type" : "MOCK", - "RequestTemplates" : { - "application/json": "{statusCode:200}" - }, - "IntegrationResponses" : [ - { - "StatusCode" : "200", - "ResponseParameters" : ${JSON.stringify(preflightIntegrationResponse)}, - "ResponseTemplates" : { - "application/json": "" - } - } - ] - }, - "ResourceId" : { "Ref": "${resourceLogicalId}" }, - "RestApiId" : { "Ref": "ApiGatewayRestApi" } - } - } - `; - const extractedResourceId = resourceLogicalId.match(/ApiGatewayResource(.*)/)[1]; - - _.merge(preflightMethods, { - [`ApiGatewayMethod${extractedResourceId}Options`]: - JSON.parse(preflightTemplate), - }); - }); - - return preflightMethods; - }; - - /** - * Lets start the real work now! - */ - _.forEach(this.serverless.service.functions, (functionObject, functionName) => { - functionObject.events.forEach(event => { - if (event.http) { - let method; - let path; - let requestPassThroughBehavior = 'NEVER'; - let integrationType = 'AWS_PROXY'; - let integrationResponseTemplate = null; - - // Validate HTTP event object - if (typeof event.http === 'object') { - method = event.http.method; - path = event.http.path; - } else if (typeof event.http === 'string') { - method = event.http.split(' ')[0]; - path = event.http.split(' ')[1]; - } else { - const errorMessage = [ - `HTTP event of function ${functionName} is not an object nor a string.`, - ' The correct syntax is: http: get users/list', - ' OR an object with "path" and "method" properties.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes - .Error(errorMessage); - } - - // Templates required to generate the cloudformation config - - const DEFAULT_JSON_REQUEST_TEMPLATE = ` - #define( $loop ) - { - #foreach($key in $map.keySet()) - "$util.escapeJavaScript($key)": - "$util.escapeJavaScript($map.get($key))" - #if( $foreach.hasNext ) , #end - #end - } - #end - - { - "body": $input.json("$"), - "method": "$context.httpMethod", - "principalId": "$context.authorizer.principalId", - "stage": "$context.stage", - - #set( $map = $input.params().header ) - "headers": $loop, - - #set( $map = $input.params().querystring ) - "query": $loop, - - #set( $map = $input.params().path ) - "path": $loop, - - #set( $map = $context.identity ) - "identity": $loop, - - #set( $map = $stageVariables ) - "stageVariables": $loop - } - `; - - const DEFAULT_FORM_URL_ENCODED_REQUEST_TEMPLATE = ` - #define( $body ) - { - #foreach( $token in $input.path('$').split('&') ) - #set( $keyVal = $token.split('=') ) - #set( $keyValSize = $keyVal.size() ) - #if( $keyValSize >= 1 ) - #set( $key = $util.escapeJavaScript($util.urlDecode($keyVal[0])) ) - #if( $keyValSize >= 2 ) - #set( $val = $util.escapeJavaScript($util.urlDecode($keyVal[1])) ) - #else - #set( $val = '' ) - #end - "$key": "$val"#if($foreach.hasNext),#end - #end - #end - } - #end - - #define( $loop ) - { - #foreach($key in $map.keySet()) - "$util.escapeJavaScript($key)": - "$util.escapeJavaScript($map.get($key))" - #if( $foreach.hasNext ) , #end - #end - } - #end - - { - "body": $body, - "method": "$context.httpMethod", - "principalId": "$context.authorizer.principalId", - "stage": "$context.stage", - - #set( $map = $input.params().header ) - "headers": $loop, - - #set( $map = $input.params().querystring ) - "query": $loop, - - #set( $map = $input.params().path ) - "path": $loop, - - #set( $map = $context.identity ) - "identity": $loop, - - #set( $map = $stageVariables ) - "stageVariables": $loop - } - `; - - // default integration request templates - const integrationRequestTemplates = { - 'application/json': DEFAULT_JSON_REQUEST_TEMPLATE, - 'application/x-www-form-urlencoded': DEFAULT_FORM_URL_ENCODED_REQUEST_TEMPLATE, - }; - - // configuring logical names for resources - const resourceLogicalId = this.resourceLogicalIds[path]; - const normalizedMethod = method[0].toUpperCase() + - method.substr(1).toLowerCase(); - const extractedResourceId = resourceLogicalId.match(/ApiGatewayResource(.*)/)[1]; - const normalizedFunctionName = functionName[0].toUpperCase() - + functionName.substr(1); - - // scaffolds for method responses headers - const methodResponseHeaders = []; - const integrationResponseHeaders = []; - const requestParameters = {}; - - // 1. Has request template - if (hasRequestTemplate(event)) { - _.forEach(event.http.request.template, (value, key) => { - const requestTemplate = {}; - requestTemplate[key] = value; - _.merge(integrationRequestTemplates, requestTemplate); - }); - } - - if (hasRequestParameters(event)) { - // only these locations are currently supported - const locations = ['querystrings', 'paths', 'headers']; - _.each(locations, (location) => { - // strip the plural s - const singular = location.substring(0, location.length - 1); - _.each(event.http.request.parameters[location], (value, key) => { - requestParameters[`method.request.${singular}.${key}`] = value; - }); - }); - } - - // 2. Has pass-through options - if (hasPassThroughRequest(event)) { - requestPassThroughBehavior = event.http.request.passThrough; - } - - // 3. Has response template - if (hasResponseTemplate(event)) { - integrationResponseTemplate = event.http.response.template; - } - - // 4. Has CORS enabled? - if (hasCors(event)) { - corsPreflight[path] = generateCorsPreflightConfig(event.http.cors, - corsPreflight[path], method); - - const corsHeader = { - 'Access-Control-Allow-Origin': - `'${corsPreflight[path].origins.join('\',\'')}'`, - }; - - _.merge(methodResponseHeaders, corsHeader); - _.merge(integrationResponseHeaders, corsHeader); - } - - // Sort out response headers - if (hasResponseHeaders(event)) { - _.merge(methodResponseHeaders, event.http.response.headers); - _.merge(integrationResponseHeaders, event.http.response.headers); - } - - // Sort out response config - const responseConfig = { - methodResponseHeaders, - integrationResponseHeaders, - integrationResponseTemplate, - }; - - // Merge in any custom response config - if (event.http.response && event.http.response.statusCodes) { - responseConfig.statusCodes = event.http.response.statusCodes; - } else { - responseConfig.statusCodes = defaultStatusCodes; - } - - const response = generateResponse(responseConfig); - - // check if LAMBDA or LAMBDA-PROXY was used for the integration type - if (typeof event.http === 'object') { - if (Boolean(event.http.integration) === true) { - // normalize the integration for further processing - const normalizedIntegration = event.http.integration.toUpperCase(); - // check if the user has entered a non-valid integration - const allowedIntegrations = [ - 'LAMBDA', 'LAMBDA-PROXY', - ]; - if (allowedIntegrations.indexOf(normalizedIntegration) === -1) { - const errorMessage = [ - `Invalid APIG integration "${event.http.integration}"`, - ` in function "${functionName}".`, - ' Supported integrations are: lambda, lambda-proxy.', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - // map the Serverless integration to the corresponding CloudFormation types - // LAMBDA --> AWS - // LAMBDA-PROXY --> AWS_PROXY - if (normalizedIntegration === 'LAMBDA') { - integrationType = 'AWS'; - } else if (normalizedIntegration === 'LAMBDA-PROXY') { - integrationType = 'AWS_PROXY'; - } else { - // default to AWS_PROXY (just in case…) - integrationType = 'AWS_PROXY'; - } - } - } - - // show a warning when request / response config is used with AWS_PROXY (LAMBDA-PROXY) - if (integrationType === 'AWS_PROXY' && ( - (!!event.http.request) || (!!event.http.response) - )) { - const warningMessage = [ - 'Warning! You\'re using the LAMBDA-PROXY in combination with request / response', - ` configuration in your function "${functionName}".`, - ' This configuration will be ignored during deployment.', - ].join(''); - this.serverless.cli.log(warningMessage); - } - - const methodTemplate = ` - { - "Type" : "AWS::ApiGateway::Method", - "Properties" : { - "AuthorizationType" : "NONE", - "HttpMethod" : "${method.toUpperCase()}", - "MethodResponses" : ${JSON.stringify(response.methodResponses)}, - "RequestParameters" : ${JSON.stringify(requestParameters)}, - "Integration" : { - "IntegrationHttpMethod" : "POST", - "Type" : "${integrationType}", - "Uri" : { - "Fn::Join": [ "", - [ - "arn:aws:apigateway:", - {"Ref" : "AWS::Region"}, - ":lambda:path/2015-03-31/functions/", - {"Fn::GetAtt" : ["${normalizedFunctionName}LambdaFunction", "Arn"]}, - "/invocations" - ] - ] - }, - "RequestTemplates" : ${JSON.stringify(integrationRequestTemplates)}, - "PassthroughBehavior": "${requestPassThroughBehavior}", - "IntegrationResponses" : ${JSON.stringify(response.integrationResponses)} - }, - "ResourceId" : { "Ref": "${resourceLogicalId}" }, - "RestApiId" : { "Ref": "ApiGatewayRestApi" } - } - } - `; - - const methodTemplateJson = JSON.parse(methodTemplate); - - // set authorizer config if available - if (event.http.authorizer) { - const authorizerName = event.http.authorizer.name; - const normalizedAuthorizerName = authorizerName[0].toUpperCase() - + authorizerName.substr(1); - const AuthorizerLogicalId = `${normalizedAuthorizerName}ApiGatewayAuthorizer`; - - methodTemplateJson.Properties.AuthorizationType = 'CUSTOM'; - methodTemplateJson.Properties.AuthorizerId = { - Ref: AuthorizerLogicalId, - }; - methodTemplateJson.DependsOn = AuthorizerLogicalId; - } - - if (event.http.private) methodTemplateJson.Properties.ApiKeyRequired = true; - - const methodObject = { - [`ApiGatewayMethod${extractedResourceId}${normalizedMethod}`]: - methodTemplateJson, - }; - - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - methodObject); - - // store a method logical id in memory to be used - // by Deployment resources "DependsOn" property - if (this.methodDependencies) { - this.methodDependencies - .push(`ApiGatewayMethod${extractedResourceId}${normalizedMethod}`); - } else { - this.methodDependencies = - [`ApiGatewayMethod${extractedResourceId}${normalizedMethod}`]; - } - } + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [methodLogicalId]: template, }); }); - if (!_.isEmpty(corsPreflight)) { - // If we have some CORS config. configure the preflight method and merge - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - configurePreflightMethods(corsPreflight, this.resourceLogicalIds)); - } - return BbPromise.resolve(); }, }; diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js index 1892be7fb..953a784c9 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js @@ -4,91 +4,78 @@ const BbPromise = require('bluebird'); const _ = require('lodash'); module.exports = { + compileResources() { - this.resourceFunctions = []; - this.resourcePaths = []; - this.resourceLogicalIds = {}; + const resourcePaths = this.getResourcePaths(); - _.forEach(this.serverless.service.functions, (functionObject, functionName) => { - functionObject.events.forEach(event => { - if (event.http) { - let path; + this.apiGatewayResourceNames = {}; + this.apiGatewayResourceLogicalIds = {}; - if (typeof event.http === 'object') { - path = event.http.path; - } else if (typeof event.http === 'string') { - path = event.http.split(' ')[1]; - } else { - const errorMessage = [ - `HTTP event of function ${functionName} is not an object nor a string.`, - ' The correct syntax is: http: get users/list', - ' OR an object with "path" and "method" proeprties.', - ' Please check the docs for more info.', - ].join(''); - throw new this.serverless.classes - .Error(errorMessage); - } + // ['users', 'users/create', 'users/create/something'] + resourcePaths.forEach(path => { + const pathArray = path.split('/'); + const resourceName = pathArray.map(this.capitalizeAlphaNumericPath).join(''); + const resourceLogicalId = `ApiGatewayResource${resourceName}`; + const pathPart = pathArray.pop(); + const parentPath = pathArray.join('/'); + const parentRef = this.getResourceId(parentPath); - while (path !== '') { - if (this.resourcePaths.indexOf(path) === -1) { - this.resourcePaths.push(path); - this.resourceFunctions.push(functionName); - } + this.apiGatewayResourceNames[path] = resourceName; + this.apiGatewayResourceLogicalIds[path] = resourceLogicalId; - const splittedPath = path.split('/'); - splittedPath.pop(); - path = splittedPath.join('/'); - } - } + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + [resourceLogicalId]: { + Type: 'AWS::ApiGateway::Resource', + Properties: { + ParentId: parentRef, + PathPart: pathPart, + RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, + }, + }, }); }); + return BbPromise.resolve(); + }, - const capitalizeAlphaNumericPath = (path) => _.upperFirst( + getResourcePaths() { + const paths = _.reduce(this.validated.events, (resourcePaths, event) => { + let path = event.http.path; + + while (path !== '') { + if (resourcePaths.indexOf(path) === -1) { + resourcePaths.push(path); + } + + const splittedPath = path.split('/'); + splittedPath.pop(); + path = splittedPath.join('/'); + } + return resourcePaths; + }, []); + // (stable) sort so that parents get processed before children + return _.sortBy(paths, path => path.split('/').length); + }, + + capitalizeAlphaNumericPath(path) { + return _.upperFirst( _.capitalize(path) .replace(/-/g, 'Dash') .replace(/\{(.*)\}/g, '$1Var') .replace(/[^0-9A-Za-z]/g, '') ); + }, - // ['users', 'users/create', 'users/create/something'] - this.resourcePaths.forEach(path => { - const resourcesArray = path.split('/'); - // resource name is the last element in the endpoint. It's not unique. - const resourceName = path.split('/')[path.split('/').length - 1]; - const resourcePath = path; - const normalizedResourceName = resourcesArray.map(capitalizeAlphaNumericPath).join(''); - const resourceLogicalId = `ApiGatewayResource${normalizedResourceName}`; - this.resourceLogicalIds[resourcePath] = resourceLogicalId; - resourcesArray.pop(); + getResourceId(path) { + if (path === '') { + return { 'Fn::GetAtt': [this.apiGatewayRestApiLogicalId, 'RootResourceId'] }; + } + return { Ref: this.apiGatewayResourceLogicalIds[path] }; + }, - let resourceParentId; - if (resourcesArray.length === 0) { - resourceParentId = '{ "Fn::GetAtt": ["ApiGatewayRestApi", "RootResourceId"] }'; - } else { - const normalizedResourceParentName = resourcesArray - .map(capitalizeAlphaNumericPath).join(''); - resourceParentId = `{ "Ref" : "ApiGatewayResource${normalizedResourceParentName}" }`; - } - - const resourceTemplate = ` - { - "Type" : "AWS::ApiGateway::Resource", - "Properties" : { - "ParentId" : ${resourceParentId}, - "PathPart" : "${resourceName}", - "RestApiId" : { "Ref" : "ApiGatewayRestApi" } - } - } - `; - - const resourceObject = { - [resourceLogicalId]: JSON.parse(resourceTemplate), - }; - - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, - resourceObject); - }); - - return BbPromise.resolve(); + getResourceName(path) { + if (path === '') { + return ''; + } + return this.apiGatewayResourceNames[path]; }, }; diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js index 99a99502c..6e51b8601 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js @@ -2,9 +2,41 @@ const _ = require('lodash'); +const NOT_FOUND = -1; +const DEFAULT_STATUS_CODES = { + 200: { + pattern: '', + }, + 400: { + pattern: '.*\\[400\\].*', + }, + 401: { + pattern: '.*\\[401\\].*', + }, + 403: { + pattern: '.*\\[403\\].*', + }, + 404: { + pattern: '.*\\[404\\].*', + }, + 422: { + pattern: '.*\\[422\\].*', + }, + 500: { + pattern: '.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*', + }, + 502: { + pattern: '.*\\[502\\].*', + }, + 504: { + pattern: '.*\\[504\\].*', + }, +}; + module.exports = { validate() { const events = []; + const corsPreflight = {}; _.forEach(this.serverless.service.functions, (functionObject, functionName) => { _.forEach(functionObject.events, (event) => { @@ -18,6 +50,60 @@ module.exports = { http.authorizer = this.getAuthorizer(http, functionName); } + if (http.cors) { + http.cors = this.getCors(http); + + const cors = corsPreflight[http.path] || {}; + + cors.headers = _.union(http.cors.headers, cors.headers); + cors.methods = _.union(http.cors.methods, cors.methods); + cors.origins = _.union(http.cors.origins, cors.origins); + + corsPreflight[http.path] = cors; + } + + http.integration = this.getIntegration(http); + + if (http.integration === 'AWS') { + if (http.request) { + http.request = this.getRequest(http); + + if (http.request.parameters) { + http.request.parameters = this.getRequestParameters(http.request); + } + } else { + http.request = {}; + } + + http.request.passThrough = this.getRequestPassThrough(http); + + if (http.response) { + http.response = this.getResponse(http); + } else { + http.response = {}; + } + + if (http.response.statusCodes) { + http.response.statusCodes = _.assign({}, http.response.statusCodes); + + if (!_.some(http.response.statusCodes, code => code.pattern === '')) { + http.response.statusCodes['200'] = DEFAULT_STATUS_CODES['200']; + } + } else { + http.response.statusCodes = DEFAULT_STATUS_CODES; + } + } else if (http.integration === 'AWS_PROXY') { + // show a warning when request / response config is used with AWS_PROXY (LAMBDA-PROXY) + if (http.request || http.response) { + const warningMessage = [ + 'Warning! You\'re using the LAMBDA-PROXY in combination with request / response', + ` configuration in your function "${functionName}".`, + ' This configuration will be ignored during deployment.', + ].join(''); + this.serverless.cli.log(warningMessage); + } + } + events.push({ functionName, http, @@ -28,6 +114,7 @@ module.exports = { return { events, + corsPreflight, }; }, @@ -146,6 +233,146 @@ module.exports = { }; }, + getCors(http) { + const headers = [ + 'Content-Type', + 'X-Amz-Date', + 'Authorization', + 'X-Api-Key', + 'X-Amz-Security-Token', + ]; + + let cors = { + origins: ['*'], + methods: ['OPTIONS'], + headers, + }; + + if (typeof http.cors === 'object') { + cors = http.cors; + cors.methods = cors.methods || []; + if (cors.headers) { + if (!Array.isArray(cors.headers)) { + const errorMessage = [ + 'CORS header values must be provided as an array.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + } else { + cors.headers = headers; + } + + if (cors.methods.indexOf('OPTIONS') === NOT_FOUND) { + cors.methods.push('OPTIONS'); + } + + if (cors.methods.indexOf(http.method.toUpperCase()) === NOT_FOUND) { + cors.methods.push(http.method.toUpperCase()); + } + } else { + cors.methods.push(http.method.toUpperCase()); + } + + return cors; + }, + + getIntegration(http) { + if (http.integration) { + const allowedIntegrations = [ + 'LAMBDA-PROXY', 'LAMBDA', + ]; + // normalize the integration for further processing + const normalizedIntegration = http.integration.toUpperCase(); + // check if the user has entered a non-valid integration + if (allowedIntegrations.indexOf(normalizedIntegration) === NOT_FOUND) { + const errorMessage = [ + `Invalid APIG integration "${http.integration}"`, + ` in function "${http.functionName}".`, + ' Supported integrations are: lambda, lambda-proxy.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + if (normalizedIntegration === 'LAMBDA') { + return 'AWS'; + } + } + return 'AWS_PROXY'; + }, + + getRequest(http) { + if (typeof http.request !== 'object') { + const errorMessage = [ + 'Request config must be provided as an object.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + if (http.request.template && typeof http.request.template !== 'object') { + const errorMessage = [ + 'Template config must be provided as an object.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + + return http.request; + }, + + getRequestParameters(httpRequest) { + const parameters = {}; + // only these locations are currently supported + const locations = ['querystrings', 'paths', 'headers']; + _.each(locations, (location) => { + // strip the plural s + const singular = location.substring(0, location.length - 1); + _.each(httpRequest.parameters[location], (value, key) => { + parameters[`method.request.${singular}.${key}`] = value; + }); + }); + return parameters; + }, + + getRequestPassThrough(http) { + const requestPassThroughBehaviors = [ + 'NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES', + ]; + + if (http.request.passThrough) { + if (requestPassThroughBehaviors.indexOf(http.request.passThrough) === -1) { + const errorMessage = [ + 'Request passThrough "', + http.request.passThrough, + '" is not one of ', + requestPassThroughBehaviors.join(', '), + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + + return http.request.passThrough; + } + + return requestPassThroughBehaviors[0]; + }, + + getResponse(http) { + if (typeof http.response !== 'object') { + const errorMessage = [ + 'Response config must be provided as an object.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + if (http.response.headers && typeof http.response.headers !== 'object') { + const errorMessage = [ + 'Response headers must be provided as an object.', + ' Please check the docs for more info.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + return http.response; + }, + getLambdaArn(name) { this.serverless.service.getFunction(name); const normalizedName = name[0].toUpperCase() + name.substr(1); diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/all.js b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/all.js index dd9f7bdc4..2f5c0a428 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/all.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/all.js @@ -5,6 +5,7 @@ require('./validate'); require('./restApi'); require('./apiKeys'); require('./resources'); +require('./cors'); require('./methods'); require('./authorizers'); require('./deployment'); diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/cors.js b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/cors.js new file mode 100644 index 000000000..5ebe97a1d --- /dev/null +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/cors.js @@ -0,0 +1,126 @@ +'use strict'; + +const expect = require('chai').expect; +const AwsCompileApigEvents = require('../index'); +const Serverless = require('../../../../../../../Serverless'); + +describe('#compileCors()', () => { + let serverless; + let awsCompileApigEvents; + + beforeEach(() => { + serverless = new Serverless(); + serverless.service.service = 'first-service'; + serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; + serverless.service.environment = { + stages: { + dev: { + regions: { + 'us-east-1': { + vars: { + IamRoleLambdaExecution: + 'arn:aws:iam::12345678:role/service-dev-IamRoleLambdaExecution-FOO12345678', + }, + }, + }, + }, + }, + }; + const options = { + stage: 'dev', + region: 'us-east-1', + }; + awsCompileApigEvents = new AwsCompileApigEvents(serverless, options); + awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'; + awsCompileApigEvents.apiGatewayResourceLogicalIds = { + 'users/create': 'ApiGatewayResourceUsersCreate', + 'users/list': 'ApiGatewayResourceUsersList', + 'users/update': 'ApiGatewayResourceUsersUpdate', + 'users/delete': 'ApiGatewayResourceUsersDelete', + }; + awsCompileApigEvents.apiGatewayResourceNames = { + 'users/create': 'UsersCreate', + 'users/list': 'UsersList', + 'users/update': 'UsersUpdate', + 'users/delete': 'UsersDelete', + }; + awsCompileApigEvents.validated = {}; + }); + + it('should create preflight method for CORS enabled resource', () => { + awsCompileApigEvents.validated.corsPreflight = { + 'users/update': { + origins: ['*'], + headers: ['*'], + methods: ['OPTIONS', 'PUT'], + }, + 'users/create': { + origins: ['*'], + headers: ['*'], + methods: ['OPTIONS', 'POST'], + }, + 'users/delete': { + origins: ['*'], + headers: ['CustomHeaderA', 'CustomHeaderB'], + methods: ['OPTIONS', 'DELETE'], + }, + }; + return awsCompileApigEvents.compileCors().then(() => { + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreateOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] + ).to.equal('\'*\''); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreateOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Allow-Headers'] + ).to.equal('\'*\''); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreateOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] + ).to.equal('\'OPTIONS,POST\''); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersUpdateOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] + ).to.equal('\'*\''); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersUpdateOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] + ).to.equal('\'OPTIONS,PUT\''); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersDeleteOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] + ).to.equal('\'*\''); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersDeleteOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Allow-Headers'] + ).to.equal('\'CustomHeaderA,CustomHeaderB\''); + + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersDeleteOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] + ).to.equal('\'OPTIONS,DELETE\''); + }); + }); +}); diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/deployment.js b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/deployment.js index 65ee42959..eb8624462 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/deployment.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/deployment.js @@ -20,7 +20,7 @@ describe('#compileDeployment()', () => { }; awsCompileApigEvents = new AwsCompileApigEvents(serverless, options); awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'; - awsCompileApigEvents.methodDependencies = ['method-dependency1', 'method-dependency2']; + awsCompileApigEvents.apiGatewayMethodLogicalIds = ['method-dependency1', 'method-dependency2']; }); it('should create a deployment resource', () => awsCompileApigEvents diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/methods.js b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/methods.js index 577b7a417..aaa51c794 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/methods.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/methods.js @@ -1,7 +1,6 @@ 'use strict'; const expect = require('chai').expect; -const sinon = require('sinon'); const AwsCompileApigEvents = require('../index'); const Serverless = require('../../../../../../../Serverless'); @@ -32,86 +31,51 @@ describe('#compileMethods()', () => { region: 'us-east-1', }; awsCompileApigEvents = new AwsCompileApigEvents(serverless, options); - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - path: 'users/create', - method: 'POST', - cors: true, - }, - }, - { - http: 'GET users/list', - }, - { - http: { - path: 'users/update', - method: 'PUT', - cors: { - origins: ['*'], - }, - }, - }, - { - http: { - path: 'users/delete', - method: 'DELETE', - cors: { - origins: ['*'], - headers: ['CustomHeaderA', 'CustomHeaderB'], - }, - }, - }, - ], - }, - }; - awsCompileApigEvents.resourceLogicalIds = { + awsCompileApigEvents.validated = {}; + awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'; + awsCompileApigEvents.apiGatewayResourceLogicalIds = { 'users/create': 'ApiGatewayResourceUsersCreate', 'users/list': 'ApiGatewayResourceUsersList', 'users/update': 'ApiGatewayResourceUsersUpdate', 'users/delete': 'ApiGatewayResourceUsersDelete', }; - }); - - it('should throw an error if http event type is not a string or an object', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: 42, - }, - ], - }, + awsCompileApigEvents.apiGatewayResourceNames = { + 'users/create': 'UsersCreate', + 'users/list': 'UsersList', + 'users/update': 'UsersUpdate', + 'users/delete': 'UsersDelete', }; - - expect(() => awsCompileApigEvents.compileMethods()).to.throw(Error); }); it('should have request parameters defined when they are set', () => { - awsCompileApigEvents.serverless.service.functions.first.events[0].http.integration = 'lambda'; - - const requestConfig = { - parameters: { - querystrings: { - foo: true, - bar: false, - }, - headers: { - foo: true, - bar: false, - }, - paths: { - foo: true, - bar: false, + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: 'users/create', + method: 'post', + integration: 'AWS', + request: { + parameters: { + 'method.request.querystring.foo': true, + 'method.request.querystring.bar': false, + 'method.request.header.foo': true, + 'method.request.header.bar': false, + 'method.request.path.foo': true, + 'method.request.path.bar': false, + }, + }, + response: { + statusCodes: { + 200: { + pattern: '', + }, + }, + }, }, }, - }; - - awsCompileApigEvents.serverless.service.functions.first.events[0].http.request = requestConfig; - - awsCompileApigEvents.compileMethods().then(() => { + ]; + return awsCompileApigEvents.compileMethods().then(() => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersCreatePost.Properties @@ -145,8 +109,24 @@ describe('#compileMethods()', () => { }); }); - it('should create method resources when http events given', () => awsCompileApigEvents - .compileMethods().then(() => { + it('should create method resources when http events given', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: 'users/create', + method: 'post', + }, + }, + { + functionName: 'Second', + http: { + method: 'get', + path: 'users/list', + }, + }, + ]; + return awsCompileApigEvents.compileMethods().then(() => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersCreatePost.Type @@ -155,19 +135,22 @@ describe('#compileMethods()', () => { awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersListGet.Type ).to.equal('AWS::ApiGateway::Method'); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdatePut.Type - ).to.equal('AWS::ApiGateway::Method'); - }) - ); - - it('should set authorizer', () => { - awsCompileApigEvents.serverless.service.functions - .first.events[0].http.authorizer = { - name: 'authorizer', - }; + }); + }); + it('should set authorizer config if given as ARN string', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + authorizer: { + name: 'Authorizer', + }, + path: 'users/create', + method: 'post', + }, + }, + ]; return awsCompileApigEvents.compileMethods().then(() => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate @@ -181,17 +164,8 @@ describe('#compileMethods()', () => { }); }); - it('should create methodDependencies array', () => awsCompileApigEvents - .compileMethods().then(() => { - expect(awsCompileApigEvents.methodDependencies.length).to.equal(4); - })); - it('should not create method resources when http events are not given', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [], - }, - }; + awsCompileApigEvents.validated.events = []; return awsCompileApigEvents.compileMethods().then(() => { expect( @@ -200,10 +174,39 @@ describe('#compileMethods()', () => { }); }); - it('should set api key as required if private endpoint', () => { - awsCompileApigEvents.serverless.service.functions - .first.events[0].http.private = true; + it('should create methodLogicalIds array', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: 'users/create', + method: 'post', + }, + }, + { + functionName: 'Second', + http: { + method: 'get', + path: 'users/list', + }, + }, + ]; + return awsCompileApigEvents.compileMethods().then(() => { + expect(awsCompileApigEvents.apiGatewayMethodLogicalIds.length).to.equal(2); + }); + }); + it('should set api key as required if private endpoint', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: 'users/create', + method: 'post', + private: true, + }, + }, + ]; return awsCompileApigEvents.compileMethods().then(() => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate @@ -213,65 +216,104 @@ describe('#compileMethods()', () => { }); it('should set the correct lambdaUri', () => { - const lambdaUriObject = { - 'Fn::Join': [ - '', [ - 'arn:aws:apigateway:', { Ref: 'AWS::Region' }, - ':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }, - '/invocations', - ], - ], - }; - + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: 'users/create', + method: 'post', + }, + }, + { + functionName: 'Second', + http: { + method: 'get', + path: 'users/list', + }, + }, + ]; return awsCompileApigEvents.compileMethods().then(() => { - expect( - JSON.stringify(awsCompileApigEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri - )).to.equal(JSON.stringify(lambdaUriObject)); - expect( - JSON.stringify(awsCompileApigEvents.serverless.service - .provider.compiledCloudFormationTemplate.Resources - .ApiGatewayMethodUsersListGet.Properties.Integration.Uri - )).to.equal(JSON.stringify(lambdaUriObject)); + expect(awsCompileApigEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersCreatePost.Properties.Integration.Uri + ).to.deep.equal({ + 'Fn::Join': [ + '', [ + 'arn:aws:apigateway:', { Ref: 'AWS::Region' }, + ':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'] }, + '/invocations', + ], + ], + }); + expect(awsCompileApigEvents.serverless.service + .provider.compiledCloudFormationTemplate.Resources + .ApiGatewayMethodUsersListGet.Properties.Integration.Uri + ).to.deep.equal({ + 'Fn::Join': [ + '', [ + 'arn:aws:apigateway:', { Ref: 'AWS::Region' }, + ':lambda:path/2015-03-31/functions/', { 'Fn::GetAtt': ['SecondLambdaFunction', 'Arn'] }, + '/invocations', + ], + ], + }); }); }); - it('should add CORS origins to method only when CORS and LAMBDA integration are enabled', () => { - const origin = '\'*\''; - - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - path: 'users/create', - method: 'POST', - integration: 'lambda', - cors: true, - }, + it('should add CORS origins to method only when CORS is enabled', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: 'users/create', + method: 'post', + integration: 'AWS', + cors: { + origins: ['*'], }, - { - http: { - path: 'users/list', - method: 'GET', - integration: 'lambda', - }, - }, - { - http: { - path: 'users/update', - method: 'PUT', - integration: 'lambda', - cors: { - origins: ['*'], + response: { + statusCodes: { + 200: { + pattern: '', }, }, }, - ], + }, }, - }; - + { + functionName: 'Second', + http: { + method: 'get', + path: 'users/list', + integration: 'AWS', + response: { + statusCodes: { + 200: { + pattern: '', + }, + }, + }, + }, + }, + { + functionName: 'Third', + http: { + path: 'users/update', + method: 'PUT', + integration: 'AWS', + response: { + statusCodes: { + 200: { + pattern: '', + }, + }, + }, + cors: { + origins: ['*'], + }, + }, + }, + ]; return awsCompileApigEvents.compileMethods().then(() => { // Check origin. expect( @@ -279,7 +321,7 @@ describe('#compileMethods()', () => { .Resources.ApiGatewayMethodUsersCreatePost.Properties .Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal(origin); + ).to.equal('\'*\''); // CORS not enabled! expect( @@ -287,177 +329,36 @@ describe('#compileMethods()', () => { .Resources.ApiGatewayMethodUsersListGet.Properties .Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.not.equal(origin); + ).to.not.equal('\'*\''); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersUpdatePut.Properties .Integration.IntegrationResponses[0] .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal(origin); - }); - }); - - it('should create preflight method for CORS enabled resource', () => { - const origin = '\'*\''; - const headers = '\'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token\''; - - return awsCompileApigEvents.compileMethods().then(() => { - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal(origin); - - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Headers'] - ).to.equal(headers); - - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreateOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] - ).to.equal('\'OPTIONS,POST\''); - - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdateOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal(origin); - - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdateOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Headers'] - ).to.equal(headers); - - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersUpdateOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] - ).to.equal('\'OPTIONS,PUT\''); - - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersDeleteOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal(origin); - - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersDeleteOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Headers'] - ).to.equal('\'CustomHeaderA,CustomHeaderB\''); - - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersDeleteOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] - ).to.equal('\'OPTIONS,DELETE\''); - }); - }); - - it('should merge all preflight origins, method, and headers for a path', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users', - cors: { - origins: [ - 'http://example.com', - ], - }, - }, - }, { - http: { - method: 'POST', - path: 'users', - cors: { - origins: [ - 'http://example2.com', - ], - }, - }, - }, { - http: { - method: 'PUT', - path: 'users/{id}', - cors: { - headers: [ - 'TestHeader', - ], - }, - }, - }, { - http: { - method: 'DELETE', - path: 'users/{id}', - cors: { - headers: [ - 'TestHeader2', - ], - }, - }, - }, - ], - }, - }; - awsCompileApigEvents.resourceLogicalIds = { - users: 'ApiGatewayResourceUsers', - 'users/{id}': 'ApiGatewayResourceUsersid', - }; - return awsCompileApigEvents.compileMethods().then(() => { - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersidOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Methods'] - ).to.equal('\'OPTIONS,DELETE,PUT\''); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Origin'] - ).to.equal('\'http://example2.com,http://example.com\''); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersidOptions - .Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Access-Control-Allow-Headers'] - ).to.equal('\'TestHeader2,TestHeader\''); + ).to.equal('\'*\''); }); }); describe('when dealing with request configuration', () => { it('should setup a default "application/json" template', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', + awsCompileApigEvents.validated.events = [ + { + functionName: 'Second', + http: { + method: 'get', + path: 'users/list', + integration: 'AWS', + response: { + statusCodes: { + 200: { + pattern: '', + }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersListGet.Properties @@ -467,20 +368,23 @@ describe('#compileMethods()', () => { }); it('should setup a default "application/x-www-form-urlencoded" template', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', + awsCompileApigEvents.validated.events = [ + { + functionName: 'Second', + http: { + method: 'get', + path: 'users/list', + integration: 'AWS', + response: { + statusCodes: { + 200: { + pattern: '', + }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersListGet.Properties @@ -489,46 +393,27 @@ describe('#compileMethods()', () => { }); }); - it('should use the default request pass-through behavior when none specified', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - }, - }, - ], - }, - }; - - return awsCompileApigEvents.compileMethods().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.PassthroughBehavior - ).to.equal('NEVER'); - }); - }); - it('should use defined pass-through behavior', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - request: { - passThrough: 'WHEN_NO_TEMPLATES', + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'GET', + path: 'users/list', + integration: 'AWS', + request: { + passThrough: 'WHEN_NO_TEMPLATES', + }, + response: { + statusCodes: { + 200: { + pattern: '', }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.PassthroughBehavior @@ -536,48 +421,30 @@ describe('#compileMethods()', () => { }); }); - it('should throw an error if an invalid pass-through value is provided', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - request: { - passThrough: 'BOGUS', - }, - }, - }, - ], - }, - }; - - expect(() => awsCompileApigEvents.compileMethods()).to.throw(Error); - }); - it('should set custom request templates', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - request: { - template: { - 'template/1': '{ "stage" : "$context.stage" }', - 'template/2': '{ "httpMethod" : "$context.httpMethod" }', - }, + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'GET', + path: 'users/list', + integration: 'AWS', + request: { + template: { + 'template/1': '{ "stage" : "$context.stage" }', + 'template/2': '{ "httpMethod" : "$context.httpMethod" }', + }, + }, + response: { + statusCodes: { + 200: { + pattern: '', }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersListGet.Properties @@ -592,25 +459,28 @@ describe('#compileMethods()', () => { }); it('should be possible to overwrite default request templates', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - request: { - template: { - 'application/json': 'overwritten-request-template-content', - }, + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'GET', + path: 'users/list', + integration: 'AWS', + request: { + template: { + 'application/json': 'overwritten-request-template-content', + }, + }, + response: { + statusCodes: { + 200: { + pattern: '', }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersListGet.Properties @@ -618,59 +488,21 @@ describe('#compileMethods()', () => { ).to.equal('overwritten-request-template-content'); }); }); - - it('should throw an error if the provided config is not an object', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - request: 'some string', - }, - }, - ], - }, - }; - - expect(() => awsCompileApigEvents.compileMethods()).to.throw(Error); - }); - - it('should throw an error if the template config is not an object', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - request: { - template: 'some string', - }, - }, - }, - ], - }, - }; - - expect(() => awsCompileApigEvents.compileMethods()).to.throw(Error); - }); }); describe('when dealing with response configuration', () => { it('should set the custom headers', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - response: { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'GET', + path: 'users/list', + integration: 'AWS', + response: { + statusCodes: { + 200: { + pattern: '', headers: { 'Content-Type': "'text/plain'", 'My-Custom-Header': 'my/custom/header', @@ -678,10 +510,9 @@ describe('#compileMethods()', () => { }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate @@ -697,23 +528,24 @@ describe('#compileMethods()', () => { }); it('should set the custom template', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - response: { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'GET', + path: 'users/list', + integration: 'AWS', + response: { + statusCodes: { + 200: { template: "$input.path('$.foo')", + pattern: '', }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate @@ -722,321 +554,146 @@ describe('#compileMethods()', () => { ).to.equal("$input.path('$.foo')"); }); }); - - it('should throw an error if the provided config is not an object', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - response: 'some string', - }, - }, - ], - }, - }; - - expect(() => awsCompileApigEvents.compileMethods()).to.throw(Error); - }); - - it('should throw an error if the headers are not objects', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - response: { - headers: 'some string', - }, - }, - }, - ], - }, - }; - - expect(() => awsCompileApigEvents.compileMethods()).to.throw(Error); - }); }); it('should add method responses for different status codes', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'get', + path: 'users/list', + integration: 'AWS', + response: { + statusCodes: { + 200: { + pattern: '', + }, + 202: { + pattern: 'foo', + }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[0].StatusCode + ).to.equal(200); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[1].StatusCode - ).to.equal(400); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[2].StatusCode - ).to.equal(401); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[3].StatusCode - ).to.equal(403); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[4].StatusCode - ).to.equal(404); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[5].StatusCode - ).to.equal(422); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[6].StatusCode - ).to.equal(500); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[7].StatusCode - ).to.equal(502); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[8].StatusCode - ).to.equal(504); + ).to.equal(202); }); }); it('should add integration responses for different status codes', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'get', + path: 'users/list', + integration: 'AWS', + response: { + statusCodes: { + 200: { + pattern: '', + }, + 202: { + pattern: 'foo', + }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] ).to.deep.equal({ - StatusCode: 400, - SelectionPattern: '.*\\[400\\].*', + StatusCode: 202, + SelectionPattern: 'foo', ResponseParameters: {}, ResponseTemplates: {}, }); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[2] + .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] ).to.deep.equal({ - StatusCode: 401, - SelectionPattern: '.*\\[401\\].*', - ResponseParameters: {}, - ResponseTemplates: {}, - }); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[3] - ).to.deep.equal({ - StatusCode: 403, - SelectionPattern: '.*\\[403\\].*', - ResponseParameters: {}, - ResponseTemplates: {}, - }); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[4] - ).to.deep.equal({ - StatusCode: 404, - SelectionPattern: '.*\\[404\\].*', - ResponseParameters: {}, - ResponseTemplates: {}, - }); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[5] - ).to.deep.equal({ - StatusCode: 422, - SelectionPattern: '.*\\[422\\].*', - ResponseParameters: {}, - ResponseTemplates: {}, - }); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[6] - ).to.deep.equal({ - StatusCode: 500, - SelectionPattern: '.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*', - ResponseParameters: {}, - ResponseTemplates: {}, - }); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[7] - ).to.deep.equal({ - StatusCode: 502, - SelectionPattern: '.*\\[502\\].*', - ResponseParameters: {}, - ResponseTemplates: {}, - }); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[8] - ).to.deep.equal({ - StatusCode: 504, - SelectionPattern: '.*\\[504\\].*', + StatusCode: 200, + SelectionPattern: '', ResponseParameters: {}, ResponseTemplates: {}, }); }); }); - it('should set "AWS_PROXY" as the default integration type', () => - awsCompileApigEvents.compileMethods().then(() => { - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.Type - ).to.equal('AWS_PROXY'); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type - ).to.equal('AWS_PROXY'); - }) - ); - - it('should set users integration type if specified', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', + it('should add fall back headers and template to statusCodes', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'GET', + path: 'users/list', + integration: 'AWS', + response: { + headers: { + 'Content-Type': 'text/csv', }, - }, - { - http: { - path: 'users/create', - method: 'POST', - integration: 'LAMBDA-PROXY', // this time use uppercase syntax - }, - }, - ], - }, - }; - - return awsCompileApigEvents.compileMethods().then(() => { - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.Type - ).to.equal('AWS'); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type - ).to.equal('AWS_PROXY'); - }); - }); - - it('should throw an error when an invalid integration type was provided', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'INVALID', - }, - }, - ], - }, - }; - - expect(() => awsCompileApigEvents.compileMethods()).to.throw(Error); - }); - - it('should show a warning message when using request / response config with LAMBDA-PROXY', () => { - // initialize so we get the log method from the CLI in place - serverless.init(); - - const logStub = sinon.stub(serverless.cli, 'log'); - - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'get', - path: 'users/list', - integration: 'lambda-proxy', // can be removed as it defaults to this - request: { - passThrough: 'NEVER', - template: { - 'template/1': '{ "stage" : "$context.stage" }', - 'template/2': '{ "httpMethod" : "$context.httpMethod" }', - }, - }, - response: { - template: "$input.path('$.foo')", + template: 'foo', + statusCodes: { + 400: { + pattern: '', }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { - expect(logStub.calledOnce).to.be.equal(true); - expect(logStub.args[0][0].length).to.be.at.least(1); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + .ResponseTemplates['application/json'] + ).to.equal('foo'); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Content-Type'] + ).to.equal('text/csv'); }); }); it('should add custom response codes', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - response: { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'GET', + path: 'users/list', + integration: 'AWS', + response: { + statusCodes: { + 200: { + pattern: '', template: '$input.path(\'$.foo\')', + }, + 404: { + pattern: '.*"statusCode":404,.*', + template: '$input.path(\'$.errorMessage\')', headers: { - 'Content-Type': 'text/csv', - }, - statusCodes: { - 404: { - pattern: '.*"statusCode":404,.*', - template: '$input.path(\'$.errorMessage\')', - headers: { - 'Content-Type': 'text/html', - }, - }, + 'Content-Type': 'text/html', }, }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate @@ -1048,11 +705,6 @@ describe('#compileMethods()', () => { .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] .SelectionPattern ).to.equal(''); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Content-Type'] - ).to.equal('text/csv'); expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] @@ -1072,38 +724,36 @@ describe('#compileMethods()', () => { }); it('should add multiple response templates for a custom response codes', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - response: { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + method: 'GET', + path: 'users/list', + integration: 'AWS', + response: { + statusCodes: { + 200: { template: '$input.path(\'$.foo\')', headers: { 'Content-Type': 'text/csv', }, - statusCodes: { - 404: { - pattern: '.*"statusCode":404,.*', - template: { - 'application/json': '$input.path(\'$.errorMessage\')', - 'application/xml': '$input.path(\'$.xml.errorMessage\')', - }, - headers: { - 'Content-Type': 'text/html', - }, - }, + }, + 404: { + pattern: '.*"statusCode":404,.*', + template: { + 'application/json': '$input.path(\'$.errorMessage\')', + 'application/xml': '$input.path(\'$.xml.errorMessage\')', + }, + headers: { + 'Content-Type': 'text/html', }, }, }, }, - ], + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate @@ -1140,93 +790,27 @@ describe('#compileMethods()', () => { .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] .ResponseParameters['method.response.header.Content-Type'] ).to.equal('text/html'); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] - .ResponseTemplates['application/json'] - ).to.equal("$input.path('$.errorMessage')"); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] - .SelectionPattern - ).to.equal('.*"statusCode":404,.*'); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] - .ResponseParameters['method.response.header.Content-Type'] - ).to.equal('text/html'); }); }); - it('should add multiple response templates for a custom response codes', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - method: 'GET', - path: 'users/list', - integration: 'lambda', - response: { - template: '$input.path(\'$.foo\')', - headers: { - 'Content-Type': 'text/csv', - }, - statusCodes: { - 404: { - pattern: '.*"statusCode":404,.*', - template: { - 'application/json': '$input.path(\'$.errorMessage\')', - 'application/xml': '$input.path(\'$.xml.errorMessage\')', - }, - headers: { - 'Content-Type': 'text/html', - }, - }, - }, - }, - }, - }, - ], + it('should handle root resource methods', () => { + awsCompileApigEvents.validated.events = [ + { + functionName: 'First', + http: { + path: '', + method: 'GET', + }, }, - }; - + ]; return awsCompileApigEvents.compileMethods().then(() => { - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] - .ResponseTemplates['application/json'] - ).to.equal("$input.path('$.foo')"); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] - .SelectionPattern - ).to.equal(''); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0] - .ResponseParameters['method.response.header.Content-Type'] - ).to.equal('text/csv'); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] - .ResponseTemplates['application/json'] - ).to.equal("$input.path('$.errorMessage')"); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] - .ResponseTemplates['application/xml'] - ).to.equal("$input.path('$.xml.errorMessage')"); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] - .SelectionPattern - ).to.equal('.*"statusCode":404,.*'); - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1] - .ResponseParameters['method.response.header.Content-Type'] - ).to.equal('text/html'); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodGet.Properties.ResourceId).to.deep.equal({ + 'Fn::GetAtt': [ + 'ApiGatewayRestApi', + 'RootResourceId', + ], + }); }); }); }); diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/resources.js b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/resources.js index 08ff6fe1c..13ab06cb3 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/resources.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/resources.js @@ -12,124 +12,232 @@ describe('#compileResources()', () => { serverless = new Serverless(); serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; awsCompileApigEvents = new AwsCompileApigEvents(serverless); - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: { - path: 'foo/bar', - method: 'POST', - }, - }, - { - http: 'GET bar/-', - }, - { - http: 'GET bar/foo', - }, - { - http: 'GET bar/{id}', - }, - { - http: 'GET bar/{id}/foobar', - }, - { - http: 'GET bar/{foo_id}', - }, - { - http: 'GET bar/{foo_id}/foobar', - }, - ], - }, - }; + awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'; + awsCompileApigEvents.validated = {}; }); - it('should throw an error if http event type is not a string or an object', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [ - { - http: 42, - }, - ], + // sorted makes parent refs easier + it('should construct the correct (sorted) resourcePaths array', () => { + awsCompileApigEvents.validated.events = [ + { + http: { + path: '', + method: 'GET', + }, }, - }; - - expect(() => awsCompileApigEvents.compileResources()).to.throw(Error); + { + http: { + path: 'foo/bar', + method: 'POST', + }, + }, + { + http: { + path: 'bar/-', + method: 'GET', + }, + }, + { + http: { + path: 'bar/foo', + method: 'GET', + }, + }, + { + http: { + path: 'bar/{id}/foobar', + method: 'GET', + }, + }, + { + http: { + path: 'bar/{id}', + method: 'GET', + }, + }, + { + http: { + path: 'bar/{foo_id}', + method: 'GET', + }, + }, + { + http: { + path: 'bar/{foo_id}/foobar', + method: 'GET', + }, + }, + ]; + expect(awsCompileApigEvents.getResourcePaths()).to.deep.equal([ + 'foo', + 'bar', + 'foo/bar', + 'bar/-', + 'bar/foo', + 'bar/{id}', + 'bar/{foo_id}', + 'bar/{id}/foobar', + 'bar/{foo_id}/foobar', + ]); }); - it('should construct the correct resourcePaths array', () => awsCompileApigEvents - .compileResources().then(() => { - const expectedResourcePaths = [ - 'foo/bar', - 'foo', - 'bar/-', - 'bar', - 'bar/foo', - 'bar/{id}', - 'bar/{id}/foobar', - 'bar/{foo_id}', - 'bar/{foo_id}/foobar', - ]; - expect(awsCompileApigEvents.resourcePaths).to.deep.equal(expectedResourcePaths); - }) - ); - - it('should construct the correct resourceLogicalIds object', () => awsCompileApigEvents - .compileResources().then(() => { - const expectedResourceLogicalIds = { - 'bar/-': 'ApiGatewayResourceBarDash', - 'foo/bar': 'ApiGatewayResourceFooBar', - foo: 'ApiGatewayResourceFoo', - 'bar/{id}/foobar': 'ApiGatewayResourceBarIdVarFoobar', - 'bar/{id}': 'ApiGatewayResourceBarIdVar', - 'bar/{foo_id}/foobar': 'ApiGatewayResourceBarFooidVarFoobar', - 'bar/{foo_id}': 'ApiGatewayResourceBarFooidVar', - 'bar/foo': 'ApiGatewayResourceBarFoo', - bar: 'ApiGatewayResourceBar', - }; - expect(awsCompileApigEvents.resourceLogicalIds).to.deep.equal(expectedResourceLogicalIds); - }) - ); - - it('should create resource resources when http events are given', () => awsCompileApigEvents - .compileResources().then(() => { - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFooBar.Properties.PathPart) - .to.equal('bar'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceFooBar.Properties.ParentId.Ref) - .to.equal('ApiGatewayResourceFoo'); + it('should reference the appropriate ParentId', () => { + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'foo/bar', + method: 'POST', + }, + }, + { + http: { + path: 'bar/-', + method: 'GET', + }, + }, + { + http: { + path: 'bar/foo', + method: 'GET', + }, + }, + { + http: { + path: 'bar/{id}/foobar', + method: 'GET', + }, + }, + { + http: { + path: 'bar/{id}', + method: 'GET', + }, + }, + ]; + return awsCompileApigEvents.compileResources().then(() => { expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayResourceFoo.Properties.ParentId['Fn::GetAtt'][0]) .to.equal('ApiGatewayRestApi'); expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceBar.Properties.ParentId['Fn::GetAtt'][1]) + .Resources.ApiGatewayResourceFoo.Properties.ParentId['Fn::GetAtt'][1]) .to.equal('RootResourceId'); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceFooBar.Properties.ParentId.Ref) + .to.equal('ApiGatewayResourceFoo'); expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate .Resources.ApiGatewayResourceBarIdVar.Properties.ParentId.Ref) .to.equal('ApiGatewayResourceBar'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceBarFooidVar.Properties.ParentId.Ref) - .to.equal('ApiGatewayResourceBar'); - expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources.ApiGatewayResourceBarFooidVarFoobar.Properties.ParentId.Ref) - .to.equal('ApiGatewayResourceBarFooidVar'); - }) - ); + }); + }); - it('should not create resource resources when http events are not given', () => { - awsCompileApigEvents.serverless.service.functions = { - first: { - events: [], + it('should construct the correct resourceLogicalIds object', () => { + awsCompileApigEvents.validated.events = [ + { + http: { + path: '', + method: 'POST', + }, }, - }; - + { + http: { + path: 'foo', + method: 'GET', + }, + }, + { + http: { + path: 'foo/{foo_id}/bar', + method: 'GET', + }, + }, + { + http: { + path: 'baz/foo', + method: 'GET', + }, + }, + ]; return awsCompileApigEvents.compileResources().then(() => { - expect( - awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate - .Resources - ).to.deep.equal({}); + expect(awsCompileApigEvents.apiGatewayResourceLogicalIds).to.deep.equal({ + baz: 'ApiGatewayResourceBaz', + 'baz/foo': 'ApiGatewayResourceBazFoo', + foo: 'ApiGatewayResourceFoo', + 'foo/{foo_id}': 'ApiGatewayResourceFooFooidVar', + 'foo/{foo_id}/bar': 'ApiGatewayResourceFooFooidVarBar', + }); + }); + }); + + it('should construct resourceLogicalIds that do not collide', () => { + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'foo/bar', + method: 'POST', + }, + }, + { + http: { + path: 'foo/{bar}', + method: 'GET', + }, + }, + ]; + return awsCompileApigEvents.compileResources().then(() => { + expect(awsCompileApigEvents.apiGatewayResourceLogicalIds).to.deep.equal({ + foo: 'ApiGatewayResourceFoo', + 'foo/bar': 'ApiGatewayResourceFooBar', + 'foo/{bar}': 'ApiGatewayResourceFooBarVar', + }); + }); + }); + + it('should set the appropriate Pathpart', () => { + awsCompileApigEvents.validated.events = [ + { + http: { + path: 'foo/{bar}', + method: 'GET', + }, + }, + { + http: { + path: 'foo/bar', + method: 'GET', + }, + }, + { + http: { + path: 'foo/{bar}/baz', + method: 'GET', + }, + }, + ]; + return awsCompileApigEvents.compileResources().then(() => { + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceFooBar.Properties.PathPart) + .to.equal('bar'); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceFooBarVar.Properties.PathPart) + .to.equal('{bar}'); + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayResourceFooBarVarBaz.Properties.PathPart) + .to.equal('baz'); + }); + }); + + it('should handle root resource references', () => { + awsCompileApigEvents.validated.events = [ + { + http: { + path: '', + method: 'GET', + }, + }, + ]; + return awsCompileApigEvents.compileResources().then(() => { + expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources).to.deep.equal({}); }); }); }); diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/validate.js b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/validate.js index f7fa28606..5e0f6b72c 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/validate.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/validate.js @@ -1,26 +1,16 @@ 'use strict'; const expect = require('chai').expect; +const sinon = require('sinon'); const AwsCompileApigEvents = require('../index'); const Serverless = require('../../../../../../../Serverless'); describe('#validate()', () => { + let serverless; let awsCompileApigEvents; beforeEach(() => { - const serverless = new Serverless(); - serverless.service.environment = { - vars: {}, - stages: { - dev: { - vars: {}, - regions: {}, - }, - }, - }; - serverless.service.environment.stages.dev.regions['us-east-1'] = { - vars: {}, - }; + serverless = new Serverless(); const options = { stage: 'dev', region: 'us-east-1', @@ -28,6 +18,20 @@ describe('#validate()', () => { awsCompileApigEvents = new AwsCompileApigEvents(serverless, options); }); + it('should ignore non-http events', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + ignored: {}, + }, + ], + }, + }; + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(0); + }); + it('should reject an invalid http event', () => { awsCompileApigEvents.serverless.service.functions = { first: { @@ -41,6 +45,20 @@ describe('#validate()', () => { expect(() => awsCompileApigEvents.validate()).to.throw(Error); }); + it('should throw an error if http event type is not a string or an object', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: 42, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + it('should validate the http events "path" property', () => { awsCompileApigEvents.serverless.service.functions = { first: { @@ -158,6 +176,28 @@ describe('#validate()', () => { expect(validated.events).to.be.an('Array').with.length(1); }); + it('should discard a starting slash from paths', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + }, + }, + { + http: 'GET /foo/bar', + }, + ], + }, + }; + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(2); + expect(validated.events[0].http).to.have.property('path', 'foo/bar'); + expect(validated.events[1].http).to.have.property('path', 'foo/bar'); + }); + it('should throw if an authorizer is an invalid value', () => { awsCompileApigEvents.serverless.service.functions = { first: { @@ -312,4 +352,748 @@ describe('#validate()', () => { expect(authorizer.identitySource).to.equal('method.request.header.Custom'); expect(authorizer.identityValidationExpression).to.equal('foo'); }); + + it('should process cors defaults', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + cors: true, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.cors).to.deep.equal({ + headers: ['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', 'X-Amz-Security-Token'], + methods: ['OPTIONS', 'POST'], + origins: ['*'], + }); + }); + + it('should throw if request is malformed', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + integration: 'lambda', + request: 'invalid', + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should throw if request.passThrough is invalid', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + integration: 'lambda', + request: { + passThrough: 'INVALID', + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should throw if request.template is malformed', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + integration: 'lambda', + request: { + template: 'invalid', + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should throw if response is malformed', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + integration: 'lambda', + response: 'invalid', + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should throw if response.headers are malformed', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + integration: 'lambda', + response: { + headers: 'invalid', + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should throw if cors headers are not an array', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + cors: { + headers: true, + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should process cors options', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + cors: { + headers: ['X-Foo-Bar'], + origins: ['acme.com'], + methods: ['POST', 'OPTIONS'], + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.cors).to.deep.equal({ + headers: ['X-Foo-Bar'], + methods: ['POST', 'OPTIONS'], + origins: ['acme.com'], + }); + }); + + it('should merge all preflight origins, method, and headers for a path', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users', + cors: { + origins: [ + 'http://example.com', + ], + }, + }, + }, { + http: { + method: 'POST', + path: 'users', + cors: { + origins: [ + 'http://example2.com', + ], + }, + }, + }, { + http: { + method: 'PUT', + path: 'users/{id}', + cors: { + headers: [ + 'TestHeader', + ], + }, + }, + }, { + http: { + method: 'DELETE', + path: 'users/{id}', + cors: { + headers: [ + 'TestHeader2', + ], + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.corsPreflight['users/{id}'].methods) + .to.deep.equal(['OPTIONS', 'DELETE', 'PUT']); + expect(validated.corsPreflight.users.origins) + .to.deep.equal(['http://example2.com', 'http://example.com']); + expect(validated.corsPreflight['users/{id}'].headers) + .to.deep.equal(['TestHeader2', 'TestHeader']); + }); + + it('should add default statusCode to custom statusCodes', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'lambda', + response: { + statusCodes: { + 404: { + pattern: '.*"statusCode":404,.*', + template: '$input.path(\'$.errorMessage\')', + headers: { + 'Content-Type': 'text/html', + }, + }, + }, + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.response.statusCodes).to.deep.equal({ + 200: { + pattern: '', + }, + 404: { + pattern: '.*"statusCode":404,.*', + template: '$input.path(\'$.errorMessage\')', + headers: { + 'Content-Type': 'text/html', + }, + }, + }); + }); + + it('should allow custom statusCode with default pattern', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'lambda', + response: { + statusCodes: { + 418: { + pattern: '', + template: '$input.path(\'$.foo\')', + }, + }, + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.response.statusCodes).to.deep.equal({ + 418: { + pattern: '', + template: '$input.path(\'$.foo\')', + }, + }); + }); + + it('should handle expicit methods', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + cors: { + methods: ['POST'], + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.cors.methods).to.deep.equal(['POST', 'OPTIONS']); + }); + + it('should throw an error if the method is invalid', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'INVALID', + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should set authorizer.arn when provided a name string', () => { + awsCompileApigEvents.serverless.service.functions = { + authorizer: {}, + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'GET', + authorizer: 'authorizer', + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.authorizer.name).to.equal('authorizer'); + expect(validated.events[0].http.authorizer.arn).to.deep.equal({ + 'Fn::GetAtt': ['AuthorizerLambdaFunction', 'Arn'], + }); + }); + + it('should set authorizer.arn when provided an ARN string', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'GET', + authorizer: 'xxx:dev-authorizer', + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.authorizer.name).to.equal('authorizer'); + expect(validated.events[0].http.authorizer.arn).to.equal('xxx:dev-authorizer'); + }); + + it('should handle authorizer.name object', () => { + awsCompileApigEvents.serverless.service.functions = { + authorizer: {}, + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'GET', + authorizer: { + name: 'authorizer', + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.authorizer.name).to.equal('authorizer'); + expect(validated.events[0].http.authorizer.arn).to.deep.equal({ + 'Fn::GetAtt': [ + 'AuthorizerLambdaFunction', + 'Arn', + ], + }); + }); + + it('should handle an authorizer.arn object', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'GET', + authorizer: { + arn: 'xxx:dev-authorizer', + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.authorizer.name).to.equal('authorizer'); + expect(validated.events[0].http.authorizer.arn).to.equal('xxx:dev-authorizer'); + }); + + it('should throw an error if the provided config is not an object', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + request: 'some string', + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should throw an error if the template config is not an object', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + request: { + template: 'some string', + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should process request parameters', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + integration: 'lambda', + path: 'foo/bar', + method: 'GET', + request: { + parameters: { + querystrings: { + foo: true, + bar: false, + }, + paths: { + foo: true, + bar: false, + }, + headers: { + foo: true, + bar: false, + }, + }, + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.request.parameters).to.deep.equal({ + 'method.request.querystring.foo': true, + 'method.request.querystring.bar': false, + 'method.request.path.foo': true, + 'method.request.path.bar': false, + 'method.request.header.foo': true, + 'method.request.header.bar': false, + }); + }); + + it('should throw an error if the provided response config is not an object', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'GET', + response: 'some string', + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should throw an error if the response headers are not objects', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'GET', + response: { + headers: 'some string', + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('throw error if authorizer property is not a string or object', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'GET', + authorizer: 2, + }, + }, + ], + }, + }; + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('throw error if authorizer property is an object but no name or arn provided', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + path: 'foo/bar', + method: 'GET', + authorizer: {}, + }, + }, + ], + }, + }; + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should set "AWS_PROXY" as the default integration type', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + }, + }, + ], + }, + }; + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.integration).to.equal('AWS_PROXY'); + }); + + it('should support LAMBDA integration', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'LAMBDA', + }, + }, + { + http: { + method: 'PUT', + path: 'users/list', + integration: 'lambda', + }, + }, + { + http: { + method: 'POST', + path: 'users/list', + integration: 'lambda-proxy', + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(3); + expect(validated.events[0].http.integration).to.equal('AWS'); + expect(validated.events[1].http.integration).to.equal('AWS'); + expect(validated.events[2].http.integration).to.equal('AWS_PROXY'); + }); + + it('should show a warning message when using request / response config with LAMBDA-PROXY', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'lambda-proxy', + request: { + template: { + 'template/1': '{ "stage" : "$context.stage" }', + 'template/2': '{ "httpMethod" : "$context.httpMethod" }', + }, + }, + response: { + template: "$input.path('$.foo')", + }, + }, + }, + ], + }, + }; + // initialize so we get the log method from the CLI in place + serverless.init(); + + const logStub = sinon.stub(serverless.cli, 'log'); + + awsCompileApigEvents.validate(); + + expect(logStub.calledOnce).to.be.equal(true); + expect(logStub.args[0][0].length).to.be.at.least(1); + }); + + it('should throw an error when an invalid integration type was provided', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'INVALID', + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + + it('should accept a valid passThrough', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'lambda', + request: { + passThrough: 'WHEN_NO_MATCH', + }, + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.request.passThrough).to.equal('WHEN_NO_MATCH'); + }); + + it('should default pass through to NEVER', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'GET', + path: 'users/list', + integration: 'lambda', + }, + }, + ], + }, + }; + + const validated = awsCompileApigEvents.validate(); + expect(validated.events).to.be.an('Array').with.length(1); + expect(validated.events[0].http.request.passThrough).to.equal('NEVER'); + }); }); From e981d41488e884caae82317c357f4871a50e5cc6 Mon Sep 17 00:00:00 2001 From: Philipp Muens Date: Tue, 8 Nov 2016 14:04:36 +0100 Subject: [PATCH 2/3] Refactor method used for capitalization of strings --- .../aws/deploy/compile/events/apiGateway/lib/authorizers.js | 3 +-- .../compile/events/apiGateway/lib/method/authorization.js | 5 +++-- .../compile/events/apiGateway/lib/method/integration.js | 3 +-- .../aws/deploy/compile/events/apiGateway/lib/methods.js | 3 +-- .../aws/deploy/compile/events/apiGateway/lib/permissions.js | 6 ++---- .../aws/deploy/compile/events/apiGateway/lib/validate.js | 2 +- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/authorizers.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/authorizers.js index baaa576cc..8308a48c0 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/authorizers.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/authorizers.js @@ -31,8 +31,7 @@ module.exports = { }); } - const normalizedAuthorizerName = authorizer.name[0].toUpperCase() - + authorizer.name.substr(1); + const normalizedAuthorizerName = _.capitalize(authorizer.name); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [`${normalizedAuthorizerName}ApiGatewayAuthorizer`]: { diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/authorization.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/authorization.js index ac0a89687..8032b6afb 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/authorization.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/authorization.js @@ -1,10 +1,11 @@ 'use strict'; +const _ = require('lodash'); + module.exports = { getMethodAuthorization(http) { if (http.authorizer) { - const normalizedAuthorizerName = http.authorizer.name[0].toUpperCase() - + http.authorizer.name.substr(1); + const normalizedAuthorizerName = _.capitalize(http.authorizer.name); const authorizerLogicalId = `${normalizedAuthorizerName}ApiGatewayAuthorizer`; return { diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js index f94269886..f5f557416 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/integration.js @@ -4,8 +4,7 @@ const _ = require('lodash'); module.exports = { getMethodIntegration(http, functionName) { - const normalizedFunctionName = functionName[0].toUpperCase() - + functionName.substr(1); + const normalizedFunctionName = _.capitalize(functionName); const integration = { IntegrationHttpMethod: 'POST', Type: http.integration, diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js index dc13a3221..f57a411b8 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js @@ -33,8 +33,7 @@ module.exports = { this.getMethodResponses(event.http) ); - const methodName = event.http.method[0].toUpperCase() + - event.http.method.substr(1).toLowerCase(); + const methodName = _.capitalize(event.http.method); const methodLogicalId = `ApiGatewayMethod${resourceName}${methodName}`; this.apiGatewayMethodLogicalIds.push(methodLogicalId); diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js index 79f705148..06771882a 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js @@ -7,8 +7,7 @@ module.exports = { compilePermissions() { this.validated.events.forEach((event) => { - const normalizedFunctionName = event.functionName[0].toUpperCase() - + event.functionName.substr(1); + const normalizedFunctionName = _.capitalize(event.functionName); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [`${normalizedFunctionName}LambdaPermissionApiGateway`]: { @@ -25,8 +24,7 @@ module.exports = { if (event.http.authorizer) { const authorizer = event.http.authorizer; - const normalizedAuthorizerName = authorizer.name[0].toUpperCase() - + authorizer.name.substr(1); + const normalizedAuthorizerName = _.capitalize(authorizer.name); _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { [`${normalizedAuthorizerName}LambdaPermissionApiGateway`]: { diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js index 6e51b8601..d76d7de64 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js @@ -375,7 +375,7 @@ module.exports = { getLambdaArn(name) { this.serverless.service.getFunction(name); - const normalizedName = name[0].toUpperCase() + name.substr(1); + const normalizedName = _.capitalize(name); return { 'Fn::GetAtt': [`${normalizedName}LambdaFunction`, 'Arn'] }; }, From f7b54cfb5f300c1180f0557a346849e9c180a594 Mon Sep 17 00:00:00 2001 From: Philipp Muens Date: Tue, 8 Nov 2016 14:27:43 +0100 Subject: [PATCH 3/3] Move method.js file into method directory --- lib/plugins/aws/deploy/compile/events/apiGateway/index.js | 2 +- .../events/apiGateway/lib/{methods.js => method/index.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/plugins/aws/deploy/compile/events/apiGateway/lib/{methods.js => method/index.js} (100%) diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/index.js b/lib/plugins/aws/deploy/compile/events/apiGateway/index.js index a019e8937..a8c5bcdad 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/index.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/index.js @@ -7,7 +7,7 @@ const compileRestApi = require('./lib/restApi'); const compileApiKeys = require('./lib/apiKeys'); const compileResources = require('./lib/resources'); const compileCors = require('./lib/cors'); -const compileMethods = require('./lib/methods'); +const compileMethods = require('./lib/method/index'); const compileAuthorizers = require('./lib/authorizers'); const compileDeployment = require('./lib/deployment'); const compilePermissions = require('./lib/permissions'); diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/index.js similarity index 100% rename from lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js rename to lib/plugins/aws/deploy/compile/events/apiGateway/lib/method/index.js