├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── package.json └── src ├── index.js └── tests ├── .eslintrc.json └── index.test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "eslint-config-silvermine/node" 4 | 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | # 4.3.2 is what AWS Lambda currently uses 3 | node_js: 4 | - "5" 5 | - "4.3.2" 6 | 7 | before_install: if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi 8 | 9 | # For code coverage: 10 | after_success: 11 | cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 12 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | var config; 6 | 7 | config = { 8 | js: { 9 | all: [ 'Gruntfile.js', 'src/**/*.js', '!**/node_modules/**/*' ], 10 | }, 11 | }; 12 | 13 | grunt.initConfig({ 14 | 15 | pkg: grunt.file.readJSON('package.json'), 16 | 17 | eslint: { 18 | target: config.js.all, 19 | }, 20 | 21 | }); 22 | 23 | grunt.loadNpmTasks('grunt-eslint'); 24 | 25 | grunt.registerTask('standards', [ 'eslint' ]); 26 | grunt.registerTask('default', [ 'standards' ]); 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Jeremy Thomerson 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WARNING: DEPRECATED PLUGIN 2 | 3 | Serverless now supports custom [response headers][sls-response-headers] and [status 4 | codes][sls-status-code]. As such, this plugin is deprecated and no longer maintained. For 5 | more information on the API Gateway proxy integration, please see 6 | https://github.com/serverless/serverless/issues/2174 7 | 8 | [sls-response-headers]: https://serverless.com/framework/docs/providers/aws/events/apigateway/#custom-response-headers 9 | [sls-status-code]: https://serverless.com/framework/docs/providers/aws/events/apigateway#status-codes 10 | 11 | # Serverless Plugin: Multiple Responses 12 | 13 | [![Build Status](https://travis-ci.org/silvermine/serverless-plugin-multiple-responses.png?branch=master)](https://travis-ci.org/silvermine/serverless-plugin-multiple-responses) 14 | [![Coverage Status](https://coveralls.io/repos/github/silvermine/serverless-plugin-multiple-responses/badge.svg?branch=master)](https://coveralls.io/github/silvermine/serverless-plugin-multiple-responses?branch=master) 15 | [![Dependency Status](https://david-dm.org/silvermine/serverless-plugin-multiple-responses.png)](https://david-dm.org/silvermine/serverless-plugin-multiple-responses) 16 | [![Dev Dependency Status](https://david-dm.org/silvermine/serverless-plugin-multiple-responses/dev-status.png)](https://david-dm.org/silvermine/serverless-plugin-multiple-responses#info=devDependencies&view=table) 17 | 18 | 19 | ## What is it? 20 | 21 | **This plugin is now deprecated and no longer supported.** It was originally a plugin for 22 | the early versions of the Serverless framework intended to address the issues outlined at 23 | https://github.com/serverless/serverless/issues/2046 (and in my - jthomerson - comments 24 | there). 25 | 26 | **Since you can now use the API Gateway proxy integration and have full control over 27 | response headers and status codes, you should no longer need this plugin. See 28 | https://github.com/serverless/serverless/issues/2174 for more information.** 29 | 30 | ## How do I use it? 31 | 32 | The basic principle is that instead of adding a `response` attribute to your 33 | HTTP event config, you will add a `responses` attribute. 34 | 35 | The responses attribute is a key/value pair where the key is the status code of 36 | the response (e.g. 200, 404, etc). The value can be: 37 | 38 | * `false` - which means "remove the default response configured by SLS (or another plugin) for this response code" 39 | * an object with the following attributes: 40 | * `headers` - just the same as the SLS `headers` attribute 41 | * `templates` - similar to request templates, this is keyed by the response content type 42 | * `properties` - any other properties to directly add to the CloudFormation template for this API method 43 | * typically this is used to add `SelectionPattern` for the error responses 44 | 45 | NOTE: leaving one response without a selection pattern attribute makes it the 46 | default response. 47 | 48 | Below we will show two examples: a standard example where 200 is what your 49 | function returns for a successful completion, and another example where it 50 | needs to return a 302 as its default (non-error) response. The 302 response 51 | example would be used if your were building a bit.ly-like endpoint, for 52 | example. 53 | 54 | ```yml 55 | # I recommend using variables to define your responses since they tend 56 | # to be the same across all of your API gateway functions, but you 57 | # certainly don't have to. 58 | custom: 59 | defaultRegion: us-east-1 60 | region: ${opt:region, self:custom.defaultRegion} 61 | stage: ${opt:stage, env:USER} 62 | standardRequest: 63 | template: 64 | application/json: ${file(templates/standard-request.tpl)} 65 | standardResponseHeaders: 66 | 'Access-Control-Allow-Origin': "'*'" 67 | 'Content-Type': 'integration.response.body.headers.Content-Type' 68 | 'Expires': 'integration.response.body.headers.Expires' 69 | 'Cache-Control': 'integration.response.body.headers.Cache-Control' 70 | 'Pragma': "'no-cache'" 71 | standardResponseTemplate: "$input.path('$.body')" 72 | errorResponseHeaders: 73 | 'Access-Control-Allow-Origin': "'*'" 74 | 'Expires': "'Thu, 19 Nov 1981 08:52:00 GMT'" 75 | 'Cache-Control': "'no-cache, max-age=0, must-revalidate'" 76 | 'Pragma': "'no-cache'" 77 | errorResponseTemplate: "$input.path('$.errorMessage')" 78 | # Here we are defining what would be under "responses" in your HTTP event 79 | # if you were not using the custom variables. 80 | standardResponses: 81 | 200: 82 | headers: ${self:custom.standardResponseHeaders} 83 | templates: 84 | 'application/json;charset=UTF-8': ${self:custom.standardResponseTemplate} 85 | 404: 86 | headers: ${self:custom.errorResponseHeaders} 87 | templates: 88 | 'application/json;charset=UTF-8': ${self:custom.errorResponseTemplate} 89 | properties: 90 | SelectionPattern: '.*\"status\":404.*' 91 | 500: 92 | headers: ${self:custom.errorResponseHeaders} 93 | templates: 94 | 'application/json;charset=UTF-8': ${self:custom.errorResponseTemplate} 95 | properties: 96 | SelectionPattern: '.*\"status\":500.*' 97 | redirectResponses: 98 | # Since we want to return 302 upon a successful completion, we remove the 99 | # built-in default of 200 100 | 200: false 101 | 302: 102 | headers: 103 | Location: "integration.response.body.headers.Location" 104 | templates: 105 | 'application/json;charset=UTF-8': "$input.path('$.body')" 106 | 'text/html;charset=UTF-8': "$input.path('$.body')" 107 | 404: 108 | headers: ${self:custom.errorResponseHeaders} 109 | templates: 110 | 'application/json;charset=UTF-8': "$input.path('$.body')" 111 | 'text/html;charset=UTF-8': "$input.path('$.body')" 112 | properties: 113 | SelectionPattern: '.*\"status\":404.*' 114 | 500: 115 | headers: ${self:custom.errorResponseHeaders} 116 | templates: 117 | 'application/json;charset=UTF-8': "$input.path('$.body')" 118 | 'text/html;charset=UTF-8': "$input.path('$.body')" 119 | properties: 120 | SelectionPattern: '.*\"status\":500.*' 121 | 122 | # Tell your service that you want to use this plugin: 123 | # (you'll need to `npm install` it first) 124 | plugins: 125 | - serverless-plugin-multiple-responses 126 | 127 | # In the function's http event configuration you see where 128 | # we have `responses` instead of the normal `response`. 129 | functions: 130 | ping: 131 | name: ${self:service}-${self:provider.stage}-ping 132 | handler: src/ping/Ping.handler 133 | memorySize: 128 134 | timeout: 2 135 | events: 136 | - http: 137 | method: GET 138 | path: ping 139 | request: ${self:custom.standardRequest} 140 | responses: ${self:custom.standardResponses} 141 | redirector: 142 | name: ${self:service}-${self:provider.stage}-redirector 143 | handler: src/redirector/Redirector.handler 144 | memorySize: 128 145 | timeout: 2 146 | events: 147 | - http: 148 | method: GET 149 | path: redirector 150 | request: ${self:custom.standardRequest} 151 | responses: ${self:custom.redirectResponses} 152 | ``` 153 | 154 | 155 | ## How do I contribute? 156 | 157 | Easy! Pull requests are welcome! Just do the following: 158 | 159 | * Clone the code 160 | * Install the dependencies with `npm install` 161 | * Create a feature branch (e.g. `git checkout -b my_new_feature`) 162 | * Make your changes and commit them with a reasonable commit message 163 | * Make sure the code passes our standards with `grunt standards` 164 | * Make sure all unit tests pass with `npm test` 165 | 166 | Our goal is 100% unit test coverage, with **good and effective** tests (it's 167 | easy to hit 100% coverage with junk tests, so we differentiate). We **will not 168 | accept pull requests for new features that do not include unit tests**. If you 169 | are submitting a pull request to fix a bug, we may accept it without unit tests 170 | (we will certainly write our own for that bug), but we *strongly encourage* you 171 | to write a unit test exhibiting the bug, commit that, and then commit a fix in 172 | a separate commit. This *greatly increases* the likelihood that we will accept 173 | your pull request and the speed with which we can process it. 174 | 175 | 176 | ## License 177 | 178 | This software is released under the MIT license. See [the license file](LICENSE) for more details. 179 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-plugin-multiple-responses", 3 | "version": "1.0.0", 4 | "description": "Plugin for the SLS 1.x branch to allow for multiple response mappings.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "grunt standards && ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- -R spec 'src/tests/**/*.test.js'" 8 | }, 9 | "author": "Jeremy Thomerson", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/silvermine/serverless-plugin-multiple-responses.git" 14 | }, 15 | "keywords": [ 16 | "serverless plugin multiple responses", 17 | "serverless", 18 | "api gateway", 19 | "api gateway error responses" 20 | ], 21 | "bugs": { 22 | "url": "https://github.com/silvermine/serverless-plugin-multiple-responses/issues" 23 | }, 24 | "homepage": "https://github.com/silvermine/serverless-plugin-multiple-responses#readme", 25 | "dependencies": { 26 | "class.extend": "0.9.2", 27 | "underscore": "1.8.3" 28 | }, 29 | "devDependencies": { 30 | "coveralls": "2.11.12", 31 | "eslint": "3.3.1", 32 | "eslint-config-silvermine": "1.1.2", 33 | "expect.js": "0.3.1", 34 | "grunt": "1.0.1", 35 | "grunt-eslint": "19.0.0", 36 | "istanbul": "0.4.4", 37 | "mocha": "3.0.2", 38 | "mocha-lcov-reporter": "1.2.0", 39 | "sinon": "1.17.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | Class = require('class.extend'); 5 | 6 | module.exports = Class.extend({ 7 | 8 | init: function(serverless, opts) { 9 | this._serverless = serverless; 10 | this._opts = opts; 11 | 12 | this.hooks = { 13 | 'before:deploy:deploy': this.amendResources.bind(this), 14 | }; 15 | }, 16 | 17 | amendResources: function() { 18 | var self = this; 19 | 20 | _.each(this._serverless.service.functions, function(fnDef, fnName) { 21 | _.each(fnDef.events, function(evt) { 22 | if (evt.http) { 23 | self.amendEvent(fnName, fnDef, evt.http); 24 | } 25 | }); 26 | }); 27 | }, 28 | 29 | amendEvent: function(fnName, fnDef, httpDef) { 30 | var normalizedPath = this._capitalizeAlphaNumericPath(httpDef.path), 31 | normalizedMethodName = 'ApiGatewayMethod' + normalizedPath + this._normalize(httpDef.method, true), 32 | cfnObj = this._serverless.service.provider.compiledCloudFormationTemplate.Resources[normalizedMethodName]; 33 | 34 | if (_.isEmpty(cfnObj)) { 35 | return this._serverless.cli.log('Error: could not find CloudFormation object for ' + fnName + ':' + httpDef.path); 36 | } 37 | 38 | if (!httpDef.responses) { 39 | return; 40 | } 41 | 42 | _.each(httpDef.responses, function(respDef, statusCode) { 43 | var search = { StatusCode: parseInt(statusCode, 10) }, 44 | cfnIntResp = _.findWhere(cfnObj.Properties.Integration.IntegrationResponses, search), 45 | cfnMethResp = _.findWhere(cfnObj.Properties.MethodResponses, search); 46 | 47 | if (respDef === false) { 48 | // this is a special case where we remove this default response that was added by another plugin 49 | cfnObj.Properties.Integration.IntegrationResponses = _.reject(cfnObj.Properties.Integration.IntegrationResponses, search); 50 | cfnObj.Properties.MethodResponses = _.reject(cfnObj.Properties.MethodResponses, search); 51 | return; 52 | } 53 | 54 | if (!cfnIntResp) { 55 | cfnIntResp = { StatusCode: statusCode }; 56 | cfnObj.Properties.Integration.IntegrationResponses.push(cfnIntResp); 57 | } 58 | 59 | if (!cfnMethResp) { 60 | cfnMethResp = { StatusCode: statusCode }; 61 | cfnObj.Properties.MethodResponses.push(cfnMethResp); 62 | } 63 | 64 | cfnIntResp.ResponseParameters = cfnIntResp.ResponseParameters || {}; 65 | cfnMethResp.ResponseParameters = cfnMethResp.ResponseParameters || {}; 66 | 67 | _.each(respDef.headers, function(val, name) { 68 | var realName = 'method.response.header.' + name; 69 | 70 | cfnIntResp.ResponseParameters[realName] = val; 71 | cfnMethResp.ResponseParameters[realName] = 'method.response.header.' + val; 72 | }); 73 | 74 | cfnIntResp.ResponseTemplates = _.extend({}, cfnIntResp.ResponseTemplates, respDef.templates); 75 | 76 | _.extend(cfnIntResp, respDef.properties); 77 | }); 78 | 79 | // if you need to debug: 80 | // console.log(require('util').inspect(cfnObj, { depth: null })); 81 | }, 82 | 83 | _normalize: function(s, lower) { 84 | if (_.isEmpty(s)) { 85 | return; 86 | } 87 | 88 | if (lower) { 89 | return s[0].toUpperCase() + s.substr(1).toLowerCase(); 90 | } 91 | 92 | return s[0].toUpperCase() + s.substr(1); 93 | }, 94 | 95 | _capitalizeAlphaNumericPath: function(path) { 96 | return _.reduce(path.split('/'), function(memo, part) { 97 | return memo + this._normalize(part.replace(/[^0-9A-Za-z]/g, ''), true); 98 | }.bind(this), ''); 99 | }, 100 | 101 | }); 102 | -------------------------------------------------------------------------------- /src/tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "eslint-config-silvermine/node-tests" 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/tests/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | expect = require('expect.js'), 5 | sinon = require('sinon'), 6 | Plugin = require('../index.js'); 7 | 8 | describe('serverless-plugin-multiple-responses', function() { 9 | 10 | describe('init', function() { 11 | 12 | it('registers the appropriate hook', function() { 13 | var plugin = new Plugin(); 14 | 15 | expect(plugin.hooks['before:deploy:deploy']).to.be.a('function'); 16 | }); 17 | 18 | it('registers a hook that calls amendResources', function() { 19 | var spy = sinon.spy(), 20 | ExtPlugin = Plugin.extend({ amendResources: spy }), 21 | plugin = new ExtPlugin(); 22 | 23 | plugin.hooks['before:deploy:deploy'](); 24 | 25 | expect(spy.called).to.be.ok(); 26 | expect(spy.calledOn(plugin)); 27 | }); 28 | 29 | }); 30 | 31 | 32 | describe('amendResources', function() { 33 | 34 | it('calls amendEvent for each function / http event combo, but not other events', function() { 35 | var fn1Def = { events: [ { http: 'fn1evt1Def' } ] }, 36 | fn2Def = { events: [ { rate: 'fn2evt1Def' }, { http: 'fn2evt2Def' } ] }, 37 | fn3Def = { events: [ { rate: 'fn31evt1Def' } ] }, 38 | fn4Def = { events: [ { http: 'fn4evt1Def' }, { http: 'fn4evt2Def' } ] }, 39 | fns = { fn1: fn1Def, fn2: fn2Def, fn3: fn3Def, fn4: fn4Def }, 40 | plugin = new Plugin({ service: { functions: fns } }), 41 | mock = sinon.mock(plugin); 42 | 43 | mock.expects('amendEvent').once().withExactArgs('fn1', fn1Def, 'fn1evt1Def'); 44 | // fn2evt1 is skipped as wrong type 45 | mock.expects('amendEvent').once().withExactArgs('fn2', fn2Def, 'fn2evt2Def'); 46 | // fn3 is skipped with no matching events 47 | mock.expects('amendEvent').once().withExactArgs('fn4', fn4Def, 'fn4evt1Def'); 48 | mock.expects('amendEvent').once().withExactArgs('fn4', fn4Def, 'fn4evt2Def'); 49 | 50 | plugin.amendResources(); 51 | 52 | mock.verify(); 53 | }); 54 | 55 | }); 56 | 57 | 58 | describe('amendEvent', function() { 59 | 60 | function getCompiledTemplate() { 61 | return { 62 | Resources: { 63 | ServerlessDeploymentBucket: { Type: 'AWS::S3::Bucket' }, 64 | PingLambdaFunction: { 65 | Type: 'AWS::Lambda::Function', 66 | Properties: { 67 | Code: { 68 | S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, 69 | S3Key: '1473599092284-2016-09-11T13:04:52.284Z/petstore.zip', 70 | }, 71 | FunctionName: 'petstore-jrthomer-ping', 72 | Handler: 'src/ping/Ping.handler', 73 | MemorySize: 128, 74 | Role: { 'Fn::GetAtt': [ 'IamRoleLambdaExecution', 'Arn' ] }, 75 | Runtime: 'nodejs4.3', 76 | Timeout: 2, 77 | }, 78 | }, 79 | ApiGatewayRestApi: { 80 | Type: 'AWS::ApiGateway::RestApi', 81 | Properties: { Name: 'jrthomer-petstore' }, 82 | }, 83 | ApiGatewayResourcePing: { 84 | Type: 'AWS::ApiGateway::Resource', 85 | Properties: { 86 | ParentId: { 'Fn::GetAtt': [ 'ApiGatewayRestApi', 'RootResourceId' ] }, 87 | PathPart: 'ping', 88 | RestApiId: { Ref: 'ApiGatewayRestApi' }, 89 | }, 90 | }, 91 | ApiGatewayMethodPingGet: { 92 | Type: 'AWS::ApiGateway::Method', 93 | Properties: { 94 | AuthorizationType: 'NONE', 95 | HttpMethod: 'GET', 96 | MethodResponses: [ 97 | { ResponseModels: {}, ResponseParameters: {}, StatusCode: 200 }, 98 | { StatusCode: 400 }, 99 | { StatusCode: 401 }, 100 | { StatusCode: 403 }, 101 | { StatusCode: 404 }, 102 | { StatusCode: 422 }, 103 | { StatusCode: 500 }, 104 | { StatusCode: 502 }, 105 | { StatusCode: 504 }, 106 | ], 107 | RequestParameters: {}, 108 | Integration: { 109 | IntegrationHttpMethod: 'POST', 110 | Type: 'AWS', 111 | Uri: 'someuri', 112 | RequestTemplates: { 113 | 'application/json': 'jsonrequesttemplate', 114 | 'application/x-www-form-urlencoded': 'formencodedrequesttemplate', 115 | }, 116 | PassthroughBehavior: 'NEVER', 117 | IntegrationResponses: [ 118 | { 119 | StatusCode: 200, 120 | ResponseParameters: {}, 121 | ResponseTemplates: {}, 122 | }, 123 | { StatusCode: 400, SelectionPattern: '.*\\[400\\].*' }, 124 | { StatusCode: 401, SelectionPattern: '.*\\[401\\].*' }, 125 | { StatusCode: 403, SelectionPattern: '.*\\[403\\].*' }, 126 | { StatusCode: 404, SelectionPattern: '.*\\[404\\].*' }, 127 | { StatusCode: 422, SelectionPattern: '.*\\[422\\].*' }, 128 | { StatusCode: 500, SelectionPattern: '.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*' }, 129 | { StatusCode: 502, SelectionPattern: '.*\\[502\\].*' }, 130 | { StatusCode: 504, SelectionPattern: '.*\\[504\\].*' }, 131 | ], 132 | }, 133 | ResourceId: { Ref: 'ApiGatewayResourcePing' }, 134 | RestApiId: { Ref: 'ApiGatewayRestApi' }, 135 | }, 136 | }, 137 | }, 138 | }; 139 | } 140 | 141 | function deleteFromArray(obj, arrName, ind) { 142 | obj[arrName] = _.reject(obj[arrName], function(v, i) { 143 | return i === ind; 144 | }); 145 | } 146 | 147 | function runTest(responses, modifier, methodPath) { 148 | var template = getCompiledTemplate(), 149 | expectedTemplate = getCompiledTemplate(), 150 | fnDef, sls, plugin; 151 | 152 | sls = { 153 | service: { 154 | provider: { 155 | compiledCloudFormationTemplate: template, 156 | }, 157 | }, 158 | cli: { log: _.noop }, 159 | }; 160 | 161 | fnDef = { 162 | name: 'petstore-jrthomer-ping', 163 | handler: 'src/ping/Ping.handler', 164 | memorySize: 128, 165 | timeout: 2, 166 | events: [ 167 | { 168 | http: { 169 | method: 'GET', 170 | path: methodPath || 'ping', 171 | request: 'irrelevant for this test', 172 | responses: responses, 173 | }, 174 | }, 175 | ], 176 | }; 177 | 178 | modifier(expectedTemplate); 179 | 180 | plugin = new Plugin(sls); 181 | plugin.amendEvent('ping', fnDef, fnDef.events[0].http); 182 | 183 | expect(sls.service.provider.compiledCloudFormationTemplate).to.eql(expectedTemplate); 184 | } 185 | 186 | it('removes existing responses when false is passed', function() { 187 | runTest({ '200': false, '422': false }, function(expectedTemplate) { 188 | deleteFromArray(expectedTemplate.Resources.ApiGatewayMethodPingGet.Properties, 'MethodResponses', 0); 189 | deleteFromArray(expectedTemplate.Resources.ApiGatewayMethodPingGet.Properties.Integration, 'IntegrationResponses', 0); 190 | 191 | // 422 is actually index 5, but will be 4 after the other modification 192 | deleteFromArray(expectedTemplate.Resources.ApiGatewayMethodPingGet.Properties, 'MethodResponses', 4); 193 | deleteFromArray(expectedTemplate.Resources.ApiGatewayMethodPingGet.Properties.Integration, 'IntegrationResponses', 4); 194 | }); 195 | }); 196 | 197 | it('handles multiple response templates, headers, etc', function() { 198 | var responses; 199 | 200 | responses = { 201 | '200': { 202 | headers: { 203 | 'Cache-Control': 'integration.response.body.headers.Cache-Control', 204 | Pragma: '\'no-cache\'', 205 | }, 206 | templates: { 207 | 'application/json;charset=UTF-8': '$input.path(\'$.body\')', 208 | 'text/html;charset=UTF-8': '$input.path(\'$.body\')', 209 | }, 210 | }, 211 | '404': { 212 | headers: { 213 | 'Cache-Control': '\'no-cache, max-age=0, must-revalidate\'', 214 | Pragma: '\'no-cache\'', 215 | }, 216 | templates: { 217 | 'application/json;charset=UTF-8': '$input.path(\'$.errorMessage\')', 218 | 'text/html;charset=UTF-8': '$input.path(\'$.errorMessage\')', 219 | }, 220 | }, 221 | }; 222 | 223 | runTest(responses, function(expectedTemplate) { 224 | var get = expectedTemplate.Resources.ApiGatewayMethodPingGet; 225 | 226 | // 200 227 | _.extend(get.Properties.MethodResponses[0].ResponseParameters, { 228 | 'method.response.header.Cache-Control': 'method.response.header.integration.response.body.headers.Cache-Control', 229 | 'method.response.header.Pragma': 'method.response.header.\'no-cache\'', 230 | }); 231 | _.extend(get.Properties.Integration.IntegrationResponses[0].ResponseParameters, { 232 | 'method.response.header.Cache-Control': 'integration.response.body.headers.Cache-Control', 233 | 'method.response.header.Pragma': '\'no-cache\'', 234 | }); 235 | _.extend(get.Properties.Integration.IntegrationResponses[0].ResponseTemplates, { 236 | 'application/json;charset=UTF-8': '$input.path(\'$.body\')', 237 | 'text/html;charset=UTF-8': '$input.path(\'$.body\')', 238 | }); 239 | 240 | // 404 241 | get.Properties.MethodResponses[4].ResponseParameters = { 242 | 'method.response.header.Cache-Control': 'method.response.header.\'no-cache, max-age=0, must-revalidate\'', 243 | 'method.response.header.Pragma': 'method.response.header.\'no-cache\'', 244 | }; 245 | get.Properties.Integration.IntegrationResponses[4].ResponseParameters = { 246 | 'method.response.header.Cache-Control': '\'no-cache, max-age=0, must-revalidate\'', 247 | 'method.response.header.Pragma': '\'no-cache\'', 248 | }; 249 | get.Properties.Integration.IntegrationResponses[4].ResponseTemplates = { 250 | 'application/json;charset=UTF-8': '$input.path(\'$.errorMessage\')', 251 | 'text/html;charset=UTF-8': '$input.path(\'$.errorMessage\')', 252 | }; 253 | }); 254 | 255 | }); 256 | 257 | it('can remove the default response and replace it with another (e.g. redirects)', function() { 258 | var responses; 259 | 260 | responses = { 261 | '200': false, 262 | '302': { 263 | headers: { 264 | 'Location': 'integration.response.body.headers.Location', 265 | Pragma: '\'no-cache\'', 266 | }, 267 | templates: { 268 | 'application/json;charset=UTF-8': '$input.path(\'$.body\')', 269 | 'text/html;charset=UTF-8': '$input.path(\'$.body\')', 270 | }, 271 | }, 272 | '404': { 273 | headers: { 274 | 'Cache-Control': '\'no-cache, max-age=0, must-revalidate\'', 275 | Pragma: '\'no-cache\'', 276 | }, 277 | templates: { 278 | 'application/json;charset=UTF-8': '$input.path(\'$.errorMessage\')', 279 | 'text/html;charset=UTF-8': '$input.path(\'$.errorMessage\')', 280 | }, 281 | }, 282 | }; 283 | 284 | runTest(responses, function(expectedTemplate) { 285 | var get = expectedTemplate.Resources.ApiGatewayMethodPingGet; 286 | 287 | // 302 288 | get.Properties.MethodResponses.push({ 289 | StatusCode: 302, 290 | ResponseParameters: { 291 | 'method.response.header.Location': 'method.response.header.integration.response.body.headers.Location', 292 | 'method.response.header.Pragma': 'method.response.header.\'no-cache\'', 293 | }, 294 | }); 295 | get.Properties.Integration.IntegrationResponses.push({ 296 | StatusCode: 302, 297 | ResponseParameters: { 298 | 'method.response.header.Location': 'integration.response.body.headers.Location', 299 | 'method.response.header.Pragma': '\'no-cache\'', 300 | }, 301 | ResponseTemplates: { 302 | 'application/json;charset=UTF-8': '$input.path(\'$.body\')', 303 | 'text/html;charset=UTF-8': '$input.path(\'$.body\')', 304 | }, 305 | }); 306 | 307 | // 404 308 | get.Properties.MethodResponses[4].ResponseParameters = { 309 | 'method.response.header.Cache-Control': 'method.response.header.\'no-cache, max-age=0, must-revalidate\'', 310 | 'method.response.header.Pragma': 'method.response.header.\'no-cache\'', 311 | }; 312 | get.Properties.Integration.IntegrationResponses[4].ResponseParameters = { 313 | 'method.response.header.Cache-Control': '\'no-cache, max-age=0, must-revalidate\'', 314 | 'method.response.header.Pragma': '\'no-cache\'', 315 | }; 316 | get.Properties.Integration.IntegrationResponses[4].ResponseTemplates = { 317 | 'application/json;charset=UTF-8': '$input.path(\'$.errorMessage\')', 318 | 'text/html;charset=UTF-8': '$input.path(\'$.errorMessage\')', 319 | }; 320 | 321 | // 200 322 | deleteFromArray(expectedTemplate.Resources.ApiGatewayMethodPingGet.Properties, 'MethodResponses', 0); 323 | deleteFromArray(expectedTemplate.Resources.ApiGatewayMethodPingGet.Properties.Integration, 'IntegrationResponses', 0); 324 | }); 325 | 326 | }); 327 | 328 | it('short circuits safely if it can not find the CloudFormation object for this function', function() { 329 | runTest({}, _.noop, 'non-existent-path'); 330 | }); 331 | 332 | it('short circuits safely if there are no responses defind on the config', function() { 333 | runTest(undefined, _.noop); 334 | }); 335 | 336 | }); 337 | 338 | 339 | describe('_normalize', function() { 340 | var plugin = new Plugin(); 341 | 342 | it('returns undefined for empty strings', function() { 343 | expect(plugin._normalize('')).to.be(undefined); 344 | expect(plugin._normalize(false)).to.be(undefined); 345 | expect(plugin._normalize()).to.be(undefined); 346 | expect(plugin._normalize('', true)).to.be(undefined); 347 | expect(plugin._normalize(false, true)).to.be(undefined); 348 | expect(plugin._normalize(undefined, true)).to.be(undefined); 349 | }); 350 | 351 | it('lowercases the rest of the string if told to do so', function() { 352 | expect(plugin._normalize('someTHING', true)).to.eql('Something'); 353 | expect(plugin._normalize('SomeTHING', true)).to.eql('Something'); 354 | expect(plugin._normalize('s', true)).to.eql('S'); 355 | expect(plugin._normalize('S', true)).to.eql('S'); 356 | }); 357 | 358 | it('only modifies the first letter by default', function() { 359 | expect(plugin._normalize('someTHING')).to.eql('SomeTHING'); 360 | expect(plugin._normalize('SomeTHING')).to.eql('SomeTHING'); 361 | expect(plugin._normalize('s')).to.eql('S'); 362 | expect(plugin._normalize('S')).to.eql('S'); 363 | }); 364 | 365 | }); 366 | 367 | 368 | describe('_capitalizeAlphaNumericPath', function() { 369 | var plugin = new Plugin(); 370 | 371 | it('removes unwanted characters and capitalizes for each piece of path', function() { 372 | expect(plugin._capitalizeAlphaNumericPath('some/path45/to_some-place%.json')).to.eql('SomePath45Tosomeplacejson'); 373 | }); 374 | 375 | }); 376 | 377 | }); 378 | --------------------------------------------------------------------------------