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.
- package/.github/CODE_OF_CONDUCT.md +46 -0
- package/.github/CONTRIBUTING.md +16 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +32 -0
- package/.github/dependabot.yml +17 -0
- package/.github/issue_template.md +9 -0
- package/.github/workflows/build.yml +89 -0
- package/.github/workflows/dependabot-validate.yml +19 -0
- package/.nycrc.yaml +14 -0
- package/.prettierignore +4 -0
- package/.prettierrc +1 -0
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/examples/basic/basic.spec.js +41 -0
- package/examples/basic/handler.js +19 -0
- package/examples/basic/package.json +9 -0
- package/examples/basic/serverless.yml +14 -0
- package/examples/comprehensive/comprehensive.spec.js +170 -0
- package/examples/comprehensive/handler.js +86 -0
- package/examples/comprehensive/package.json +9 -0
- package/examples/comprehensive/serverless.yml +95 -0
- package/examples/comprehensive/subdir/.keep +1 -0
- package/index.js +289 -0
- package/package.json +57 -0
- package/pre-commit.sh +11 -0
- package/release.config.js +11 -0
- package/test/support/setup.js +4 -0
- package/test-data/no-serverless-found/.keep +1 -0
|
@@ -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,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
package/.prettierignore
ADDED
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
|
+
[](https://www.npmjs.com/package/serverless-http-invoker)
|
|
2
|
+
[](https://www.npmjs.com/package/serverless-http-invoker)
|
|
3
|
+
[](https://github.com/activescott/serverless-http-invoker/actions)
|
|
4
|
+
[](https://coveralls.io/github/activescott/serverless-http-invoker)
|
|
5
|
+
[](https://github.com/activescott/serverless-http-invoker/blob/main/LICENSE)
|
|
6
|
+
[](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,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,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 @@
|
|
|
1
|
+
just to keep this directory here for a test.
|