You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

deliver_test.go 8.1KB

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