mirror of
https://github.com/adnanh/webhook.git
synced 2025-05-10 07:34:54 +00:00
The payload-hash-* options are imprecisely named. Clarify their function as HMAC validations by renaming them. The existing options will continue to work but are deprecated. Log a warning if the old options are used. All tests, examples, and documentation are updated. Fixes #289
724 lines
32 KiB
Go
724 lines
32 KiB
Go
package hook
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestGetParameter(t *testing.T) {
|
|
for _, test := range []struct {
|
|
key string
|
|
val interface{}
|
|
expect interface{}
|
|
ok bool
|
|
}{
|
|
// True
|
|
{"a", map[string]interface{}{"a": "1"}, "1", true},
|
|
{"a.b", map[string]interface{}{"a.b": "1"}, "1", true},
|
|
{"a.c", map[string]interface{}{"a": map[string]interface{}{"c": 2}}, 2, true},
|
|
{"a.1", map[string]interface{}{"a": map[string]interface{}{"1": 3}}, 3, true},
|
|
{"a.1", map[string]interface{}{"a": []interface{}{"a", "b"}}, "b", true},
|
|
{"0", []interface{}{"a", "b"}, "a", true},
|
|
|
|
// False
|
|
{"z", map[string]interface{}{"a": "1"}, nil, false},
|
|
{"a.z", map[string]interface{}{"a": map[string]interface{}{"b": 2}}, nil, false},
|
|
{"z.b", map[string]interface{}{"a": map[string]interface{}{"z": 2}}, nil, false},
|
|
{"a.2", map[string]interface{}{"a": []interface{}{"a", "b"}}, nil, false},
|
|
} {
|
|
res, err := GetParameter(test.key, test.val)
|
|
if (err == nil) != test.ok {
|
|
t.Errorf("unexpected result given {%q, %q}: %s\n", test.key, test.val, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(res, test.expect) {
|
|
t.Errorf("failed given {%q, %q}:\nexpected {%#v}\ngot {%#v}\n", test.key, test.val, test.expect, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
var checkPayloadSignatureTests = []struct {
|
|
payload []byte
|
|
secret string
|
|
signature string
|
|
mac string
|
|
ok bool
|
|
}{
|
|
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
|
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
|
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
|
{[]byte(``), "secret", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", true},
|
|
// failures
|
|
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
|
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
|
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
|
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
|
|
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
|
|
{[]byte(``), "secret", "XXXf6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", false},
|
|
}
|
|
|
|
func TestCheckPayloadSignature(t *testing.T) {
|
|
for _, tt := range checkPayloadSignatureTests {
|
|
mac, err := CheckPayloadSignature(tt.payload, tt.secret, tt.signature)
|
|
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))
|
|
}
|
|
|
|
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
|
t.Errorf("error message should not disclose expected mac: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var checkPayloadSignature256Tests = []struct {
|
|
payload []byte
|
|
secret string
|
|
signature string
|
|
mac string
|
|
ok bool
|
|
}{
|
|
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
|
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
|
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
|
{[]byte(``), "secret", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", true},
|
|
// failures
|
|
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
|
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
|
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
|
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
|
|
{[]byte(``), "secret", "XXX66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", false},
|
|
}
|
|
|
|
func TestCheckPayloadSignature256(t *testing.T) {
|
|
for _, tt := range checkPayloadSignature256Tests {
|
|
mac, err := CheckPayloadSignature256(tt.payload, tt.secret, tt.signature)
|
|
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))
|
|
}
|
|
|
|
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
|
t.Errorf("error message should not disclose expected mac: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var checkPayloadSignature512Tests = []struct {
|
|
payload []byte
|
|
secret string
|
|
signature string
|
|
mac string
|
|
ok bool
|
|
}{
|
|
{[]byte(`{"a": "z"}`), "secret", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
|
{[]byte(`{"a": "z"}`), "secret", "sha512=4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
|
{[]byte(``), "secret", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", true},
|
|
// failures
|
|
{[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false},
|
|
{[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false},
|
|
{[]byte(``), "secret", "XXX9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", false},
|
|
}
|
|
|
|
func TestCheckPayloadSignature512(t *testing.T) {
|
|
for _, tt := range checkPayloadSignature512Tests {
|
|
mac, err := CheckPayloadSignature512(tt.payload, tt.secret, tt.signature)
|
|
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))
|
|
}
|
|
|
|
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
|
t.Errorf("error message should not disclose expected mac: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var checkScalrSignatureTests = []struct {
|
|
description string
|
|
headers map[string]interface{}
|
|
body []byte
|
|
secret string
|
|
expectedSignature string
|
|
ok bool
|
|
}{
|
|
{
|
|
"Valid signature",
|
|
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
|
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
|
"48e395e38ac48988929167df531eb2da00063a7d", true,
|
|
},
|
|
{
|
|
"Wrong signature",
|
|
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
|
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
|
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
|
},
|
|
{
|
|
"Missing Date header",
|
|
map[string]interface{}{"X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
|
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
|
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
|
},
|
|
{
|
|
"Missing X-Signature header",
|
|
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC"},
|
|
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
|
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
|
},
|
|
{
|
|
"Missing signing key",
|
|
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
|
[]byte(`{"a": "b"}`), "",
|
|
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
|
},
|
|
}
|
|
|
|
func TestCheckScalrSignature(t *testing.T) {
|
|
for _, testCase := range checkScalrSignatureTests {
|
|
r := &Request{
|
|
Headers: testCase.headers,
|
|
Body: testCase.body,
|
|
}
|
|
valid, err := CheckScalrSignature(r, testCase.secret, false)
|
|
if valid != testCase.ok {
|
|
t.Errorf("failed to check scalr signature fot test case: %s\nexpected ok:%#v, got ok:%#v}",
|
|
testCase.description, testCase.ok, valid)
|
|
}
|
|
|
|
if err != nil && testCase.secret != "" && strings.Contains(err.Error(), testCase.expectedSignature) {
|
|
t.Errorf("error message should not disclose expected mac: %s on test case %s", err, testCase.description)
|
|
}
|
|
}
|
|
}
|
|
|
|
var checkIPWhitelistTests = []struct {
|
|
addr string
|
|
ipRange string
|
|
expect bool
|
|
ok bool
|
|
}{
|
|
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 ", true, true},
|
|
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0 ", false, true},
|
|
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 10.0.0.1 ", true, true},
|
|
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0/31 ", true, true},
|
|
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 ", true, true},
|
|
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 2001:db8:1::/64", true, true},
|
|
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/64 ", false, true},
|
|
}
|
|
|
|
func TestCheckIPWhitelist(t *testing.T) {
|
|
for _, tt := range checkIPWhitelistTests {
|
|
result, err := CheckIPWhitelist(tt.addr, tt.ipRange)
|
|
if (err == nil) != tt.ok || result != tt.expect {
|
|
t.Errorf("ip whitelist test failed {%q, %q}:\nwant {expect:%#v, ok:%#v},\ngot {result:%#v, ok:%#v}", tt.addr, tt.ipRange, tt.expect, tt.ok, result, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var extractParameterTests = []struct {
|
|
s string
|
|
params interface{}
|
|
value string
|
|
ok bool
|
|
}{
|
|
{"a", map[string]interface{}{"a": "z"}, "z", true},
|
|
{"a.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "z", true},
|
|
{"a.b.c", map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}, "z", true},
|
|
{"a.b.0", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "x", true},
|
|
{"a.1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "z", true},
|
|
{"a.1.b.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": map[string]interface{}{"c": "y"}}, map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}}, "z", true},
|
|
{"b", map[string]interface{}{"b": map[string]interface{}{"z": 1}}, `{"z":1}`, true},
|
|
{"c", map[string]interface{}{"c": []interface{}{"y", "z"}}, `["y","z"]`, true},
|
|
{"d", map[string]interface{}{"d": [2]interface{}{"y", "z"}}, `["y","z"]`, true},
|
|
// failures
|
|
{"check_nil", nil, "", false},
|
|
{"a.X", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent parameter reference
|
|
{"a.X.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-integer slice index
|
|
{"a.-1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // negative slice index
|
|
{"a.500.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent slice
|
|
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
|
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
|
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
|
|
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
|
}
|
|
|
|
func TestExtractParameter(t *testing.T) {
|
|
for _, tt := range extractParameterTests {
|
|
value, err := ExtractParameterAsString(tt.s, tt.params)
|
|
if (err == nil) != tt.ok || value != tt.value {
|
|
t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.s, tt.value, tt.ok, value, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var argumentGetTests = []struct {
|
|
source, name string
|
|
headers, query, payload map[string]interface{}
|
|
value string
|
|
ok bool
|
|
}{
|
|
{"header", "a", map[string]interface{}{"A": "z"}, nil, nil, "z", true},
|
|
{"url", "a", nil, map[string]interface{}{"a": "z"}, nil, "z", true},
|
|
{"payload", "a", nil, nil, map[string]interface{}{"a": "z"}, "z", true},
|
|
{"string", "a", nil, nil, map[string]interface{}{"a": "z"}, "a", true},
|
|
// failures
|
|
{"header", "a", nil, map[string]interface{}{"a": "z"}, map[string]interface{}{"a": "z"}, "", false}, // nil headers
|
|
{"url", "a", map[string]interface{}{"A": "z"}, nil, map[string]interface{}{"a": "z"}, "", false}, // nil query
|
|
{"payload", "a", map[string]interface{}{"A": "z"}, map[string]interface{}{"a": "z"}, nil, "", false}, // nil payload
|
|
{"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, "", false}, // invalid source
|
|
}
|
|
|
|
func TestArgumentGet(t *testing.T) {
|
|
for _, tt := range argumentGetTests {
|
|
a := Argument{tt.source, tt.name, "", false}
|
|
r := &Request{
|
|
Headers: tt.headers,
|
|
Query: tt.query,
|
|
Payload: tt.payload,
|
|
}
|
|
value, err := a.Get(r)
|
|
if (err == nil) != tt.ok || value != tt.value {
|
|
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.source, tt.name, tt.value, tt.ok, value, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var hookParseJSONParametersTests = []struct {
|
|
params []Argument
|
|
headers, query, payload map[string]interface{}
|
|
rheaders, rquery, rpayload map[string]interface{}
|
|
ok bool
|
|
}{
|
|
{[]Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
|
{[]Argument{Argument{"url", "a", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
|
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
|
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
|
// failures
|
|
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
|
|
{[]Argument{Argument{"header", "y", "", false}}, map[string]interface{}{"X": `{}`}, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
|
{[]Argument{Argument{"string", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
|
}
|
|
|
|
func TestHookParseJSONParameters(t *testing.T) {
|
|
for _, tt := range hookParseJSONParametersTests {
|
|
h := &Hook{JSONStringParameters: tt.params}
|
|
r := &Request{
|
|
Headers: tt.headers,
|
|
Query: tt.query,
|
|
Payload: tt.payload,
|
|
}
|
|
err := h.ParseJSONParameters(r)
|
|
if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) {
|
|
t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, tt.rheaders, tt.ok, tt.headers, (err == nil))
|
|
}
|
|
}
|
|
}
|
|
|
|
var hookExtractCommandArgumentsTests = []struct {
|
|
exec string
|
|
args []Argument
|
|
headers, query, payload map[string]interface{}
|
|
value []string
|
|
ok bool
|
|
}{
|
|
{"test", []Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
|
|
// failures
|
|
{"fail", []Argument{Argument{"payload", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
|
|
}
|
|
|
|
func TestHookExtractCommandArguments(t *testing.T) {
|
|
for _, tt := range hookExtractCommandArgumentsTests {
|
|
h := &Hook{ExecuteCommand: tt.exec, PassArgumentsToCommand: tt.args}
|
|
r := &Request{
|
|
Headers: tt.headers,
|
|
Query: tt.query,
|
|
Payload: tt.payload,
|
|
}
|
|
value, err := h.ExtractCommandArguments(r)
|
|
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
|
t.Errorf("failed to extract args {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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", "", false}},
|
|
map[string]interface{}{"A": "z"}, nil, nil,
|
|
[]string{"HOOK_a=z"},
|
|
true,
|
|
},
|
|
{
|
|
"test",
|
|
[]Argument{Argument{"header", "a", "MYKEY", false}},
|
|
map[string]interface{}{"A": "z"}, nil, nil,
|
|
[]string{"MYKEY=z"},
|
|
true,
|
|
},
|
|
// failures
|
|
{
|
|
"fail",
|
|
[]Argument{Argument{"payload", "a", "", false}},
|
|
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}
|
|
r := &Request{
|
|
Headers: tt.headers,
|
|
Query: tt.query,
|
|
Payload: tt.payload,
|
|
}
|
|
value, err := h.ExtractCommandArgumentsForEnv(r)
|
|
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 {
|
|
path string
|
|
asTemplate bool
|
|
ok bool
|
|
}{
|
|
{"../../hooks.json.example", false, true},
|
|
{"../../hooks.yaml.example", false, true},
|
|
{"../../hooks.json.tmpl.example", true, true},
|
|
{"../../hooks.yaml.tmpl.example", true, true},
|
|
{"", false, true},
|
|
// failures
|
|
{"missing.json", false, false},
|
|
}
|
|
|
|
func TestHooksLoadFromFile(t *testing.T) {
|
|
secret := `foo"123`
|
|
os.Setenv("XXXTEST_SECRET", secret)
|
|
|
|
for _, tt := range hooksLoadFromFileTests {
|
|
h := &Hooks{}
|
|
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
|
if (err == nil) != tt.ok {
|
|
t.Errorf(err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHooksTemplateLoadFromFile(t *testing.T) {
|
|
secret := `foo"123`
|
|
os.Setenv("XXXTEST_SECRET", secret)
|
|
|
|
for _, tt := range hooksLoadFromFileTests {
|
|
if !tt.asTemplate {
|
|
continue
|
|
}
|
|
|
|
h := &Hooks{}
|
|
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
|
if (err == nil) != tt.ok {
|
|
t.Errorf(err.Error())
|
|
continue
|
|
}
|
|
|
|
s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret
|
|
if s != secret {
|
|
t.Errorf("Expected secret of %q, got %q", secret, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
var hooksMatchTests = []struct {
|
|
id string
|
|
hooks Hooks
|
|
value *Hook
|
|
}{
|
|
{"a", Hooks{Hook{ID: "a"}}, &Hook{ID: "a"}},
|
|
{"X", Hooks{Hook{ID: "a"}}, new(Hook)},
|
|
}
|
|
|
|
func TestHooksMatch(t *testing.T) {
|
|
for _, tt := range hooksMatchTests {
|
|
value := tt.hooks.Match(tt.id)
|
|
if reflect.DeepEqual(reflect.ValueOf(value), reflect.ValueOf(tt.value)) {
|
|
t.Errorf("failed to match %q:\nexpected %#v,\ngot %#v", tt.id, tt.value, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
var matchRuleTests = []struct {
|
|
typ, regex, secret, value, ipRange string
|
|
param Argument
|
|
headers, query, payload map[string]interface{}
|
|
body []byte
|
|
remoteAddr string
|
|
ok bool
|
|
err bool
|
|
}{
|
|
{"value", "", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
|
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
|
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
|
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
|
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
|
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
|
// failures
|
|
{"value", "", "", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
|
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
|
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header
|
|
// errors
|
|
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
|
|
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
|
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
|
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
|
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
|
{"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
|
{"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
|
// IP whitelisting, valid cases
|
|
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
|
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
|
{"ip-whitelist", "", "", "", "192.168.0.1", Argument{}, nil, nil, nil, []byte{}, "192.168.0.1:9000", true, false}, // valid IPv4, no range
|
|
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, with range
|
|
{"ip-whitelist", "", "", "", "::1", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, no range
|
|
// IP whitelisting, invalid cases
|
|
{"ip-whitelist", "", "", "", "192.168.0.1/a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, with range
|
|
{"ip-whitelist", "", "", "", "192.168.0.a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, no range
|
|
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.a:9000", false, true}, // invalid IPv4 address
|
|
{"ip-whitelist", "", "", "", "::1/a", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, with range
|
|
{"ip-whitelist", "", "", "", "::z", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, no range
|
|
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::z]:9000", false, true}, // invalid IPv6 address
|
|
}
|
|
|
|
func TestMatchRule(t *testing.T) {
|
|
for i, tt := range matchRuleTests {
|
|
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange}
|
|
req := &Request{
|
|
Headers: tt.headers,
|
|
Query: tt.query,
|
|
Payload: tt.payload,
|
|
Body: tt.body,
|
|
RawRequest: &http.Request{
|
|
RemoteAddr: tt.remoteAddr,
|
|
},
|
|
}
|
|
ok, err := r.Evaluate(req)
|
|
if ok != tt.ok || (err != nil) != tt.err {
|
|
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var andRuleTests = []struct {
|
|
desc string // description of the test case
|
|
rule AndRule
|
|
headers, query, payload map[string]interface{}
|
|
body []byte
|
|
ok bool
|
|
err bool
|
|
}{
|
|
{
|
|
"(a=z, b=y): a=z && b=y",
|
|
AndRule{
|
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
|
},
|
|
map[string]interface{}{"A": "z", "B": "y"}, nil, nil,
|
|
[]byte{},
|
|
true, false,
|
|
},
|
|
{
|
|
"(a=z, b=Y): a=z && b=y",
|
|
AndRule{
|
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
|
},
|
|
map[string]interface{}{"A": "z", "B": "Y"}, nil, nil,
|
|
[]byte{},
|
|
false, false,
|
|
},
|
|
// Complex test to cover Rules.Evaluate
|
|
{
|
|
"(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{
|
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
|
{
|
|
And: &AndRule{
|
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
|
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false}, ""}},
|
|
},
|
|
},
|
|
{
|
|
Or: &OrRule{
|
|
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false}, ""}},
|
|
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false}, ""}},
|
|
},
|
|
},
|
|
{
|
|
Not: &NotRule{
|
|
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false}, ""},
|
|
},
|
|
},
|
|
},
|
|
map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil,
|
|
[]byte{},
|
|
true, false,
|
|
},
|
|
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
|
// failures
|
|
{
|
|
"invalid rule",
|
|
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
|
|
map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
|
false, true,
|
|
},
|
|
}
|
|
|
|
func TestAndRule(t *testing.T) {
|
|
for _, tt := range andRuleTests {
|
|
r := &Request{
|
|
Headers: tt.headers,
|
|
Query: tt.query,
|
|
Payload: tt.payload,
|
|
Body: tt.body,
|
|
}
|
|
ok, err := tt.rule.Evaluate(r)
|
|
if ok != tt.ok || (err != nil) != tt.err {
|
|
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var orRuleTests = []struct {
|
|
desc string // description of the test case
|
|
rule OrRule
|
|
headers, query, payload map[string]interface{}
|
|
body []byte
|
|
ok bool
|
|
err bool
|
|
}{
|
|
{
|
|
"(a=z, b=X): a=z || b=y",
|
|
OrRule{
|
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
|
},
|
|
map[string]interface{}{"A": "z", "B": "X"}, nil, nil,
|
|
[]byte{},
|
|
true, false,
|
|
},
|
|
{
|
|
"(a=X, b=y): a=z || b=y",
|
|
OrRule{
|
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
|
},
|
|
map[string]interface{}{"A": "X", "B": "y"}, nil, nil,
|
|
[]byte{},
|
|
true, false,
|
|
},
|
|
{
|
|
"(a=Z, b=Y): a=z || b=y",
|
|
OrRule{
|
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
|
},
|
|
map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil,
|
|
[]byte{},
|
|
false, false,
|
|
},
|
|
// failures
|
|
{
|
|
"invalid rule",
|
|
OrRule{
|
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
|
},
|
|
map[string]interface{}{"Y": "Z"}, nil, nil,
|
|
[]byte{},
|
|
false, true,
|
|
},
|
|
}
|
|
|
|
func TestOrRule(t *testing.T) {
|
|
for _, tt := range orRuleTests {
|
|
r := &Request{
|
|
Headers: tt.headers,
|
|
Query: tt.query,
|
|
Payload: tt.payload,
|
|
Body: tt.body,
|
|
}
|
|
ok, err := tt.rule.Evaluate(r)
|
|
if ok != tt.ok || (err != nil) != tt.err {
|
|
t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var notRuleTests = []struct {
|
|
desc string // description of the test case
|
|
rule NotRule
|
|
headers, query, payload map[string]interface{}
|
|
body []byte
|
|
ok bool
|
|
err bool
|
|
}{
|
|
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
|
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
|
}
|
|
|
|
func TestNotRule(t *testing.T) {
|
|
for _, tt := range notRuleTests {
|
|
r := &Request{
|
|
Headers: tt.headers,
|
|
Query: tt.query,
|
|
Payload: tt.payload,
|
|
Body: tt.body,
|
|
}
|
|
ok, err := tt.rule.Evaluate(r)
|
|
if ok != tt.ok || (err != nil) != tt.err {
|
|
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompare(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
a, b string
|
|
ok bool
|
|
}{
|
|
{"abcd", "abcd", true},
|
|
{"zyxw", "abcd", false},
|
|
} {
|
|
if ok := compare(tt.a, tt.b); ok != tt.ok {
|
|
t.Errorf("compare failed for %q and %q: got %v\n", tt.a, tt.b, ok)
|
|
}
|
|
}
|
|
}
|