diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml index 6dbb10151..d573406b3 100644 --- a/models/fixtures/hook_task.yml +++ b/models/fixtures/hook_task.yml @@ -3,3 +3,35 @@ hook_id: 1 uuid: uuid1 is_delivered: true + is_succeed: false + request_content: > + { + "url": "/matrix-delivered", + "http_method":"PUT", + "headers": { + "X-Head": "42" + }, + "body": "{}" + } + +- + id: 2 + hook_id: 1 + uuid: uuid2 + is_delivered: false + +- + id: 3 + hook_id: 1 + uuid: uuid3 + is_delivered: true + is_succeed: true + payload_content: '{"key":"value"}' # legacy task, payload saved in payload_content (and not in request_content) + request_content: > + { + "url": "/matrix-success", + "http_method":"PUT", + "headers": { + "X-Head": "42" + } + } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index d40866f3e..ce77432db 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -564,6 +564,8 @@ var migrations = []Migration{ NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable), // v289 -> v290 NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch), + // v290 -> v291 + NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_22/v290.go b/models/migrations/v1_22/v290.go new file mode 100644 index 000000000..e9b7f504b --- /dev/null +++ b/models/migrations/v1_22/v290.go @@ -0,0 +1,17 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +type HookTask struct { + PayloadVersion int `xorm:"DEFAULT 1"` +} + +func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error { + // create missing column + return x.Sync(new(HookTask)) +} diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go index 2fb655ebc..ff3fdbadb 100644 --- a/models/webhook/hooktask.go +++ b/models/webhook/hooktask.go @@ -5,13 +5,13 @@ package webhook import ( "context" + "errors" "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -31,6 +31,7 @@ type HookRequest struct { URL string `json:"url"` HTTPMethod string `json:"http_method"` Headers map[string]string `json:"headers"` + Body string `json:"body"` } // HookResponse represents hook task response information. @@ -45,11 +46,15 @@ type HookTask struct { ID int64 `xorm:"pk autoincr"` HookID int64 `xorm:"index"` UUID string `xorm:"unique"` - api.Payloader `xorm:"-"` PayloadContent string `xorm:"LONGTEXT"` - EventType webhook_module.HookEventType - IsDelivered bool - Delivered timeutil.TimeStampNano + // PayloadVersion number to allow for smooth version upgrades: + // - PayloadVersion 1: PayloadContent contains the JSON as sent to the URL + // - PayloadVersion 2: PayloadContent contains the original event + PayloadVersion int `xorm:"DEFAULT 1"` + + EventType webhook_module.HookEventType + IsDelivered bool + Delivered timeutil.TimeStampNano // History info. IsSucceed bool @@ -115,16 +120,12 @@ func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error) // it handles conversion from Payload to PayloadContent. func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) { t.UUID = gouuid.New().String() - if t.Payloader != nil { - data, err := t.Payloader.JSONPayload() - if err != nil { - return nil, err - } - t.PayloadContent = string(data) - } if t.Delivered == 0 { t.Delivered = timeutil.TimeStampNanoNow() } + if t.PayloadVersion == 0 { + return nil, errors.New("missing HookTask.PayloadVersion") + } return t, db.Insert(ctx, t) } @@ -165,6 +166,7 @@ func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, HookID: task.HookID, PayloadContent: task.PayloadContent, EventType: task.EventType, + PayloadVersion: task.PayloadVersion, }) } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index c70c8e99f..f4403776c 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/optional" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -35,8 +34,10 @@ func TestWebhook_History(t *testing.T) { webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1}) tasks, err := webhook.History(db.DefaultContext, 0) assert.NoError(t, err) - if assert.Len(t, tasks, 1) { - assert.Equal(t, int64(1), tasks[0].ID) + if assert.Len(t, tasks, 3) { + assert.Equal(t, int64(3), tasks[0].ID) + assert.Equal(t, int64(2), tasks[1].ID) + assert.Equal(t, int64(1), tasks[2].ID) } webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) @@ -197,8 +198,10 @@ func TestHookTasks(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTasks, err := HookTasks(db.DefaultContext, 1, 1) assert.NoError(t, err) - if assert.Len(t, hookTasks, 1) { - assert.Equal(t, int64(1), hookTasks[0].ID) + if assert.Len(t, hookTasks, 3) { + assert.Equal(t, int64(3), hookTasks[0].ID) + assert.Equal(t, int64(2), hookTasks[1].ID) + assert.Equal(t, int64(1), hookTasks[2].ID) } hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1) @@ -209,8 +212,8 @@ func TestHookTasks(t *testing.T) { func TestCreateHookTask(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 3, - Payloader: &api.PushPayload{}, + HookID: 3, + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -232,10 +235,10 @@ func TestUpdateHookTask(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 3, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNanoNow(), + HookID: 3, + IsDelivered: true, + Delivered: timeutil.TimeStampNanoNow(), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -249,9 +252,9 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: false, + HookID: 4, + IsDelivered: false, + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -265,10 +268,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNanoNow(), + HookID: 4, + IsDelivered: true, + Delivered: timeutil.TimeStampNanoNow(), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -282,10 +285,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 3, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()), + HookID: 3, + IsDelivered: true, + Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -299,9 +302,9 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: false, + HookID: 4, + IsDelivered: false, + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -315,10 +318,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()), + HookID: 4, + IsDelivered: true, + Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 935981d29..b2c0a7378 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -32,36 +32,17 @@ import ( "github.com/gobwas/glob" ) -// Deliver deliver hook task -func Deliver(ctx context.Context, t *webhook_model.HookTask) error { - w, err := webhook_model.GetWebhookByID(ctx, t.HookID) - if err != nil { - return err - } - - defer func() { - err := recover() - if err == nil { - return - } - // There was a panic whilst delivering a hook... - log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2)) - }() - - t.IsDelivered = true - - var req *http.Request - +func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) { switch w.HTTPMethod { case "": - log.Info("HTTP Method for webhook %s empty, setting to POST as default", w.URL) + log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID) fallthrough case http.MethodPost: switch w.ContentType { case webhook_model.ContentTypeJSON: req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent)) if err != nil { - return err + return nil, nil, err } req.Header.Set("Content-Type", "application/json") @@ -72,50 +53,58 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode())) if err != nil { - return err + return nil, nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + default: + return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType) } case http.MethodGet: u, err := url.Parse(w.URL) if err != nil { - return fmt.Errorf("unable to deliver webhook task[%d] as cannot parse webhook url %s: %w", t.ID, w.URL, err) + return nil, nil, fmt.Errorf("invalid URL: %w", err) } vals := u.Query() vals["payload"] = []string{t.PayloadContent} u.RawQuery = vals.Encode() req, err = http.NewRequest("GET", u.String(), nil) if err != nil { - return fmt.Errorf("unable to deliver webhook task[%d] as unable to create HTTP request for webhook url %s: %w", t.ID, w.URL, err) + return nil, nil, err } case http.MethodPut: switch w.Type { - case webhook_module.MATRIX: + case webhook_module.MATRIX: // used when t.Version == 1 txnID, err := getMatrixTxnID([]byte(t.PayloadContent)) if err != nil { - return err + return nil, nil, err } url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID)) req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent)) if err != nil { - return fmt.Errorf("unable to deliver webhook task[%d] as cannot create matrix request for webhook url %s: %w", t.ID, w.URL, err) + return nil, nil, err } default: - return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod) + return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) } default: - return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod) + return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) } + body = []byte(t.PayloadContent) + return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) +} + +func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { var signatureSHA1 string var signatureSHA256 string - if len(w.Secret) > 0 { - sig1 := hmac.New(sha1.New, []byte(w.Secret)) - sig256 := hmac.New(sha256.New, []byte(w.Secret)) - _, err = io.MultiWriter(sig1, sig256).Write([]byte(t.PayloadContent)) + if len(secret) > 0 { + sig1 := hmac.New(sha1.New, secret) + sig256 := hmac.New(sha256.New, secret) + _, err := io.MultiWriter(sig1, sig256).Write(payloadContent) if err != nil { - log.Error("prepareWebhooks.sigWrite: %v", err) + // this error should never happen, since the hashes are writing to []byte and always return a nil error. + return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) } signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) @@ -136,15 +125,36 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { req.Header["X-GitHub-Delivery"] = []string{t.UUID} req.Header["X-GitHub-Event"] = []string{event} req.Header["X-GitHub-Event-Type"] = []string{eventType} + return nil +} - // Add Authorization Header - authorization, err := w.HeaderAuthorization() +// Deliver creates the [http.Request] (depending on the webhook type), sends it +// and records the status and response. +func Deliver(ctx context.Context, t *webhook_model.HookTask) error { + w, err := webhook_model.GetWebhookByID(ctx, t.HookID) if err != nil { - log.Error("Webhook could not get Authorization header [%d]: %v", w.ID, err) return err } - if authorization != "" { - req.Header["Authorization"] = []string{authorization} + + defer func() { + err := recover() + if err == nil { + return + } + // There was a panic whilst delivering a hook... + log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2)) + }() + + t.IsDelivered = true + + newRequest := webhookRequesters[w.Type] + if t.PayloadVersion == 1 || newRequest == nil { + newRequest = newDefaultRequest + } + + req, body, err := newRequest(ctx, w, t) + if err != nil { + return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err) } // Record delivery information. @@ -152,11 +162,22 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { URL: req.URL.String(), HTTPMethod: req.Method, Headers: map[string]string{}, + Body: string(body), } for k, vals := range req.Header { t.RequestInfo.Headers[k] = strings.Join(vals, ",") } + // Add Authorization Header + authorization, err := w.HeaderAuthorization() + if err != nil { + return fmt.Errorf("cannot get Authorization header for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err) + } + if authorization != "" { + req.Header.Set("Authorization", authorization) + t.RequestInfo.Headers["Authorization"] = "******" + } + t.ResponseInfo = &webhook_model.HookResponse{ Headers: map[string]string{}, } diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go index 72aa00478..24924ab21 100644 --- a/services/webhook/deliver_test.go +++ b/services/webhook/deliver_test.go @@ -5,9 +5,11 @@ package webhook import ( "context" + "io" "net/http" "net/http/httptest" "net/url" + "strings" "testing" "time" @@ -16,7 +18,6 @@ import ( webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/hostmatcher" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/stretchr/testify/assert" @@ -107,13 +108,15 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) { assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook)) db.GetEngine(db.DefaultContext).NoAutoTime().DB().Logger.ShowSQL(true) - hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush, Payloader: &api.PushPayload{}} + hookTask := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadVersion: 2, + } hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask) assert.NoError(t, err) - if !assert.NotNil(t, hookTask) { - return - } + assert.NotNil(t, hookTask) assert.NoError(t, Deliver(context.Background(), hookTask)) select { @@ -123,4 +126,193 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) { } 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) + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + switch r.URL.Path { + case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98": + // Version 1 + assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) + assert.Equal(t, "", r.Header.Get("Content-Type")) + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, `{"data": 42}`, string(body)) + + case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51": + // 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.Len(t, body, 2147) + + default: + w.WriteHeader(404) + t.Fatalf("unexpected url path %s", r.URL.Path) + return + } + w.WriteHeader(200) + 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(db.DefaultContext, hook)) + + t.Run("Version 1", func(t *testing.T) { + hookTask := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: `{"data": 42}`, + PayloadVersion: 1, + } + + hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) + assert.NotNil(t, hookTask) + + assert.NoError(t, Deliver(context.Background(), 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) + + hookTask := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) + assert.NotNil(t, hookTask) + + assert.NoError(t, Deliver(context.Background(), 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 + } + + cases := map[string]hookCase{ + webhook_module.SLACK: { + gotBody: make(chan []byte, 1), + }, + webhook_module.DISCORD: { + gotBody: make(chan []byte, 1), + }, + webhook_module.DINGTALK: { + gotBody: make(chan []byte, 1), + }, + webhook_module.TELEGRAM: { + gotBody: make(chan []byte, 1), + }, + webhook_module.MSTEAMS: { + gotBody: make(chan []byte, 1), + }, + webhook_module.FEISHU: { + gotBody: make(chan []byte, 1), + }, + webhook_module.MATRIX: { + gotBody: make(chan []byte, 1), + }, + webhook_module.WECHATWORK: { + gotBody: make(chan []byte, 1), + }, + webhook_module.PACKAGIST: { + gotBody: make(chan []byte, 1), + }, + } + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path) + + typ := strings.Split(r.URL.Path, "/")[1] // take first segment (after skipping leading slash) + hc := cases[typ] + require.NotNil(t, hc.gotBody, r.URL.Path) + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + w.WriteHeader(200) + hc.gotBody <- body + })) + t.Cleanup(s.Close) + + p := pushTestPayload() + data, err := p.JSONPayload() + assert.NoError(t, err) + + for typ, hc := range cases { + typ := typ + hc := hc + t.Run(typ, func(t *testing.T) { + t.Parallel() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: typ, + URL: s.URL + "/" + typ, + HTTPMethod: "POST", + ContentType: 0, // set to 0 so that falling back to default request fails with "invalid content type" + Meta: "{}", + } + assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook)) + + hookTask := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) + assert.NotNil(t, hookTask) + + assert.NoError(t, Deliver(context.Background(), hookTask)) + select { + case gotBody := <-hc.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), "request body was not saved") + case <-time.After(5 * time.Second): + t.Fatal("waited to long for request to happen") + } + + assert.True(t, hookTask.IsSucceed) + }) + } } diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index d615e7254..c57d04415 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -4,12 +4,14 @@ package webhook import ( + "context" "fmt" + "net/http" "net/url" "strings" + webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -22,19 +24,8 @@ type ( DingtalkPayload dingtalk.Payload ) -var _ PayloadConvertor = &DingtalkPayload{} - -// JSONPayload Marshals the DingtalkPayload to json -func (d *DingtalkPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(d, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - // Create implements PayloadConvertor Create method -func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Create(p *api.CreatePayload) (DingtalkPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -43,7 +34,7 @@ func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Delete(p *api.DeletePayload) (DingtalkPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -52,14 +43,14 @@ func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (d *DingtalkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Fork(p *api.ForkPayload) (DingtalkPayload, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return createDingtalkPayload(title, title, fmt.Sprintf("view forked repo %s", p.Repo.FullName), p.Repo.HTMLURL), nil } // Push implements PayloadConvertor Push method -func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -100,14 +91,14 @@ func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (d *DingtalkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Issue(p *api.IssuePayload) (DingtalkPayload, error) { text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view issue", p.Issue.HTMLURL), nil } // Wiki implements PayloadConvertor Wiki method -func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Wiki(p *api.WikiPayload) (DingtalkPayload, error) { text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true) url := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page) @@ -115,27 +106,27 @@ func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { } // IssueComment implements PayloadConvertor IssueComment method -func (d *DingtalkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) IssueComment(p *api.IssueCommentPayload) (DingtalkPayload, error) { text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+p.Comment.Body, "view issue comment", p.Comment.HTMLURL), nil } // PullRequest implements PayloadConvertor PullRequest method -func (d *DingtalkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) PullRequest(p *api.PullRequestPayload) (DingtalkPayload, error) { text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view pull request", p.PullRequest.HTMLURL), nil } // Review implements PayloadConvertor Review method -func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (dc dingtalkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DingtalkPayload, error) { var text, title string switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return DingtalkPayload{}, err } title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -146,14 +137,14 @@ func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module } // Repository implements PayloadConvertor Repository method -func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Repository(p *api.RepositoryPayload) (DingtalkPayload, error) { switch p.Action { case api.HookRepoCreated: title := fmt.Sprintf("[%s] Repository created", p.Repository.FullName) return createDingtalkPayload(title, title, "view repository", p.Repository.HTMLURL), nil case api.HookRepoDeleted: title := fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) - return &DingtalkPayload{ + return DingtalkPayload{ MsgType: "text", Text: struct { Content string `json:"content"` @@ -163,24 +154,24 @@ func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e }, nil } - return nil, nil + return DingtalkPayload{}, nil } // Release implements PayloadConvertor Release method -func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Release(p *api.ReleasePayload) (DingtalkPayload, error) { text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil } -func (d *DingtalkPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, error) { text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true) return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil } -func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload { - return &DingtalkPayload{ +func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload { + return DingtalkPayload{ MsgType: "actionCard", ActionCard: dingtalk.ActionCard{ Text: strings.TrimSpace(text), @@ -195,7 +186,10 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) *Dingtalk } } -// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload -func GetDingtalkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(DingtalkPayload), p, event) +type dingtalkConvertor struct{} + +var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{} + +func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(dingtalkConvertor{}, w, t, true) } diff --git a/services/webhook/dingtalk_test.go b/services/webhook/dingtalk_test.go index a03fa46f1..25f47347d 100644 --- a/services/webhook/dingtalk_test.go +++ b/services/webhook/dingtalk_test.go @@ -4,9 +4,12 @@ package webhook import ( + "context" "net/url" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -24,248 +27,226 @@ func TestDingTalkPayload(t *testing.T) { } return "" } + dc := dingtalkConvertor{} t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(DingtalkPayload) - pl, err := d.Create(p) + pl, err := dc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Title) + assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(DingtalkPayload) - pl, err := d.Delete(p) + pl, err := dc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Title) + assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(DingtalkPayload) - pl, err := d.Fork(p) + pl, err := dc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view forked repo test/repo", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Text) + assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Title) + assert.Equal(t, "view forked repo test/repo", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(DingtalkPayload) - pl, err := d.Push(p) + pl, err := dc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view commits", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo:test] 2 new commits", pl.ActionCard.Title) + assert.Equal(t, "view commits", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(DingtalkPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := dc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.ActionCard.Text) + assert.Equal(t, "#2 crash", pl.ActionCard.Title) + assert.Equal(t, "view issue", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL)) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = dc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.ActionCard.Text) + assert.Equal(t, "#2 crash", pl.ActionCard.Title) + assert.Equal(t, "view issue", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(DingtalkPayload) - pl, err := d.IssueComment(p) + pl, err := dc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.ActionCard.Text) + assert.Equal(t, "#2 crash", pl.ActionCard.Title) + assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(DingtalkPayload) - pl, err := d.PullRequest(p) + pl, err := dc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.ActionCard.Text) + assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title) + assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(DingtalkPayload) - pl, err := d.IssueComment(p) + pl, err := dc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.ActionCard.Text) + assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title) + assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(DingtalkPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.ActionCard.Title) + assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(DingtalkPayload) - pl, err := d.Repository(p) + pl, err := dc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view repository", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Title) + assert.Equal(t, "view repository", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(DingtalkPayload) - pl, err := d.Package(p) + pl, err := dc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view package", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Text) + assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Title) + assert.Equal(t, "view package", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(DingtalkPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Title) + assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL)) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Title) + assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL)) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Title) + assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL)) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(DingtalkPayload) - pl, err := d.Release(p) + pl, err := dc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Text) - assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view release", pl.(*DingtalkPayload).ActionCard.SingleTitle) - assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) + assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Text) + assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Title) + assert.Equal(t, "view release", pl.ActionCard.SingleTitle) + assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.ActionCard.SingleURL)) }) } func TestDingTalkJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(DingtalkPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DingtalkPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.DINGTALK, + URL: "https://dingtalk.example.com/", + Meta: ``, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newDingtalkRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://dingtalk.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 DingtalkPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.ActionCard.Text) } diff --git a/services/webhook/discord.go b/services/webhook/discord.go index e2ac1410b..659754d5e 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -4,8 +4,10 @@ package webhook import ( + "context" "errors" "fmt" + "net/http" "net/url" "strconv" "strings" @@ -98,19 +100,8 @@ var ( redColor = color("ff3232") ) -// JSONPayload Marshals the DiscordPayload to json -func (d *DiscordPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(d, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &DiscordPayload{} - // Create implements PayloadConvertor Create method -func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (d discordConvertor) Create(p *api.CreatePayload) (DiscordPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -119,7 +110,7 @@ func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (d discordConvertor) Delete(p *api.DeletePayload) (DiscordPayload, error) { // deleted tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -128,14 +119,14 @@ func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (d *DiscordPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (d discordConvertor) Fork(p *api.ForkPayload) (DiscordPayload, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL, greenColor), nil } // Push implements PayloadConvertor Push method -func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -170,35 +161,35 @@ func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (d *DiscordPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (d discordConvertor) Issue(p *api.IssuePayload) (DiscordPayload, error) { title, _, text, color := getIssuesPayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, title, text, p.Issue.HTMLURL, color), nil } // IssueComment implements PayloadConvertor IssueComment method -func (d *DiscordPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (d discordConvertor) IssueComment(p *api.IssueCommentPayload) (DiscordPayload, error) { title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, title, p.Comment.Body, p.Comment.HTMLURL, color), nil } // PullRequest implements PayloadConvertor PullRequest method -func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (d discordConvertor) PullRequest(p *api.PullRequestPayload) (DiscordPayload, error) { title, _, text, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil } // Review implements PayloadConvertor Review method -func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (d discordConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DiscordPayload, error) { var text, title string var color int switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return DiscordPayload{}, err } title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -220,7 +211,7 @@ func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module. } // Repository implements PayloadConvertor Repository method -func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload, error) { var title, url string var color int switch p.Action { @@ -237,7 +228,7 @@ func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er } // Wiki implements PayloadConvertor Wiki method -func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (d discordConvertor) Wiki(p *api.WikiPayload) (DiscordPayload, error) { text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false) htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page) @@ -250,30 +241,35 @@ func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { } // Release implements PayloadConvertor Release method -func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (d discordConvertor) Release(p *api.ReleasePayload) (DiscordPayload, error) { text, color := getReleasePayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil } -func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) { text, color := getPackagePayloadInfo(p, noneLinkFormatter, false) return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil } -// GetDiscordPayload converts a discord webhook into a DiscordPayload -func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(DiscordPayload) +type discordConvertor struct { + Username string + AvatarURL string +} - discord := &DiscordMeta{} - if err := json.Unmarshal([]byte(meta), &discord); err != nil { - return s, errors.New("GetDiscordPayload meta json:" + err.Error()) +var _ payloadConvertor[DiscordPayload] = discordConvertor{} + +func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &DiscordMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err) } - s.Username = discord.Username - s.AvatarURL = discord.IconURL - - return convertPayloader(s, p, event) + sc := discordConvertor{ + Username: meta.Username, + AvatarURL: meta.IconURL, + } + return newJSONRequest(sc, w, t, true) } func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) { @@ -291,8 +287,8 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, } } -func (d *DiscordPayload) createPayload(s *api.User, title, text, url string, color int) *DiscordPayload { - return &DiscordPayload{ +func (d discordConvertor) createPayload(s *api.User, title, text, url string, color int) DiscordPayload { + return DiscordPayload{ Username: d.Username, AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go index b567cbc39..c04b95383 100644 --- a/services/webhook/discord_test.go +++ b/services/webhook/discord_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -15,295 +18,274 @@ import ( ) func TestDiscordPayload(t *testing.T) { + dc := discordConvertor{} + t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(DiscordPayload) - pl, err := d.Create(p) + pl, err := dc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] branch test created", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] branch test created", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(DiscordPayload) - pl, err := d.Delete(p) + pl, err := dc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] branch test deleted", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(DiscordPayload) - pl, err := d.Fork(p) + pl, err := dc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "test/repo2 is forked to test/repo", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(DiscordPayload) - pl, err := d.Push(p) + pl, err := dc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(DiscordPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := dc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "issue body", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Embeds[0].Title) + assert.Equal(t, "issue body", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = dc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(DiscordPayload) - pl, err := d.IssueComment(p) + pl, err := dc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "more info needed", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Embeds[0].Title) + assert.Equal(t, "more info needed", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(DiscordPayload) - pl, err := d.PullRequest(p) + pl, err := dc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "fixes bug #2", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Embeds[0].Title) + assert.Equal(t, "fixes bug #2", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(DiscordPayload) - pl, err := d.IssueComment(p) + pl, err := dc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "changes requested", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Embeds[0].Title) + assert.Equal(t, "changes requested", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(DiscordPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "good job", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Embeds[0].Title) + assert.Equal(t, "good job", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(DiscordPayload) - pl, err := d.Repository(p) + pl, err := dc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Repository created", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Repository created", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(DiscordPayload) - pl, err := d.Package(p) + pl, err := dc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "Package created: GiteaContainer:latest", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(DiscordPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Embeds[0].Title) + assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Embeds[0].Title) + assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = dc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*DiscordPayload).Embeds[0].Title) - assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Embeds[0].Title) + assert.Empty(t, pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(DiscordPayload) - pl, err := d.Release(p) + pl, err := dc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - assert.Len(t, pl.(*DiscordPayload).Embeds, 1) - assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*DiscordPayload).Embeds[0].Title) - assert.Equal(t, "Note of first stable release", pl.(*DiscordPayload).Embeds[0].Description) - assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*DiscordPayload).Embeds[0].URL) - assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name) - assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL) - assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo] Release created: v1.0", pl.Embeds[0].Title) + assert.Equal(t, "Note of first stable release", pl.Embeds[0].Description) + assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.Embeds[0].URL) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) } func TestDiscordJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(DiscordPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &DiscordPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.DISCORD, + URL: "https://discord.example.com/", + Meta: `{}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newDiscordRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://discord.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 DiscordPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Embeds[0].Description) } diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 556443e70..1ec436894 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -4,11 +4,13 @@ package webhook import ( + "context" "fmt" + "net/http" "strings" + webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -23,8 +25,8 @@ type ( } ) -func newFeishuTextPayload(text string) *FeishuPayload { - return &FeishuPayload{ +func newFeishuTextPayload(text string) FeishuPayload { + return FeishuPayload{ MsgType: "text", Content: struct { Text string `json:"text"` @@ -34,19 +36,8 @@ func newFeishuTextPayload(text string) *FeishuPayload { } } -// JSONPayload Marshals the FeishuPayload to json -func (f *FeishuPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(f, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &FeishuPayload{} - // Create implements PayloadConvertor Create method -func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (fc feishuConvertor) Create(p *api.CreatePayload) (FeishuPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() text := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -55,7 +46,7 @@ func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (fc feishuConvertor) Delete(p *api.DeletePayload) (FeishuPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() text := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -64,14 +55,14 @@ func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (f *FeishuPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (fc feishuConvertor) Fork(p *api.ForkPayload) (FeishuPayload, error) { text := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return newFeishuTextPayload(text), nil } // Push implements PayloadConvertor Push method -func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -96,48 +87,40 @@ func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (f *FeishuPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (fc feishuConvertor) Issue(p *api.IssuePayload) (FeishuPayload, error) { title, link, by, operator, result, assignees := getIssuesInfo(p) - var res api.Payloader if assignees != "" { if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body)) - } else { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body)) + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body)), nil } - } else { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body)) + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body)), nil } - return res, nil + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body)), nil } // IssueComment implements PayloadConvertor IssueComment method -func (f *FeishuPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (fc feishuConvertor) IssueComment(p *api.IssueCommentPayload) (FeishuPayload, error) { title, link, by, operator := getIssuesCommentInfo(p) return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Comment.Body)), nil } // PullRequest implements PayloadConvertor PullRequest method -func (f *FeishuPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (fc feishuConvertor) PullRequest(p *api.PullRequestPayload) (FeishuPayload, error) { title, link, by, operator, result, assignees := getPullRequestInfo(p) - var res api.Payloader if assignees != "" { if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body)) - } else { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body)) + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body)), nil } - } else { - res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body)) + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body)), nil } - return res, nil + return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body)), nil } // Review implements PayloadConvertor Review method -func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (fc feishuConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (FeishuPayload, error) { action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return FeishuPayload{}, err } title := fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -147,7 +130,7 @@ func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.H } // Repository implements PayloadConvertor Repository method -func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, error) { var text string switch p.Action { case api.HookRepoCreated: @@ -158,30 +141,33 @@ func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err return newFeishuTextPayload(text), nil } - return nil, nil + return FeishuPayload{}, nil } // Wiki implements PayloadConvertor Wiki method -func (f *FeishuPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (fc feishuConvertor) Wiki(p *api.WikiPayload) (FeishuPayload, error) { text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true) return newFeishuTextPayload(text), nil } // Release implements PayloadConvertor Release method -func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (fc feishuConvertor) Release(p *api.ReleasePayload) (FeishuPayload, error) { text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) return newFeishuTextPayload(text), nil } -func (f *FeishuPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) { text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true) return newFeishuTextPayload(text), nil } -// GetFeishuPayload converts a ding talk webhook into a FeishuPayload -func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(FeishuPayload), p, event) +type feishuConvertor struct{} + +var _ payloadConvertor[FeishuPayload] = feishuConvertor{} + +func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(feishuConvertor{}, w, t, true) } diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go index 98bc50ded..ef18333fd 100644 --- a/services/webhook/feishu_test.go +++ b/services/webhook/feishu_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,199 +17,177 @@ import ( ) func TestFeishuPayload(t *testing.T) { + fc := feishuConvertor{} t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(FeishuPayload) - pl, err := d.Create(p) + pl, err := fc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, `[test/repo] branch test created`, pl.(*FeishuPayload).Content.Text) + assert.Equal(t, `[test/repo] branch test created`, pl.Content.Text) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(FeishuPayload) - pl, err := d.Delete(p) + pl, err := fc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, `[test/repo] branch test deleted`, pl.(*FeishuPayload).Content.Text) + assert.Equal(t, `[test/repo] branch test deleted`, pl.Content.Text) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(FeishuPayload) - pl, err := d.Fork(p) + pl, err := fc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*FeishuPayload).Content.Text) + assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Content.Text) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(FeishuPayload) - pl, err := d.Push(p) + pl, err := fc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Content.Text) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(FeishuPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := fc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = fc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(FeishuPayload) - pl, err := d.IssueComment(p) + pl, err := fc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.Content.Text) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(FeishuPayload) - pl, err := d.PullRequest(p) + pl, err := fc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.Content.Text) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(FeishuPayload) - pl, err := d.IssueComment(p) + pl, err := fc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.Content.Text) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(FeishuPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := fc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.Content.Text) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(FeishuPayload) - pl, err := d.Repository(p) + pl, err := fc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Repository created", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Repository created", pl.Content.Text) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(FeishuPayload) - pl, err := d.Package(p) + pl, err := fc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.Content.Text) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(FeishuPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := fc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.Content.Text) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = fc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.Content.Text) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = fc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.Content.Text) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(FeishuPayload) - pl, err := d.Release(p) + pl, err := fc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*FeishuPayload).Content.Text) + assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.Content.Text) }) } func TestFeishuJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(FeishuPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &FeishuPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.FEISHU, + URL: "https://feishu.example.com/", + Meta: `{}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newFeishuRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://feishu.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 FeishuPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text) } diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 602d16ef3..0329804a8 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -4,11 +4,12 @@ package webhook import ( + "bytes" + "context" "crypto/sha1" "encoding/hex" - "errors" "fmt" - "html" + "net/http" "net/url" "regexp" "strings" @@ -23,6 +24,37 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +func newMatrixRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &MatrixMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("GetMatrixPayload meta json: %w", err) + } + mc := matrixConvertor{ + MsgType: messageTypeText[meta.MessageType], + } + payload, err := newPayload(mc, []byte(t.PayloadContent), t.EventType) + if err != nil { + return nil, nil, err + } + + body, err := json.MarshalIndent(payload, "", " ") + if err != nil { + return nil, nil, err + } + + txnID, err := getMatrixTxnID(body) + if err != nil { + return nil, nil, err + } + req, err := http.NewRequest(http.MethodPut, w.URL+"/"+txnID, bytes.NewReader(body)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/json") + + return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially +} + const matrixPayloadSizeLimit = 1024 * 64 // MatrixMeta contains the Matrix metadata @@ -46,8 +78,6 @@ func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta { return s } -var _ PayloadConvertor = &MatrixPayload{} - // MatrixPayload contains payload for a Matrix room type MatrixPayload struct { Body string `json:"body"` @@ -57,90 +87,79 @@ type MatrixPayload struct { Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"` } -// JSONPayload Marshals the MatrixPayload to json -func (m *MatrixPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(m, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil +var _ payloadConvertor[MatrixPayload] = matrixConvertor{} + +type matrixConvertor struct { + MsgType string } -// MatrixLinkFormatter creates a link compatible with Matrix -func MatrixLinkFormatter(url, text string) string { - return fmt.Sprintf(`%s`, html.EscapeString(url), html.EscapeString(text)) +func (m matrixConvertor) newPayload(text string, commits ...*api.PayloadCommit) (MatrixPayload, error) { + return MatrixPayload{ + Body: getMessageBody(text), + MsgType: m.MsgType, + Format: "org.matrix.custom.html", + FormattedBody: text, + Commits: commits, + }, nil } -// MatrixLinkToRef Matrix-formatter link to a repo ref -func MatrixLinkToRef(repoURL, ref string) string { - refName := git.RefName(ref).ShortName() - switch { - case strings.HasPrefix(ref, git.BranchPrefix): - return MatrixLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName) - case strings.HasPrefix(ref, git.TagPrefix): - return MatrixLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName) - default: - return MatrixLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName) - } -} - -// Create implements PayloadConvertor Create method -func (m *MatrixPayload) Create(p *api.CreatePayload) (api.Payloader, error) { - repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) +// Create implements payloadConvertor Create method +func (m matrixConvertor) Create(p *api.CreatePayload) (MatrixPayload, error) { + repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } // Delete composes Matrix payload for delete a branch or tag. -func (m *MatrixPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (m matrixConvertor) Delete(p *api.DeletePayload) (MatrixPayload, error) { refName := git.RefName(p.Ref).ShortName() - repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) + repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } // Fork composes Matrix payload for forked by a repository. -func (m *MatrixPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { - baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) - forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) +func (m matrixConvertor) Fork(p *api.ForkPayload) (MatrixPayload, error) { + baseLink := htmlLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) + forkLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Issue implements PayloadConvertor Issue method -func (m *MatrixPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { - text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true) +// Issue implements payloadConvertor Issue method +func (m matrixConvertor) Issue(p *api.IssuePayload) (MatrixPayload, error) { + text, _, _, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// IssueComment implements PayloadConvertor IssueComment method -func (m *MatrixPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { - text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true) +// IssueComment implements payloadConvertor IssueComment method +func (m matrixConvertor) IssueComment(p *api.IssueCommentPayload) (MatrixPayload, error) { + text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Wiki implements PayloadConvertor Wiki method -func (m *MatrixPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { - text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true) +// Wiki implements payloadConvertor Wiki method +func (m matrixConvertor) Wiki(p *api.WikiPayload) (MatrixPayload, error) { + text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Release implements PayloadConvertor Release method -func (m *MatrixPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { - text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true) +// Release implements payloadConvertor Release method +func (m matrixConvertor) Release(p *api.ReleasePayload) (MatrixPayload, error) { + text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Push implements PayloadConvertor Push method -func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) { +// Push implements payloadConvertor Push method +func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) { var commitDesc string if p.TotalCommits == 1 { @@ -149,13 +168,13 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) { commitDesc = fmt.Sprintf("%d commits", p.TotalCommits) } - repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) + repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) text := fmt.Sprintf("[%s] %s pushed %s to %s:", repoLink, p.Pusher.UserName, commitDesc, branchLink) // for each commit, generate a new line text for i, commit := range p.Commits { - text += fmt.Sprintf("%s: %s - %s", MatrixLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) + text += fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text += "" @@ -163,41 +182,41 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) { } - return getMatrixPayload(text, p.Commits, m.MsgType), nil + return m.newPayload(text, p.Commits...) } -// PullRequest implements PayloadConvertor PullRequest method -func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { - text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true) +// PullRequest implements payloadConvertor PullRequest method +func (m matrixConvertor) PullRequest(p *api.PullRequestPayload) (MatrixPayload, error) { + text, _, _, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true) - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Review implements PayloadConvertor Review method -func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) +// Review implements payloadConvertor Review method +func (m matrixConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MatrixPayload, error) { + senderLink := htmlLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) - titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title) - repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) + titleLink := htmlLinkFormatter(p.PullRequest.HTMLURL, title) + repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return MatrixPayload{}, err } text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink) } - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -// Repository implements PayloadConvertor Repository method -func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) - repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) +// Repository implements payloadConvertor Repository method +func (m matrixConvertor) Repository(p *api.RepositoryPayload) (MatrixPayload, error) { + senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string switch p.Action { @@ -206,13 +225,12 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err case api.HookRepoDeleted: text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink) } - - return getMatrixPayload(text, nil, m.MsgType), nil + return m.newPayload(text) } -func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) { - senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) - packageLink := MatrixLinkFormatter(p.Package.HTMLURL, p.Package.Name) +func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) { + senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + packageLink := htmlLinkFormatter(p.Package.HTMLURL, p.Package.Name) var text string switch p.Action { @@ -222,31 +240,7 @@ func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) { text = fmt.Sprintf("[%s] Package deleted by %s", packageLink, senderLink) } - return getMatrixPayload(text, nil, m.MsgType), nil -} - -// GetMatrixPayload converts a Matrix webhook into a MatrixPayload -func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(MatrixPayload) - - matrix := &MatrixMeta{} - if err := json.Unmarshal([]byte(meta), &matrix); err != nil { - return s, errors.New("GetMatrixPayload meta json:" + err.Error()) - } - - s.MsgType = messageTypeText[matrix.MessageType] - - return convertPayloader(s, p, event) -} - -func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload { - p := MatrixPayload{} - p.FormattedBody = text - p.Body = getMessageBody(text) - p.Format = "org.matrix.custom.html" - p.MsgType = msgType - p.Commits = commits - return &p + return m.newPayload(text) } var urlRegex = regexp.MustCompile(`]*?href="([^">]*?)">(.*?)`) @@ -271,3 +265,16 @@ func getMatrixTxnID(payload []byte) (string, error) { return hex.EncodeToString(h.Sum(nil)), nil } + +// MatrixLinkToRef Matrix-formatter link to a repo ref +func MatrixLinkToRef(repoURL, ref string) string { + refName := git.RefName(ref).ShortName() + switch { + case strings.HasPrefix(ref, git.BranchPrefix): + return htmlLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName) + case strings.HasPrefix(ref, git.TagPrefix): + return htmlLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName) + default: + return htmlLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName) + } +} diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go index 99a22fbd7..058f8e3c5 100644 --- a/services/webhook/matrix_test.go +++ b/services/webhook/matrix_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,217 +17,213 @@ import ( ) func TestMatrixPayload(t *testing.T) { + mc := matrixConvertor{ + MsgType: "m.text", + } + t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(MatrixPayload) - pl, err := d.Create(p) + pl, err := mc.Create(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo:test] branch created by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.Body) + assert.Equal(t, `[test/repo:test] branch created by user1`, pl.FormattedBody) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(MatrixPayload) - pl, err := d.Delete(p) + pl, err := mc.Delete(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo:test] branch deleted by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.Body) + assert.Equal(t, `[test/repo:test] branch deleted by user1`, pl.FormattedBody) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(MatrixPayload) - pl, err := d.Fork(p) + pl, err := mc.Fork(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayload).Body) - assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.Body) + assert.Equal(t, `test/repo2 is forked to test/repo`, pl.FormattedBody) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(MatrixPayload) - pl, err := d.Push(p) + pl, err := mc.Push(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[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", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] user1 pushed 2 commits to test:2020558: commit message - user12020558: commit message - user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[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", pl.Body) + assert.Equal(t, `[test/repo] user1 pushed 2 commits to test:2020558: commit message - user12020558: commit message - user1`, pl.FormattedBody) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(MatrixPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := mc.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1`, pl.FormattedBody) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = mc.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.FormattedBody) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(MatrixPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1`, pl.FormattedBody) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(MatrixPayload) - pl, err := d.PullRequest(p) + pl, err := mc.PullRequest(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1`, pl.FormattedBody) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(MatrixPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1`, pl.FormattedBody) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(MatrixPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug by user1`, pl.FormattedBody) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(MatrixPayload) - pl, err := d.Repository(p) + pl, err := mc.Repository(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Repository created by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.Body) + assert.Equal(t, `[test/repo] Repository created by user1`, pl.FormattedBody) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(MatrixPayload) - pl, err := d.Package(p) + pl, err := mc.Package(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body) - assert.Equal(t, `[GiteaContainer] Package published by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.Body) + assert.Equal(t, `[GiteaContainer] Package published by user1`, pl.FormattedBody) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(MatrixPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := mc.Wiki(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.FormattedBody) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.FormattedBody) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.FormattedBody) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(MatrixPayload) - pl, err := d.Release(p) + pl, err := mc.Release(p) require.NoError(t, err) require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body) - assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.(*MatrixPayload).FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.Body) + assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.FormattedBody) }) } func TestMatrixJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(MatrixPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MatrixPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.MATRIX, + URL: "https://matrix.example.com/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message", + Meta: `{"message_type":0}`, // text + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newMatrixRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "PUT", req.Method) + assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path) + assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + var body MatrixPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[[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", body.Body) } func Test_getTxnID(t *testing.T) { diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 37810b4cd..99d010618 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -4,12 +4,14 @@ package webhook import ( + "context" "fmt" + "net/http" "net/url" "strings" + webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -56,19 +58,8 @@ type ( } ) -// JSONPayload Marshals the MSTeamsPayload to json -func (m *MSTeamsPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(m, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &MSTeamsPayload{} - // Create implements PayloadConvertor Create method -func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (m msteamsConvertor) Create(p *api.CreatePayload) (MSTeamsPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -85,7 +76,7 @@ func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (m msteamsConvertor) Delete(p *api.DeletePayload) (MSTeamsPayload, error) { // deleted tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -102,7 +93,7 @@ func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (m msteamsConvertor) Fork(p *api.ForkPayload) (MSTeamsPayload, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return createMSTeamsPayload( @@ -117,7 +108,7 @@ func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { } // Push implements PayloadConvertor Push method -func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -160,7 +151,7 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) { title, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -175,7 +166,7 @@ func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { } // IssueComment implements PayloadConvertor IssueComment method -func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPayload, error) { title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -190,7 +181,7 @@ func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader } // PullRequest implements PayloadConvertor PullRequest method -func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) { title, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -205,14 +196,14 @@ func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, } // Review implements PayloadConvertor Review method -func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MSTeamsPayload, error) { var text, title string var color int switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return MSTeamsPayload{}, err } title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -242,7 +233,7 @@ func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module. } // Repository implements PayloadConvertor Repository method -func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload, error) { var title, url string var color int switch p.Action { @@ -267,7 +258,7 @@ func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er } // Wiki implements PayloadConvertor Wiki method -func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) { title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -282,7 +273,7 @@ func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { } // Release implements PayloadConvertor Release method -func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (m msteamsConvertor) Release(p *api.ReleasePayload) (MSTeamsPayload, error) { title, color := getReleasePayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -296,7 +287,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { ), nil } -func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) { title, color := getPackagePayloadInfo(p, noneLinkFormatter, false) return createMSTeamsPayload( @@ -310,12 +301,7 @@ func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) { ), nil } -// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload -func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(MSTeamsPayload), p, event) -} - -func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) *MSTeamsPayload { +func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload { facts := make([]MSTeamsFact, 0, 2) if r != nil { facts = append(facts, MSTeamsFact{ @@ -327,7 +313,7 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar facts = append(facts, *fact) } - return &MSTeamsPayload{ + return MSTeamsPayload{ Type: "MessageCard", Context: "https://schema.org/extensions", ThemeColor: fmt.Sprintf("%x", color), @@ -356,3 +342,11 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar }, } } + +type msteamsConvertor struct{} + +var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{} + +func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(msteamsConvertor{}, w, t, true) +} diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go index 8d1aed604..01e08b918 100644 --- a/services/webhook/msteams_test.go +++ b/services/webhook/msteams_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,22 +17,20 @@ import ( ) func TestMSTeamsPayload(t *testing.T) { + mc := msteamsConvertor{} t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Create(p) + pl, err := mc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] branch test created", pl.Title) + assert.Equal(t, "[test/repo] branch test created", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "branch:" { @@ -38,27 +39,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Delete(p) + pl, err := mc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] branch test deleted", pl.Title) + assert.Equal(t, "[test/repo] branch test deleted", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "branch:" { @@ -67,27 +65,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Fork(p) + pl, err := mc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "test/repo2 is forked to test/repo", pl.Title) + assert.Equal(t, "test/repo2 is forked to test/repo", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "Forkee:" { @@ -96,27 +91,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Push(p) + pl, err := mc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo:test] 2 new commits", pl.Title) + assert.Equal(t, "[test/repo:test] 2 new commits", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repo.FullName, fact.Value) } else if fact.Name == "Commit count:" { @@ -125,28 +117,25 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(MSTeamsPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := mc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "issue body", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Title) + assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "issue body", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -155,23 +144,21 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = mc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Title) + assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -180,27 +167,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(MSTeamsPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "more info needed", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Title) + assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "more info needed", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -209,27 +193,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.PotentialAction[0].Targets[0].URI) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(MSTeamsPayload) - pl, err := d.PullRequest(p) + pl, err := mc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "fixes bug #2", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Title) + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "fixes bug #2", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Pull request #:" { @@ -238,27 +219,24 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(MSTeamsPayload) - pl, err := d.IssueComment(p) + pl, err := mc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "changes requested", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Title) + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "changes requested", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Issue #:" { @@ -267,28 +245,25 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(MSTeamsPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "good job", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Title) + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "good job", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Pull request #:" { @@ -297,155 +272,139 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Repository(p) + pl, err := mc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Repository created", pl.Title) + assert.Equal(t, "[test/repo] Repository created", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 1) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Package(p) + pl, err := mc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "Package created: GiteaContainer:latest", pl.Title) + assert.Equal(t, "Package created: GiteaContainer:latest", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 1) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Package:" { assert.Equal(t, p.Package.Name, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(MSTeamsPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := mc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Title) + assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Title) + assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Equal(t, "", pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = mc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Title) + assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(MSTeamsPayload) - pl, err := d.Release(p) + pl, err := mc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Title) - assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Summary) - assert.Len(t, pl.(*MSTeamsPayload).Sections, 1) - assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle) - assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text) - assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2) - for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts { + assert.Equal(t, "[test/repo] Release created: v1.0", pl.Title) + assert.Equal(t, "[test/repo] Release created: v1.0", pl.Summary) + assert.Len(t, pl.Sections, 1) + assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) + assert.Empty(t, pl.Sections[0].Text) + assert.Len(t, pl.Sections[0].Facts, 2) + for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { assert.Equal(t, p.Repository.FullName, fact.Value) } else if fact.Name == "Tag:" { @@ -454,21 +413,43 @@ func TestMSTeamsPayload(t *testing.T) { t.Fail() } } - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1) - assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI) + assert.Len(t, pl.PotentialAction, 1) + assert.Len(t, pl.PotentialAction[0].Targets, 1) + assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.PotentialAction[0].Targets[0].URI) }) } func TestMSTeamsJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(MSTeamsPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &MSTeamsPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.MSTEAMS, + URL: "https://msteams.example.com/", + Meta: ``, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://msteams.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 MSTeamsPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[test/repo:test] 2 new commits", body.Summary) } diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index 714a4c076..7880d8b60 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -4,7 +4,9 @@ package webhook import ( - "errors" + "context" + "fmt" + "net/http" webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/json" @@ -38,84 +40,85 @@ func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta { return s } -// JSONPayload Marshals the PackagistPayload to json -func (f *PackagistPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(f, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -var _ PayloadConvertor = &PackagistPayload{} - // Create implements PayloadConvertor Create method -func (f *PackagistPayload) Create(_ *api.CreatePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Create(_ *api.CreatePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Delete implements PayloadConvertor Delete method -func (f *PackagistPayload) Delete(_ *api.DeletePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Delete(_ *api.DeletePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Fork implements PayloadConvertor Fork method -func (f *PackagistPayload) Fork(_ *api.ForkPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Fork(_ *api.ForkPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Push implements PayloadConvertor Push method -func (f *PackagistPayload) Push(_ *api.PushPayload) (api.Payloader, error) { - return f, nil +// https://packagist.org/about +func (pc packagistConvertor) Push(_ *api.PushPayload) (PackagistPayload, error) { + return PackagistPayload{ + PackagistRepository: struct { + URL string `json:"url"` + }{ + URL: pc.PackageURL, + }, + }, nil } // Issue implements PayloadConvertor Issue method -func (f *PackagistPayload) Issue(_ *api.IssuePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Issue(_ *api.IssuePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // IssueComment implements PayloadConvertor IssueComment method -func (f *PackagistPayload) IssueComment(_ *api.IssueCommentPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) IssueComment(_ *api.IssueCommentPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // PullRequest implements PayloadConvertor PullRequest method -func (f *PackagistPayload) PullRequest(_ *api.PullRequestPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) PullRequest(_ *api.PullRequestPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Review implements PayloadConvertor Review method -func (f *PackagistPayload) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Repository implements PayloadConvertor Repository method -func (f *PackagistPayload) Repository(_ *api.RepositoryPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Repository(_ *api.RepositoryPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Wiki implements PayloadConvertor Wiki method -func (f *PackagistPayload) Wiki(_ *api.WikiPayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Wiki(_ *api.WikiPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } // Release implements PayloadConvertor Release method -func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Release(_ *api.ReleasePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } -func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) { - return nil, nil +func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, error) { + return PackagistPayload{}, nil } -// GetPackagistPayload converts a packagist webhook into a PackagistPayload -func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(PackagistPayload) +type packagistConvertor struct { + PackageURL string +} - packagist := &PackagistMeta{} - if err := json.Unmarshal([]byte(meta), &packagist); err != nil { - return s, errors.New("GetPackagistPayload meta json:" + err.Error()) +var _ payloadConvertor[PackagistPayload] = packagistConvertor{} + +func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &PackagistMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err) } - s.PackagistRepository.URL = packagist.PackageURL - return convertPayloader(s, p, event) + pc := packagistConvertor{ + PackageURL: meta.PackageURL, + } + return newJSONRequest(pc, w, t, true) } diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go index 26d01b055..e9b0695ba 100644 --- a/services/webhook/packagist_test.go +++ b/services/webhook/packagist_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,155 +17,199 @@ import ( ) func TestPackagistPayload(t *testing.T) { + pc := packagistConvertor{ + PackageURL: "https://packagist.org/packages/example", + } t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(PackagistPayload) - pl, err := d.Create(p) + pl, err := pc.Create(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(PackagistPayload) - pl, err := d.Delete(p) + pl, err := pc.Delete(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(PackagistPayload) - pl, err := d.Fork(p) + pl, err := pc.Fork(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(PackagistPayload) - d.PackagistRepository.URL = "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN" - pl, err := d.Push(p) + pl, err := pc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &PackagistPayload{}, pl) - assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", pl.(*PackagistPayload).PackagistRepository.URL) + assert.Equal(t, "https://packagist.org/packages/example", pl.PackagistRepository.URL) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(PackagistPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := pc.Issue(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = pc.Issue(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(PackagistPayload) - pl, err := d.IssueComment(p) + pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(PackagistPayload) - pl, err := d.PullRequest(p) + pl, err := pc.PullRequest(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(PackagistPayload) - pl, err := d.IssueComment(p) + pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(PackagistPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(PackagistPayload) - pl, err := d.Repository(p) + pl, err := pc.Repository(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(PackagistPayload) - pl, err := d.Package(p) + pl, err := pc.Package(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(PackagistPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := pc.Wiki(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = pc.Wiki(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = pc.Wiki(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(PackagistPayload) - pl, err := d.Release(p) + pl, err := pc.Release(p) require.NoError(t, err) - require.Nil(t, pl) + require.Equal(t, pl, PackagistPayload{}) }) } func TestPackagistJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(PackagistPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &PackagistPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.PACKAGIST, + URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", + Meta: `{"package_url":"https://packagist.org/packages/example"}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newPackagistRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", 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 PackagistPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "https://packagist.org/packages/example", body.PackagistRepository.URL) +} + +func TestPackagistEmptyPayload(t *testing.T) { + p := createTestPayload() + data, err := p.JSONPayload() + require.NoError(t, err) + + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.PACKAGIST, + URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", + Meta: `{"package_url":"https://packagist.org/packages/example"}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventCreate, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newPackagistRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) + require.NoError(t, err) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", 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 PackagistPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "", body.PackagistRepository.URL) } diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go index bd482c04e..abf9946cc 100644 --- a/services/webhook/payloader.go +++ b/services/webhook/payloader.go @@ -4,58 +4,104 @@ package webhook import ( + "bytes" + "fmt" + "net/http" + + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" ) -// PayloadConvertor defines the interface to convert system webhook payload to external payload -type PayloadConvertor interface { - api.Payloader - Create(*api.CreatePayload) (api.Payloader, error) - Delete(*api.DeletePayload) (api.Payloader, error) - Fork(*api.ForkPayload) (api.Payloader, error) - Issue(*api.IssuePayload) (api.Payloader, error) - IssueComment(*api.IssueCommentPayload) (api.Payloader, error) - Push(*api.PushPayload) (api.Payloader, error) - PullRequest(*api.PullRequestPayload) (api.Payloader, error) - Review(*api.PullRequestPayload, webhook_module.HookEventType) (api.Payloader, error) - Repository(*api.RepositoryPayload) (api.Payloader, error) - Release(*api.ReleasePayload) (api.Payloader, error) - Wiki(*api.WikiPayload) (api.Payloader, error) - Package(*api.PackagePayload) (api.Payloader, error) +// payloadConvertor defines the interface to convert system payload to webhook payload +type payloadConvertor[T any] interface { + Create(*api.CreatePayload) (T, error) + Delete(*api.DeletePayload) (T, error) + Fork(*api.ForkPayload) (T, error) + Issue(*api.IssuePayload) (T, error) + IssueComment(*api.IssueCommentPayload) (T, error) + Push(*api.PushPayload) (T, error) + PullRequest(*api.PullRequestPayload) (T, error) + Review(*api.PullRequestPayload, webhook_module.HookEventType) (T, error) + Repository(*api.RepositoryPayload) (T, error) + Release(*api.ReleasePayload) (T, error) + Wiki(*api.WikiPayload) (T, error) + Package(*api.PackagePayload) (T, error) } -func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) { +func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) { + var p P + if err := json.Unmarshal(data, &p); err != nil { + var t T + return t, fmt.Errorf("could not unmarshal payload: %w", err) + } + return convert(p) +} + +func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) { switch event { case webhook_module.HookEventCreate: - return s.Create(p.(*api.CreatePayload)) + return convertUnmarshalledJSON(rc.Create, data) case webhook_module.HookEventDelete: - return s.Delete(p.(*api.DeletePayload)) + return convertUnmarshalledJSON(rc.Delete, data) case webhook_module.HookEventFork: - return s.Fork(p.(*api.ForkPayload)) + return convertUnmarshalledJSON(rc.Fork, data) case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone: - return s.Issue(p.(*api.IssuePayload)) + return convertUnmarshalledJSON(rc.Issue, data) case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return s.IssueComment(pl) - } - return s.PullRequest(p.(*api.PullRequestPayload)) + // previous code sometimes sent s.PullRequest(p.(*api.PullRequestPayload)) + // however I couldn't find in notifier.go such a payload with an HookEvent***Comment event + + // History (most recent first): + // - refactored in https://github.com/go-gitea/gitea/pull/12310 + // - assertion added in https://github.com/go-gitea/gitea/pull/12046 + // - issue raised in https://github.com/go-gitea/gitea/issues/11940#issuecomment-645713996 + // > That's because for HookEventPullRequestComment event, some places use IssueCommentPayload and others use PullRequestPayload + + // In modules/actions/workflows.go:183 the type assertion is always payload.(*api.IssueCommentPayload) + return convertUnmarshalledJSON(rc.IssueComment, data) case webhook_module.HookEventPush: - return s.Push(p.(*api.PushPayload)) + return convertUnmarshalledJSON(rc.Push, data) case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel, webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest: - return s.PullRequest(p.(*api.PullRequestPayload)) + return convertUnmarshalledJSON(rc.PullRequest, data) case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment: - return s.Review(p.(*api.PullRequestPayload), event) + return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) { + return rc.Review(p, event) + }, data) case webhook_module.HookEventRepository: - return s.Repository(p.(*api.RepositoryPayload)) + return convertUnmarshalledJSON(rc.Repository, data) case webhook_module.HookEventRelease: - return s.Release(p.(*api.ReleasePayload)) + return convertUnmarshalledJSON(rc.Release, data) case webhook_module.HookEventWiki: - return s.Wiki(p.(*api.WikiPayload)) + return convertUnmarshalledJSON(rc.Wiki, data) case webhook_module.HookEventPackage: - return s.Package(p.(*api.PackagePayload)) + return convertUnmarshalledJSON(rc.Package, data) } - return s, nil + var t T + return t, fmt.Errorf("newPayload unsupported event: %s", event) +} + +func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { + payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType) + if err != nil { + return nil, nil, err + } + + body, err := json.MarshalIndent(payload, "", " ") + if err != nil { + return nil, nil, err + } + + req, err := http.NewRequest(w.HTTPMethod, w.URL, bytes.NewReader(body)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/json") + + if withDefaultHeaders { + return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) + } + return req, body, nil } diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 945b0662d..ba8bac27d 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -4,8 +4,9 @@ package webhook import ( - "errors" + "context" "fmt" + "net/http" "regexp" "strings" @@ -39,7 +40,6 @@ func GetSlackHook(w *webhook_model.Webhook) *SlackMeta { type SlackPayload struct { Channel string `json:"channel"` Text string `json:"text"` - Color string `json:"-"` Username string `json:"username"` IconURL string `json:"icon_url"` UnfurlLinks int `json:"unfurl_links"` @@ -56,15 +56,6 @@ type SlackAttachment struct { Text string `json:"text"` } -// JSONPayload Marshals the SlackPayload to json -func (s *SlackPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(s, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - // SlackTextFormatter replaces &, <, > with HTML characters // see: https://api.slack.com/docs/formatting func SlackTextFormatter(s string) string { @@ -98,10 +89,8 @@ func SlackLinkToRef(repoURL, ref string) string { return SlackLinkFormatter(url, refName) } -var _ PayloadConvertor = &SlackPayload{} - -// Create implements PayloadConvertor Create method -func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +// Create implements payloadConvertor Create method +func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) { repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) @@ -110,7 +99,7 @@ func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete composes Slack payload for delete a branch or tag. -func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (s slackConvertor) Delete(p *api.DeletePayload) (SlackPayload, error) { refName := git.RefName(p.Ref).ShortName() repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName) @@ -119,7 +108,7 @@ func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork composes Slack payload for forked by a repository. -func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (s slackConvertor) Fork(p *api.ForkPayload) (SlackPayload, error) { baseLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) forkLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink) @@ -127,8 +116,8 @@ func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { return s.createPayload(text, nil), nil } -// Issue implements PayloadConvertor Issue method -func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +// Issue implements payloadConvertor Issue method +func (s slackConvertor) Issue(p *api.IssuePayload) (SlackPayload, error) { text, issueTitle, attachmentText, color := getIssuesPayloadInfo(p, SlackLinkFormatter, true) var attachments []SlackAttachment @@ -146,8 +135,8 @@ func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { return s.createPayload(text, attachments), nil } -// IssueComment implements PayloadConvertor IssueComment method -func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +// IssueComment implements payloadConvertor IssueComment method +func (s slackConvertor) IssueComment(p *api.IssueCommentPayload) (SlackPayload, error) { text, issueTitle, color := getIssueCommentPayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, []SlackAttachment{{ @@ -158,28 +147,28 @@ func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, }}), nil } -// Wiki implements PayloadConvertor Wiki method -func (s *SlackPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +// Wiki implements payloadConvertor Wiki method +func (s slackConvertor) Wiki(p *api.WikiPayload) (SlackPayload, error) { text, _, _ := getWikiPayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, nil), nil } -// Release implements PayloadConvertor Release method -func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +// Release implements payloadConvertor Release method +func (s slackConvertor) Release(p *api.ReleasePayload) (SlackPayload, error) { text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, nil), nil } -func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) { text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true) return s.createPayload(text, nil), nil } -// Push implements PayloadConvertor Push method -func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) { +// Push implements payloadConvertor Push method +func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) { // n new commits var ( commitDesc string @@ -219,8 +208,8 @@ func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) { }}), nil } -// PullRequest implements PayloadConvertor PullRequest method -func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +// PullRequest implements payloadConvertor PullRequest method +func (s slackConvertor) PullRequest(p *api.PullRequestPayload) (SlackPayload, error) { text, issueTitle, attachmentText, color := getPullRequestPayloadInfo(p, SlackLinkFormatter, true) var attachments []SlackAttachment @@ -238,8 +227,8 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er return s.createPayload(text, attachments), nil } -// Review implements PayloadConvertor Review method -func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +// Review implements payloadConvertor Review method +func (s slackConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (SlackPayload, error) { senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index) @@ -250,7 +239,7 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return SlackPayload{}, err } text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink) @@ -259,8 +248,8 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho return s.createPayload(text, nil), nil } -// Repository implements PayloadConvertor Repository method -func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +// Repository implements payloadConvertor Repository method +func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, error) { senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string @@ -275,8 +264,8 @@ func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, erro return s.createPayload(text, nil), nil } -func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) *SlackPayload { - return &SlackPayload{ +func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload { + return SlackPayload{ Channel: s.Channel, Text: text, Username: s.Username, @@ -285,21 +274,27 @@ func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) } } -// GetSlackPayload converts a slack webhook into a SlackPayload -func GetSlackPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) { - s := new(SlackPayload) +type slackConvertor struct { + Channel string + Username string + IconURL string + Color string +} - slack := &SlackMeta{} - if err := json.Unmarshal([]byte(meta), &slack); err != nil { - return s, errors.New("GetSlackPayload meta json:" + err.Error()) +var _ payloadConvertor[SlackPayload] = slackConvertor{} + +func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + meta := &SlackMeta{} + if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { + return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err) } - - s.Channel = slack.Channel - s.Username = slack.Username - s.IconURL = slack.IconURL - s.Color = slack.Color - - return convertPayloader(s, p, event) + sc := slackConvertor{ + Channel: meta.Channel, + Username: meta.Username, + IconURL: meta.IconURL, + Color: meta.Color, + } + return newJSONRequest(sc, w, t, true) } var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`) diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go index b1340963e..7ebf16aba 100644 --- a/services/webhook/slack_test.go +++ b/services/webhook/slack_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,201 +17,180 @@ import ( ) func TestSlackPayload(t *testing.T) { + sc := slackConvertor{} + t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(SlackPayload) - pl, err := d.Create(p) + pl, err := sc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[:] branch created by user1", pl.(*SlackPayload).Text) + assert.Equal(t, "[:] branch created by user1", pl.Text) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(SlackPayload) - pl, err := d.Delete(p) + pl, err := sc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[:test] branch deleted by user1", pl.(*SlackPayload).Text) + assert.Equal(t, "[:test] branch deleted by user1", pl.Text) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(SlackPayload) - pl, err := d.Fork(p) + pl, err := sc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, " is forked to ", pl.(*SlackPayload).Text) + assert.Equal(t, " is forked to ", pl.Text) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(SlackPayload) - pl, err := d.Push(p) + pl, err := sc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[:] 2 new commits pushed by user1", pl.(*SlackPayload).Text) + assert.Equal(t, "[:] 2 new commits pushed by user1", pl.Text) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(SlackPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := sc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Issue opened: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Issue opened: by ", pl.Text) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = sc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Issue closed: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Issue closed: by ", pl.Text) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(SlackPayload) - pl, err := d.IssueComment(p) + pl, err := sc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] New comment on issue by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] New comment on issue by ", pl.Text) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(SlackPayload) - pl, err := d.PullRequest(p) + pl, err := sc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Pull request opened: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Pull request opened: by ", pl.Text) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(SlackPayload) - pl, err := d.IssueComment(p) + pl, err := sc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] New comment on pull request by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] New comment on pull request by ", pl.Text) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(SlackPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := sc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by ", pl.Text) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(SlackPayload) - pl, err := d.Repository(p) + pl, err := sc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Repository created by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Repository created by ", pl.Text) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(SlackPayload) - pl, err := d.Package(p) + pl, err := sc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "Package created: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "Package created: by ", pl.Text) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(SlackPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := sc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] New wiki page '' (Wiki change comment) by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] New wiki page '' (Wiki change comment) by ", pl.Text) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = sc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Wiki page '' edited (Wiki change comment) by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Wiki page '' edited (Wiki change comment) by ", pl.Text) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = sc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Wiki page '' deleted by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Wiki page '' deleted by ", pl.Text) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(SlackPayload) - pl, err := d.Release(p) + pl, err := sc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - assert.Equal(t, "[] Release created: by ", pl.(*SlackPayload).Text) + assert.Equal(t, "[] Release created: by ", pl.Text) }) } func TestSlackJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(SlackPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &SlackPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.SLACK, + URL: "https://slack.example.com/", + Meta: `{}`, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newSlackRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://slack.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 SlackPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[:] 2 new commits pushed by user1", body.Text) } func TestIsValidSlackChannel(t *testing.T) { diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 1bdc74e18..e4a5b5a42 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -4,14 +4,15 @@ package webhook import ( + "context" "fmt" + "net/http" "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/markup" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -41,22 +42,8 @@ func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta { return s } -var _ PayloadConvertor = &TelegramPayload{} - -// JSONPayload Marshals the TelegramPayload to json -func (t *TelegramPayload) JSONPayload() ([]byte, error) { - t.ParseMode = "HTML" - t.DisableWebPreview = true - t.Message = markup.Sanitize(t.Message) - data, err := json.MarshalIndent(t, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - // Create implements PayloadConvertor Create method -func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (t telegramConvertor) Create(p *api.CreatePayload) (TelegramPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf(`[%s] %s %s created`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, @@ -66,7 +53,7 @@ func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) { } // Delete implements PayloadConvertor Delete method -func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (t telegramConvertor) Delete(p *api.DeletePayload) (TelegramPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf(`[%s] %s %s deleted`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, @@ -76,14 +63,14 @@ func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { } // Fork implements PayloadConvertor Fork method -func (t *TelegramPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (t telegramConvertor) Fork(p *api.ForkPayload) (TelegramPayload, error) { title := fmt.Sprintf(`%s is forked to %s`, p.Forkee.FullName, p.Repo.HTMLURL, p.Repo.FullName) return createTelegramPayload(title), nil } // Push implements PayloadConvertor Push method -func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -121,34 +108,34 @@ func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (t *TelegramPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (t telegramConvertor) Issue(p *api.IssuePayload) (TelegramPayload, error) { text, _, attachmentText, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text + "\n\n" + attachmentText), nil } // IssueComment implements PayloadConvertor IssueComment method -func (t *TelegramPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (t telegramConvertor) IssueComment(p *api.IssueCommentPayload) (TelegramPayload, error) { text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text + "\n" + p.Comment.Body), nil } // PullRequest implements PayloadConvertor PullRequest method -func (t *TelegramPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (t telegramConvertor) PullRequest(p *api.PullRequestPayload) (TelegramPayload, error) { text, _, attachmentText, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text + "\n" + attachmentText), nil } // Review implements PayloadConvertor Review method -func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (t telegramConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (TelegramPayload, error) { var text, attachmentText string switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return TelegramPayload{}, err } text = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) @@ -159,7 +146,7 @@ func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module } // Repository implements PayloadConvertor Repository method -func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (t telegramConvertor) Repository(p *api.RepositoryPayload) (TelegramPayload, error) { var title string switch p.Action { case api.HookRepoCreated: @@ -169,36 +156,39 @@ func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) return createTelegramPayload(title), nil } - return nil, nil + return TelegramPayload{}, nil } // Wiki implements PayloadConvertor Wiki method -func (t *TelegramPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (t telegramConvertor) Wiki(p *api.WikiPayload) (TelegramPayload, error) { text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text), nil } // Release implements PayloadConvertor Release method -func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (t telegramConvertor) Release(p *api.ReleasePayload) (TelegramPayload, error) { text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text), nil } -func (t *TelegramPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, error) { text, _ := getPackagePayloadInfo(p, htmlLinkFormatter, true) return createTelegramPayload(text), nil } -// GetTelegramPayload converts a telegram webhook into a TelegramPayload -func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(TelegramPayload), p, event) -} - -func createTelegramPayload(message string) *TelegramPayload { - return &TelegramPayload{ +func createTelegramPayload(message string) TelegramPayload { + return TelegramPayload{ Message: strings.TrimSpace(message), } } + +type telegramConvertor struct{} + +var _ payloadConvertor[TelegramPayload] = telegramConvertor{} + +func newTelegramRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(telegramConvertor{}, w, t, true) +} diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go index 5b9927d05..27ab96cd0 100644 --- a/services/webhook/telegram_test.go +++ b/services/webhook/telegram_test.go @@ -4,8 +4,11 @@ package webhook import ( + "context" "testing" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -14,199 +17,177 @@ import ( ) func TestTelegramPayload(t *testing.T) { + tc := telegramConvertor{} t.Run("Create", func(t *testing.T) { p := createTestPayload() - d := new(TelegramPayload) - pl, err := d.Create(p) + pl, err := tc.Create(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] branch test created`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] branch test created`, pl.Message) }) t.Run("Delete", func(t *testing.T) { p := deleteTestPayload() - d := new(TelegramPayload) - pl, err := d.Delete(p) + pl, err := tc.Delete(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] branch test deleted`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] branch test deleted`, pl.Message) }) t.Run("Fork", func(t *testing.T) { p := forkTestPayload() - d := new(TelegramPayload) - pl, err := d.Fork(p) + pl, err := tc.Fork(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*TelegramPayload).Message) + assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Message) }) t.Run("Push", func(t *testing.T) { p := pushTestPayload() - d := new(TelegramPayload) - pl, err := d.Push(p) + pl, err := tc.Push(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo:test] 2 new commits\n[2020558] commit message - user1\n[2020558] commit message - user1", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo:test] 2 new commits\n[2020558] commit message - user1\n[2020558] commit message - user1", pl.Message) }) t.Run("Issue", func(t *testing.T) { p := issueTestPayload() - d := new(TelegramPayload) p.Action = api.HookIssueOpened - pl, err := d.Issue(p) + pl, err := tc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\n\nissue body", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\n\nissue body", pl.Message) p.Action = api.HookIssueClosed - pl, err = d.Issue(p) + pl, err = tc.Issue(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.Message) }) t.Run("IssueComment", func(t *testing.T) { p := issueCommentTestPayload() - d := new(TelegramPayload) - pl, err := d.IssueComment(p) + pl, err := tc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\nmore info needed", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\nmore info needed", pl.Message) }) t.Run("PullRequest", func(t *testing.T) { p := pullRequestTestPayload() - d := new(TelegramPayload) - pl, err := d.PullRequest(p) + pl, err := tc.PullRequest(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\nfixes bug #2", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\nfixes bug #2", pl.Message) }) t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() - d := new(TelegramPayload) - pl, err := d.IssueComment(p) + pl, err := tc.IssueComment(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\nchanges requested", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\nchanges requested", pl.Message) }) t.Run("Review", func(t *testing.T) { p := pullRequestTestPayload() p.Action = api.HookIssueReviewed - d := new(TelegramPayload) - pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved) + pl, err := tc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.(*TelegramPayload).Message) + assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.Message) }) t.Run("Repository", func(t *testing.T) { p := repositoryTestPayload() - d := new(TelegramPayload) - pl, err := d.Repository(p) + pl, err := tc.Repository(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Repository created`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Repository created`, pl.Message) }) t.Run("Package", func(t *testing.T) { p := packageTestPayload() - d := new(TelegramPayload) - pl, err := d.Package(p) + pl, err := tc.Package(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.Message) }) t.Run("Wiki", func(t *testing.T) { p := wikiTestPayload() - d := new(TelegramPayload) p.Action = api.HookWikiCreated - pl, err := d.Wiki(p) + pl, err := tc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.Message) p.Action = api.HookWikiEdited - pl, err = d.Wiki(p) + pl, err = tc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.Message) p.Action = api.HookWikiDeleted - pl, err = d.Wiki(p) + pl, err = tc.Wiki(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.Message) }) t.Run("Release", func(t *testing.T) { p := pullReleaseTestPayload() - d := new(TelegramPayload) - pl, err := d.Release(p) + pl, err := tc.Release(p) require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.Message) }) } func TestTelegramJSONPayload(t *testing.T) { p := pushTestPayload() - - pl, err := new(TelegramPayload).Push(p) + data, err := p.JSONPayload() require.NoError(t, err) - require.NotNil(t, pl) - require.IsType(t, &TelegramPayload{}, pl) - json, err := pl.JSONPayload() + hook := &webhook_model.Webhook{ + RepoID: 3, + IsActive: true, + Type: webhook_module.TELEGRAM, + URL: "https://telegram.example.com/", + Meta: ``, + HTTPMethod: "POST", + } + task := &webhook_model.HookTask{ + HookID: hook.ID, + EventType: webhook_module.HookEventPush, + PayloadContent: string(data), + PayloadVersion: 2, + } + + req, reqBody, err := newTelegramRequest(context.Background(), hook, task) + require.NotNil(t, req) + require.NotNil(t, reqBody) require.NoError(t, err) - assert.NotEmpty(t, json) + + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "https://telegram.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 TelegramPayload + err = json.NewDecoder(req.Body).Decode(&body) + assert.NoError(t, err) + assert.Equal(t, "[test/repo:test] 2 new commits\n[2020558] commit message - user1\n[2020558] commit message - user1", body.Message) } diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 35c760dc6..e0e8fa2fc 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "net/http" "strings" "code.gitea.io/gitea/models/db" @@ -26,48 +27,16 @@ import ( "github.com/gobwas/glob" ) -type webhook struct { - name webhook_module.HookType - payloadCreator func(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) -} - -var webhooks = map[webhook_module.HookType]*webhook{ - webhook_module.SLACK: { - name: webhook_module.SLACK, - payloadCreator: GetSlackPayload, - }, - webhook_module.DISCORD: { - name: webhook_module.DISCORD, - payloadCreator: GetDiscordPayload, - }, - webhook_module.DINGTALK: { - name: webhook_module.DINGTALK, - payloadCreator: GetDingtalkPayload, - }, - webhook_module.TELEGRAM: { - name: webhook_module.TELEGRAM, - payloadCreator: GetTelegramPayload, - }, - webhook_module.MSTEAMS: { - name: webhook_module.MSTEAMS, - payloadCreator: GetMSTeamsPayload, - }, - webhook_module.FEISHU: { - name: webhook_module.FEISHU, - payloadCreator: GetFeishuPayload, - }, - webhook_module.MATRIX: { - name: webhook_module.MATRIX, - payloadCreator: GetMatrixPayload, - }, - webhook_module.WECHATWORK: { - name: webhook_module.WECHATWORK, - payloadCreator: GetWechatworkPayload, - }, - webhook_module.PACKAGIST: { - name: webhook_module.PACKAGIST, - payloadCreator: GetPackagistPayload, - }, +var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){ + webhook_module.SLACK: newSlackRequest, + webhook_module.DISCORD: newDiscordRequest, + webhook_module.DINGTALK: newDingtalkRequest, + webhook_module.TELEGRAM: newTelegramRequest, + webhook_module.MSTEAMS: newMSTeamsRequest, + webhook_module.FEISHU: newFeishuRequest, + webhook_module.MATRIX: newMatrixRequest, + webhook_module.WECHATWORK: newWechatworkRequest, + webhook_module.PACKAGIST: newPackagistRequest, } // IsValidHookTaskType returns true if a webhook registered @@ -75,7 +44,7 @@ func IsValidHookTaskType(name string) bool { if name == webhook_module.GITEA || name == webhook_module.GOGS { return true } - _, ok := webhooks[name] + _, ok := webhookRequesters[name] return ok } @@ -159,7 +128,9 @@ func checkBranch(w *webhook_model.Webhook, branch string) bool { return g.Match(branch) } -// PrepareWebhook creates a hook task and enqueues it for processing +// PrepareWebhook creates a hook task and enqueues it for processing. +// The payload is saved as-is. The adjustments depending on the webhook type happen +// right before delivery, in the [Deliver] method. func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error { // Skip sending if webhooks are disabled. if setting.DisableWebhooks { @@ -193,25 +164,19 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook } } - var payloader api.Payloader - var err error - webhook, ok := webhooks[w.Type] - if ok { - payloader, err = webhook.payloadCreator(p, event, w.Meta) - if err != nil { - return fmt.Errorf("create payload for %s[%s]: %w", w.Type, event, err) - } - } else { - payloader = p + payload, err := p.JSONPayload() + if err != nil { + return fmt.Errorf("JSONPayload for %s: %w", event, err) } task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{ - HookID: w.ID, - Payloader: payloader, - EventType: event, + HookID: w.ID, + PayloadContent: string(payload), + EventType: event, + PayloadVersion: 2, }) if err != nil { - return fmt.Errorf("CreateHookTask: %w", err) + return fmt.Errorf("CreateHookTask for %s: %w", event, err) } return enqueueHookTask(task.ID) diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 338b94360..5f5c14623 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -77,7 +77,3 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { unittest.AssertNotExistsBean(t, hookTask) } } - -// TODO TestHookTask_deliver - -// TODO TestDeliverHooks diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 80245c7e7..46e7856ec 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -4,11 +4,13 @@ package webhook import ( + "context" "fmt" + "net/http" "strings" + webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -28,20 +30,8 @@ type ( } ) -// SetSecret sets the Wechatwork secret -func (f *WechatworkPayload) SetSecret(_ string) {} - -// JSONPayload Marshals the WechatworkPayload to json -func (f *WechatworkPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(f, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -func newWechatworkMarkdownPayload(title string) *WechatworkPayload { - return &WechatworkPayload{ +func newWechatworkMarkdownPayload(title string) WechatworkPayload { + return WechatworkPayload{ Msgtype: "markdown", Markdown: struct { Content string `json:"content"` @@ -51,10 +41,8 @@ func newWechatworkMarkdownPayload(title string) *WechatworkPayload { } } -var _ PayloadConvertor = &WechatworkPayload{} - // Create implements PayloadConvertor Create method -func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Create(p *api.CreatePayload) (WechatworkPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -63,7 +51,7 @@ func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error) } // Delete implements PayloadConvertor Delete method -func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Delete(p *api.DeletePayload) (WechatworkPayload, error) { // created tag/branch refName := git.RefName(p.Ref).ShortName() title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -72,14 +60,14 @@ func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) } // Fork implements PayloadConvertor Fork method -func (f *WechatworkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Fork(p *api.ForkPayload) (WechatworkPayload, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return newWechatworkMarkdownPayload(title), nil } // Push implements PayloadConvertor Push method -func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error) { var ( branchName = git.RefName(p.Ref).ShortName() commitDesc string @@ -108,7 +96,7 @@ func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) { } // Issue implements PayloadConvertor Issue method -func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Issue(p *api.IssuePayload) (WechatworkPayload, error) { text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true) var content string content += fmt.Sprintf(" >%s\n >%s \n > %s \n [%s](%s)", text, attachmentText, issueTitle, p.Issue.HTMLURL, p.Issue.HTMLURL) @@ -117,7 +105,7 @@ func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { } // IssueComment implements PayloadConvertor IssueComment method -func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) IssueComment(p *api.IssueCommentPayload) (WechatworkPayload, error) { text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true) var content string content += fmt.Sprintf(" >%s\n >%s \n >%s \n [%s](%s)", text, p.Comment.Body, issueTitle, p.Comment.HTMLURL, p.Comment.HTMLURL) @@ -126,7 +114,7 @@ func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloa } // PullRequest implements PayloadConvertor PullRequest method -func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) PullRequest(p *api.PullRequestPayload) (WechatworkPayload, error) { text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true) pr := fmt.Sprintf("> %s \r\n > %s \r\n > %s \r\n", text, issueTitle, attachmentText) @@ -135,13 +123,13 @@ func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloade } // Review implements PayloadConvertor Review method -func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) { +func (wc wechatworkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (WechatworkPayload, error) { var text, title string switch p.Action { case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { - return nil, err + return WechatworkPayload{}, err } title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) text = p.Review.Content @@ -151,7 +139,7 @@ func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_modu } // Repository implements PayloadConvertor Repository method -func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Repository(p *api.RepositoryPayload) (WechatworkPayload, error) { var title string switch p.Action { case api.HookRepoCreated: @@ -162,30 +150,33 @@ func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, return newWechatworkMarkdownPayload(title), nil } - return nil, nil + return WechatworkPayload{}, nil } // Wiki implements PayloadConvertor Wiki method -func (f *WechatworkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Wiki(p *api.WikiPayload) (WechatworkPayload, error) { text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true) return newWechatworkMarkdownPayload(text), nil } // Release implements PayloadConvertor Release method -func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Release(p *api.ReleasePayload) (WechatworkPayload, error) { text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) return newWechatworkMarkdownPayload(text), nil } -func (f *WechatworkPayload) Package(p *api.PackagePayload) (api.Payloader, error) { +func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, error) { text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true) return newWechatworkMarkdownPayload(text), nil } -// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload -func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) { - return convertPayloader(new(WechatworkPayload), p, event) +type wechatworkConvertor struct{} + +var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{} + +func newWechatworkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + return newJSONRequest(wechatworkConvertor{}, w, t, true) } diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl index 3c21a4242..4e0f0e9c3 100644 --- a/templates/repo/settings/webhook/history.tmpl +++ b/templates/repo/settings/webhook/history.tmpl @@ -19,6 +19,8 @@ {{if .IsSucceed}} {{svg "octicon-check"}} + {{else if not .IsDelivered}} + {{svg "octicon-stopwatch"}} {{else}} {{svg "octicon-alert"}} {{end}} @@ -62,7 +64,7 @@ {{range $key, $val := .RequestInfo.Headers}}{{$key}}: {{$val}} {{end}} {{ctx.Locale.Tr "repo.settings.webhook.payload"}} - {{.PayloadContent}} + {{or .RequestInfo.Body .PayloadContent}} {{else}} - {{end}}
{{.PayloadContent}}
{{or .RequestInfo.Body .PayloadContent}}