git-webhook-handler2
Version:
Web handler / middleware for processing GitCode, GitHub, Gitee, Gitlab Webhooks
395 lines (315 loc) • 9.97 kB
JavaScript
const test = require('tape')
const crypto = require('crypto')
const handler = require('./')
const through2 = require('through2')
const series = require('run-series')
function signBlob (key, blob) {
return `sha1=${crypto.createHmac('sha1', key).update(blob).digest('hex')}`
}
function mkReq (url, method) {
const req = through2()
req.method = method || 'POST'
req.url = url
req.headers = {
'x-hub-signature': 'bogus',
'x-github-event': 'bogus',
'x-github-delivery': 'bogus'
}
return req
}
function mkRes () {
const res = {
writeHead: function (statusCode, headers) {
res.$statusCode = statusCode
res.$headers = headers
},
end: function (content) {
res.$end = content
}
}
return res
}
test('handler without full options throws', (t) => {
t.plan(4)
t.equal(typeof handler, 'function', 'handler exports a function')
t.throws(handler, /must provide an options object/, 'throws if no options')
t.throws(handler.bind(null, {}), /must provide a 'path' option/, 'throws if no path option')
t.throws(handler.bind(null, { path: '/' }), /must provide a 'secret' option/, 'throws if no secret option')
})
test('handler without full options throws in array', (t) => {
t.plan(2)
t.throws(handler.bind(null, [{}]), /must provide a 'path' option/, 'throws if no path option')
t.throws(handler.bind(null, [{ path: '/' }]), /must provide a 'secret' option/, 'throws if no secret option')
})
test('handler ignores invalid urls', (t) => {
const options = { path: '/some/url', secret: 'bogus' }
const h = handler(options)
t.plan(6)
h(mkReq('/'), mkRes(), (err) => {
t.error(err)
t.ok(true, 'request was ignored')
})
// near match
h(mkReq('/some/url/'), mkRes(), (err) => {
t.error(err)
t.ok(true, 'request was ignored')
})
// partial match
h(mkReq('/some'), mkRes(), (err) => {
t.error(err)
t.ok(true, 'request was ignored')
})
})
test('handler ingores non-POST requests', (t) => {
const options = { path: '/some/url', secret: 'bogus' }
const h = handler(options)
t.plan(4)
h(mkReq('/some/url', 'GET'), mkRes(), (err) => {
t.error(err)
t.ok(true, 'request was ignored')
})
h(mkReq('/some/url?test=param', 'GET'), mkRes(), (err) => {
t.error(err)
t.ok(true, 'request was ignored')
})
})
test('handler accepts valid urls', (t) => {
const options = { path: '/some/url', secret: 'bogus' }
const h = handler(options)
t.plan(1)
h(mkReq('/some/url'), mkRes(), (err) => {
t.error(err)
t.fail(false, 'should not call')
})
setTimeout(t.ok.bind(t, true, 'done'))
})
test('handler accepts valid urls in Array', (t) => {
const options = [{ path: '/some/url', secret: 'bogus' }, { path: '/someOther/url', secret: 'bogus' }]
const h = handler(options)
t.plan(1)
h(mkReq('/some/url'), mkRes(), (err) => {
t.error(err)
t.fail(false, 'should not call')
})
h(mkReq('/someOther/url'), mkRes(), (err) => {
t.error(err)
t.fail(false, 'should not call')
})
setTimeout(t.ok.bind(t, true, 'done'))
})
test('handler can reject events', (t) => {
const acceptableEvents = {
undefined: undefined,
'a string equal to the event': 'bogus',
'a string equal to *': '*',
'an array containing the event': ['bogus'],
'an array containing *': ['not-bogus', '*']
}
const unacceptableEvents = {
'a string not equal to the event or *': 'not-bogus',
'an array not containing the event or *': ['not-bogus']
}
const acceptable = Object.keys(acceptableEvents)
const unacceptable = Object.keys(unacceptableEvents)
const acceptableTests = acceptable.map((events) => {
return acceptableReq.bind(null, events)
})
const unacceptableTests = unacceptable.map((events) => {
return unacceptableReq.bind(null, events)
})
t.plan(acceptable.length + unacceptable.length)
series(acceptableTests.concat(unacceptableTests))
function acceptableReq (events, callback) {
const h = handler({
path: '/some/url',
secret: 'bogus',
events: acceptableEvents[events]
})
h(mkReq('/some/url'), mkRes(), (err) => {
t.error(err)
t.fail(false, 'should not call')
})
setTimeout(() => {
t.ok(true, 'accepted because options.events was ' + events)
callback()
})
}
function unacceptableReq (events, callback) {
const h = handler({
path: '/some/url',
secret: 'bogus',
events: unacceptableEvents[events]
})
h.on('error', () => {})
h(mkReq('/some/url'), mkRes(), (err) => {
t.ok(err, 'rejected because options.events was ' + events)
callback()
})
}
})
// because we don't inherit in a traditional way
test('handler is an EventEmitter', (t) => {
t.plan(5)
const h = handler({ path: '/', secret: 'bogus' })
t.equal(typeof h.on, 'function', 'has h.on()')
t.equal(typeof h.emit, 'function', 'has h.emit()')
t.equal(typeof h.removeListener, 'function', 'has h.removeListener()')
h.on('ping', (pong) => {
t.equal(pong, 'pong', 'got event')
})
h.emit('ping', 'pong')
t.throws(h.emit.bind(h, 'error', new Error('threw an error')), /threw an error/, 'acts like an EE')
})
test('handler accepts a signed blob', (t) => {
t.plan(4)
const obj = { some: 'github', object: 'with', properties: true }
const json = JSON.stringify(obj)
const h = handler({ path: '/', secret: 'bogus' })
const req = mkReq('/')
const res = mkRes()
req.headers['x-hub-signature'] = signBlob('bogus', json)
req.headers['x-github-event'] = 'push'
h.on('push', (event) => {
t.deepEqual(event, {
event: 'push',
id: 'bogus',
payload: obj,
url: '/',
host: undefined,
protocol: undefined,
path: '/'
})
t.equal(res.$statusCode, 200, 'correct status code')
t.deepEqual(res.$headers, { 'content-type': 'application/json' })
t.equal(res.$end, '{"ok":true}', 'got correct content')
})
h(req, res, (err) => {
t.error(err)
t.fail(true, 'should not get here!')
})
process.nextTick(() => {
req.end(json)
})
})
test('handler accepts multi blob in Array', (t) => {
t.plan(4)
const obj = { some: 'github', object: 'with', properties: true }
const json = JSON.stringify(obj)
const h = handler([{ path: '/', secret: 'bogus' }, { path: '/some/url', secret: 'bogus' }])
const req = mkReq('/some/url')
const res = mkRes()
req.headers['x-hub-signature'] = signBlob('bogus', json)
req.headers['x-github-event'] = 'push'
h.on('push', (event) => {
t.deepEqual(event, {
event: 'push',
id: 'bogus',
payload: obj,
url: '/some/url',
host: undefined,
protocol: undefined,
path: '/some/url'
})
t.equal(res.$statusCode, 200, 'correct status code')
t.deepEqual(res.$headers, { 'content-type': 'application/json' })
t.equal(res.$end, '{"ok":true}', 'got correct content')
})
h(req, res, (err) => {
t.error(err)
t.fail(true, 'should not get here!')
})
process.nextTick(() => {
req.end(json)
})
})
test('handler accepts a signed blob with alt event', (t) => {
t.plan(4)
const obj = { some: 'github', object: 'with', properties: true }
const json = JSON.stringify(obj)
const h = handler({ path: '/', secret: 'bogus' })
const req = mkReq('/')
const res = mkRes()
req.headers['x-hub-signature'] = signBlob('bogus', json)
req.headers['x-github-event'] = 'issue'
h.on('push', (event) => {
t.fail(true, 'should not get here!')
})
h.on('issue', (event) => {
t.deepEqual(event, {
event: 'issue',
id: 'bogus',
payload: obj,
url: '/',
host: undefined,
protocol: undefined,
path: '/'
})
t.equal(res.$statusCode, 200, 'correct status code')
t.deepEqual(res.$headers, { 'content-type': 'application/json' })
t.equal(res.$end, '{"ok":true}', 'got correct content')
})
h(req, res, (err) => {
t.error(err)
t.fail(true, 'should not get here!')
})
process.nextTick(() => {
req.end(json)
})
})
test('handler rejects a badly signed blob', (t) => {
t.plan(6)
const obj = { some: 'github', object: 'with', properties: true }
const json = JSON.stringify(obj)
const h = handler({ path: '/', secret: 'bogus' })
const req = mkReq('/')
const res = mkRes()
req.headers['x-hub-signature'] = signBlob('bogus', json)
// break signage by a tiny bit
req.headers['x-hub-signature'] = '0' + req.headers['x-hub-signature'].substring(1)
h.on('error', (err, _req) => {
t.ok(err, 'got an error')
t.strictEqual(_req, req, 'was given original request object')
t.equal(res.$statusCode, 400, 'correct status code')
t.deepEqual(res.$headers, { 'content-type': 'application/json' })
t.equal(res.$end, '{"error":"x-hub-signature does not match blob signature"}', 'got correct content')
})
h.on('push', (event) => {
t.fail(true, 'should not get here!')
})
h(req, res, (err) => {
t.ok(err, 'got error on callback')
})
process.nextTick(() => {
req.end(json)
})
})
test('handler responds on a bl error', (t) => {
t.plan(4)
const obj = { some: 'github', object: 'with', properties: true }
const json = JSON.stringify(obj)
const h = handler({ path: '/', secret: 'bogus' })
const req = mkReq('/')
const res = mkRes()
req.headers['x-hub-signature'] = signBlob('bogus', json)
req.headers['x-github-event'] = 'issue'
h.on('push', (event) => {
t.fail(true, 'should not get here!')
})
h.on('issue', (event) => {
t.fail(true, 'should never get here!')
})
h.on('error', (err) => {
t.ok(err, 'got an error')
t.equal(res.$statusCode, 400, 'correct status code')
})
h(req, res, (err) => {
t.ok(err)
})
res.end = () => {
t.equal(res.$statusCode, 400, 'correct status code')
}
req.write('{')
process.nextTick(() => {
req.emit('error', new Error('simulated explosion'))
})
})