mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
10 KiB
346 lines
10 KiB
// Copyright 2019 The Gitea Authors. All rights reserved. |
|
// SPDX-License-Identifier: MIT |
|
|
|
package webhook |
|
|
|
import ( |
|
"io" |
|
"net/http" |
|
"net/http/httptest" |
|
"net/url" |
|
"strings" |
|
"testing" |
|
"time" |
|
|
|
"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" |
|
"code.gitea.io/gitea/modules/util" |
|
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 |
|
t.Cleanup(func() { |
|
setting.Webhook = oldWebhook |
|
}) |
|
|
|
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(http.MethodPost, 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(http.StatusOK) |
|
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(t.Context(), hook)) |
|
|
|
hookTask := &webhook_model.HookTask{ |
|
HookID: hook.ID, |
|
EventType: webhook_module.HookEventPush, |
|
PayloadVersion: 2, |
|
} |
|
|
|
hookTask, err = webhook_model.CreateHookTask(t.Context(), hookTask) |
|
assert.NoError(t, err) |
|
assert.NotNil(t, hookTask) |
|
|
|
assert.NoError(t, Deliver(t.Context(), 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, "******", hookTask.RequestInfo.Headers["Authorization"]) |
|
} |
|
|
|
func TestWebhookDeliverHookTask(t *testing.T) { |
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
|
|
done := make(chan struct{}, 1) |
|
version2Body := `{ |
|
"body": "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", |
|
"msgtype": "", |
|
"format": "org.matrix.custom.html", |
|
"formatted_body": "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] user1 pushed 2 commits to <a href=\"http://localhost:3000/test/repo/src/branch/test\">test</a>:<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1", |
|
"io.gitea.commits": [ |
|
{ |
|
"id": "2020558fe2e34debb818a514715839cabd25e778", |
|
"message": "commit message", |
|
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778", |
|
"author": { |
|
"name": "user1", |
|
"email": "user1@localhost", |
|
"username": "user1" |
|
}, |
|
"committer": { |
|
"name": "user1", |
|
"email": "user1@localhost", |
|
"username": "user1" |
|
}, |
|
"verification": null, |
|
"timestamp": "0001-01-01T00:00:00Z", |
|
"added": null, |
|
"removed": null, |
|
"modified": null |
|
}, |
|
{ |
|
"id": "2020558fe2e34debb818a514715839cabd25e778", |
|
"message": "commit message", |
|
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778", |
|
"author": { |
|
"name": "user1", |
|
"email": "user1@localhost", |
|
"username": "user1" |
|
}, |
|
"committer": { |
|
"name": "user1", |
|
"email": "user1@localhost", |
|
"username": "user1" |
|
}, |
|
"verification": null, |
|
"timestamp": "0001-01-01T00:00:00Z", |
|
"added": null, |
|
"removed": null, |
|
"modified": null |
|
} |
|
] |
|
}` |
|
|
|
testVersion := 0 |
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
assert.Equal(t, "PUT", r.Method) |
|
assert.True(t, strings.HasPrefix(r.URL.Path, "/webhook/")) |
|
assert.Len(t, r.URL.Path, len("/webhook/")+40) // +40 for txnID, a unique ID from payload's sha1 hash |
|
switch testVersion { |
|
case 1: // Version 1 |
|
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) |
|
assert.Empty(t, r.Header.Get("Content-Type")) |
|
body, err := io.ReadAll(r.Body) |
|
assert.NoError(t, err) |
|
assert.Equal(t, `{"data": 42}`, string(body)) |
|
|
|
case 2: // Version 2 |
|
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) |
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type")) |
|
body, err := io.ReadAll(r.Body) |
|
assert.NoError(t, err) |
|
assert.JSONEq(t, version2Body, string(body)) |
|
|
|
default: |
|
w.WriteHeader(http.StatusNotFound) |
|
t.Fatalf("unexpected url path %s", r.URL.Path) |
|
return |
|
} |
|
w.WriteHeader(http.StatusOK) |
|
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(t.Context(), hook)) |
|
|
|
t.Run("Version 1", func(t *testing.T) { |
|
testVersion = 1 |
|
hookTask := &webhook_model.HookTask{ |
|
HookID: hook.ID, |
|
EventType: webhook_module.HookEventPush, |
|
PayloadContent: `{"data": 42}`, |
|
PayloadVersion: 1, |
|
} |
|
|
|
hookTask, err := webhook_model.CreateHookTask(t.Context(), hookTask) |
|
assert.NoError(t, err) |
|
assert.NotNil(t, hookTask) |
|
|
|
assert.NoError(t, Deliver(t.Context(), 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) |
|
|
|
testVersion = 2 |
|
hookTask := &webhook_model.HookTask{ |
|
HookID: hook.ID, |
|
EventType: webhook_module.HookEventPush, |
|
PayloadContent: string(data), |
|
PayloadVersion: 2, |
|
} |
|
|
|
hookTask, err = webhook_model.CreateHookTask(t.Context(), hookTask) |
|
assert.NoError(t, err) |
|
assert.NotNil(t, hookTask) |
|
|
|
assert.NoError(t, Deliver(t.Context(), 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 |
|
httpMethod string // default to POST |
|
} |
|
|
|
cases := map[string]*hookCase{ |
|
webhook_module.SLACK: {}, |
|
webhook_module.DISCORD: {}, |
|
webhook_module.DINGTALK: {}, |
|
webhook_module.TELEGRAM: {}, |
|
webhook_module.MSTEAMS: {}, |
|
webhook_module.FEISHU: {}, |
|
webhook_module.MATRIX: {httpMethod: "PUT"}, |
|
webhook_module.WECHATWORK: {}, |
|
webhook_module.PACKAGIST: {}, |
|
} |
|
|
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
typ := strings.Split(r.URL.Path, "/")[1] // URL: "/{webhook_type}/other-path" |
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path) |
|
assert.Equal(t, util.IfZero(cases[typ].httpMethod, "POST"), r.Method, "webhook test request %q", r.URL.Path) |
|
body, _ := io.ReadAll(r.Body) // read request and send it back to the test by testcase's chan |
|
cases[typ].gotBody <- body |
|
w.WriteHeader(http.StatusNoContent) |
|
})) |
|
t.Cleanup(s.Close) |
|
|
|
p := pushTestPayload() |
|
data, err := p.JSONPayload() |
|
assert.NoError(t, err) |
|
|
|
for typ := range cases { |
|
cases[typ].gotBody = make(chan []byte, 1) |
|
t.Run(typ, func(t *testing.T) { |
|
t.Parallel() |
|
hook := &webhook_model.Webhook{ |
|
RepoID: 3, |
|
IsActive: true, |
|
Type: typ, |
|
URL: s.URL + "/" + typ, |
|
Meta: "{}", |
|
} |
|
assert.NoError(t, webhook_model.CreateWebhook(t.Context(), hook)) |
|
|
|
hookTask := &webhook_model.HookTask{ |
|
HookID: hook.ID, |
|
EventType: webhook_module.HookEventPush, |
|
PayloadContent: string(data), |
|
PayloadVersion: 2, |
|
} |
|
|
|
hookTask, err := webhook_model.CreateHookTask(t.Context(), hookTask) |
|
assert.NoError(t, err) |
|
assert.NotNil(t, hookTask) |
|
|
|
assert.NoError(t, Deliver(t.Context(), hookTask)) |
|
|
|
select { |
|
case gotBody := <-cases[typ].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), "delivered webhook payload doesn't match saved request") |
|
case <-time.After(5 * time.Second): |
|
t.Fatal("waited to long for request to happen") |
|
} |
|
|
|
assert.True(t, hookTask.IsSucceed) |
|
}) |
|
} |
|
}
|
|
|