github-webhook-handler 1.0.0 → 2.0.0
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/dependabot.yml +20 -0
- package/.github/workflows/test-and-release.yml +56 -0
- package/CHANGELOG.md +9 -0
- package/README.md +22 -18
- package/github-webhook-handler.d.ts +12 -10
- package/github-webhook-handler.js +8 -10
- package/package.json +101 -9
- package/test.js +171 -191
- package/.travis.yml +0 -14
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: 'github-actions'
|
|
4
|
+
directory: '/'
|
|
5
|
+
schedule:
|
|
6
|
+
interval: 'weekly'
|
|
7
|
+
commit-message:
|
|
8
|
+
prefix: 'chore'
|
|
9
|
+
include: 'scope'
|
|
10
|
+
cooldown:
|
|
11
|
+
default-days: 5
|
|
12
|
+
- package-ecosystem: 'npm'
|
|
13
|
+
directory: '/'
|
|
14
|
+
schedule:
|
|
15
|
+
interval: 'weekly'
|
|
16
|
+
commit-message:
|
|
17
|
+
prefix: 'chore'
|
|
18
|
+
include: 'scope'
|
|
19
|
+
cooldown:
|
|
20
|
+
default-days: 5
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Test & Maybe Release
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
test:
|
|
6
|
+
strategy:
|
|
7
|
+
fail-fast: false
|
|
8
|
+
matrix:
|
|
9
|
+
node: [lts/*, current]
|
|
10
|
+
os: [macos-latest, ubuntu-latest, windows-latest]
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout Repository
|
|
14
|
+
uses: actions/checkout@v6
|
|
15
|
+
- name: Use Node.js ${{ matrix.node }}
|
|
16
|
+
uses: actions/setup-node@v6
|
|
17
|
+
with:
|
|
18
|
+
node-version: ${{ matrix.node }}
|
|
19
|
+
- name: Install Dependencies
|
|
20
|
+
run: npm install --no-progress
|
|
21
|
+
- name: Check build is up to date
|
|
22
|
+
run: |
|
|
23
|
+
npm run build
|
|
24
|
+
git diff --exit-code || (echo "::error::Build artifacts not committed. Run 'npm run build' and commit the changes." && exit 1)
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: npm test
|
|
27
|
+
|
|
28
|
+
release:
|
|
29
|
+
name: Release
|
|
30
|
+
needs: test
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
|
33
|
+
permissions:
|
|
34
|
+
contents: write
|
|
35
|
+
issues: write
|
|
36
|
+
pull-requests: write
|
|
37
|
+
id-token: write
|
|
38
|
+
steps:
|
|
39
|
+
- name: Checkout
|
|
40
|
+
uses: actions/checkout@v6
|
|
41
|
+
with:
|
|
42
|
+
fetch-depth: 0
|
|
43
|
+
- name: Setup Node.js
|
|
44
|
+
uses: actions/setup-node@v6
|
|
45
|
+
with:
|
|
46
|
+
node-version: lts/*
|
|
47
|
+
registry-url: 'https://registry.npmjs.org'
|
|
48
|
+
- name: Install dependencies
|
|
49
|
+
run: npm install --no-progress --no-package-lock --no-save
|
|
50
|
+
- name: Build
|
|
51
|
+
run: npm run build
|
|
52
|
+
- name: Release
|
|
53
|
+
env:
|
|
54
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
55
|
+
NPM_CONFIG_PROVENANCE: true
|
|
56
|
+
run: npx semantic-release
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## [2.0.0](https://github.com/rvagg/github-webhook-handler/compare/v1.0.0...v2.0.0) (2026-01-28)
|
|
2
|
+
|
|
3
|
+
### ⚠ BREAKING CHANGES
|
|
4
|
+
|
|
5
|
+
* modernise, ESM, promises, update deps, GHA, auto-release (#58)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* modernise, ESM, promises, update deps, GHA, auto-release ([#58](https://github.com/rvagg/github-webhook-handler/issues/58)) ([bc84845](https://github.com/rvagg/github-webhook-handler/commit/bc848457d5c28110ac24335af520ec3b921355fc))
|
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# github-webhook-handler
|
|
2
2
|
|
|
3
|
-
[](https://nodei.co/npm/github-webhook-handler/)
|
|
3
|
+
[](https://nodei.co/npm/github-webhook-handler/)
|
|
6
4
|
|
|
7
5
|
GitHub allows you to register **[Webhooks](https://developer.github.com/webhooks/)** for your repositories. Each time an event occurs on your repository, whether it be pushing code, filling issues or creating pull requests, the webhook address you register can be configured to be pinged with details.
|
|
8
6
|
|
|
9
7
|
This library is a small handler (or "middleware" if you must) for Node.js web servers that handles all the logic of receiving and verifying webhook requests from GitHub.
|
|
10
8
|
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
Node.js >= 20
|
|
12
|
+
|
|
11
13
|
## Tips
|
|
12
14
|
|
|
13
15
|
In Github Webhooks settings, Content type must be `application/json`.
|
|
@@ -17,28 +19,29 @@ In Github Webhooks settings, Content type must be `application/json`.
|
|
|
17
19
|
## Example
|
|
18
20
|
|
|
19
21
|
```js
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
var handler = createHandler({ path: '/webhook', secret: 'myhashsecret' })
|
|
22
|
+
import http from 'node:http'
|
|
23
|
+
import createHandler from 'github-webhook-handler'
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const handler = createHandler({ path: '/webhook', secret: 'myhashsecret' })
|
|
26
|
+
|
|
27
|
+
http.createServer((req, res) => {
|
|
28
|
+
handler(req, res, (err) => {
|
|
26
29
|
res.statusCode = 404
|
|
27
30
|
res.end('no such location')
|
|
28
31
|
})
|
|
29
32
|
}).listen(7777)
|
|
30
33
|
|
|
31
|
-
handler.on('error',
|
|
34
|
+
handler.on('error', (err) => {
|
|
32
35
|
console.error('Error:', err.message)
|
|
33
36
|
})
|
|
34
37
|
|
|
35
|
-
handler.on('push',
|
|
38
|
+
handler.on('push', (event) => {
|
|
36
39
|
console.log('Received a push event for %s to %s',
|
|
37
40
|
event.payload.repository.name,
|
|
38
41
|
event.payload.ref)
|
|
39
42
|
})
|
|
40
43
|
|
|
41
|
-
handler.on('issues',
|
|
44
|
+
handler.on('issues', (event) => {
|
|
42
45
|
console.log('Received an issue event for %s action=%s: #%d %s',
|
|
43
46
|
event.payload.repository.name,
|
|
44
47
|
event.payload.action,
|
|
@@ -47,11 +50,11 @@ handler.on('issues', function (event) {
|
|
|
47
50
|
})
|
|
48
51
|
```
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
For multiple handlers, see [multiple-handlers-issue](https://github.com/rvagg/github-webhook-handler/pull/22#issuecomment-274301907).
|
|
51
54
|
|
|
52
55
|
## API
|
|
53
56
|
|
|
54
|
-
github-webhook-handler exports a single function
|
|
57
|
+
github-webhook-handler exports a single function. Use this function to *create* a webhook handler by passing in an *options* object. Your options object should contain:
|
|
55
58
|
|
|
56
59
|
* `"path"`: the complete case sensitive path/route to match when looking at `req.url` for incoming requests. Any request not matching this path will cause the callback function to the handler to be called (sometimes called the `next` handler).
|
|
57
60
|
* `"secret"`: this is a hash key used for creating the SHA-1 HMAC signature of the JSON blob sent by GitHub. You should register the same secret key with GitHub. Any request not delivering a `X-Hub-Signature` that matches the signature generated using this key against the blob will be rejected and cause an `'error'` event (also the callback will be called with an `Error` object).
|
|
@@ -66,13 +69,14 @@ See the [GitHub Webhooks documentation](https://developer.github.com/webhooks/)
|
|
|
66
69
|
Included in the distribution is an *events.json* file which maps the event names to descriptions taken from the API:
|
|
67
70
|
|
|
68
71
|
```js
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
import events from 'github-webhook-handler/events.json' with { type: 'json' }
|
|
73
|
+
|
|
74
|
+
for (const [event, description] of Object.entries(events)) {
|
|
75
|
+
console.log(event, '=', description)
|
|
76
|
+
}
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
Additionally, there is a special `'*'`
|
|
79
|
+
Additionally, there is a special `'*'` event you can listen to in order to receive _everything_.
|
|
76
80
|
|
|
77
81
|
## License
|
|
78
82
|
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="node" />
|
|
2
2
|
|
|
3
|
-
import { IncomingMessage, ServerResponse } from
|
|
4
|
-
import { EventEmitter } from
|
|
3
|
+
import { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
|
+
import { EventEmitter } from 'node:events'
|
|
5
5
|
|
|
6
6
|
interface CreateHandlerOptions {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
path: string
|
|
8
|
+
secret: string
|
|
9
|
+
events?: string | string[]
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
interface
|
|
13
|
-
|
|
12
|
+
interface Handler extends EventEmitter {
|
|
13
|
+
(req: IncomingMessage, res: ServerResponse, callback: (err?: Error) => void): void
|
|
14
|
+
sign(data: string | Buffer): string
|
|
15
|
+
verify(signature: string, data: string | Buffer): boolean
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
declare function createHandler(options: CreateHandlerOptions|CreateHandlerOptions[]):
|
|
18
|
+
declare function createHandler (options: CreateHandlerOptions | CreateHandlerOptions[]): Handler
|
|
17
19
|
|
|
18
|
-
export
|
|
20
|
+
export default createHandler
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import crypto from 'node:crypto'
|
|
3
|
+
import bl from 'bl'
|
|
4
4
|
|
|
5
5
|
function findHandler (url, arr) {
|
|
6
6
|
if (!Array.isArray(arr)) {
|
|
@@ -23,17 +23,16 @@ function checkType (options) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
if (typeof options.path !== 'string') {
|
|
26
|
-
throw new TypeError(
|
|
26
|
+
throw new TypeError("must provide a 'path' option")
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
if (typeof options.secret !== 'string') {
|
|
30
|
-
throw new TypeError(
|
|
30
|
+
throw new TypeError("must provide a 'secret' option")
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function create (initOptions) {
|
|
35
35
|
let options
|
|
36
|
-
// validate type of options
|
|
37
36
|
if (Array.isArray(initOptions)) {
|
|
38
37
|
for (let i = 0; i < initOptions.length; i++) {
|
|
39
38
|
checkType(initOptions[i])
|
|
@@ -42,7 +41,6 @@ function create (initOptions) {
|
|
|
42
41
|
checkType(initOptions)
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
// make it an EventEmitter
|
|
46
44
|
Object.setPrototypeOf(handler, EventEmitter.prototype)
|
|
47
45
|
EventEmitter.call(handler)
|
|
48
46
|
|
|
@@ -130,8 +128,8 @@ function create (initOptions) {
|
|
|
130
128
|
res.end('{"ok":true}')
|
|
131
129
|
|
|
132
130
|
const emitData = {
|
|
133
|
-
event
|
|
134
|
-
id
|
|
131
|
+
event,
|
|
132
|
+
id,
|
|
135
133
|
payload: obj,
|
|
136
134
|
protocol: req.protocol,
|
|
137
135
|
host: req.headers.host,
|
|
@@ -145,4 +143,4 @@ function create (initOptions) {
|
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
|
|
148
|
-
|
|
146
|
+
export default create
|
package/package.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-webhook-handler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Web handler / middleware for processing GitHub Webhooks",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "github-webhook-handler.js",
|
|
7
|
+
"exports": "./github-webhook-handler.js",
|
|
6
8
|
"types": "github-webhook-handler.d.ts",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20"
|
|
11
|
+
},
|
|
7
12
|
"scripts": {
|
|
8
|
-
"lint": "standard
|
|
9
|
-
"
|
|
13
|
+
"lint": "standard",
|
|
14
|
+
"build": "true",
|
|
15
|
+
"test:unit": "node --test test.js",
|
|
16
|
+
"test": "npm run lint && npm run test:unit"
|
|
10
17
|
},
|
|
11
18
|
"keywords": [
|
|
12
19
|
"github",
|
|
@@ -19,13 +26,98 @@
|
|
|
19
26
|
},
|
|
20
27
|
"license": "MIT",
|
|
21
28
|
"dependencies": {
|
|
22
|
-
"bl": "
|
|
29
|
+
"bl": "^6.1.6"
|
|
23
30
|
},
|
|
24
31
|
"devDependencies": {
|
|
25
|
-
"@
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
32
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
33
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
34
|
+
"@semantic-release/git": "^10.0.1",
|
|
35
|
+
"@semantic-release/github": "^12.0.2",
|
|
36
|
+
"@semantic-release/npm": "^13.1.3",
|
|
37
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
38
|
+
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
39
|
+
"semantic-release": "^25.0.2",
|
|
40
|
+
"standard": "^17.1.2"
|
|
41
|
+
},
|
|
42
|
+
"release": {
|
|
43
|
+
"branches": [
|
|
44
|
+
"master"
|
|
45
|
+
],
|
|
46
|
+
"plugins": [
|
|
47
|
+
[
|
|
48
|
+
"@semantic-release/commit-analyzer",
|
|
49
|
+
{
|
|
50
|
+
"preset": "conventionalcommits",
|
|
51
|
+
"releaseRules": [
|
|
52
|
+
{
|
|
53
|
+
"breaking": true,
|
|
54
|
+
"release": "major"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"revert": true,
|
|
58
|
+
"release": "patch"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "feat",
|
|
62
|
+
"release": "minor"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"type": "fix",
|
|
66
|
+
"release": "patch"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"type": "chore",
|
|
70
|
+
"release": "patch"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"type": "docs",
|
|
74
|
+
"release": "patch"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"type": "test",
|
|
78
|
+
"release": "patch"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"scope": "no-release",
|
|
82
|
+
"release": false
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"@semantic-release/release-notes-generator",
|
|
89
|
+
{
|
|
90
|
+
"preset": "conventionalcommits",
|
|
91
|
+
"presetConfig": {
|
|
92
|
+
"types": [
|
|
93
|
+
{
|
|
94
|
+
"type": "feat",
|
|
95
|
+
"section": "Features"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "fix",
|
|
99
|
+
"section": "Bug Fixes"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"type": "chore",
|
|
103
|
+
"section": "Trivial Changes"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"type": "docs",
|
|
107
|
+
"section": "Trivial Changes"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"type": "test",
|
|
111
|
+
"section": "Tests"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"@semantic-release/changelog",
|
|
118
|
+
"@semantic-release/npm",
|
|
119
|
+
"@semantic-release/github",
|
|
120
|
+
"@semantic-release/git"
|
|
121
|
+
]
|
|
30
122
|
}
|
|
31
123
|
}
|
package/test.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { PassThrough } from 'node:stream'
|
|
5
|
+
import handler from './github-webhook-handler.js'
|
|
6
6
|
|
|
7
7
|
function signBlob (key, blob) {
|
|
8
8
|
return `sha1=${crypto.createHmac('sha1', key).update(blob).digest('hex')}`
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function mkReq (url, method) {
|
|
12
|
-
const req =
|
|
12
|
+
const req = new PassThrough()
|
|
13
13
|
req.method = method || 'POST'
|
|
14
14
|
req.url = url
|
|
15
15
|
req.headers = {
|
|
@@ -22,116 +22,102 @@ function mkReq (url, method) {
|
|
|
22
22
|
|
|
23
23
|
function mkRes () {
|
|
24
24
|
const res = {
|
|
25
|
-
writeHead
|
|
25
|
+
writeHead (statusCode, headers) {
|
|
26
26
|
res.$statusCode = statusCode
|
|
27
27
|
res.$headers = headers
|
|
28
28
|
},
|
|
29
|
-
|
|
30
|
-
end: function (content) {
|
|
29
|
+
end (content) {
|
|
31
30
|
res.$end = content
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
|
-
|
|
35
33
|
return res
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
test('handler without full options throws', (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
t.throws(handler, /must provide an options object/, 'throws if no options')
|
|
44
|
-
|
|
45
|
-
t.throws(handler.bind(null, {}), /must provide a 'path' option/, 'throws if no path option')
|
|
46
|
-
|
|
47
|
-
t.throws(handler.bind(null, { path: '/' }), /must provide a 'secret' option/, 'throws if no secret option')
|
|
36
|
+
test('handler without full options throws', () => {
|
|
37
|
+
assert.strictEqual(typeof handler, 'function', 'handler exports a function')
|
|
38
|
+
assert.throws(() => handler(), /must provide an options object/, 'throws if no options')
|
|
39
|
+
assert.throws(() => handler({}), /must provide a 'path' option/, 'throws if no path option')
|
|
40
|
+
assert.throws(() => handler({ path: '/' }), /must provide a 'secret' option/, 'throws if no secret option')
|
|
48
41
|
})
|
|
49
42
|
|
|
50
|
-
test('handler without full options throws in array', (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
t.throws(handler.bind(null, [{}]), /must provide a 'path' option/, 'throws if no path option')
|
|
54
|
-
|
|
55
|
-
t.throws(handler.bind(null, [{ path: '/' }]), /must provide a 'secret' option/, 'throws if no secret option')
|
|
43
|
+
test('handler without full options throws in array', () => {
|
|
44
|
+
assert.throws(() => handler([{}]), /must provide a 'path' option/, 'throws if no path option')
|
|
45
|
+
assert.throws(() => handler([{ path: '/' }]), /must provide a 'secret' option/, 'throws if no secret option')
|
|
56
46
|
})
|
|
57
47
|
|
|
58
|
-
test('handler ignores invalid urls', (
|
|
48
|
+
test('handler ignores invalid urls', async () => {
|
|
59
49
|
const options = { path: '/some/url', secret: 'bogus' }
|
|
60
50
|
const h = handler(options)
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
52
|
+
await new Promise((resolve) => {
|
|
53
|
+
h(mkReq('/'), mkRes(), (err) => {
|
|
54
|
+
assert.ifError(err)
|
|
55
|
+
resolve()
|
|
56
|
+
})
|
|
67
57
|
})
|
|
68
58
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
59
|
+
await new Promise((resolve) => {
|
|
60
|
+
h(mkReq('/some/url/'), mkRes(), (err) => {
|
|
61
|
+
assert.ifError(err)
|
|
62
|
+
resolve()
|
|
63
|
+
})
|
|
73
64
|
})
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
await new Promise((resolve) => {
|
|
67
|
+
h(mkReq('/some'), mkRes(), (err) => {
|
|
68
|
+
assert.ifError(err)
|
|
69
|
+
resolve()
|
|
70
|
+
})
|
|
79
71
|
})
|
|
80
72
|
})
|
|
81
73
|
|
|
82
|
-
test('handler
|
|
74
|
+
test('handler ignores non-POST requests', async () => {
|
|
83
75
|
const options = { path: '/some/url', secret: 'bogus' }
|
|
84
76
|
const h = handler(options)
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
78
|
+
await new Promise((resolve) => {
|
|
79
|
+
h(mkReq('/some/url', 'GET'), mkRes(), (err) => {
|
|
80
|
+
assert.ifError(err)
|
|
81
|
+
resolve()
|
|
82
|
+
})
|
|
91
83
|
})
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
await new Promise((resolve) => {
|
|
86
|
+
h(mkReq('/some/url?test=param', 'GET'), mkRes(), (err) => {
|
|
87
|
+
assert.ifError(err)
|
|
88
|
+
resolve()
|
|
89
|
+
})
|
|
96
90
|
})
|
|
97
91
|
})
|
|
98
92
|
|
|
99
|
-
test('handler accepts valid urls', (
|
|
93
|
+
test('handler accepts valid urls', async () => {
|
|
100
94
|
const options = { path: '/some/url', secret: 'bogus' }
|
|
101
95
|
const h = handler(options)
|
|
102
96
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
t.error(err)
|
|
107
|
-
t.fail(false, 'should not call')
|
|
97
|
+
let callbackCalled = false
|
|
98
|
+
h(mkReq('/some/url'), mkRes(), () => {
|
|
99
|
+
callbackCalled = true
|
|
108
100
|
})
|
|
109
101
|
|
|
110
|
-
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
103
|
+
assert.strictEqual(callbackCalled, false, 'callback should not be called for valid POST')
|
|
111
104
|
})
|
|
112
105
|
|
|
113
|
-
test('handler accepts valid urls in Array', (
|
|
106
|
+
test('handler accepts valid urls in Array', async () => {
|
|
114
107
|
const options = [{ path: '/some/url', secret: 'bogus' }, { path: '/someOther/url', secret: 'bogus' }]
|
|
115
108
|
const h = handler(options)
|
|
116
109
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
h(mkReq('/
|
|
120
|
-
t.error(err)
|
|
121
|
-
t.fail(false, 'should not call')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
h(mkReq('/someOther/url'), mkRes(), (err) => {
|
|
125
|
-
t.error(err)
|
|
126
|
-
t.fail(false, 'should not call')
|
|
127
|
-
})
|
|
110
|
+
let callbackCalled = false
|
|
111
|
+
h(mkReq('/some/url'), mkRes(), () => { callbackCalled = true })
|
|
112
|
+
h(mkReq('/someOther/url'), mkRes(), () => { callbackCalled = true })
|
|
128
113
|
|
|
129
|
-
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
115
|
+
assert.strictEqual(callbackCalled, false, 'callback should not be called for valid POSTs')
|
|
130
116
|
})
|
|
131
117
|
|
|
132
|
-
test('handler can reject events', (
|
|
118
|
+
test('handler can reject events', async () => {
|
|
133
119
|
const acceptableEvents = {
|
|
134
|
-
undefined
|
|
120
|
+
undefined,
|
|
135
121
|
'a string equal to the event': 'bogus',
|
|
136
122
|
'a string equal to *': '*',
|
|
137
123
|
'an array containing the event': ['bogus'],
|
|
@@ -141,74 +127,55 @@ test('handler can reject events', (t) => {
|
|
|
141
127
|
'a string not equal to the event or *': 'not-bogus',
|
|
142
128
|
'an array not containing the event or *': ['not-bogus']
|
|
143
129
|
}
|
|
144
|
-
const acceptable = Object.keys(acceptableEvents)
|
|
145
|
-
const unacceptable = Object.keys(unacceptableEvents)
|
|
146
|
-
const acceptableTests = acceptable.map((events) => {
|
|
147
|
-
return acceptableReq.bind(null, events)
|
|
148
|
-
})
|
|
149
|
-
const unacceptableTests = unacceptable.map((events) => {
|
|
150
|
-
return unacceptableReq.bind(null, events)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
t.plan(acceptable.length + unacceptable.length)
|
|
154
|
-
series(acceptableTests.concat(unacceptableTests))
|
|
155
130
|
|
|
156
|
-
|
|
131
|
+
for (const [desc, events] of Object.entries(acceptableEvents)) {
|
|
157
132
|
const h = handler({
|
|
158
133
|
path: '/some/url',
|
|
159
134
|
secret: 'bogus',
|
|
160
|
-
events
|
|
135
|
+
events
|
|
161
136
|
})
|
|
162
137
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
t.fail(false, 'should not call')
|
|
166
|
-
})
|
|
138
|
+
let callbackCalled = false
|
|
139
|
+
h(mkReq('/some/url'), mkRes(), () => { callbackCalled = true })
|
|
167
140
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
callback()
|
|
171
|
-
})
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
142
|
+
assert.strictEqual(callbackCalled, false, `accepted because options.events was ${desc}`)
|
|
172
143
|
}
|
|
173
144
|
|
|
174
|
-
|
|
145
|
+
for (const [desc, events] of Object.entries(unacceptableEvents)) {
|
|
175
146
|
const h = handler({
|
|
176
147
|
path: '/some/url',
|
|
177
148
|
secret: 'bogus',
|
|
178
|
-
events
|
|
149
|
+
events
|
|
179
150
|
})
|
|
180
151
|
|
|
181
152
|
h.on('error', () => {})
|
|
182
153
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
154
|
+
await new Promise((resolve) => {
|
|
155
|
+
h(mkReq('/some/url'), mkRes(), (err) => {
|
|
156
|
+
assert.ok(err, `rejected because options.events was ${desc}`)
|
|
157
|
+
resolve()
|
|
158
|
+
})
|
|
186
159
|
})
|
|
187
160
|
}
|
|
188
161
|
})
|
|
189
162
|
|
|
190
|
-
|
|
191
|
-
test('handler is an EventEmitter', (t) => {
|
|
192
|
-
t.plan(5)
|
|
193
|
-
|
|
163
|
+
test('handler is an EventEmitter', () => {
|
|
194
164
|
const h = handler({ path: '/', secret: 'bogus' })
|
|
195
165
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
h.on('ping', (pong) => {
|
|
201
|
-
t.equal(pong, 'pong', 'got event')
|
|
202
|
-
})
|
|
166
|
+
assert.strictEqual(typeof h.on, 'function', 'has h.on()')
|
|
167
|
+
assert.strictEqual(typeof h.emit, 'function', 'has h.emit()')
|
|
168
|
+
assert.strictEqual(typeof h.removeListener, 'function', 'has h.removeListener()')
|
|
203
169
|
|
|
170
|
+
let received
|
|
171
|
+
h.on('ping', (pong) => { received = pong })
|
|
204
172
|
h.emit('ping', 'pong')
|
|
173
|
+
assert.strictEqual(received, 'pong', 'got event')
|
|
205
174
|
|
|
206
|
-
|
|
175
|
+
assert.throws(() => h.emit('error', new Error('threw an error')), /threw an error/, 'acts like an EE')
|
|
207
176
|
})
|
|
208
177
|
|
|
209
|
-
test('handler accepts a signed blob', (
|
|
210
|
-
t.plan(4)
|
|
211
|
-
|
|
178
|
+
test('handler accepts a signed blob', async () => {
|
|
212
179
|
const obj = { some: 'github', object: 'with', properties: true }
|
|
213
180
|
const json = JSON.stringify(obj)
|
|
214
181
|
const h = handler({ path: '/', secret: 'bogus' })
|
|
@@ -218,54 +185,69 @@ test('handler accepts a signed blob', (t) => {
|
|
|
218
185
|
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
219
186
|
req.headers['x-github-event'] = 'push'
|
|
220
187
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
188
|
+
const eventPromise = new Promise((resolve) => {
|
|
189
|
+
h.on('push', (event) => {
|
|
190
|
+
assert.deepStrictEqual(event, {
|
|
191
|
+
event: 'push',
|
|
192
|
+
id: 'bogus',
|
|
193
|
+
payload: obj,
|
|
194
|
+
url: '/',
|
|
195
|
+
host: undefined,
|
|
196
|
+
protocol: undefined,
|
|
197
|
+
path: '/'
|
|
198
|
+
})
|
|
199
|
+
assert.strictEqual(res.$statusCode, 200, 'correct status code')
|
|
200
|
+
assert.deepStrictEqual(res.$headers, { 'content-type': 'application/json' })
|
|
201
|
+
assert.strictEqual(res.$end, '{"ok":true}', 'got correct content')
|
|
202
|
+
resolve()
|
|
203
|
+
})
|
|
226
204
|
})
|
|
227
205
|
|
|
228
|
-
h(req, res, (
|
|
229
|
-
|
|
230
|
-
t.fail(true, 'should not get here!')
|
|
206
|
+
h(req, res, () => {
|
|
207
|
+
assert.fail('should not get here')
|
|
231
208
|
})
|
|
232
209
|
|
|
233
|
-
process.nextTick(() =>
|
|
234
|
-
|
|
235
|
-
})
|
|
210
|
+
process.nextTick(() => req.end(json))
|
|
211
|
+
await eventPromise
|
|
236
212
|
})
|
|
237
213
|
|
|
238
|
-
test('handler accepts multi blob in Array', (
|
|
239
|
-
t.plan(4)
|
|
240
|
-
|
|
214
|
+
test('handler accepts multi blob in Array', async () => {
|
|
241
215
|
const obj = { some: 'github', object: 'with', properties: true }
|
|
242
216
|
const json = JSON.stringify(obj)
|
|
243
217
|
const h = handler([{ path: '/', secret: 'bogus' }, { path: '/some/url', secret: 'bogus' }])
|
|
244
218
|
const req = mkReq('/some/url')
|
|
245
219
|
const res = mkRes()
|
|
220
|
+
|
|
246
221
|
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
247
222
|
req.headers['x-github-event'] = 'push'
|
|
248
223
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
224
|
+
const eventPromise = new Promise((resolve) => {
|
|
225
|
+
h.on('push', (event) => {
|
|
226
|
+
assert.deepStrictEqual(event, {
|
|
227
|
+
event: 'push',
|
|
228
|
+
id: 'bogus',
|
|
229
|
+
payload: obj,
|
|
230
|
+
url: '/some/url',
|
|
231
|
+
host: undefined,
|
|
232
|
+
protocol: undefined,
|
|
233
|
+
path: '/some/url'
|
|
234
|
+
})
|
|
235
|
+
assert.strictEqual(res.$statusCode, 200, 'correct status code')
|
|
236
|
+
assert.deepStrictEqual(res.$headers, { 'content-type': 'application/json' })
|
|
237
|
+
assert.strictEqual(res.$end, '{"ok":true}', 'got correct content')
|
|
238
|
+
resolve()
|
|
239
|
+
})
|
|
254
240
|
})
|
|
255
241
|
|
|
256
|
-
h(req, res, (
|
|
257
|
-
|
|
258
|
-
t.fail(true, 'should not get here!')
|
|
242
|
+
h(req, res, () => {
|
|
243
|
+
assert.fail('should not get here')
|
|
259
244
|
})
|
|
260
245
|
|
|
261
|
-
process.nextTick(() =>
|
|
262
|
-
|
|
263
|
-
})
|
|
246
|
+
process.nextTick(() => req.end(json))
|
|
247
|
+
await eventPromise
|
|
264
248
|
})
|
|
265
249
|
|
|
266
|
-
test('handler accepts a signed blob with alt event', (
|
|
267
|
-
t.plan(4)
|
|
268
|
-
|
|
250
|
+
test('handler accepts a signed blob with alt event', async () => {
|
|
269
251
|
const obj = { some: 'github', object: 'with', properties: true }
|
|
270
252
|
const json = JSON.stringify(obj)
|
|
271
253
|
const h = handler({ path: '/', secret: 'bogus' })
|
|
@@ -275,30 +257,35 @@ test('handler accepts a signed blob with alt event', (t) => {
|
|
|
275
257
|
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
276
258
|
req.headers['x-github-event'] = 'issue'
|
|
277
259
|
|
|
278
|
-
h.on('push', (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
260
|
+
h.on('push', () => assert.fail('should not get here'))
|
|
261
|
+
|
|
262
|
+
const eventPromise = new Promise((resolve) => {
|
|
263
|
+
h.on('issue', (event) => {
|
|
264
|
+
assert.deepStrictEqual(event, {
|
|
265
|
+
event: 'issue',
|
|
266
|
+
id: 'bogus',
|
|
267
|
+
payload: obj,
|
|
268
|
+
url: '/',
|
|
269
|
+
host: undefined,
|
|
270
|
+
protocol: undefined,
|
|
271
|
+
path: '/'
|
|
272
|
+
})
|
|
273
|
+
assert.strictEqual(res.$statusCode, 200, 'correct status code')
|
|
274
|
+
assert.deepStrictEqual(res.$headers, { 'content-type': 'application/json' })
|
|
275
|
+
assert.strictEqual(res.$end, '{"ok":true}', 'got correct content')
|
|
276
|
+
resolve()
|
|
277
|
+
})
|
|
287
278
|
})
|
|
288
279
|
|
|
289
|
-
h(req, res, (
|
|
290
|
-
|
|
291
|
-
t.fail(true, 'should not get here!')
|
|
280
|
+
h(req, res, () => {
|
|
281
|
+
assert.fail('should not get here')
|
|
292
282
|
})
|
|
293
283
|
|
|
294
|
-
process.nextTick(() =>
|
|
295
|
-
|
|
296
|
-
})
|
|
284
|
+
process.nextTick(() => req.end(json))
|
|
285
|
+
await eventPromise
|
|
297
286
|
})
|
|
298
287
|
|
|
299
|
-
test('handler rejects a badly signed blob', (
|
|
300
|
-
t.plan(6)
|
|
301
|
-
|
|
288
|
+
test('handler rejects a badly signed blob', async () => {
|
|
302
289
|
const obj = { some: 'github', object: 'with', properties: true }
|
|
303
290
|
const json = JSON.stringify(obj)
|
|
304
291
|
const h = handler({ path: '/', secret: 'bogus' })
|
|
@@ -306,33 +293,30 @@ test('handler rejects a badly signed blob', (t) => {
|
|
|
306
293
|
const res = mkRes()
|
|
307
294
|
|
|
308
295
|
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
309
|
-
// break signage by a tiny bit
|
|
310
296
|
req.headers['x-hub-signature'] = '0' + req.headers['x-hub-signature'].substring(1)
|
|
311
297
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
298
|
+
const errorPromise = new Promise((resolve) => {
|
|
299
|
+
h.on('error', (err, _req) => {
|
|
300
|
+
assert.ok(err, 'got an error')
|
|
301
|
+
assert.strictEqual(_req, req, 'was given original request object')
|
|
302
|
+
assert.strictEqual(res.$statusCode, 400, 'correct status code')
|
|
303
|
+
assert.deepStrictEqual(res.$headers, { 'content-type': 'application/json' })
|
|
304
|
+
assert.strictEqual(res.$end, '{"error":"X-Hub-Signature does not match blob signature"}', 'got correct content')
|
|
305
|
+
resolve()
|
|
306
|
+
})
|
|
318
307
|
})
|
|
319
308
|
|
|
320
|
-
h.on('push', (
|
|
321
|
-
t.fail(true, 'should not get here!')
|
|
322
|
-
})
|
|
309
|
+
h.on('push', () => assert.fail('should not get here'))
|
|
323
310
|
|
|
324
311
|
h(req, res, (err) => {
|
|
325
|
-
|
|
312
|
+
assert.ok(err, 'got error on callback')
|
|
326
313
|
})
|
|
327
314
|
|
|
328
|
-
process.nextTick(() =>
|
|
329
|
-
|
|
330
|
-
})
|
|
315
|
+
process.nextTick(() => req.end(json))
|
|
316
|
+
await errorPromise
|
|
331
317
|
})
|
|
332
318
|
|
|
333
|
-
test('handler responds on a
|
|
334
|
-
t.plan(4)
|
|
335
|
-
|
|
319
|
+
test('handler responds on a stream error', async () => {
|
|
336
320
|
const obj = { some: 'github', object: 'with', properties: true }
|
|
337
321
|
const json = JSON.stringify(obj)
|
|
338
322
|
const h = handler({ path: '/', secret: 'bogus' })
|
|
@@ -342,29 +326,25 @@ test('handler responds on a bl error', (t) => {
|
|
|
342
326
|
req.headers['x-hub-signature'] = signBlob('bogus', json)
|
|
343
327
|
req.headers['x-github-event'] = 'issue'
|
|
344
328
|
|
|
345
|
-
h.on('push', (
|
|
346
|
-
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
h.on('issue', (event) => {
|
|
350
|
-
t.fail(true, 'should never get here!')
|
|
351
|
-
})
|
|
329
|
+
h.on('push', () => assert.fail('should not get here'))
|
|
330
|
+
h.on('issue', () => assert.fail('should never get here'))
|
|
352
331
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
332
|
+
const errorPromise = new Promise((resolve) => {
|
|
333
|
+
h.on('error', (err) => {
|
|
334
|
+
assert.ok(err, 'got an error')
|
|
335
|
+
assert.strictEqual(res.$statusCode, 400, 'correct status code')
|
|
336
|
+
resolve()
|
|
337
|
+
})
|
|
356
338
|
})
|
|
357
339
|
|
|
358
340
|
h(req, res, (err) => {
|
|
359
|
-
|
|
341
|
+
assert.ok(err)
|
|
360
342
|
})
|
|
361
343
|
|
|
362
|
-
res.end = () => {
|
|
363
|
-
t.equal(res.$statusCode, 400, 'correct status code')
|
|
364
|
-
}
|
|
365
|
-
|
|
366
344
|
req.write('{')
|
|
367
345
|
process.nextTick(() => {
|
|
368
|
-
req.
|
|
346
|
+
req.destroy(new Error('simulated explosion'))
|
|
369
347
|
})
|
|
348
|
+
|
|
349
|
+
await errorPromise
|
|
370
350
|
})
|