Skip to content

Commit 0905961

Browse files
author
Earl Warren
committed
Merge pull request '[BUG] webhook: fix admin-hooks and add more tests' (#3125) from oliverpool/forgejo:webhook_admin_fix into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3125 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
2 parents fd0c8f5 + 9a94019 commit 0905961

5 files changed

Lines changed: 169 additions & 110 deletions

File tree

models/webhook/webhook_system.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,11 @@ import (
88
"fmt"
99

1010
"code.gitea.io/gitea/models/db"
11-
"code.gitea.io/gitea/modules/optional"
1211
)
1312

1413
// GetDefaultWebhooks returns all admin-default webhooks.
1514
func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
16-
webhooks := make([]*Webhook, 0, 5)
17-
return webhooks, db.GetEngine(ctx).
18-
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, false).
19-
Find(&webhooks)
15+
return getAdminWebhooks(ctx, false)
2016
}
2117

2218
// GetSystemOrDefaultWebhook returns admin system or default webhook by given ID.
@@ -34,15 +30,21 @@ func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error)
3430
}
3531

3632
// GetSystemWebhooks returns all admin system webhooks.
37-
func GetSystemWebhooks(ctx context.Context, isActive optional.Option[bool]) ([]*Webhook, error) {
33+
func GetSystemWebhooks(ctx context.Context, onlyActive bool) ([]*Webhook, error) {
34+
return getAdminWebhooks(ctx, true, onlyActive)
35+
}
36+
37+
func getAdminWebhooks(ctx context.Context, systemWebhooks bool, onlyActive ...bool) ([]*Webhook, error) {
3838
webhooks := make([]*Webhook, 0, 5)
39-
if !isActive.Has() {
39+
if len(onlyActive) > 0 && onlyActive[0] {
4040
return webhooks, db.GetEngine(ctx).
41-
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true).
41+
Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, systemWebhooks, true).
42+
OrderBy("id").
4243
Find(&webhooks)
4344
}
4445
return webhooks, db.GetEngine(ctx).
45-
Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.Value()).
46+
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, systemWebhooks).
47+
OrderBy("id").
4648
Find(&webhooks)
4749
}
4850

routers/api/v1/admin/hooks.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"net/http"
99

1010
"code.gitea.io/gitea/models/webhook"
11-
"code.gitea.io/gitea/modules/optional"
1211
"code.gitea.io/gitea/modules/setting"
1312
api "code.gitea.io/gitea/modules/structs"
1413
"code.gitea.io/gitea/modules/util"
@@ -38,7 +37,7 @@ func ListHooks(ctx *context.APIContext) {
3837
// "200":
3938
// "$ref": "#/responses/HookList"
4039

41-
sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]())
40+
sysHooks, err := webhook.GetSystemWebhooks(ctx, false)
4241
if err != nil {
4342
ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
4443
return

routers/web/admin/hooks.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import (
88

99
"code.gitea.io/gitea/models/webhook"
1010
"code.gitea.io/gitea/modules/base"
11-
"code.gitea.io/gitea/modules/optional"
1211
"code.gitea.io/gitea/modules/setting"
1312
"code.gitea.io/gitea/services/context"
13+
webhook_service "code.gitea.io/gitea/services/webhook"
1414
)
1515

1616
const (
@@ -35,9 +35,10 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
3535

3636
sys["Title"] = ctx.Tr("admin.systemhooks")
3737
sys["Description"] = ctx.Tr("admin.systemhooks.desc")
38-
sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, optional.None[bool]())
38+
sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, false)
3939
sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
4040
sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
41+
sys["WebhookList"] = webhook_service.List()
4142
if err != nil {
4243
ctx.ServerError("GetWebhooksAdmin", err)
4344
return
@@ -48,6 +49,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
4849
def["Webhooks"], err = webhook.GetDefaultWebhooks(ctx)
4950
def["BaseLink"] = setting.AppSubURL + "/admin/hooks"
5051
def["BaseLinkNew"] = setting.AppSubURL + "/admin/default-hooks"
52+
def["WebhookList"] = webhook_service.List()
5153
if err != nil {
5254
ctx.ServerError("GetWebhooksAdmin", err)
5355
return

services/webhook/webhook.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
243243
}
244244

245245
// Add any admin-defined system webhooks
246-
systemHooks, err := webhook_model.GetSystemWebhooks(ctx, optional.Some(true))
246+
systemHooks, err := webhook_model.GetSystemWebhooks(ctx, true)
247247
if err != nil {
248248
return fmt.Errorf("GetSystemWebhooks: %w", err)
249249
}

tests/integration/repo_webhook_test.go

Lines changed: 152 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"net/http"
88
"net/http/httptest"
99
"net/url"
10-
"strings"
1110
"testing"
1211

1312
gitea_context "code.gitea.io/gitea/services/context"
@@ -37,30 +36,58 @@ func TestNewWebHookLink(t *testing.T) {
3736
for _, url := range tests {
3837
resp := session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK)
3938
htmlDoc := NewHTMLParser(t, resp.Body)
40-
menus := htmlDoc.doc.Find(".ui.top.attached.header .ui.dropdown .menu a")
41-
menus.Each(func(i int, menu *goquery.Selection) {
42-
url, exist := menu.Attr("href")
43-
assert.True(t, exist)
44-
assert.True(t, strings.HasPrefix(url, baseurl))
45-
})
46-
assert.Equal(t, webhooksLen, htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), "not all webhooks are listed in the 'new' dropdown")
39+
assert.Equal(t,
40+
webhooksLen,
41+
htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(),
42+
"not all webhooks are listed in the 'new' dropdown")
43+
4744
csrfToken = htmlDoc.GetCSRF()
4845
}
4946

