serverless-http-invoker 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ ## Our Standards
8
+
9
+ Examples of behavior that contributes to creating a positive environment include:
10
+
11
+ - Using welcoming and inclusive language
12
+ - Being respectful of differing viewpoints and experiences
13
+ - Gracefully accepting constructive criticism
14
+ - Focusing on what is best for the community
15
+ - Showing empathy towards other community members
16
+
17
+ Examples of unacceptable behavior by participants include:
18
+
19
+ - The use of sexualized language or imagery and unwelcome sexual attention or advances
20
+ - Trolling, insulting/derogatory comments, and personal or political attacks
21
+ - Public or private harassment
22
+ - Publishing others' private information, such as a physical or electronic address, without explicit permission
23
+ - Other conduct which could reasonably be considered inappropriate in a professional setting
24
+
25
+ ## Our Responsibilities
26
+
27
+ Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28
+
29
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30
+
31
+ ## Scope
32
+
33
+ This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34
+
35
+ ## Enforcement
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at serverless-http-invoker@willeke.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38
+
39
+ Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40
+
41
+ ## Attribution
42
+
43
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44
+
45
+ [homepage]: http://contributor-covenant.org
46
+ [version]: http://contributor-covenant.org/version/1/4/
@@ -0,0 +1,16 @@
1
+ # Contributing
2
+
3
+ :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4
+
5
+ The following is a set of guidelines for contributing to this project. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
6
+
7
+ ## How Can I Contribute?
8
+
9
+ Feel free to check [issues page](https://github.com/activescott/serverless-http-invoker/issues) to find an enhancement to implement or bug to fix. You could also **[Improve the documentation](https://github.com/activescott/serverless-http-invoker/edit/main/README.md)**, **Report a Bug**, or **Suggest an Enhancement**.
10
+
11
+ ### Contribute Code
12
+
13
+ - Fork this repository and [submit a pull request](https://help.github.com/articles/creating-a-pull-request/).
14
+ - Write tests
15
+ - Ensure the linter passes with `npm run lint`
16
+ - Ensure the tests pass at [the CI Server](https://travis-ci.org/activescott/serverless-http-invoker) by [following the status](https://help.github.com/articles/about-statuses/) of your pull request.
@@ -0,0 +1,32 @@
1
+ ## What did you implement:
2
+
3
+ Closes #XXXXX
4
+
5
+ <!--
6
+ Briefly describe the feature if no issue exists for this PR
7
+ -->
8
+
9
+ ## How did you implement it:
10
+
11
+ <!--
12
+ If this is a nontrivial change please briefly describe your implementation so its easy for us to understand and review your code.
13
+ -->
14
+
15
+ ## How can we verify it:
16
+
17
+ <!--
18
+ Add any applicable config, commands, screenshots or other resources
19
+ to make it easy for us to verify this works. The easier you make it for us
20
+ to review a PR, the faster we can review and merge it.
21
+
22
+ Examples:
23
+ * serverless.yml - Fully functioning to easily deploy changes
24
+ * Screenshots - Showing the difference between your output and the master
25
+ * Other - Anything else that comes to mind to help us evaluate
26
+ -->
27
+
28
+ ## Todos:
29
+
30
+ - [ ] Write tests
31
+ - [ ] Fix linting errors
32
+ - [ ] All tests pass on our CI Server: https://travis-ci.org/activescott/serverless-http-invoker
@@ -0,0 +1,17 @@
1
+ version: 2
2
+ updates:
3
+ # docs v2 https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates
4
+ - package-ecosystem: "npm"
5
+ schedule:
6
+ interval: "monthly"
7
+ # Check for npm updates on Sundays
8
+ day: "saturday"
9
+ allow:
10
+ - dependency-type: "production"
11
+ directory: "/"
12
+ commit-message:
13
+ # for production deps, prefix commit messages with "fix" (trigger a patch release)
14
+ prefix: "fix"
15
+ # for development deps, prefix commit messages with "chore" (do NOT trigger an npm release)
16
+ prefix-development: "chore"
17
+ include: "scope"
@@ -0,0 +1,9 @@
1
+ ### Expected behavior:
2
+
3
+ ### Actual behavior:
4
+
5
+ ### Steps to reproduce the problem:
6
+
7
+ ### Environment:
8
+
9
+ Please include version of serverless-http-invoker, version of serverless, version of node, and operating system name and version
@@ -0,0 +1,89 @@
1
+ name: build
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-20.04
12
+
13
+ steps:
14
+ - uses: actions/checkout@v1
15
+ - name: "Use Node.js (deliberately not using matrix)"
16
+ uses: actions/setup-node@v1
17
+ with:
18
+ node-version: v12.x
19
+
20
+ - name: install dependencies
21
+ run: |
22
+ npm i
23
+
24
+ - name: lint
25
+ run: |
26
+ npm run lint
27
+
28
+ build_and_test:
29
+ runs-on: ubuntu-20.04
30
+ strategy:
31
+ matrix:
32
+ node: [10, 12, 14]
33
+
34
+ steps:
35
+ - uses: actions/checkout@v2
36
+ - name: Use Node.js ${{ matrix.node }}
37
+ uses: actions/setup-node@v2
38
+ with:
39
+ node-version: ${{ matrix.node }}
40
+
41
+ - name: install dependencies
42
+ run: |
43
+ npm i
44
+
45
+ - name: test
46
+ env:
47
+ CI_NODE_VERSION: ${{ matrix.node }}
48
+ run: |
49
+ npm run test
50
+
51
+ - name: publish coverage
52
+ uses: coverallsapp/github-action@master
53
+ continue-on-error: true
54
+ with:
55
+ github-token: ${{ secrets.GITHUB_TOKEN }}
56
+ flag-name: nodejs-${{ matrix.node }}
57
+ parallel: true
58
+
59
+ finish_tests:
60
+ needs: build_and_test
61
+ runs-on: ubuntu-20.04
62
+ steps:
63
+ - name: Coveralls Finished
64
+ uses: coverallsapp/github-action@master
65
+ continue-on-error: true
66
+ with:
67
+ github-token: ${{ secrets.GITHUB_TOKEN }}
68
+ parallel-finished: true
69
+
70
+ deploy_package:
71
+ needs: finish_tests
72
+ runs-on: ubuntu-20.04
73
+ steps:
74
+ - uses: actions/checkout@v1
75
+
76
+ - name: Use Node.js v12.x
77
+ uses: actions/setup-node@v1
78
+ with:
79
+ node-version: v12.x
80
+
81
+ #- name: debug deploy_package
82
+ # uses: actions/bin/debug@master
83
+
84
+ - name: deploy via semantic-release
85
+ env:
86
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
87
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
88
+ run: |
89
+ npx semantic-release@17
@@ -0,0 +1,19 @@
1
+ name: dependabot validate
2
+
3
+ on:
4
+ pull_request:
5
+ paths:
6
+ - ".github/dependabot.yml"
7
+ - ".github/workflows/dependabot-validate.yml"
8
+ jobs:
9
+ validate:
10
+ runs-on: ubuntu-20.04
11
+ steps:
12
+ - uses: actions/checkout@v2
13
+ - uses: marocchino/validate-dependabot@v1
14
+ id: validate
15
+ - uses: marocchino/sticky-pull-request-comment@v2
16
+ if: always()
17
+ with:
18
+ header: validate-dependabot
19
+ message: ${{ steps.validate.outputs.markdown }}
package/.nycrc.yaml ADDED
@@ -0,0 +1,14 @@
1
+ reporter:
2
+ - lcov
3
+ - html
4
+ check-coverage: true
5
+ branches: 90
6
+ lines: 90
7
+ functions: 90
8
+ statements: 90
9
+ all: true
10
+ exclude:
11
+ - scripts/
12
+ - test/
13
+ - coverage/
14
+ - release.config.js
@@ -0,0 +1,4 @@
1
+ .nyc_output/
2
+ coverage/
3
+ dist/
4
+ docs/todo.md
package/.prettierrc ADDED
@@ -0,0 +1 @@
1
+ semi: false
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 scott willeke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ [![npm version](https://badge.fury.io/js/serverless-http-invoker.svg)](https://www.npmjs.com/package/serverless-http-invoker)
2
+ [![npm downloads](https://img.shields.io/npm/dt/serverless-http-invoker.svg?logo=npm)](https://www.npmjs.com/package/serverless-http-invoker)
3
+ [![Build Status](https://github.com/activescott/serverless-http-invoker/workflows/build/badge.svg)](https://github.com/activescott/serverless-http-invoker/actions)
4
+ [![Coverage Status](https://coveralls.io/repos/github/activescott/serverless-http-invoker/badge.svg)](https://coveralls.io/github/activescott/serverless-http-invoker)
5
+ [![License](https://img.shields.io/github/license/activescott/serverless-http-invoker.svg)](https://github.com/activescott/serverless-http-invoker/blob/main/LICENSE)
6
+ [![GitHub stars](https://img.shields.io/github/stars/activescott/serverless-http-invoker.svg?style=social)](https://github.com/activescott/serverless-http-invoker)
7
+
8
+ # serverless-http-invoker
9
+
10
+ Locally invoke [Serverless](https://github.com/serverless/serverless) functions via their HTTP event as specified in Serverless.yml.
11
+
12
+ It makes it easy to test not only your handler logic, but also ensures that you have your http events setup properly in serverless.yml without deploying.
13
+
14
+ <!-- TOC -->
15
+
16
+ - [Usage / Quick Start](#usage--quick-start)
17
+ - [Prerequisites / Usage Requirements](#prerequisites--usage-requirements)
18
+ - [Install](#install)
19
+ - [Features](#features)
20
+ - [Contributing 🤝](#contributing-🤝)
21
+ - [Show your support](#show-your-support)
22
+ - [Release Process (Deploying to NPM)](#release-process-deploying-to-npm)
23
+ - [License 📝](#license-📝)
24
+
25
+ <!-- /TOC -->
26
+
27
+ ## Usage / Quick Start
28
+
29
+ Use it in tests of Serverless functions to test your HTTP endpoints along with the handler code. For example, you can write the following to test a Serverless function:
30
+
31
+ it('should invoke simple path', function () {
32
+ let response = invoker.invoke('GET api/hello')
33
+ return expect(response).to.eventually.have.property('statusCode', 200)
34
+ })
35
+
36
+ The test above is a test of a Serverless function defined in a `Serverless.yml` as follows:
37
+
38
+ functions:
39
+ hello:
40
+ handler: handler.hello
41
+ events:
42
+ - http:
43
+ path: api/hello
44
+ method: get
45
+
46
+ Some of the more common use cases are demonstrated in the basic tests at [the basic test cases](examples/basic/basic.spec.js).
47
+ An exhaustive list of what is supported in Some of the more common use cases are demonstrated in the basic tests at [the comprehensive test cases](examples/comprehensive/comprehensive.spec.js).
48
+
49
+ ## Prerequisites / Usage Requirements
50
+
51
+ Requires Node.js latest, LTS, and v10 ([tested](https://travis-ci.org/activescott/serverless-http-invoker)).
52
+
53
+ If you need Node.js v6.x - v9.x support you can use [serverless-http-invoker@0.8.6](https://www.npmjs.com/package/serverless-http-invoker/v/0.8.6).
54
+
55
+ Requires Serverless Framework v1.x.
56
+ If you are new to the Serverless Framework, check out the [Serverless Framework Getting Started Guide](https://serverless.com/framework/docs/getting-started/).
57
+
58
+ ## Install
59
+
60
+ npm (`npm install serverless-http-invoker --save-dev`) or yarn (`yarn add serverless-http-invoker --dev`)
61
+
62
+ ## Features
63
+
64
+ - Simple to reference your handler
65
+ - Tests the serverless route is configured in serverless.yml as well as your handler code
66
+ - Test Framework agnostic (mocha, jest, etc.)
67
+
68
+ ## Contributing 🤝
69
+
70
+ This is a community project. We invite your participation through issues and pull requests! You can peruse the [contributing guidelines](.github/CONTRIBUTING.md).
71
+
72
+ ## Show your support
73
+
74
+ Give a ⭐️ if this project helped you!
75
+
76
+ ## Release Process (Deploying to NPM)
77
+
78
+ We use [semantic-release](https://github.com/semantic-release/semantic-release) to consistently release [semver](https://semver.org/)-compatible versions. This project deploys to multiple [npm distribution tags](https://docs.npmjs.com/cli/dist-tag). Each of the below branches correspond to the following npm distribution tags:
79
+
80
+ | branch | npm distribution tag |
81
+ | ------ | -------------------- |
82
+ | main | latest |
83
+ | beta | beta |
84
+
85
+ To trigger a release use a Conventional Commit following [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) on one of the above branches.
86
+
87
+ ## License 📝
88
+
89
+ Copyright © 2017 [Scott Willeke](https://github.com/activescott).
90
+
91
+ This project is [MIT](https://github.com/activescott/serverless-http-invoker/blob/main/LICENSE) licensed.
@@ -0,0 +1,41 @@
1
+ "use strict"
2
+ require("../../test/support/setup")
3
+ const path = require("path")
4
+ const expect = require("chai").expect
5
+ const ServerlessInvoker = require("../../index")
6
+
7
+ describe("basic", function() {
8
+ let sls = null
9
+ beforeEach(function() {
10
+ sls = new ServerlessInvoker(path.join(__dirname))
11
+ })
12
+
13
+ it("should invoke simple path", function() {
14
+ const response = sls.invoke("GET api/hello")
15
+ return expect(response).to.eventually.have.property("statusCode", 200)
16
+ })
17
+
18
+ it("should have event.httpMethod", function() {
19
+ const response = sls.invoke("GET api/hello")
20
+ return expect(response).to.eventually.have.deep.nested.property(
21
+ "body.input.httpMethod",
22
+ "GET"
23
+ )
24
+ })
25
+
26
+ it("should have event.path", function() {
27
+ const response = sls.invoke("GET api/hello")
28
+ return expect(response).to.eventually.have.deep.nested.property(
29
+ "body.input.path",
30
+ "/api/hello"
31
+ )
32
+ })
33
+
34
+ it("should have event.resource", function() {
35
+ const response = sls.invoke("GET api/hello")
36
+ return expect(response).to.eventually.have.deep.nested.property(
37
+ "body.input.resource",
38
+ "/api/hello"
39
+ )
40
+ })
41
+ })
@@ -0,0 +1,19 @@
1
+ "use strict"
2
+ const assert = require("assert")
3
+
4
+ module.exports.hello = async event => {
5
+ return {
6
+ statusCode: 200,
7
+ headers: {
8
+ "Content-Type": "application/json"
9
+ },
10
+ body: JSON.stringify(
11
+ {
12
+ message: "Go Serverless v1.0! Your function executed successfully!",
13
+ input: event
14
+ },
15
+ null,
16
+ 2
17
+ )
18
+ }
19
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "basic",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "license": "MIT",
6
+ "devDependencies": {
7
+ "serverless": "^1.46.1"
8
+ }
9
+ }
@@ -0,0 +1,14 @@
1
+ service: basic-example
2
+
3
+ provider:
4
+ name: aws
5
+ runtime: nodejs10.x
6
+ region: us-west-2
7
+
8
+ functions:
9
+ hello:
10
+ handler: handler.hello
11
+ events:
12
+ - http:
13
+ path: api/hello
14
+ method: get
@@ -0,0 +1,170 @@
1
+ "use strict"
2
+ require("../../test/support/setup")
3
+ const path = require("path")
4
+ const expect = require("chai").expect
5
+ const ServerlessInvoker = require("../../index")
6
+
7
+ let sls = null
8
+
9
+ beforeEach(function() {
10
+ process.chdir(path.join(__dirname))
11
+ sls = new ServerlessInvoker(path.join(__dirname))
12
+ })
13
+
14
+ it("should work with callbacks too", function() {
15
+ const response = sls.invoke("GET api/callback")
16
+ return expect(response).to.eventually.have.property("statusCode", 200)
17
+ })
18
+
19
+ it("should invoke path with params", function() {
20
+ const response = sls.invoke("GET api/hello/world")
21
+ return expect(response).to.eventually.have.property("statusCode", 200)
22
+ })
23
+
24
+ it("should invoke path with shorthand", function() {
25
+ const response = sls.invoke("GET api/shorthand")
26
+ return expect(response).to.eventually.have.property("statusCode", 200)
27
+ })
28
+
29
+ it("should invoke path with multiple params", function() {
30
+ const response = sls.invoke("GET api/res1/1111/res2/2222")
31
+ return expect(response).to.eventually.have.property("statusCode", 200)
32
+ })
33
+
34
+ it("should parse json response body", function() {
35
+ const response = sls.invoke("GET api/hello")
36
+ expect(response).to.eventually.have.property("statusCode", 200)
37
+ return expect(response).to.eventually.have.deep.nested.property(
38
+ "body.message",
39
+ "Go Serverless v1.0! Your function executed successfully!"
40
+ )
41
+ })
42
+
43
+ it("should load environment", function() {
44
+ const response = sls.invoke("GET api/env")
45
+ expect(response).to.eventually.have.property("statusCode", 200)
46
+ return expect(response).to.eventually.have.deep.nested.property(
47
+ "body.message",
48
+ "process.env.MY_SIMPLE==simple value"
49
+ )
50
+ })
51
+
52
+ it("should pass data to POST", function() {
53
+ const response = sls.invoke("POST api/postit", { body: "boo" })
54
+ return expect(response).to.eventually.have.deep.property("body", {
55
+ message: "postit:boo"
56
+ })
57
+ })
58
+
59
+ it("should pass pathParameters with values when present", function() {
60
+ const response = sls.invoke("GET api/res1/xxx/res2/yyy")
61
+ return response.then(resp => {
62
+ return expect(resp.body.input).to.have.deep.property("pathParameters", {
63
+ res1ID: "xxx",
64
+ res2ID: "yyy"
65
+ })
66
+ })
67
+ })
68
+
69
+ it("should pass pathParameters along with existing event too", function() {
70
+ const response = sls.invoke("GET api/res1/xxx/res2/yyy", {
71
+ requestPayload: "boo"
72
+ })
73
+ return response.then(resp => {
74
+ expect(resp.body).to.have.property("input")
75
+ expect(resp.body.input).to.have.deep.property("pathParameters", {
76
+ res1ID: "xxx",
77
+ res2ID: "yyy"
78
+ })
79
+ expect(resp.body.input).to.have.property("requestPayload", "boo")
80
+ })
81
+ })
82
+
83
+ it("should pass pathParameters empty when not present", function() {
84
+ const response = sls.invoke("GET api/hello")
85
+ return response.then(resp => {
86
+ return expect(resp.body.input).to.have.deep.property("pathParameters", {})
87
+ })
88
+ })
89
+
90
+ it("should pass greedy path params", function() {
91
+ const response = sls.invoke("GET api/greedy/blah/blah/blah")
92
+ return response.then(resp => {
93
+ return expect(resp.body.input).to.have.deep.property("pathParameters", {
94
+ money: "blah/blah/blah"
95
+ })
96
+ })
97
+ })
98
+
99
+ it("should pass queryStringParameters with values when present", function() {
100
+ const response = sls.invoke("GET api/with_querystring_params?p1=val1&p2=val2")
101
+ return response.then(resp => {
102
+ return expect(resp.body.input).to.have.deep.property(
103
+ "queryStringParameters",
104
+ {
105
+ p1: "val1",
106
+ p2: "val2"
107
+ }
108
+ )
109
+ })
110
+ })
111
+
112
+ it("should pass queryStringParameters even when not present", function() {
113
+ const response = sls.invoke("GET api/with_querystring_params")
114
+ return response.then(resp => {
115
+ return expect(resp.body.input).to.have.deep.property(
116
+ "queryStringParameters",
117
+ {}
118
+ )
119
+ })
120
+ })
121
+
122
+ it("should match urls with query strings and path params in url", function() {
123
+ const response = sls.invoke(
124
+ "GET api/with_querystring_params_and_pathparams/ppvalue?qs1=qsval1"
125
+ )
126
+ return response.then(resp => {
127
+ expect(resp.body.input).to.have.deep.property("queryStringParameters", {
128
+ qs1: "qsval1"
129
+ })
130
+ return expect(resp.body.input).to.have.deep.property("pathParameters", {
131
+ pathparam1: "ppvalue"
132
+ })
133
+ })
134
+ })
135
+
136
+ it("should marshal raw lambda exceptions back as http responses", function() {
137
+ const response = sls.invoke("GET api/throwWorld")
138
+ return expect(response).to.eventually.have.property("statusCode", 502)
139
+ })
140
+
141
+ it("should marshal raw handled lambda errors back as http responses", function() {
142
+ const response = sls.invoke("GET api/errorWorld")
143
+ return expect(response).to.eventually.have.property("statusCode", 502)
144
+ })
145
+
146
+ it("should error if path isn't found", function() {
147
+ const response = sls.invoke("GET api/DOES_NOT_EXIST")
148
+ return expect(response).to.eventually.be.rejectedWith(
149
+ /^Serverless http event not found for HTTP request/
150
+ )
151
+ })
152
+
153
+ it("should try to find service path in same dir", function() {
154
+ process.chdir(path.join(__dirname))
155
+ const localSls = new ServerlessInvoker()
156
+ expect(localSls.servicePath).to.match(/examples\/comprehensive$/)
157
+ })
158
+
159
+ it("should try to find service path in parent dir", function() {
160
+ process.chdir(path.join(__dirname, "./subdir"))
161
+ const localSls = new ServerlessInvoker()
162
+ expect(localSls.servicePath).to.match(/examples\/comprehensive$/)
163
+ })
164
+
165
+ it("should fail if serverless.yml not found", function() {
166
+ process.chdir(path.join(__dirname, "../../test-data/no-serverless-found"))
167
+ expect(() => new ServerlessInvoker()).to.throw(
168
+ /^Cannot find serverless.yml. Started search in working directory/
169
+ )
170
+ })
@@ -0,0 +1,86 @@
1
+ "use strict"
2
+ const assert = require("assert")
3
+
4
+ module.exports.hello = async event => {
5
+ return {
6
+ statusCode: 200,
7
+ headers: {
8
+ "Content-Type": "application/json"
9
+ },
10
+ body: JSON.stringify(
11
+ {
12
+ message: "Go Serverless v1.0! Your function executed successfully!",
13
+ input: event
14
+ },
15
+ null,
16
+ 2
17
+ )
18
+ }
19
+ }
20
+
21
+ module.exports.callback = (event, context, callback) => {
22
+ const response = {
23
+ statusCode: 200,
24
+ headers: {
25
+ "Content-Type": "application/json"
26
+ },
27
+ body: JSON.stringify(
28
+ {
29
+ message: "Go Serverless v1.00! Your function executed successfully!",
30
+ input: event
31
+ },
32
+ null,
33
+ 2
34
+ )
35
+ }
36
+ callback(null, response)
37
+ }
38
+
39
+ module.exports.throwWorld = async () => {
40
+ throw new Error("throw world")
41
+ }
42
+
43
+ module.exports.errorWorld = (event, context, callback) => {
44
+ callback(new Error("throw world"), null)
45
+ }
46
+
47
+ module.exports.with_querystring_params = async event => {
48
+ return {
49
+ statusCode: 200,
50
+ headers: {
51
+ "Content-Type": "application/json"
52
+ },
53
+ body: JSON.stringify({
54
+ message: "QueryStringParams on prop",
55
+ queryStringParameters: event.queryStringParameters,
56
+ input: event
57
+ })
58
+ }
59
+ }
60
+
61
+ module.exports.env = async event => {
62
+ assert(process.env.MY_SIMPLE === "simple value")
63
+
64
+ return {
65
+ statusCode: 200,
66
+ headers: {
67
+ "Content-Type": "application/json"
68
+ },
69
+ body: JSON.stringify({
70
+ message: "process.env.MY_SIMPLE==" + process.env.MY_SIMPLE,
71
+ input: event
72
+ })
73
+ }
74
+ }
75
+
76
+ module.exports.postit = async event => {
77
+ return {
78
+ statusCode: 200,
79
+ headers: {
80
+ "Content-Type": "application/json"
81
+ },
82
+ body: JSON.stringify({
83
+ message: "postit:" + event.body
84
+ })
85
+ }
86
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "comprehensive",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "license": "MIT",
6
+ "devDependencies": {
7
+ "serverless": "^1.46.1"
8
+ }
9
+ }
@@ -0,0 +1,95 @@
1
+ service: basic-example # NOTE: update this with your service name
2
+
3
+ provider:
4
+ name: aws
5
+ runtime: nodejs10.x
6
+ region: us-west-2
7
+
8
+ environment:
9
+ MY_SIMPLE: "simple value"
10
+ MY_DYNAMIC: "${self:provider.stage}"
11
+
12
+ functions:
13
+ hello:
14
+ handler: handler.hello
15
+ events:
16
+ - http:
17
+ path: api/hello
18
+ method: get
19
+
20
+ with_params:
21
+ handler: handler.hello
22
+ events:
23
+ - http:
24
+ path: api/hello/{greeting}
25
+ method: get
26
+
27
+ with_greedy_path_params:
28
+ handler: handler.hello
29
+ events:
30
+ - http:
31
+ path: api/greedy/{money+}
32
+ method: get
33
+
34
+ shorthand_path_syntax:
35
+ handler: handler.hello
36
+ events:
37
+ - http: GET api/shorthand
38
+
39
+ malformed_http_event:
40
+ handler: handler.hello
41
+ events:
42
+ - http:
43
+ path: note_how_path_isnt_indented_enough
44
+ method: get
45
+
46
+ path_with_multiple_params:
47
+ handler: handler.hello
48
+ events:
49
+ - http: GET api/res1/{res1ID}/res2/{res2ID}
50
+
51
+ env:
52
+ handler: handler.env
53
+ events:
54
+ - http:
55
+ path: api/env
56
+ method: get
57
+
58
+ postit:
59
+ handler: handler.postit
60
+ events:
61
+ - http:
62
+ path: api/postit
63
+ method: POST
64
+
65
+ with_querystring_params:
66
+ handler: handler.with_querystring_params
67
+ events:
68
+ - http:
69
+ path: api/with_querystring_params
70
+ method: get
71
+
72
+ with_querystring_params_and_pathparams:
73
+ handler: handler.with_querystring_params
74
+ events:
75
+ - http:
76
+ path: api/with_querystring_params_and_pathparams/{pathparam1}
77
+ method: get
78
+ throwWorld:
79
+ handler: handler.throwWorld
80
+ events:
81
+ - http:
82
+ path: api/throwWorld
83
+ method: get
84
+ errorWorld:
85
+ handler: handler.errorWorld
86
+ events:
87
+ - http:
88
+ path: api/errorWorld
89
+ method: get
90
+ callback:
91
+ handler: handler.callback
92
+ events:
93
+ - http:
94
+ path: api/callback
95
+ method: get
@@ -0,0 +1 @@
1
+ just to keep this directory here for a test.
package/index.js ADDED
@@ -0,0 +1,289 @@
1
+ "use strict"
2
+ const path = require("path")
3
+ const fs = require("fs")
4
+ const Serverless = require("serverless")
5
+ const Promise = require("bluebird")
6
+ const { wrap } = require("lambda-wrapper")
7
+ const assert = require("assert")
8
+ const url = require("url")
9
+ const querystring = require("querystring")
10
+
11
+ class ServerlessInvoker {
12
+ /**
13
+ * Initializes an instance of the class.
14
+ * @param {*string} servicePath Path to the directory containing the serverless file.
15
+ */
16
+ constructor(servicePath) {
17
+ this.servicePath = servicePath || this.findServicePath()
18
+ this.serverless = null
19
+ this.serverlessEvents = null
20
+ }
21
+
22
+ findServicePath() {
23
+ let dir = process.cwd()
24
+ while (dir !== "/" && !fs.existsSync(path.join(dir, "serverless.yml"))) {
25
+ dir = path.dirname(dir)
26
+ }
27
+ if (dir === "/") {
28
+ throw new Error(
29
+ `Cannot find serverless.yml. Started search in working directory ${process.cwd()}`
30
+ )
31
+ }
32
+ return dir
33
+ }
34
+
35
+ initializeServerless() {
36
+ const config = {
37
+ servicePath: this.servicePath
38
+ }
39
+ const sls = new Serverless(config)
40
+ // NOTE: I've seen sls.init() run very slowly; nearly 500ms!
41
+ return sls.init().then(() => {
42
+ return sls.variables.populateService().then(() => {
43
+ sls.service.setFunctionNames({})
44
+ sls.service.mergeArrays()
45
+ sls.service.validate()
46
+ this.serverless = sls
47
+ })
48
+ })
49
+ }
50
+
51
+ /**
52
+ * Invokes the serverless function bound to the HTTP event with the provided specification.
53
+ * @param {*string} httpRequest A method+path like 'GET api/users/me'.
54
+ * @param {*object} event The event that should be submitted to the http endpoint.
55
+ * @param {*object} context The context passed to the lambda function
56
+ */
57
+ invoke(httpRequest, event, context) {
58
+ // Read the serverless.yml file
59
+ return this.initializeServerless()
60
+ .then(() => this.loadServerlessEvents())
61
+ .then(httpEvents => {
62
+ // find the event that matches the specified httpRequest
63
+ let httpEvent = httpEvents.find(e => e.test(httpRequest))
64
+ if (!httpEvent) {
65
+ throw new Error(
66
+ `Serverless http event not found for HTTP request "${httpRequest}" in service path "${this.servicePath}".`
67
+ )
68
+ }
69
+
70
+ const parsedPath = ServerlessInvoker.parsePath(httpRequest)
71
+ event = Object.assign({}, event, {
72
+ path: parsedPath,
73
+ resource: parsedPath,
74
+ pathParameters: ServerlessInvoker.parsePathParameters(
75
+ httpEvent,
76
+ httpRequest
77
+ ),
78
+ queryStringParameters: ServerlessInvoker.parseQueryStringParameters(
79
+ httpRequest
80
+ ),
81
+ httpMethod: ServerlessInvoker.parseHttpMethod(httpRequest)
82
+ })
83
+
84
+ return this.loadServerlessEnvironment().then(() => {
85
+ return this.invokeWithLambdaWrapper(httpEvent, event, context)
86
+ .then(response => {
87
+ if (
88
+ response &&
89
+ response.headers &&
90
+ Object.keys(response.headers).includes("Content-Type") &&
91
+ response.headers["Content-Type"] === "application/json"
92
+ ) {
93
+ if (response.body && typeof response.body === "string") {
94
+ response.body = JSON.parse(response.body)
95
+ }
96
+ }
97
+ return response
98
+ })
99
+ .catch(eLambdaFuncError => {
100
+ // In this situation API Gateway returns 502 (bad gateway) and sets the response body to `{"message": "Internal server error"}`. We are adding a bit more detail to the error.
101
+ console.error(
102
+ "serverless-http-invoker error invoking function:",
103
+ eLambdaFuncError
104
+ )
105
+ const response = {
106
+ statusCode: 502,
107
+ body: {
108
+ message: "Internal server error",
109
+ test_debug_error_message: eLambdaFuncError.toString(),
110
+ test_debug_error_stack: eLambdaFuncError.stack
111
+ }
112
+ }
113
+ return response
114
+ })
115
+ })
116
+ })
117
+ }
118
+
119
+ static parseHttpMethod(httpRequest) {
120
+ const parts = httpRequest.split(" ")
121
+ assert(
122
+ parts.length >= 1,
123
+ "expected httpRequest to be a method seperated by a space and then the request path"
124
+ )
125
+ return parts[0]
126
+ }
127
+
128
+ static parsePathParameters(httpEvent, httpRequest) {
129
+ let pathParamValues = httpEvent.matcher.exec(httpRequest)
130
+ if (pathParamValues.length > 0) {
131
+ pathParamValues = pathParamValues.slice(1)
132
+ }
133
+ const pathParametersMap = {}
134
+ assert(
135
+ httpEvent.pathParamNames.length === pathParamValues.length,
136
+ `expected param names and param values to have same length, but were: \n\tnames: ${JSON.stringify(
137
+ httpEvent.pathParamNames
138
+ )}\n\t!==\n\tvalues: ${JSON.stringify(pathParamValues)}`
139
+ )
140
+ for (let i = 0; i < httpEvent.pathParamNames.length; i++) {
141
+ let paramName = httpEvent.pathParamNames[i]
142
+ pathParametersMap[paramName] = pathParamValues[i]
143
+ }
144
+ return pathParametersMap
145
+ }
146
+
147
+ static parseQueryStringParameters(requestUrl) {
148
+ const myURL = url.parse(
149
+ "https://fakehost.com/" + requestUrl.split(" ")[1],
150
+ true
151
+ )
152
+ const search =
153
+ myURL.search && myURL.search.length > 0
154
+ ? myURL.search.slice(1)
155
+ : myURL.search
156
+ return querystring.parse(search)
157
+ }
158
+
159
+ static parsePath(requestUrl) {
160
+ const myURL = url.parse("https://fakehost.com/" + requestUrl.split(" ")[1])
161
+ return myURL.pathname
162
+ }
163
+
164
+ loadServerlessEnvironment() {
165
+ return Promise.try(() => {
166
+ let env = this.serverless.service.provider.environment
167
+ Object.assign(process.env, env)
168
+ return env
169
+ })
170
+ }
171
+
172
+ invokeWithLambdaWrapper(httpEvent, event, context) {
173
+ return Promise.try(() => {
174
+ let handlerModule = require(path.join(
175
+ this.servicePath,
176
+ httpEvent.handlerPath
177
+ ))
178
+ let lambda = wrap(handlerModule, { handler: httpEvent.handlerName })
179
+ lambda = Promise.promisifyAll(lambda)
180
+ return lambda.runHandler(event, context || {})
181
+ })
182
+ }
183
+
184
+ loadServerlessEvents() {
185
+ let funcs = this.serverless.service
186
+ .getAllFunctions()
187
+ .map(fname => {
188
+ let funcObj = this.serverless.service.getFunction(fname)
189
+ let events = this.serverless.service.getAllEventsInFunction(fname)
190
+ let f = {
191
+ name: fname,
192
+ handler: funcObj.handler,
193
+ events: events.filter(
194
+ e => Object.keys(e).includes("http") && e.http !== null
195
+ )
196
+ }
197
+ return f
198
+ })
199
+ .filter(f => f.events.length > 0)
200
+ .map(f => {
201
+ f.events = f.events.map(evt => {
202
+ // add a path parser regex:
203
+ RegExp.escape = function(s) {
204
+ // https://stackoverflow.com/a/3561711/51061
205
+ return s.replace(/[-/\\^$*+?.()|[\]]/g, "\\$&")
206
+ }
207
+ let path = null
208
+ let method = null
209
+ if (typeof evt.http === "object") {
210
+ path = evt.http.path
211
+ method = evt.http.method
212
+ } else {
213
+ assert(
214
+ typeof evt.http === "string",
215
+ `Expected http event to have a type of object or string but was ${typeof evt.http}.`
216
+ )
217
+ method = evt.http.split(" ")[0]
218
+ path = evt.http.split(" ")[1]
219
+ }
220
+ let pattern = RegExp.escape(path)
221
+ // collect the pathParamNames:
222
+ // first the "greedy" path params like {pname+} (with a '+' postfix)
223
+ let matchPathParamNamesPattern = pattern.replace(
224
+ /\/\{[^}]*\+\}/gi,
225
+ "/(.*)"
226
+ )
227
+ // then the normal path params
228
+ matchPathParamNamesPattern = pattern.replace(
229
+ /\/\{[^}]*\}/gi,
230
+ "/([^/]*)"
231
+ )
232
+ let matchPathParamNames = new RegExp(matchPathParamNamesPattern, "gi")
233
+ let pathParamNames = matchPathParamNames.exec(path).slice(1) // the first element is full matched text so slice it off
234
+ // remove the surrounding bracket characters:
235
+ pathParamNames = pathParamNames.map(p =>
236
+ p.replace(/^\{([^}]+)\}$/, "$1")
237
+ )
238
+ // remove the '+' postfix if it exists
239
+ pathParamNames = pathParamNames.map(p =>
240
+ p.endsWith("+") ? p.substring(0, p.length - 1) : p
241
+ )
242
+ // console.log('pathParamNames:', pathParamNames, 'path:', path)
243
+ // now collect the values for the params:
244
+ let optionalQueryStringPattern = "(?:\\?.*)?$"
245
+ // first match greedy, then the normal ones again:
246
+ let matchPathParamValuesPattern = pattern.replace(
247
+ /\/\{[^}]*\+\}/gi,
248
+ "/(.+)"
249
+ )
250
+ matchPathParamValuesPattern = matchPathParamValuesPattern.replace(
251
+ /\/\{[^}]*\}/gi,
252
+ "/([^/\\?]+)"
253
+ )
254
+ let matcher = new RegExp(
255
+ "^" +
256
+ method +
257
+ "\\s+" +
258
+ matchPathParamValuesPattern +
259
+ optionalQueryStringPattern,
260
+ "i"
261
+ )
262
+ // console.log('path:', path, 'matcher:', matcher)
263
+ return Object.assign(evt.http, {
264
+ matcher: matcher,
265
+ pathParamNames: pathParamNames,
266
+ test: request => {
267
+ const result = matcher.test(request)
268
+ // console.log(`${method} ${path}: ${request} == ${result} \n pattern:${matcher.source}`)
269
+ return result
270
+ }
271
+ })
272
+ })
273
+ return f
274
+ })
275
+
276
+ let events = []
277
+ for (let f of funcs) {
278
+ for (let e of f.events) {
279
+ e.function = f.name
280
+ e.handlerPath = f.handler.split(".")[0]
281
+ e.handlerName = f.handler.split(".")[1]
282
+ events.push(e)
283
+ }
284
+ }
285
+ return Promise.resolve(events)
286
+ }
287
+ }
288
+
289
+ module.exports = ServerlessInvoker
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "serverless-http-invoker",
3
+ "version": "1.0.9",
4
+ "description": "Locally invoke Serverless functions via their HTTP event as specified in Serverless.yml for testing.",
5
+ "main": "index.js",
6
+ "author": "Scott Willeke <scott@willeke.com> (https:/scott.willeke.com/)",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "serverless",
10
+ "serverless framework",
11
+ "serverless applications",
12
+ "serverless modules",
13
+ "api gateway",
14
+ "lambda",
15
+ "aws",
16
+ "aws lambda",
17
+ "amazon",
18
+ "amazon web services",
19
+ "serverless.com",
20
+ "mocha",
21
+ "test",
22
+ "testing",
23
+ "bdd",
24
+ "tdd"
25
+ ],
26
+ "engines": {
27
+ "node": ">=6"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/activescott/serverless-http-invoker.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/activescott/serverless-http-invoker/issues"
35
+ },
36
+ "scripts": {
37
+ "test": "nyc ./node_modules/.bin/mocha ./examples/**/*.spec.js",
38
+ "pretest": "npm run lint",
39
+ "lint": "prettier -l \"{,!(node_modules)/**/}*.{ts,tsx,js,jsx,md,yml,json,html}\"",
40
+ "lint-fix": "prettier --write \"{,!(node_modules)/**/}*.{ts,tsx,js,jsx,md,yml,json,html}\""
41
+ },
42
+ "dependencies": {
43
+ "lambda-wrapper": "^0.3.0"
44
+ },
45
+ "peerDependencies": {
46
+ "serverless": "^1.27.3"
47
+ },
48
+ "devDependencies": {
49
+ "chai": "^4.1.0",
50
+ "chai-as-promised": "^7.1.1",
51
+ "coveralls": "^3.0.4",
52
+ "mocha": "^8.3.2",
53
+ "nyc": "^15.0.0",
54
+ "prettier": "^1.18.2",
55
+ "serverless": "^2.46.0"
56
+ }
57
+ }
package/pre-commit.sh ADDED
@@ -0,0 +1,11 @@
1
+ # RUN `ln -sf ../../pre-commit.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit` to set this up as the git pre-commit hook
2
+
3
+ die () {
4
+ echo ""
5
+ echo "git pre-commit hook FAILED! See above for details."
6
+ echo ""
7
+ exit $@
8
+ }
9
+
10
+ npm run lint || die $?
11
+ npm run test || die $?
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ // https://github.com/semantic-release/semantic-release/blob/beta/docs/usage/configuration.md
3
+ branches: [
4
+ "+([0-9])?(.{+([0-9]),x}).x",
5
+ "main",
6
+ "next",
7
+ "next-major",
8
+ { name: "beta", prerelease: true },
9
+ { name: "alpha", prerelease: true }
10
+ ]
11
+ }
@@ -0,0 +1,4 @@
1
+ "use strict"
2
+ const chai = require("chai")
3
+ var chaiAsPromised = require("chai-as-promised")
4
+ chai.use(chaiAsPromised)
@@ -0,0 +1 @@
1
+ just to keep this directory here for a test.