From cb0f3611718593eec8239bfbccdc7511325cb611 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Tue, 30 Apr 2024 09:55:54 +0200 Subject: [PATCH 1/2] test: webhook gitea tag creation ref --- services/webhook/default_test.go | 224 +++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 services/webhook/default_test.go diff --git a/services/webhook/default_test.go b/services/webhook/default_test.go new file mode 100644 index 0000000000..29f1521cca --- /dev/null +++ b/services/webhook/default_test.go @@ -0,0 +1,224 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package webhook + +import ( + "context" + "testing" + + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" + webhook_module "code.gitea.io/gitea/modules/webhook" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGiteaPayload(t *testing.T) { + dh := defaultHandler{ + forgejo: false, + } + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.GITEA, + URL: "https://gitea.example.com/", + Meta: ``, + HTTPMethod: "POST", + ContentType: webhook_model.ContentTypeJSON, + } + + // Woodpecker expects the ref to be short on tag creation only + // https://github.com/woodpecker-ci/woodpecker/blob/00ccec078cdced80cf309cd4da460a5041d7991a/server/forge/gitea/helper.go#L134 + // see https://codeberg.org/codeberg/community/issues/1556 + t.Run("Create", func(t *testing.T) { + p := createTestPayload() + data, err := p.JSONPayload() + require.NoError(t, err) + + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventCreate, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := dh.NewRequest(context.Background(), hook, task) + require.NoError(t, err) + require.NotNil(t, req) + require.NotNil(t, reqBody) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://gitea.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body struct { + Ref string `json:"ref"` + } + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "test", body.Ref) // short ref + }) + + t.Run("Push", func(t *testing.T) { + p := pushTestPayload() + data, err := p.JSONPayload() + require.NoError(t, err) + + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := dh.NewRequest(context.Background(), hook, task) + require.NoError(t, err) + require.NotNil(t, req) + require.NotNil(t, reqBody) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://gitea.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body struct { + Ref string `json:"ref"` + } + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "refs/heads/test", body.Ref) // full ref + }) + + t.Run("Delete", func(t *testing.T) { + p := deleteTestPayload() + data, err := p.JSONPayload() + require.NoError(t, err) + + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventDelete, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := dh.NewRequest(context.Background(), hook, task) + require.NoError(t, err) + require.NotNil(t, req) + require.NotNil(t, reqBody) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://gitea.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body struct { + Ref string `json:"ref"` + } + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "test", body.Ref) // short ref + }) +} + +func TestForgejoPayload(t *testing.T) { + dh := defaultHandler{ + forgejo: true, + } + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.FORGEJO, + URL: "https://forgejo.example.com/", + Meta: ``, + HTTPMethod: "POST", + ContentType: webhook_model.ContentTypeJSON, + } + + // always return the full ref for consistency + t.Run("Create", func(t *testing.T) { + p := createTestPayload() + data, err := p.JSONPayload() + require.NoError(t, err) + + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventCreate, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := dh.NewRequest(context.Background(), hook, task) + require.NoError(t, err) + require.NotNil(t, req) + require.NotNil(t, reqBody) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://forgejo.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body struct { + Ref string `json:"ref"` + } + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "refs/heads/test", body.Ref) // full ref + }) + + t.Run("Push", func(t *testing.T) { + p := pushTestPayload() + data, err := p.JSONPayload() + require.NoError(t, err) + + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := dh.NewRequest(context.Background(), hook, task) + require.NoError(t, err) + require.NotNil(t, req) + require.NotNil(t, reqBody) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://forgejo.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body struct { + Ref string `json:"ref"` + } + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "refs/heads/test", body.Ref) // full ref + }) + + t.Run("Delete", func(t *testing.T) { + p := deleteTestPayload() + data, err := p.JSONPayload() + require.NoError(t, err) + + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventDelete, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := dh.NewRequest(context.Background(), hook, task) + require.NoError(t, err) + require.NotNil(t, req) + require.NotNil(t, reqBody) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://forgejo.example.com/", req.URL.String()) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body struct { + Ref string `json:"ref"` + } + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "refs/heads/test", body.Ref) // full ref + }) +} From 0d3a9e64918ae5f91e8b4b501742f01ed71f9586 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Tue, 30 Apr 2024 10:18:02 +0200 Subject: [PATCH 2/2] webhook: send short ref on gitea create/delete payload --- services/webhook/default.go | 42 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/services/webhook/default.go b/services/webhook/default.go index 314f539648..ea877698c8 100644 --- a/services/webhook/default.go +++ b/services/webhook/default.go @@ -12,6 +12,8 @@ import ( "strings" webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/svg" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -67,6 +69,18 @@ func (defaultHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { } func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) { + payloadContent := t.PayloadContent + if w.Type == webhook_module.GITEA && + (t.EventType == webhook_module.HookEventCreate || t.EventType == webhook_module.HookEventDelete) { + // Woodpecker expects the ref to be short on tag creation only + // https://github.com/woodpecker-ci/woodpecker/blob/00ccec078cdced80cf309cd4da460a5041d7991a/server/forge/gitea/helper.go#L134 + // see https://codeberg.org/codeberg/community/issues/1556 + payloadContent, err = substituteRefShortName(payloadContent) + if err != nil { + return nil, nil, fmt.Errorf("could not substiture ref: %w", err) + } + } + switch w.HTTPMethod { case "": log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID) @@ -74,7 +88,7 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, case http.MethodPost: switch w.ContentType { case webhook_model.ContentTypeJSON: - req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent)) + req, err = http.NewRequest("POST", w.URL, strings.NewReader(payloadContent)) if err != nil { return nil, nil, err } @@ -82,7 +96,7 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, req.Header.Set("Content-Type", "application/json") case webhook_model.ContentTypeForm: forms := url.Values{ - "payload": []string{t.PayloadContent}, + "payload": []string{payloadContent}, } req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode())) @@ -100,7 +114,7 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, return nil, nil, fmt.Errorf("invalid URL: %w", err) } vals := u.Query() - vals["payload"] = []string{t.PayloadContent} + vals["payload"] = []string{payloadContent} u.RawQuery = vals.Encode() req, err = http.NewRequest("GET", u.String(), nil) if err != nil { @@ -109,12 +123,12 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, case http.MethodPut: switch w.Type { case webhook_module.MATRIX: // used when t.Version == 1 - txnID, err := getMatrixTxnID([]byte(t.PayloadContent)) + txnID, err := getMatrixTxnID([]byte(payloadContent)) if err != nil { return nil, nil, err } url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID)) - req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent)) + req, err = http.NewRequest("PUT", url, strings.NewReader(payloadContent)) if err != nil { return nil, nil, err } @@ -125,6 +139,22 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) } - body = []byte(t.PayloadContent) + body = []byte(payloadContent) return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body) } + +func substituteRefShortName(body string) (string, error) { + var m map[string]any + if err := json.Unmarshal([]byte(body), &m); err != nil { + return body, err + } + ref, ok := m["ref"].(string) + if !ok { + return body, fmt.Errorf("expected string 'ref', got %T", m["ref"]) + } + + m["ref"] = git.RefName(ref).ShortName() + + buf, err := json.Marshal(m) + return string(buf), err +}