5047
// ensure that the "failure" pages has the full dropdown as well
5148
resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", baseurl+"/gitea/new", map[string]string{"_csrf": csrfToken}), http.StatusUnprocessableEntity)
5249
htmlDoc := NewHTMLParser(t, resp.Body)
53-
assert.Equal(t, webhooksLen, htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), "not all webhooks are listed in the 'new' dropdown on failure")
50+
assert.Equal(t,
51+
webhooksLen,
52+
htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(),
53+
"not all webhooks are listed in the 'new' dropdown on failure")
5454

5555
resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", baseurl+"/1", map[string]string{"_csrf": csrfToken}), http.StatusUnprocessableEntity)
5656
htmlDoc = NewHTMLParser(t, resp.Body)
57-
assert.Equal(t, webhooksLen, htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), "not all webhooks are listed in the 'new' dropdown on failure")
57+
assert.Equal(t,
58+
webhooksLen,
59+
htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(),
60+
"not all webhooks are listed in the 'new' dropdown on failure")
61+
62+
adminSession := loginUser(t, "user1")
63+
t.Run("org3", func(t *testing.T) {
64+
baseurl := "/org/org3/settings/hooks"
65+
resp := adminSession.MakeRequest(t, NewRequest(t, "GET", baseurl), http.StatusOK)
66+
htmlDoc := NewHTMLParser(t, resp.Body)
67+
assert.Equal(t,
68+
webhooksLen,
69+
htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(),
70+
"not all webhooks are listed in the 'new' dropdown")
71+
})
72+
t.Run("admin", func(t *testing.T) {
73+
baseurl := "/admin/hooks"
74+
resp := adminSession.MakeRequest(t, NewRequest(t, "GET", baseurl), http.StatusOK)
75+
htmlDoc := NewHTMLParser(t, resp.Body)
76+
assert.Equal(t,
77+
webhooksLen,
78+
htmlDoc.Find(`a[href^="/admin/default-hooks/"][href$="/new"]`).Length(),
79+
"not all webhooks are listed in the 'new' dropdown for default-hooks")
80+
assert.Equal(t,
81+
webhooksLen,
82+
htmlDoc.Find(`a[href^="/admin/system-hooks/"][href$="/new"]`).Length(),
83+
"not all webhooks are listed in the 'new' dropdown for system-hooks")
84+
})
5885
}
5986

