summaryrefslogtreecommitdiffstats
path: root/tests/integration/repo_webhook_test.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tests/integration/repo_webhook_test.go473
1 files changed, 473 insertions, 0 deletions
diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go
new file mode 100644
index 0000000..8f65b5c
--- /dev/null
+++ b/tests/integration/repo_webhook_test.go
@@ -0,0 +1,473 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ gitea_context "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/webhook"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewWebHookLink(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+
+ webhooksLen := len(webhook.List())
+ baseurl := "/user2/repo1/settings/hooks"
+ tests := []string{
+ // webhook list page
+ baseurl,
+ // new webhook page
+ baseurl + "/gitea/new",
+ // edit webhook page
+ baseurl + "/1",
+ }
+
+ var csrfToken string
+ for _, url := range tests {
+ resp := session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Equal(t,
+ webhooksLen,
+ htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(),
+ "not all webhooks are listed in the 'new' dropdown")
+
+ csrfToken = htmlDoc.GetCSRF()
+ }
+
+ // ensure that the "failure" pages has the full dropdown as well
+ resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", baseurl+"/gitea/new", map[string]string{"_csrf": csrfToken}), http.StatusUnprocessableEntity)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Equal(t,
+ webhooksLen,
+ htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(),
+ "not all webhooks are listed in the 'new' dropdown on failure")
+
+ resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", baseurl+"/1", map[string]string{"_csrf": csrfToken}), http.StatusUnprocessableEntity)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ assert.Equal(t,
+ webhooksLen,
+ htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(),
+ "not all webhooks are listed in the 'new' dropdown on failure")
+
+ adminSession := loginUser(t, "user1")
+ t.Run("org3", func(t *testing.T) {
+ baseurl := "/org/org3/settings/hooks"
+ resp := adminSession.MakeRequest(t, NewRequest(t, "GET", baseurl), http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Equal(t,
+ webhooksLen,
+ htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(),
+ "not all webhooks are listed in the 'new' dropdown")
+ })
+ t.Run("admin", func(t *testing.T) {
+ baseurl := "/admin/hooks"
+ resp := adminSession.MakeRequest(t, NewRequest(t, "GET", baseurl), http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Equal(t,
+ webhooksLen,
+ htmlDoc.Find(`a[href^="/admin/default-hooks/"][href$="/new"]`).Length(),
+ "not all webhooks are listed in the 'new' dropdown for default-hooks")
+ assert.Equal(t,
+ webhooksLen,
+ htmlDoc.Find(`a[href^="/admin/system-hooks/"][href$="/new"]`).Length(),
+ "not all webhooks are listed in the 'new' dropdown for system-hooks")
+ })
+}
+
+func TestWebhookForms(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user1")
+
+ t.Run("forgejo/required", testWebhookForms("forgejo", session, map[string]string{
+ "payload_url": "https://forgejo.example.com",
+ "http_method": "POST",
+ "content_type": "1", // json
+ }, map[string]string{
+ "payload_url": "",
+ }, map[string]string{
+ "http_method": "",
+ }, map[string]string{
+ "content_type": "",
+ }, map[string]string{
+ "payload_url": "invalid_url",
+ }, map[string]string{
+ "http_method": "INVALID",
+ }))
+ t.Run("forgejo/optional", testWebhookForms("forgejo", session, map[string]string{
+ "payload_url": "https://forgejo.example.com",
+ "http_method": "POST",
+ "content_type": "1", // json
+ "secret": "s3cr3t",
+
+ "branch_filter": "forgejo/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("gitea/required", testWebhookForms("gitea", session, map[string]string{
+ "payload_url": "https://gitea.example.com",
+ "http_method": "POST",
+ "content_type": "1", // json
+ }, map[string]string{
+ "payload_url": "",
+ }, map[string]string{
+ "http_method": "",
+ }, map[string]string{
+ "content_type": "",
+ }, map[string]string{
+ "payload_url": "invalid_url",
+ }, map[string]string{
+ "http_method": "INVALID",
+ }))
+ t.Run("gitea/optional", testWebhookForms("gitea", session, map[string]string{
+ "payload_url": "https://gitea.example.com",
+ "http_method": "POST",
+ "content_type": "1", // json
+ "secret": "s3cr3t",
+
+ "branch_filter": "gitea/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("gogs/required", testWebhookForms("gogs", session, map[string]string{
+ "payload_url": "https://gogs.example.com",
+ "content_type": "1", // json
+ }))
+ t.Run("gogs/optional", testWebhookForms("gogs", session, map[string]string{
+ "payload_url": "https://gogs.example.com",
+ "content_type": "1", // json
+ "secret": "s3cr3t",
+
+ "branch_filter": "gogs/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("slack/required", testWebhookForms("slack", session, map[string]string{
+ "payload_url": "https://slack.example.com",
+ "channel": "general",
+ }, map[string]string{
+ "channel": "",
+ }, map[string]string{
+ "channel": "invalid channel name",
+ }))
+ t.Run("slack/optional", testWebhookForms("slack", session, map[string]string{
+ "payload_url": "https://slack.example.com",
+ "channel": "#general",
+ "username": "john",
+ "icon_url": "https://slack.example.com/icon.png",
+ "color": "#dd4b39",
+
+ "branch_filter": "slack/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("discord/required", testWebhookForms("discord", session, map[string]string{
+ "username": "john",
+ "payload_url": "https://discord.example.com",
+ }))
+ t.Run("discord/optional", testWebhookForms("discord", session, map[string]string{
+ "payload_url": "https://discord.example.com",
+ "username": "john",
+ "icon_url": "https://discord.example.com/icon.png",
+
+ "branch_filter": "discord/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("dingtalk/required", testWebhookForms("dingtalk", session, map[string]string{
+ "payload_url": "https://dingtalk.example.com",
+ }))
+ t.Run("dingtalk/optional", testWebhookForms("dingtalk", session, map[string]string{
+ "payload_url": "https://dingtalk.example.com",
+
+ "branch_filter": "discord/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("telegram/required", testWebhookForms("telegram", session, map[string]string{
+ "bot_token": "123456",
+ "chat_id": "789",
+ }))
+ t.Run("telegram/optional", testWebhookForms("telegram", session, map[string]string{
+ "bot_token": "123456",
+ "chat_id": "789",
+ "thread_id": "abc",
+
+ "branch_filter": "telegram/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("msteams/required", testWebhookForms("msteams", session, map[string]string{
+ "payload_url": "https://msteams.example.com",
+ }))
+ t.Run("msteams/optional", testWebhookForms("msteams", session, map[string]string{
+ "payload_url": "https://msteams.example.com",
+
+ "branch_filter": "msteams/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("feishu/required", testWebhookForms("feishu", session, map[string]string{
+ "payload_url": "https://feishu.example.com",
+ }))
+ t.Run("feishu/optional", testWebhookForms("feishu", session, map[string]string{
+ "payload_url": "https://feishu.example.com",
+
+ "branch_filter": "feishu/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("matrix/required", testWebhookForms("matrix", session, map[string]string{
+ "homeserver_url": "https://matrix.example.com",
+ "access_token": "123456",
+ "room_id": "123",
+ }, map[string]string{
+ "access_token": "",
+ }))
+ t.Run("matrix/optional", testWebhookForms("matrix", session, map[string]string{
+ "homeserver_url": "https://matrix.example.com",
+ "access_token": "123456",
+ "room_id": "123",
+ "message_type": "1", // m.text
+
+ "branch_filter": "matrix/*",
+ }))
+
+ t.Run("wechatwork/required", testWebhookForms("wechatwork", session, map[string]string{
+ "payload_url": "https://wechatwork.example.com",
+ }))
+ t.Run("wechatwork/optional", testWebhookForms("wechatwork", session, map[string]string{
+ "payload_url": "https://wechatwork.example.com",
+
+ "branch_filter": "wechatwork/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("packagist/required", testWebhookForms("packagist", session, map[string]string{
+ "username": "john",
+ "api_token": "secret",
+ "package_url": "https://packagist.org/packages/example/framework",
+ }))
+ t.Run("packagist/optional", testWebhookForms("packagist", session, map[string]string{
+ "username": "john",
+ "api_token": "secret",
+ "package_url": "https://packagist.org/packages/example/framework",
+
+ "branch_filter": "packagist/*",
+ "authorization_header": "Bearer 123456",
+ }))
+
+ t.Run("sourcehut_builds/required", testWebhookForms("sourcehut_builds", session, map[string]string{
+ "payload_url": "https://sourcehut_builds.example.com",
+ "manifest_path": ".build.yml",
+ "visibility": "PRIVATE",
+ "access_token": "123456",
+ }, map[string]string{
+ "access_token": "",
+ }, map[string]string{
+ "manifest_path": "",
+ }, map[string]string{
+ "manifest_path": "/absolute",
+ }, map[string]string{
+ "visibility": "",
+ }, map[string]string{
+ "visibility": "INVALID",
+ }))
+ t.Run("sourcehut_builds/optional", testWebhookForms("sourcehut_builds", session, map[string]string{
+ "payload_url": "https://sourcehut_builds.example.com",
+ "manifest_path": ".build.yml",
+ "visibility": "PRIVATE",
+ "secrets": "on",
+ "access_token": "123456",
+
+ "branch_filter": "srht/*",
+ }))
+}
+
+func assertInput(t testing.TB, form *goquery.Selection, name string) string {
+ t.Helper()
+ input := form.Find(`input[name="` + name + `"]`)
+ if input.Length() != 1 {
+ form.Find("input").Each(func(i int, s *goquery.Selection) {
+ t.Logf("found <input name=%q />", s.AttrOr("name", ""))
+ })
+ t.Errorf("field <input name=%q /> found %d times, expected once", name, input.Length())
+ }
+ switch input.AttrOr("type", "") {
+ case "checkbox":
+ if _, checked := input.Attr("checked"); checked {
+ return "on"
+ }
+ return ""
+ default:
+ return input.AttrOr("value", "")
+ }
+}
+
+func testWebhookForms(name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) func(t *testing.T) {
+ return func(t *testing.T) {
+ t.Run("repo1", func(t *testing.T) {
+ testWebhookFormsShared(t, "/user2/repo1/settings/hooks", name, session, validFields, invalidPatches...)
+ })
+ t.Run("org3", func(t *testing.T) {
+ testWebhookFormsShared(t, "/org/org3/settings/hooks", name, session, validFields, invalidPatches...)
+ })
+ t.Run("system", func(t *testing.T) {
+ testWebhookFormsShared(t, "/admin/system-hooks", name, session, validFields, invalidPatches...)
+ })
+ t.Run("default", func(t *testing.T) {
+ testWebhookFormsShared(t, "/admin/default-hooks", name, session, validFields, invalidPatches...)
+ })
+ }
+}
+
+func testWebhookFormsShared(t *testing.T, endpoint, name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) {
+ // new webhook form
+ resp := session.MakeRequest(t, NewRequest(t, "GET", endpoint+"/"+name+"/new"), http.StatusOK)
+ htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`)
+
+ // fill the form
+ payload := map[string]string{
+ "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
+ "events": "send_everything",
+ }
+ for k, v := range validFields {
+ assertInput(t, htmlForm, k)
+ payload[k] = v
+ }
+ if t.Failed() {
+ t.FailNow() // prevent further execution if the form could not be filled properly
+ }
+
+ // create the webhook (this redirects back to the hook list)
+ resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", endpoint+"/"+name+"/new", payload), http.StatusSeeOther)
+ assertHasFlashMessages(t, resp, "success")
+ listEndpoint := resp.Header().Get("Location")
+ updateEndpoint := endpoint + "/"
+ if endpoint == "/admin/system-hooks" || endpoint == "/admin/default-hooks" {
+ updateEndpoint = "/admin/hooks/"
+ }
+
+ // find last created hook in the hook list
+ // (a bit hacky, but the list should be sorted)
+ resp = session.MakeRequest(t, NewRequest(t, "GET", listEndpoint), http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ selector := `a[href^="` + updateEndpoint + `"]`
+ if endpoint == "/admin/system-hooks" {
+ // system-hooks and default-hooks are listed on the same page
+ // add a specifier to select the latest system-hooks
+ // (the default-hooks are at the end, so no further specifier needed)
+ selector = `.admin-setting-content > div:first-of-type ` + selector
+ }
+ editFormURL := htmlDoc.Find(selector).Last().AttrOr("href", "")
+ assert.NotEmpty(t, editFormURL)
+
+ // edit webhook form
+ resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK)
+ htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="` + updateEndpoint + `"]`)
+ editPostURL := htmlForm.AttrOr("action", "")
+ assert.NotEmpty(t, editPostURL)
+
+ // fill the form
+ payload = map[string]string{
+ "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
+ "events": "push_only",
+ }
+ for k, v := range validFields {
+ assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v)
+ payload[k] = v
+ }
+
+ // update the webhook
+ resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", editPostURL, payload), http.StatusSeeOther)
+ assertHasFlashMessages(t, resp, "success")
+
+ // check the updated webhook
+ resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK)
+ htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="` + updateEndpoint + `"]`)
+ for k, v := range validFields {
+ assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v)
+ }
+
+ if len(invalidPatches) > 0 {
+ // check that invalid fields are rejected
+ resp := session.MakeRequest(t, NewRequest(t, "GET", endpoint+"/"+name+"/new"), http.StatusOK)
+ htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`)
+
+ for _, invalidPatch := range invalidPatches {
+ t.Run("invalid", func(t *testing.T) {
+ // fill the form
+ payload := map[string]string{
+ "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
+ "events": "send_everything",
+ }
+ for k, v := range validFields {
+ payload[k] = v
+ }
+ for k, v := range invalidPatch {
+ if v == "" {
+ delete(payload, k)
+ } else {
+ payload[k] = v
+ }
+ }
+
+ resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", endpoint+"/"+name+"/new", payload), http.StatusUnprocessableEntity)
+ // check that the invalid form is pre-filled
+ htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`)
+ for k, v := range payload {
+ if k == "_csrf" || k == "events" || v == "" {
+ // the 'events' is a radio input, which is buggy below
+ continue
+ }
+ assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v)
+ }
+ if t.Failed() {
+ t.Log(invalidPatch)
+ }
+ })
+ }
+ }
+}
+
+func assertHasFlashMessages(t *testing.T, resp *httptest.ResponseRecorder, expectedKeys ...string) {
+ seenKeys := make(map[string][]string, len(expectedKeys))
+
+ for _, cookie := range resp.Result().Cookies() {
+ if cookie.Name != gitea_context.CookieNameFlash {
+ continue
+ }
+ flash, _ := url.ParseQuery(cookie.Value)
+ for key, value := range flash {
+ // the key is itself url-encoded
+ if flash, err := url.ParseQuery(key); err == nil {
+ for key, value := range flash {
+ seenKeys[key] = value
+ }
+ } else {
+ seenKeys[key] = value
+ }
+ }
+ }
+
+ for _, k := range expectedKeys {
+ if len(seenKeys[k]) == 0 {
+ t.Errorf("missing expected flash message %q", k)
+ }
+ delete(seenKeys, k)
+ }
+
+ for k, v := range seenKeys {
+ t.Errorf("unexpected flash message %q: %q", k, v)
+ }
+}