diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /tests/integration/repo_webhook_test.go | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r-- | tests/integration/repo_webhook_test.go | 473 |
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) + } +} |