6087
func TestWebhookForms(t *testing.T) {
6188
defer tests.PrepareTestEnv(t)()
6289

63-
session := loginUser(t, "user2")
90+
session := loginUser(t, "user1")
6491

6592
t.Run("forgejo/required", testWebhookForms("forgejo", session, map[string]string{
6693
"payload_url": "https://forgejo.example.com",
@@ -272,7 +299,9 @@ func assertInput(t testing.TB, form *goquery.Selection, name string) string {
272299
t.Helper()
273300
input := form.Find(`input[name="` + name + `"]`)
274301
if input.Length() != 1 {
275-
t.Log(form.Html())
302+
form.Find("input").Each(func(i int, s *goquery.Selection) {
303+
t.Logf("found <input name=%q />", s.AttrOr("name", ""))
304+
})
276305
t.Errorf("field <input name=%q /> found %d times, expected once", name, input.Length())
277306
}
278307
switch input.AttrOr("type", "") {
@@ -288,99 +317,126 @@ func assertInput(t testing.TB, form *goquery.Selection, name string) string {
288317

289318
func testWebhookForms(name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) func(t *testing.T) {
290319
return func(t *testing.T) {
291-
// new webhook form
292-
resp := session.MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/settings/hooks/"+name+"/new"), http.StatusOK)
293-
htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`)
294-
295-
// fill the form
296-
payload := map[string]string{
297-
"_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
298-
"events": "send_everything",
299-
}
300-
for k, v := range validFields {
301-
assertInput(t, htmlForm, k)
302-
payload[k] = v
303-
}
304-
if t.Failed() {
305-
t.FailNow() // prevent further execution if the form could not be filled properly
306-
}
320+
t.Run("repo1", func(t *testing.T) {
321+
testWebhookFormsShared(t, "/user2/repo1/settings/hooks", name, session, validFields, invalidPatches...)
322+
})
323+
t.Run("org3", func(t *testing.T) {
324+
testWebhookFormsShared(t, "/org/org3/settings/hooks", name, session, validFields, invalidPatches...)
325+
})
326+
t.Run("system", func(t *testing.T) {
327+
testWebhookFormsShared(t, "/admin/system-hooks", name, session, validFields, invalidPatches...)
328+
})
329+
t.Run("default", func(t *testing.T) {
330+
testWebhookFormsShared(t, "/admin/default-hooks", name, session, validFields, invalidPatches...)
331+
})
332+
}
333+
}
307334

308-
// create the webhook (this redirects back to the hook list)
309-
resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user2/repo1/settings/hooks/"+name+"/new", payload), http.StatusSeeOther)
310-
assertHasFlashMessages(t, resp, "success")
335+
func testWebhookFormsShared(t *testing.T, endpoint, name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) {
336+
// new webhook form
337+
resp := session.MakeRequest(t, NewRequest(t, "GET", endpoint+"/"+name+"/new"), http.StatusOK)
338+
htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`)
311339

312-
// find last created hook in the hook list
313-
// (a bit hacky, but the list should be sorted)
314-
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/settings/hooks"), http.StatusOK)
315-
htmlDoc := NewHTMLParser(t, resp.Body)
316-
editFormURL := htmlDoc.Find(`a[href^="/user2/repo1/settings/hooks/"]`).Last().AttrOr("href", "")
317-
assert.NotEmpty(t, editFormURL)
318-
319-
// edit webhook form
320-
resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK)
321-
htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`)
322-
editPostURL := htmlForm.AttrOr("action", "")
323-
assert.NotEmpty(t, editPostURL)
324-
325-
// fill the form
326-
payload = map[string]string{
327-
"_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
328-
"events": "push_only",
329-
}
330-
for k, v := range validFields {
331-
assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v)
332-
payload[k] = v
333-
}
340+
// fill the form
341+
payload := map[string]string{
342+
"_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
343+
"events": "send_everything",
344+
}
345+
for k, v := range validFields {
346+
assertInput(t, htmlForm, k)
347+
payload[k] = v
348+
}
349+
if t.Failed() {
350+
t.FailNow() // prevent further execution if the form could not be filled properly
351+
}
334352

335-
// update the webhook
336-
resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", editPostURL, payload), http.StatusSeeOther)
337-
assertHasFlashMessages(t, resp, "success")
353+
// create the webhook (this redirects back to the hook list)
354+
resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", endpoint+"/"+name+"/new", payload), http.StatusSeeOther)
355+
assertHasFlashMessages(t, resp, "success")
356+
listEndpoint := resp.Header().Get("Location")
357+
updateEndpoint := endpoint + "/"
358+
if endpoint == "/admin/system-hooks" || endpoint == "/admin/default-hooks" {
359+
updateEndpoint = "/admin/hooks/"
360+
}
338361

339-
// check the updated webhook
340-
resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK)
341-
htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`)
342-
for k, v := range validFields {
343-
assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v)
344-
}
362+
// find last created hook in the hook list
363+
// (a bit hacky, but the list should be sorted)
364+
resp = session.MakeRequest(t, NewRequest(t, "GET", listEndpoint), http.StatusOK)
365+
htmlDoc := NewHTMLParser(t, resp.Body)
366+
selector := `a[href^="` + updateEndpoint + `"]`
367+
if endpoint == "/admin/system-hooks" {
368+
// system-hooks and default-hooks are listed on the same page
369+
// add a specifier to select the latest system-hooks
370+
// (the default-hooks are at the end, so no further specifier needed)
371+
selector = `.admin-setting-content > div:first-of-type ` + selector
372+
}
373+
editFormURL := htmlDoc.Find(selector).Last().AttrOr("href", "")
374+
assert.NotEmpty(t, editFormURL)
375+
376+
// edit webhook form
377+
resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK)
378+
htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="` + updateEndpoint + `"]`)
379+
editPostURL := htmlForm.AttrOr("action", "")
380+
assert.NotEmpty(t, editPostURL)
381+
382+
// fill the form
383+
payload = map[string]string{
384+
"_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
385+
"events": "push_only",
386+
}
387+
for k, v := range validFields {
388+
assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v)
389+
payload[k] = v
390+
}
345391

346-
if len(invalidPatches) > 0 {
347-
// check that invalid fields are rejected
348-
resp := session.MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/settings/hooks/"+name+"/new"), http.StatusOK)
349-
htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`)
350-
351-
for _, invalidPatch := range invalidPatches {
352-
t.Run("invalid", func(t *testing.T) {
353-
// fill the form
354-
payload := map[string]string{
355-
"_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
356-
"events": "send_everything",
357-
}
358-
for k, v := range validFields {
392+
// update the webhook
393+
resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", editPostURL, payload), http.StatusSeeOther)
394+
assertHasFlashMessages(t, resp, "success")
395+
396+
// check the updated webhook
397+
resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK)
398+
htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="` + updateEndpoint + `"]`)
399+
for k, v := range validFields {
400+
assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v)
401+
}
402+
403+
if len(invalidPatches) > 0 {
404+
// check that invalid fields are rejected
405+
resp := session.MakeRequest(t, NewRequest(t, "GET", endpoint+"/"+name+"/new"), http.StatusOK)
406+
htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`)
407+
408+
for _, invalidPatch := range invalidPatches {
409+
t.Run("invalid", func(t *testing.T) {
410+
// fill the form
411+
payload := map[string]string{
412+
"_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""),
413+
"events": "send_everything",
414+
}
415+
for k, v := range validFields {
416+
payload[k] = v
417+
}
418+
for k, v := range invalidPatch {
419+
if v == "" {
420+
delete(payload, k)
421+
} else {
359422
payload[k] = v
360423
}
361-
for k, v := range invalidPatch {
362-
if v == "" {
363-
delete(payload, k)
364-
} else {
365-
payload[k] = v
366-
}
367-
}
424+
}
368425

369-
resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user2/repo1/settings/hooks/"+name+"/new", payload), http.StatusUnprocessableEntity)
370-
// check that the invalid form is pre-filled
371-
htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`)
372-
for k, v := range payload {
373-
if k == "_csrf" || k == "events" || v == "" {
374-
// the 'events' is a radio input, which is buggy below
375-
continue
376-
}
377-
assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v)
426+
resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", endpoint+"/"+name+"/new", payload), http.StatusUnprocessableEntity)
427+
// check that the invalid form is pre-filled
428+
htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`)
429+
for k, v := range payload {
430+
if k == "_csrf" || k == "events" || v == "" {
431+
// the 'events' is a radio input, which is buggy below
432+
continue
378433
}
379-
if t.Failed() {
380-
t.Log(invalidPatch)
381-
}
382-
})
383-
}
434+
assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v)
435+
}
436+
if t.Failed() {
437+
t.Log(invalidPatch)
438+
}
439+
})
384440
}
385441
}
386442
}

0 commit comments

Comments
 (0)