forgejo/services/webhook/deliver_test.go

333 lines
8.6 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package webhook
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/hostmatcher"
"code.gitea.io/gitea/modules/setting"
webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWebhookProxy(t *testing.T) {
oldWebhook := setting.Webhook
oldHTTPProxy := os.Getenv("http_proxy")
oldHTTPSProxy := os.Getenv("https_proxy")
t.Cleanup(func() {
setting.Webhook = oldWebhook
os.Setenv("http_proxy", oldHTTPProxy)
os.Setenv("https_proxy", oldHTTPSProxy)
})
os.Unsetenv("http_proxy")
os.Unsetenv("https_proxy")
setting.Webhook.ProxyURL = "http://localhost:8080"
setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL)
setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"}
allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com")
tests := []struct {
req string
want string
wantErr bool
}{
{
req: "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx",
want: "http://localhost:8080",
wantErr: false,
},
{
req: "http://s.discordapp.com/assets/xxxxxx",
want: "http://localhost:8080",
wantErr: false,
},
{
req: "http://github.com/a/b",
want: "",
wantErr: false,
},
{
req: "http://www.discordapp.com/assets/xxxxxx",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.req, func(t *testing.T) {
req, err := http.NewRequest("POST", tt.req, nil)
require.NoError(t, err)
u, err := webhookProxy(allowedHostMatcher)(req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
got := ""
if u != nil {
got = u.String()
}
assert.Equal(t, tt.want, got)
})
}
}
func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
done := make(chan struct{}, 1)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/webhook", r.URL.Path)
assert.Equal(t, "Bearer s3cr3t-t0ken", r.Header.Get("Authorization"))
w.WriteHeader(200)
done <- struct{}{}
}))
t.Cleanup(s.Close)
hook := &webhook_model.Webhook{
RepoID: 3,
URL: s.URL + "/webhook",
ContentType: webhook_model.ContentTypeJSON,
IsActive: true,
Type: webhook_module.GITEA,
}
err := hook.SetHeaderAuthorization("Bearer s3cr3t-t0ken")
assert.NoError(t, err)
assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadVersion: 2,
}
hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
assert.NoError(t, err)
assert.NotNil(t, hookTask)
assert.NoError(t, Deliver(context.Background(), hookTask))
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("waited to long for request to happen")
}
assert.True(t, hookTask.IsSucceed)
assert.Equal(t, "Bearer ******", hookTask.RequestInfo.Headers["Authorization"])
}
func TestWebhookDeliverHookTask(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
done := make(chan struct{}, 1)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "PUT", r.Method)
switch r.URL.Path {
case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
// Version 1
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
assert.Equal(t, "", r.Header.Get("Content-Type"))
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Equal(t, `{"data": 42}`, string(body))
case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51":
// Version 2
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Len(t, body, 2147)
default:
w.WriteHeader(404)
t.Fatalf("unexpected url path %s", r.URL.Path)
return
}
w.WriteHeader(200)
done <- struct{}{}
}))
t.Cleanup(s.Close)
hook := &webhook_model.Webhook{
RepoID: 3,
IsActive: true,
Type: webhook_module.MATRIX,
URL: s.URL + "/webhook",
HTTPMethod: "PUT",
ContentType: webhook_model.ContentTypeJSON,
Meta: `{"message_type":0}`, // text
}
assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
t.Run("Version 1", func(t *testing.T) {
hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadContent: `{"data": 42}`,
PayloadVersion: 1,
}
hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
assert.NoError(t, err)
assert.NotNil(t, hookTask)
assert.NoError(t, Deliver(context.Background(), hookTask))
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("waited to long for request to happen")
}
assert.True(t, hookTask.IsSucceed)
})
t.Run("Version 2", func(t *testing.T) {
p := pushTestPayload()
data, err := p.JSONPayload()
assert.NoError(t, err)
hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadContent: string(data),
PayloadVersion: 2,
}
hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
assert.NoError(t, err)
assert.NotNil(t, hookTask)
assert.NoError(t, Deliver(context.Background(), hookTask))
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("waited to long for request to happen")
}
assert.True(t, hookTask.IsSucceed)
})
}
func TestWebhookDeliverSpecificTypes(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
type hookCase struct {
gotBody chan []byte
expectedMethod string
}
cases := map[string]hookCase{
webhook_module.SLACK: {
gotBody: make(chan []byte, 1),
},
webhook_module.DISCORD: {
gotBody: make(chan []byte, 1),
},
webhook_module.DINGTALK: {
gotBody: make(chan []byte, 1),
},
webhook_module.TELEGRAM: {
gotBody: make(chan []byte, 1),
},
webhook_module.MSTEAMS: {
gotBody: make(chan []byte, 1),
},
webhook_module.FEISHU: {
gotBody: make(chan []byte, 1),
},
webhook_module.MATRIX: {
gotBody: make(chan []byte, 1),
expectedMethod: "PUT",
},
webhook_module.WECHATWORK: {
gotBody: make(chan []byte, 1),
},
webhook_module.PACKAGIST: {
gotBody: make(chan []byte, 1),
},
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path)
typ := strings.Split(r.URL.Path, "/")[1] // take first segment (after skipping leading slash)
hc := cases[typ]
if hc.expectedMethod != "" {
assert.Equal(t, hc.expectedMethod, r.Method, r.URL.Path)
} else {
assert.Equal(t, "POST", r.Method, r.URL.Path)
}
require.NotNil(t, hc.gotBody, r.URL.Path)
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
w.WriteHeader(200)
hc.gotBody <- body
}))
t.Cleanup(s.Close)
p := pushTestPayload()
data, err := p.JSONPayload()
assert.NoError(t, err)
for typ, hc := range cases {
typ := typ
hc := hc
t.Run(typ, func(t *testing.T) {
t.Parallel()
hook := &webhook_model.Webhook{
RepoID: 3,
IsActive: true,
Type: typ,
URL: s.URL + "/" + typ,
HTTPMethod: "", // should fallback to POST, when left unset by the specific hook
ContentType: 0, // set to 0 so that falling back to default request fails with "invalid content type"
Meta: "{}",
}
assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadContent: string(data),
PayloadVersion: 2,
}
hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
assert.NoError(t, err)
assert.NotNil(t, hookTask)
assert.NoError(t, Deliver(context.Background(), hookTask))
select {
case gotBody := <-hc.gotBody:
assert.NotEqual(t, string(data), string(gotBody), "request body must be different from the event payload")
assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "request body was not saved")
case <-time.After(5 * time.Second):
t.Fatal("waited to long for request to happen")
}
assert.True(t, hookTask.IsSucceed)
})
}
}