Compare commits

..

35 Commits

Author SHA1 Message Date
Adnan Hajdarević 86cef3e421 Merge pull request #101 from adnanh/development
webhook 2.5.0
2016-09-29 20:11:46 +02:00
Adnan Hajdarevic 75cf8952be remove \n 2016-09-29 20:11:20 +02:00
Adnan Hajdarevic c53596df59 override content type header when returning error message 2016-09-29 20:08:47 +02:00
Adnan Hajdarevic cc0d9b2cba fix tests, return raw output, return 500 if the command did not execute properly - fixes #87
return raw stdout instead of json wrapped message - fixes #88
2016-09-29 19:57:06 +02:00
Adnan Hajdarević c6530b17e7 Merge pull request #100 from adnanh/env-names
Do not prefix EnvName with HOOK_
2016-09-29 19:20:06 +02:00
Adnan Hajdarevic 1943c5311f bump version to 2.5.0 2016-09-29 19:16:47 +02:00
Adnan Hajdarevic 3b59539a33 do not prefix specified environment variable name with HOOK_ (fixes #98) 2016-09-29 19:15:51 +02:00
Adnan Hajdarević 923b0c6daa Merge pull request #95 from denji/Makefile
Makefile cross-platform binary
2016-09-13 09:20:11 +02:00
Denis Denisov 8530255ae6 Makefile build cross-binary 2016-09-12 23:09:05 +03:00
Adnan Hajdarević 032c74451f Merge pull request #93 from adnanh/development
Development
2016-09-02 18:30:31 +02:00
Adnan Hajdarević 1c50853d8d Merge pull request #92 from moorereason/iss91
Update negroni Logger usage
2016-09-02 18:29:27 +02:00
Cameron Moore b5ed4cbea7 Update negroni Logger usage
negroni made a breaking change to the Logger struct.

Fixes #91
2016-09-02 08:57:46 -05:00
Adnan Hajdarević b6d176705e Merge pull request #90 from adnanh/development
Development
2016-08-25 23:45:27 +02:00
Adnan Hajdarević 421fc2cbcd Hotfix backmerge (#89)
* fixes #76, fixes #78, fixes #82, fixes #83 (#84)

* Never disclose expected payload signature (#86)

Fixes #85
2016-08-25 23:42:33 +02:00
Cameron Moore 10d65dd2fd Never disclose expected payload signature (#86)
Fixes #85
2016-08-25 23:41:05 +02:00
Adnan Hajdarević 54a9dbe1d6 fixes #76, fixes #78, fixes #82, fixes #83 (#84) 2016-06-27 22:15:37 +02:00
Adnan Hajdarevic 30baec91df fixes #76, fixes #78, fixes #82, fixes #83 2016-06-27 22:13:00 +02:00
Adnan Hajdarevic 3bcf6d5e2b bump version to 2.3.9 2016-06-18 15:33:37 +02:00
Adnan Hajdarevic 67343e281d Merge branch 'master' into development 2016-06-18 15:31:13 +02:00
Florent Aide 18b0573bc4 Add support for naming env variables (#75)
* Adding ignore patterns

* Adding support for env var naming

* Fixed typo in docstring

* Adding tests for the env var extraction w & w/o explicit naming

* remove coverage script from ignore patterns

* Adding the coverage script to help see which code is tested and which is not

* remove coverage script from sources

* Ignore coverage script from sources tree
2016-05-26 23:33:56 +02:00
Adnan Hajdarević ec42679305 Merge pull request #69 from adnanh/development
version 2.3.8
2016-03-24 16:55:34 +01:00
Adnan Hajdarevic e85e0592dd Merge branch 'master' into development 2016-03-24 16:53:37 +01:00
Adnan Hajdarević 4d20af8027 Update webhook.go 2016-03-24 16:43:42 +01:00
Adnan Hajdarević d4e772c815 Add almir's docker image to the readme. 2016-03-15 19:09:06 +01:00
Adnan Hajdarević 04a2b2a680 Update README.md 2016-03-09 14:12:37 +01:00
Adnan Hajdarevic 4914a4131f Merge branch 'master' into development 2016-02-27 22:16:28 +01:00
Adnan Hajdarevic 37698e63b6 add support for setting global response headers using -header flag
add support for setting response headers for a successfuly triggered hook
2016-02-27 22:13:09 +01:00
Adnan Hajdarević 80aa9800bf Merge pull request #64 from adnanh/development
Update README.md to include Dockerfile discussion
2016-02-04 15:13:07 +01:00
Adnan Hajdarević f620cb056b Update README.md 2016-02-04 15:12:24 +01:00
Adnan Hajdarević e55e7efe14 Merge pull request #62 from adnanh/development
Update README.md
2015-12-29 20:39:41 +01:00
Adnan Hajdarević 9fa02f7341 Update README.md 2015-12-29 20:38:36 +01:00
Adnan Hajdarevic f59f0a5c84 - added omitempty to json fields 2015-12-27 20:05:52 +01:00
Adnan Hajdarević 5594a62f8f Merge pull request #61 from adnanh/development
- added omitempty to json fields
2015-12-27 20:04:03 +01:00
Adnan Hajdarevic 642516d46e Merge branch 'master' into development 2015-11-27 09:57:44 +01:00
Adnan Hajdarevic 9cef8ed882 add omitempty to json fields 2015-11-21 17:06:02 +01:00
9 changed files with 310 additions and 156 deletions
+4
View File
@@ -0,0 +1,4 @@
.idea
.cover
coverage
webhook
+29
View File
@@ -0,0 +1,29 @@
OS = darwin freebsd linux openbsd windows
ARCHS = 386 arm amd64 arm64
all: build release
build: deps
go build
release: clean deps
@for arch in $(ARCHS);\
do \
for os in $(OS);\
do \
echo "Building $$os-$$arch"; \
mkdir -p build/webhook-$$os-$$arch/; \
GOOS=$$os GOARCH=$$arch go build -o build/webhook-$$os-$$arch/webhook; \
tar cz -C build -f build/webhook-$$os-$$arch.tar.gz webhook-$$os-$$arch; \
done \
done
test: deps
go test ./...
deps:
go get -d -v -t ./...
clean:
rm -rf build
rm -f webhook
+7 -1
View File
@@ -1,4 +1,4 @@
[![Join the chat at https://gitter.im/adnanh/webhook](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Flattr this](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook) [![ghit.me](https://ghit.me/badge.svg?repo=adnanh/webhook)](https://ghit.me/repo/adnanh/webhook)[![Join the chat at https://gitter.im/adnanh/webhook](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Flattr this](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
# What is webhook? # What is webhook?
[webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered. [webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
@@ -59,6 +59,12 @@ However, hook defined like that could pose a security threat to your system, bec
# Using HTTPS # Using HTTPS
[webhook](https://github.com/adnanh/webhook/) by default serves hooks using http. If you want [webhook](https://github.com/adnanh/webhook/) to serve secure content using https, you can use the `-secure` flag while starting [webhook](https://github.com/adnanh/webhook/). Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate. [webhook](https://github.com/adnanh/webhook/) by default serves hooks using http. If you want [webhook](https://github.com/adnanh/webhook/) to serve secure content using https, you can use the `-secure` flag while starting [webhook](https://github.com/adnanh/webhook/). Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
# CORS Headers
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook](https://github.com/adnanh/webhook/) to set the appropriate CORS headers that will be returned with each response.
# Interested in running webhook inside of a Docker container?
You can use [almir/webhook](https://hub.docker.com/r/almir/webhook/) docker image, or create your own (please read [this discussion](https://github.com/adnanh/webhook/issues/63)).
# Examples # Examples
Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examples) for more complex examples of hooks. Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examples) for more complex examples of hooks.
+67 -46
View File
@@ -5,6 +5,7 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"reflect" "reflect"
@@ -92,7 +93,7 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
expectedMAC := hex.EncodeToString(mac.Sum(nil)) expectedMAC := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) { if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
return expectedMAC, &SignatureError{expectedMAC} return expectedMAC, &SignatureError{signature}
} }
return expectedMAC, err return expectedMAC, err
} }
@@ -195,8 +196,9 @@ func ExtractParameterAsString(s string, params interface{}) (string, bool) {
// Argument type specifies the parameter key name and the source it should // Argument type specifies the parameter key name and the source it should
// be extracted from // be extracted from
type Argument struct { type Argument struct {
Source string `json:"source"` Source string `json:"source,omitempty"`
Name string `json:"name"` Name string `json:"name,omitempty"`
EnvName string `json:"envname,omitempty"`
} }
// Get Argument method returns the value for the Argument's key name // Get Argument method returns the value for the Argument's key name
@@ -246,17 +248,54 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return "", false return "", false
} }
// Header is a structure containing header name and it's value
type Header struct {
Name string `json:"name"`
Value string `json:"value"`
}
// ResponseHeaders is a slice of Header objects
type ResponseHeaders []Header
func (h *ResponseHeaders) String() string {
// a 'hack' to display name=value in flag usage listing
if len(*h) == 0 {
return "name=value"
}
result := make([]string, len(*h))
for idx, responseHeader := range *h {
result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
}
return fmt.Sprint(strings.Join(result, ", "))
}
// Set method appends new Header object from header=value notation
func (h *ResponseHeaders) Set(value string) error {
splitResult := strings.SplitN(value, "=", 2)
if len(splitResult) != 2 {
return errors.New("header flag must be in name=value format")
}
*h = append(*h, Header{Name: splitResult[0], Value: splitResult[1]})
return nil
}
// Hook type is a structure containing details for a single hook // Hook type is a structure containing details for a single hook
type Hook struct { type Hook struct {
ID string `json:"id"` ID string `json:"id,omitempty"`
ExecuteCommand string `json:"execute-command"` ExecuteCommand string `json:"execute-command,omitempty"`
CommandWorkingDirectory string `json:"command-working-directory"` CommandWorkingDirectory string `json:"command-working-directory,omitempty"`
ResponseMessage string `json:"response-message"` ResponseMessage string `json:"response-message,omitempty"`
CaptureCommandOutput bool `json:"include-command-output-in-response"` ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command"` CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"` PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
JSONStringParameters []Argument `json:"parse-parameters-as-json"` PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
TriggerRule *Rules `json:"trigger-rule"` JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
TriggerRule *Rules `json:"trigger-rule,omitempty"`
} }
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the // ParseJSONParameters decodes specified arguments to JSON objects and replaces the
@@ -326,7 +365,13 @@ func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string
for i := range h.PassEnvironmentToCommand { for i := range h.PassEnvironmentToCommand {
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok { if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg) if h.PassEnvironmentToCommand[i].EnvName != "" {
// first try to use the EnvName if specified
args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
} else {
// then fallback on the name
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
}
} else { } else {
return args, &ArgumentError{h.PassEnvironmentToCommand[i]} return args, &ArgumentError{h.PassEnvironmentToCommand[i]}
} }
@@ -367,29 +412,12 @@ func (h *Hooks) Match(id string) *Hook {
return nil return nil
} }
// MatchAll iterates through Hooks and returns all of the hooks that match the
// given ID, if no hook matches the given ID, nil is returned
func (h *Hooks) MatchAll(id string) []*Hook {
var matchedHooks []*Hook
for i := range *h {
if (*h)[i].ID == id {
matchedHooks = append(matchedHooks, &(*h)[i])
}
}
if len(matchedHooks) > 0 {
return matchedHooks
}
return nil
}
// Rules is a structure that contains one of the valid rule types // Rules is a structure that contains one of the valid rule types
type Rules struct { type Rules struct {
And *AndRule `json:"and"` And *AndRule `json:"and,omitempty"`
Or *OrRule `json:"or"` Or *OrRule `json:"or,omitempty"`
Not *NotRule `json:"not"` Not *NotRule `json:"not,omitempty"`
Match *MatchRule `json:"match"` Match *MatchRule `json:"match,omitempty"`
} }
// Evaluate finds the first rule property that is not nil and returns the value // Evaluate finds the first rule property that is not nil and returns the value
@@ -464,11 +492,11 @@ func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body
// MatchRule will evaluate to true based on the type // MatchRule will evaluate to true based on the type
type MatchRule struct { type MatchRule struct {
Type string `json:"type"` Type string `json:"type,omitempty"`
Regex string `json:"regex"` Regex string `json:"regex,omitempty"`
Secret string `json:"secret"` Secret string `json:"secret,omitempty"`
Value string `json:"value"` Value string `json:"value,omitempty"`
Parameter Argument `json:"parameter"` Parameter Argument `json:"parameter,omitempty"`
} }
// Constants for the MatchRule type // Constants for the MatchRule type
@@ -493,10 +521,3 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
} }
return false, nil return false, nil
} }
// CommandStatusResponse type encapsulates the executed command exit code, message, stdout and stderr
type CommandStatusResponse struct {
ResponseMessage string `json:"message,omitempty"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
}
+104 -38
View File
@@ -2,6 +2,7 @@ package hook
import ( import (
"reflect" "reflect"
"strings"
"testing" "testing"
) )
@@ -25,6 +26,10 @@ func TestCheckPayloadSignature(t *testing.T) {
if (err == nil) != tt.ok || mac != tt.mac { if (err == nil) != tt.ok || mac != tt.mac {
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil)) t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
} }
if err != nil && strings.Contains(err.Error(), tt.mac) {
t.Errorf("error message should not disclose expected mac: %s", err)
}
} }
} }
@@ -80,7 +85,7 @@ var argumentGetTests = []struct {
func TestArgumentGet(t *testing.T) { func TestArgumentGet(t *testing.T) {
for _, tt := range argumentGetTests { for _, tt := range argumentGetTests {
a := Argument{tt.source, tt.name} a := Argument{tt.source, tt.name, ""}
value, ok := a.Get(tt.headers, tt.query, tt.payload) value, ok := a.Get(tt.headers, tt.query, tt.payload)
if ok != tt.ok || value != tt.value { if ok != tt.ok || value != tt.value {
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok) t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
@@ -94,14 +99,14 @@ var hookParseJSONParametersTests = []struct {
rheaders, rquery, rpayload *map[string]interface{} rheaders, rquery, rpayload *map[string]interface{}
ok bool ok bool
}{ }{
{[]Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil, true}, {[]Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil, true},
{[]Argument{Argument{"url", "a"}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true}, {[]Argument{Argument{"url", "a", ""}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
{[]Argument{Argument{"payload", "a"}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true}, {[]Argument{Argument{"payload", "a", ""}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": `{}`}, nil, nil, &map[string]interface{}{"z": map[string]interface{}{}}, nil, nil, true}, {[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"z": `{}`}, nil, nil, &map[string]interface{}{"z": map[string]interface{}{}}, nil, nil, true},
// failures // failures
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // empty string {[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // empty string
{[]Argument{Argument{"header", "y"}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter {[]Argument{Argument{"header", "y", ""}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
{[]Argument{Argument{"string", "z"}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // invalid argument source {[]Argument{Argument{"string", "z", ""}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // invalid argument source
} }
func TestHookParseJSONParameters(t *testing.T) { func TestHookParseJSONParameters(t *testing.T) {
@@ -121,9 +126,9 @@ var hookExtractCommandArgumentsTests = []struct {
value []string value []string
ok bool ok bool
}{ }{
{"test", []Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"test", "z"}, true}, {"test", []Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"test", "z"}, true},
// failures // failures
{"fail", []Argument{Argument{"payload", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"fail", ""}, false}, {"fail", []Argument{Argument{"payload", "a", ""}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"fail", ""}, false},
} }
func TestHookExtractCommandArguments(t *testing.T) { func TestHookExtractCommandArguments(t *testing.T) {
@@ -136,6 +141,67 @@ func TestHookExtractCommandArguments(t *testing.T) {
} }
} }
// Here we test the extraction of env variables when the user defined a hook
// with the "pass-environment-to-command" directive
// we test both cases where the name of the data is used as the name of the
// env key & the case where the hook definition sets the env var name to a
// fixed value using the envname construct like so::
// [
// {
// "id": "push",
// "execute-command": "bb2mm",
// "command-working-directory": "/tmp",
// "pass-environment-to-command":
// [
// {
// "source": "entire-payload",
// "envname": "PAYLOAD"
// },
// ]
// }
// ]
var hookExtractCommandArgumentsForEnvTests = []struct {
exec string
args []Argument
headers, query, payload *map[string]interface{}
value []string
ok bool
}{
// successes
{
"test",
[]Argument{Argument{"header", "a", ""}},
&map[string]interface{}{"a": "z"}, nil, nil,
[]string{"HOOK_a=z"},
true,
},
{
"test",
[]Argument{Argument{"header", "a", "MYKEY"}},
&map[string]interface{}{"a": "z"}, nil, nil,
[]string{"MYKEY=z"},
true,
},
// failures
{
"fail",
[]Argument{Argument{"payload", "a", ""}},
&map[string]interface{}{"a": "z"}, nil, nil,
[]string{},
false,
},
}
func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
for _, tt := range hookExtractCommandArgumentsForEnvTests {
h := &Hook{ExecuteCommand: tt.exec, PassEnvironmentToCommand: tt.args}
value, err := h.ExtractCommandArgumentsForEnv(tt.headers, tt.query, tt.payload)
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
t.Errorf("failed to extract args for env {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
}
}
}
var hooksLoadFromFileTests = []struct { var hooksLoadFromFileTests = []struct {
path string path string
ok bool ok bool
@@ -182,16 +248,16 @@ var matchRuleTests = []struct {
ok bool ok bool
err bool err bool
}{ }{
{"value", "", "", "z", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false}, {"value", "", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false},
{"regex", "^z", "", "z", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false}, {"regex", "^z", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false},
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a"}, &map[string]interface{}{"a": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false}, {"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"a": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false},
// failures // failures
{"value", "", "", "X", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false}, {"value", "", "", "X", Argument{"header", "a", ""}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
{"regex", "^X", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false}, {"regex", "^X", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
{"value", "", "2", "X", Argument{"header", "a"}, &map[string]interface{}{"y": "z"}, nil, nil, []byte{}, false, false}, // reference invalid header {"value", "", "2", "X", Argument{"header", "a", ""}, &map[string]interface{}{"y": "z"}, nil, nil, []byte{}, false, false}, // reference invalid header
// errors // errors
{"regex", "*", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, true}, // invalid regex {"regex", "*", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, true}, // invalid regex
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a"}, &map[string]interface{}{"a": ""}, nil, nil, []byte{}, false, true}, // invalid hmac {"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"a": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
} }
func TestMatchRule(t *testing.T) { func TestMatchRule(t *testing.T) {
@@ -215,8 +281,8 @@ var andRuleTests = []struct {
{ {
"(a=z, b=y): a=z && b=y", "(a=z, b=y): a=z && b=y",
AndRule{ AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
}, },
&map[string]interface{}{"a": "z", "b": "y"}, nil, nil, []byte{}, &map[string]interface{}{"a": "z", "b": "y"}, nil, nil, []byte{},
true, false, true, false,
@@ -224,8 +290,8 @@ var andRuleTests = []struct {
{ {
"(a=z, b=Y): a=z && b=y", "(a=z, b=Y): a=z && b=y",
AndRule{ AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
}, },
&map[string]interface{}{"a": "z", "b": "Y"}, nil, nil, []byte{}, &map[string]interface{}{"a": "z", "b": "Y"}, nil, nil, []byte{},
false, false, false, false,
@@ -234,22 +300,22 @@ var andRuleTests = []struct {
{ {
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u", "(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
AndRule{ AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
{ {
And: &AndRule{ And: &AndRule{
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c"}}}, {Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}}},
}, },
}, },
{ {
Or: &OrRule{ Or: &OrRule{
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d"}}}, {Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}}},
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e"}}}, {Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}}},
}, },
}, },
{ {
Not: &NotRule{ Not: &NotRule{
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f"}}, Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", ""}},
}, },
}, },
}, },
@@ -260,7 +326,7 @@ var andRuleTests = []struct {
// failures // failures
{ {
"invalid rule", "invalid rule",
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a"}}}}, AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}},
&map[string]interface{}{"y": "z"}, nil, nil, nil, &map[string]interface{}{"y": "z"}, nil, nil, nil,
false, false, false, false,
}, },
@@ -286,8 +352,8 @@ var orRuleTests = []struct {
{ {
"(a=z, b=X): a=z || b=y", "(a=z, b=X): a=z || b=y",
OrRule{ OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
}, },
&map[string]interface{}{"a": "z", "b": "X"}, nil, nil, []byte{}, &map[string]interface{}{"a": "z", "b": "X"}, nil, nil, []byte{},
true, false, true, false,
@@ -295,8 +361,8 @@ var orRuleTests = []struct {
{ {
"(a=X, b=y): a=z || b=y", "(a=X, b=y): a=z || b=y",
OrRule{ OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
}, },
&map[string]interface{}{"a": "X", "b": "y"}, nil, nil, []byte{}, &map[string]interface{}{"a": "X", "b": "y"}, nil, nil, []byte{},
true, false, true, false,
@@ -304,8 +370,8 @@ var orRuleTests = []struct {
{ {
"(a=Z, b=Y): a=z || b=y", "(a=Z, b=Y): a=z || b=y",
OrRule{ OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
}, },
&map[string]interface{}{"a": "Z", "b": "Y"}, nil, nil, []byte{}, &map[string]interface{}{"a": "Z", "b": "Y"}, nil, nil, []byte{},
false, false, false, false,
@@ -314,7 +380,7 @@ var orRuleTests = []struct {
{ {
"invalid rule", "invalid rule",
OrRule{ OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
}, },
&map[string]interface{}{"y": "Z"}, nil, nil, []byte{}, &map[string]interface{}{"y": "Z"}, nil, nil, []byte{},
false, false, false, false,
@@ -338,8 +404,8 @@ var notRuleTests = []struct {
ok bool ok bool
err bool err bool
}{ }{
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a"}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false}, {"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false},
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false}, {"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
} }
func TestNotRule(t *testing.T) { func TestNotRule(t *testing.T) {
+7
View File
@@ -4,6 +4,13 @@
"execute-command": "/home/adnan/redeploy-go-webhook.sh", "execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go", "command-working-directory": "/home/adnan/go",
"response-message": "I got the payload!", "response-message": "I got the payload!",
"response-headers":
[
{
"name": "Access-Control-Allow-Origin",
"value": "*"
}
],
"pass-arguments-to-command": "pass-arguments-to-command":
[ [
{ {
+1 -2
View File
@@ -57,7 +57,7 @@
"id": "bitbucket", "id": "bitbucket",
"execute-command": "{{ .Hookecho }}", "execute-command": "{{ .Hookecho }}",
"command-working-directory": "/", "command-working-directory": "/",
"include-command-output-in-response": true, "include-command-output-in-response": false,
"response-message": "success", "response-message": "success",
"parse-parameters-as-json": [ "parse-parameters-as-json": [
{ {
@@ -136,4 +136,3 @@
} }
} }
] ]
+80 -64
View File
@@ -21,11 +21,11 @@ import (
) )
const ( const (
version = "2.3.6" version = "2.5.0"
) )
var ( var (
ip = flag.String("ip", "", "ip the webhook should serve hooks on") ip = flag.String("ip", "0.0.0.0", "ip the webhook should serve hooks on")
port = flag.Int("port", 9000, "port the webhook should serve hooks on") port = flag.Int("port", 9000, "port the webhook should serve hooks on")
verbose = flag.Bool("verbose", false, "show verbose output") verbose = flag.Bool("verbose", false, "show verbose output")
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode") noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
@@ -36,6 +36,8 @@ var (
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file") cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file") key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
responseHeaders hook.ResponseHeaders
watcher *fsnotify.Watcher watcher *fsnotify.Watcher
signals chan os.Signal signals chan os.Signal
@@ -45,6 +47,8 @@ var (
func main() { func main() {
hooks = hook.Hooks{} hooks = hook.Hooks{}
flag.Var(&responseHeaders, "header", "response header to return, specified in format name=value, use multiple times to set multiple headers")
flag.Parse() flag.Parse()
log.SetPrefix("[webhook] ") log.SetPrefix("[webhook] ")
@@ -72,10 +76,16 @@ func main() {
log.Printf("couldn't load hooks from file! %+v\n", err) log.Printf("couldn't load hooks from file! %+v\n", err)
} else { } else {
log.Printf("loaded %d hook(s) from file\n", len(hooks)) seenHooksIds := make(map[string]bool)
log.Printf("found %d hook(s) in file\n", len(hooks))
for _, hook := range hooks { for _, hook := range hooks {
log.Printf("\t> %s\n", hook.ID) if seenHooksIds[hook.ID] == true {
log.Fatalf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!\n", hook.ID)
}
seenHooksIds[hook.ID] = true
log.Printf("\tloaded: %s\n", hook.ID)
} }
} }
@@ -101,10 +111,10 @@ func main() {
} }
l := negroni.NewLogger() l := negroni.NewLogger()
l.Logger = log.New(os.Stderr, "[webhook] ", log.Ldate|log.Ltime) l.ALogger = log.New(os.Stderr, "[webhook] ", log.Ldate|log.Ltime)
negroniRecovery := &negroni.Recovery{ negroniRecovery := &negroni.Recovery{
Logger: l.Logger, Logger: l.ALogger,
PrintStack: true, PrintStack: true,
StackAll: false, StackAll: false,
StackSize: 1024 * 8, StackSize: 1024 * 8,
@@ -127,22 +137,24 @@ func main() {
n.UseHandler(router) n.UseHandler(router)
if *secure { if *secure {
log.Printf("starting secure (https) webhook on %s:%d", *ip, *port) log.Printf("serving hooks on https://%s:%d%s", *ip, *port, hooksURL)
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n)) log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n))
} else { } else {
log.Printf("starting insecure (http) webhook on %s:%d", *ip, *port) log.Printf("serving hooks on http://%s:%d%s", *ip, *port, hooksURL)
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n)) log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
} }
} }
func hookHandler(w http.ResponseWriter, r *http.Request) { func hookHandler(w http.ResponseWriter, r *http.Request) {
for _, responseHeader := range responseHeaders {
w.Header().Set(responseHeader.Name, responseHeader.Value)
}
id := mux.Vars(r)["id"] id := mux.Vars(r)["id"]
matchedHooks := hooks.MatchAll(id) if matchedHook := hooks.Match(id); matchedHook != nil {
log.Printf("%s got matched\n", id)
if matchedHooks != nil {
log.Printf("%s got matched (%d time(s))\n", id, len(matchedHooks))
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
@@ -179,48 +191,55 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
} }
// handle hook // handle hook
for _, h := range matchedHooks { if err = matchedHook.ParseJSONParameters(&headers, &query, &payload); err != nil {
err := h.ParseJSONParameters(&headers, &query, &payload) msg := fmt.Sprintf("error parsing JSON parameters: %s", err)
log.Printf(msg)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Unable to parse JSON parameters.")
return
}
var ok bool
if matchedHook.TriggerRule == nil {
ok = true
} else {
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body)
if err != nil { if err != nil {
msg := fmt.Sprintf("error parsing JSON: %s", err) msg := fmt.Sprintf("error evaluating hook: %s", err)
log.Printf(msg) log.Printf(msg)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, msg) fmt.Fprintf(w, "Error occurred while evaluating hook rules.")
return
}
var ok bool
if h.TriggerRule == nil {
ok = true
} else {
ok, err = h.TriggerRule.Evaluate(&headers, &query, &payload, &body)
if err != nil {
msg := fmt.Sprintf("error evaluating hook: %s", err)
log.Printf(msg)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, msg)
return
}
}
if ok {
log.Printf("%s hook triggered successfully\n", h.ID)
if h.CaptureCommandOutput {
response := handleHook(h, &headers, &query, &payload, &body)
fmt.Fprintf(w, response)
} else {
go handleHook(h, &headers, &query, &payload, &body)
fmt.Fprintf(w, h.ResponseMessage)
}
return return
} }
} }
if ok {
log.Printf("%s hook triggered successfully\n", matchedHook.ID)
for _, responseHeader := range matchedHook.ResponseHeaders {
w.Header().Set(responseHeader.Name, responseHeader.Value)
}
if matchedHook.CaptureCommandOutput {
response, err := handleHook(matchedHook, &headers, &query, &payload, &body)
if err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
} else {
fmt.Fprintf(w, response)
}
} else {
go handleHook(matchedHook, &headers, &query, &payload, &body)
fmt.Fprintf(w, matchedHook.ResponseMessage)
}
return
}
// if none of the hooks got triggered // if none of the hooks got triggered
log.Printf("%s got matched (%d time(s)), but didn't get triggered because the trigger rules were not satisfied\n", matchedHooks[0].ID, len(matchedHooks)) log.Printf("%s got matched, but didn't get triggered because the trigger rules were not satisfied\n", matchedHook.ID)
fmt.Fprintf(w, "Hook rules were not satisfied.") fmt.Fprintf(w, "Hook rules were not satisfied.")
} else { } else {
@@ -229,7 +248,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) string { func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) (string, error) {
var err error var err error
cmd := exec.Command(h.ExecuteCommand) cmd := exec.Command(h.ExecuteCommand)
@@ -249,28 +268,17 @@ func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, b
log.Printf("executing %s (%s) with arguments %q and environment %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir) log.Printf("executing %s (%s) with arguments %q and environment %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir)
out, err := cmd.CombinedOutput() out, err := cmd.Output()
log.Printf("command output: %s\n", out) log.Printf("command output: %s\n", out)
var errorResponse string
if err != nil { if err != nil {
log.Printf("error occurred: %+v\n", err) log.Printf("error occurred: %+v\n", err)
errorResponse = fmt.Sprintf("%+v", err)
} }
log.Printf("finished handling %s\n", h.ID) log.Printf("finished handling %s\n", h.ID)
var response []byte return string(out), err
response, err = json.Marshal(&hook.CommandStatusResponse{ResponseMessage: h.ResponseMessage, Output: string(out), Error: errorResponse})
if err != nil {
log.Printf("error marshalling response: %+v", err)
return h.ResponseMessage
}
return string(response)
} }
func reloadHooks() { func reloadHooks() {
@@ -284,10 +292,18 @@ func reloadHooks() {
if err != nil { if err != nil {
log.Printf("couldn't load hooks from file! %+v\n", err) log.Printf("couldn't load hooks from file! %+v\n", err)
} else { } else {
log.Printf("loaded %d hook(s) from file\n", len(hooks)) seenHooksIds := make(map[string]bool)
for _, hook := range hooks { log.Printf("found %d hook(s) in file\n", len(newHooks))
log.Printf("\t> %s\n", hook.ID)
for _, hook := range newHooks {
if seenHooksIds[hook.ID] == true {
log.Printf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!", hook.ID)
log.Println("reverting hooks back to the previous configuration")
return
}
seenHooksIds[hook.ID] = true
log.Printf("\tloaded: %s\n", hook.ID)
} }
hooks = newHooks hooks = newHooks
+11 -5
View File
@@ -372,7 +372,9 @@ var hookHandlerTests = []struct {
}`, }`,
false, false,
http.StatusOK, http.StatusOK,
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\nenv: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00\n"}`, `arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
`,
}, },
{ {
"bitbucket", // bitbucket sends their payload using uriencoded params. "bitbucket", // bitbucket sends their payload using uriencoded params.
@@ -381,7 +383,7 @@ var hookHandlerTests = []struct {
`payload={"canon_url": "https://bitbucket.org","commits": [{"author": "marcus","branch": "master","files": [{"file": "somefile.py","type": "modified"}],"message": "Added some more things to somefile.py\n","node": "620ade18607a","parents": ["702c70160afc"],"raw_author": "Marcus Bertrand <marcus@somedomain.com>","raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9","revision": null,"size": -1,"timestamp": "2012-05-30 05:58:56","utctimestamp": "2014-11-07 15:19:02+00:00"}],"repository": {"absolute_url": "/webhook/testing/","fork": false,"is_private": true,"name": "Project X","owner": "marcus","scm": "git","slug": "project-x","website": "https://atlassian.com/"},"user": "marcus"}`, `payload={"canon_url": "https://bitbucket.org","commits": [{"author": "marcus","branch": "master","files": [{"file": "somefile.py","type": "modified"}],"message": "Added some more things to somefile.py\n","node": "620ade18607a","parents": ["702c70160afc"],"raw_author": "Marcus Bertrand <marcus@somedomain.com>","raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9","revision": null,"size": -1,"timestamp": "2012-05-30 05:58:56","utctimestamp": "2014-11-07 15:19:02+00:00"}],"repository": {"absolute_url": "/webhook/testing/","fork": false,"is_private": true,"name": "Project X","owner": "marcus","scm": "git","slug": "project-x","website": "https://atlassian.com/"},"user": "marcus"}`,
true, true,
http.StatusOK, http.StatusOK,
`{"message":"success"}`, `success`,
}, },
{ {
"gitlab", "gitlab",
@@ -431,7 +433,8 @@ var hookHandlerTests = []struct {
}`, }`,
false, false,
http.StatusOK, http.StatusOK,
`{"message":"success","output":"arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com\n"}`, `arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com
`,
}, },
{ {
@@ -469,7 +472,9 @@ var hookHandlerTests = []struct {
}`, }`,
false, false,
http.StatusOK, http.StatusOK,
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\nenv: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00\n"}`, `arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
`,
}, },
{ {
@@ -506,7 +511,8 @@ var hookHandlerTests = []struct {
}`, }`,
false, false,
http.StatusOK, http.StatusOK,
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\n"}`, `arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
`,
}, },
{"empty payload", "github", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`}, {"empty payload", "github", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},