code.gitea.io/gitea@v1.22.3/services/webhook/deliver_test.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package webhook 5 6 import ( 7 "context" 8 "io" 9 "net/http" 10 "net/http/httptest" 11 "net/url" 12 "strings" 13 "testing" 14 "time" 15 16 "code.gitea.io/gitea/models/db" 17 "code.gitea.io/gitea/models/unittest" 18 webhook_model "code.gitea.io/gitea/models/webhook" 19 "code.gitea.io/gitea/modules/hostmatcher" 20 "code.gitea.io/gitea/modules/setting" 21 "code.gitea.io/gitea/modules/util" 22 webhook_module "code.gitea.io/gitea/modules/webhook" 23 24 "github.com/stretchr/testify/assert" 25 "github.com/stretchr/testify/require" 26 ) 27 28 func TestWebhookProxy(t *testing.T) { 29 oldWebhook := setting.Webhook 30 t.Cleanup(func() { 31 setting.Webhook = oldWebhook 32 }) 33 34 setting.Webhook.ProxyURL = "http://localhost:8080" 35 setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL) 36 setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"} 37 38 allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com") 39 40 tests := []struct { 41 req string 42 want string 43 wantErr bool 44 }{ 45 { 46 req: "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx", 47 want: "http://localhost:8080", 48 wantErr: false, 49 }, 50 { 51 req: "http://s.discordapp.com/assets/xxxxxx", 52 want: "http://localhost:8080", 53 wantErr: false, 54 }, 55 { 56 req: "http://github.com/a/b", 57 want: "", 58 wantErr: false, 59 }, 60 { 61 req: "http://www.discordapp.com/assets/xxxxxx", 62 want: "", 63 wantErr: true, 64 }, 65 } 66 for _, tt := range tests { 67 t.Run(tt.req, func(t *testing.T) { 68 req, err := http.NewRequest("POST", tt.req, nil) 69 require.NoError(t, err) 70 71 u, err := webhookProxy(allowedHostMatcher)(req) 72 if tt.wantErr { 73 assert.Error(t, err) 74 return 75 } 76 77 assert.NoError(t, err) 78 79 got := "" 80 if u != nil { 81 got = u.String() 82 } 83 assert.Equal(t, tt.want, got) 84 }) 85 } 86 } 87 88 func TestWebhookDeliverAuthorizationHeader(t *testing.T) { 89 assert.NoError(t, unittest.PrepareTestDatabase()) 90 91 done := make(chan struct{}, 1) 92 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 93 assert.Equal(t, "/webhook", r.URL.Path) 94 assert.Equal(t, "Bearer s3cr3t-t0ken", r.Header.Get("Authorization")) 95 w.WriteHeader(200) 96 done <- struct{}{} 97 })) 98 t.Cleanup(s.Close) 99 100 hook := &webhook_model.Webhook{ 101 RepoID: 3, 102 URL: s.URL + "/webhook", 103 ContentType: webhook_model.ContentTypeJSON, 104 IsActive: true, 105 Type: webhook_module.GITEA, 106 } 107 err := hook.SetHeaderAuthorization("Bearer s3cr3t-t0ken") 108 assert.NoError(t, err) 109 assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook)) 110 111 hookTask := &webhook_model.HookTask{ 112 HookID: hook.ID, 113 EventType: webhook_module.HookEventPush, 114 PayloadVersion: 2, 115 } 116 117 hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask) 118 assert.NoError(t, err) 119 assert.NotNil(t, hookTask) 120 121 assert.NoError(t, Deliver(context.Background(), hookTask)) 122 select { 123 case <-done: 124 case <-time.After(5 * time.Second): 125 t.Fatal("waited to long for request to happen") 126 } 127 128 assert.True(t, hookTask.IsSucceed) 129 assert.Equal(t, "******", hookTask.RequestInfo.Headers["Authorization"]) 130 } 131 132 func TestWebhookDeliverHookTask(t *testing.T) { 133 assert.NoError(t, unittest.PrepareTestDatabase()) 134 135 done := make(chan struct{}, 1) 136 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 137 assert.Equal(t, "PUT", r.Method) 138 switch r.URL.Path { 139 case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98": 140 // Version 1 141 assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) 142 assert.Equal(t, "", r.Header.Get("Content-Type")) 143 body, err := io.ReadAll(r.Body) 144 assert.NoError(t, err) 145 assert.Equal(t, `{"data": 42}`, string(body)) 146 147 case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51": 148 // Version 2 149 assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) 150 assert.Equal(t, "application/json", r.Header.Get("Content-Type")) 151 body, err := io.ReadAll(r.Body) 152 assert.NoError(t, err) 153 assert.Len(t, body, 2147) 154 155 default: 156 w.WriteHeader(404) 157 t.Fatalf("unexpected url path %s", r.URL.Path) 158 return 159 } 160 w.WriteHeader(200) 161 done <- struct{}{} 162 })) 163 t.Cleanup(s.Close) 164 165 hook := &webhook_model.Webhook{ 166 RepoID: 3, 167 IsActive: true, 168 Type: webhook_module.MATRIX, 169 URL: s.URL + "/webhook", 170 HTTPMethod: "PUT", 171 ContentType: webhook_model.ContentTypeJSON, 172 Meta: `{"message_type":0}`, // text 173 } 174 assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook)) 175 176 t.Run("Version 1", func(t *testing.T) { 177 hookTask := &webhook_model.HookTask{ 178 HookID: hook.ID, 179 EventType: webhook_module.HookEventPush, 180 PayloadContent: `{"data": 42}`, 181 PayloadVersion: 1, 182 } 183 184 hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask) 185 assert.NoError(t, err) 186 assert.NotNil(t, hookTask) 187 188 assert.NoError(t, Deliver(context.Background(), hookTask)) 189 select { 190 case <-done: 191 case <-time.After(5 * time.Second): 192 t.Fatal("waited to long for request to happen") 193 } 194 195 assert.True(t, hookTask.IsSucceed) 196 }) 197 198 t.Run("Version 2", func(t *testing.T) { 199 p := pushTestPayload() 200 data, err := p.JSONPayload() 201 assert.NoError(t, err) 202 203 hookTask := &webhook_model.HookTask{ 204 HookID: hook.ID, 205 EventType: webhook_module.HookEventPush, 206 PayloadContent: string(data), 207 PayloadVersion: 2, 208 } 209 210 hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask) 211 assert.NoError(t, err) 212 assert.NotNil(t, hookTask) 213 214 assert.NoError(t, Deliver(context.Background(), hookTask)) 215 select { 216 case <-done: 217 case <-time.After(5 * time.Second): 218 t.Fatal("waited to long for request to happen") 219 } 220 221 assert.True(t, hookTask.IsSucceed) 222 }) 223 } 224 225 func TestWebhookDeliverSpecificTypes(t *testing.T) { 226 assert.NoError(t, unittest.PrepareTestDatabase()) 227 228 type hookCase struct { 229 gotBody chan []byte 230 httpMethod string // default to POST 231 } 232 233 cases := map[string]*hookCase{ 234 webhook_module.SLACK: {}, 235 webhook_module.DISCORD: {}, 236 webhook_module.DINGTALK: {}, 237 webhook_module.TELEGRAM: {}, 238 webhook_module.MSTEAMS: {}, 239 webhook_module.FEISHU: {}, 240 webhook_module.MATRIX: {httpMethod: "PUT"}, 241 webhook_module.WECHATWORK: {}, 242 webhook_module.PACKAGIST: {}, 243 } 244 245 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 246 typ := strings.Split(r.URL.Path, "/")[1] // URL: "/{webhook_type}/other-path" 247 assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path) 248 assert.Equal(t, util.IfZero(cases[typ].httpMethod, "POST"), r.Method, "webhook test request %q", r.URL.Path) 249 body, _ := io.ReadAll(r.Body) // read request and send it back to the test by testcase's chan 250 cases[typ].gotBody <- body 251 w.WriteHeader(http.StatusNoContent) 252 })) 253 t.Cleanup(s.Close) 254 255 p := pushTestPayload() 256 data, err := p.JSONPayload() 257 assert.NoError(t, err) 258 259 for typ := range cases { 260 cases[typ].gotBody = make(chan []byte, 1) 261 t.Run(typ, func(t *testing.T) { 262 t.Parallel() 263 hook := &webhook_model.Webhook{ 264 RepoID: 3, 265 IsActive: true, 266 Type: typ, 267 URL: s.URL + "/" + typ, 268 Meta: "{}", 269 } 270 assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook)) 271 272 hookTask := &webhook_model.HookTask{ 273 HookID: hook.ID, 274 EventType: webhook_module.HookEventPush, 275 PayloadContent: string(data), 276 PayloadVersion: 2, 277 } 278 279 hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask) 280 assert.NoError(t, err) 281 assert.NotNil(t, hookTask) 282 283 assert.NoError(t, Deliver(context.Background(), hookTask)) 284 285 select { 286 case gotBody := <-cases[typ].gotBody: 287 assert.NotEqual(t, string(data), string(gotBody), "request body must be different from the event payload") 288 assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "delivered webhook payload doesn't match saved request") 289 case <-time.After(5 * time.Second): 290 t.Fatal("waited to long for request to happen") 291 } 292 293 assert.True(t, hookTask.IsSucceed) 294 }) 295 } 296 }