github.com/levb/mattermost-server@v5.3.1+incompatible/web/webhook_test.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package web 5 6 import ( 7 "bytes" 8 "fmt" 9 "net/http" 10 "strings" 11 "testing" 12 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 16 "github.com/mattermost/mattermost-server/model" 17 ) 18 19 func TestIncomingWebhook(t *testing.T) { 20 th := Setup().InitBasic() 21 defer th.TearDown() 22 23 if !th.App.Config().ServiceSettings.EnableIncomingWebhooks { 24 _, err := http.Post(ApiClient.Url+"/hooks/123", "", strings.NewReader("123")) 25 assert.NotNil(t, err, "should have errored - webhooks turned off") 26 return 27 } 28 29 hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id}) 30 require.Nil(t, err) 31 32 url := ApiClient.Url + "/hooks/" + hook.Id 33 34 tooLongText := "" 35 for i := 0; i < 8200; i++ { 36 tooLongText += "a" 37 } 38 39 t.Run("WebhookBasics", func(t *testing.T) { 40 payload := "payload={\"text\": \"test text\"}" 41 resp, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload)) 42 require.Nil(t, err) 43 assert.True(t, resp.StatusCode == http.StatusOK) 44 45 payload = "payload={\"text\": \"\"}" 46 resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload)) 47 require.Nil(t, err) 48 assert.True(t, resp.StatusCode != http.StatusOK, "should have errored - no text to post") 49 50 payload = "payload={\"text\": \"test text\", \"channel\": \"junk\"}" 51 resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload)) 52 require.Nil(t, err) 53 assert.True(t, resp.StatusCode != http.StatusOK, "should have errored - bad channel") 54 55 payload = "payload={\"text\": \"test text\"}" 56 resp, err = http.Post(ApiClient.Url+"/hooks/abc123", "application/x-www-form-urlencoded", strings.NewReader(payload)) 57 require.Nil(t, err) 58 assert.True(t, resp.StatusCode != http.StatusOK, "should have errored - bad hook") 59 60 resp, err = http.Post(url, "application/json", strings.NewReader("{\"text\":\"this is a test\"}")) 61 require.Nil(t, err) 62 assert.True(t, resp.StatusCode == http.StatusOK) 63 64 text := `this is a \"test\" 65 that contains a newline and a tab` 66 resp, err = http.Post(url, "application/json", strings.NewReader("{\"text\":\""+text+"\"}")) 67 require.Nil(t, err) 68 assert.True(t, resp.StatusCode == http.StatusOK) 69 70 resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name))) 71 require.Nil(t, err) 72 assert.True(t, resp.StatusCode == http.StatusOK) 73 74 resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"#%s\"}", th.BasicChannel.Name))) 75 require.Nil(t, err) 76 assert.True(t, resp.StatusCode == http.StatusOK) 77 78 resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"@%s\"}", th.BasicUser.Username))) 79 require.Nil(t, err) 80 assert.True(t, resp.StatusCode == http.StatusOK) 81 82 resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("payload={\"text\":\"this is a test\"}")) 83 require.Nil(t, err) 84 assert.True(t, resp.StatusCode == http.StatusOK) 85 86 resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("payload={\"text\":\""+text+"\"}")) 87 assert.Nil(t, err) 88 assert.True(t, resp.StatusCode == http.StatusOK) 89 90 resp, err = http.Post(url, "application/json", strings.NewReader("{\"text\":\""+tooLongText+"\"}")) 91 require.Nil(t, err) 92 assert.True(t, resp.StatusCode == http.StatusOK) 93 94 payloadMultiPart := "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\nwebhook-bot\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nthis is a test :tada:\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--" 95 resp, err = http.Post(ApiClient.Url+"/hooks/"+hook.Id, "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW", strings.NewReader(payloadMultiPart)) 96 require.Nil(t, err) 97 assert.True(t, resp.StatusCode == http.StatusOK) 98 }) 99 100 t.Run("WebhookExperimentalReadOnly", func(t *testing.T) { 101 th.App.SetLicense(model.NewTestLicense()) 102 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true }) 103 104 // Read only default channel should fail. 105 resp, err := http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL))) 106 require.Nil(t, err) 107 assert.True(t, resp.StatusCode != http.StatusOK) 108 109 // None-default channel should still work. 110 resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name))) 111 require.Nil(t, err) 112 assert.True(t, resp.StatusCode == http.StatusOK) 113 114 // System-Admin Owned Hook 115 adminHook, err := th.App.CreateIncomingWebhookForChannel(th.SystemAdminUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id}) 116 require.Nil(t, err) 117 adminUrl := ApiClient.Url + "/hooks/" + adminHook.Id 118 119 resp, err = http.Post(adminUrl, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL))) 120 require.Nil(t, err) 121 assert.True(t, resp.StatusCode == http.StatusOK) 122 123 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = false }) 124 }) 125 126 t.Run("WebhookAttachments", func(t *testing.T) { 127 attachmentPayload := `{ 128 "text": "this is a test", 129 "attachments": [ 130 { 131 "fallback": "Required plain-text summary of the attachment.", 132 133 "color": "#36a64f", 134 135 "pretext": "Optional text that appears above the attachment block", 136 137 "author_name": "Bobby Tables", 138 "author_link": "http://flickr.com/bobby/", 139 "author_icon": "http://flickr.com/icons/bobby.jpg", 140 141 "title": "Slack API Documentation", 142 "title_link": "https://api.slack.com/", 143 144 "text": "Optional text that appears within the attachment", 145 146 "fields": [ 147 { 148 "title": "Priority", 149 "value": "High", 150 "short": false 151 } 152 ], 153 154 "image_url": "http://my-website.com/path/to/image.jpg", 155 "thumb_url": "http://example.com/path/to/thumb.png" 156 } 157 ] 158 }` 159 160 resp, err := http.Post(url, "application/json", strings.NewReader(attachmentPayload)) 161 require.Nil(t, err) 162 assert.True(t, resp.StatusCode == http.StatusOK) 163 164 attachmentPayload = `{ 165 "text": "this is a test", 166 "attachments": [ 167 { 168 "fallback": "Required plain-text summary of the attachment.", 169 170 "color": "#36a64f", 171 172 "pretext": "Optional text that appears above the attachment block", 173 174 "author_name": "Bobby Tables", 175 "author_link": "http://flickr.com/bobby/", 176 "author_icon": "http://flickr.com/icons/bobby.jpg", 177 178 "title": "Slack API Documentation", 179 "title_link": "https://api.slack.com/", 180 181 "text": "` + tooLongText + `", 182 183 "fields": [ 184 { 185 "title": "Priority", 186 "value": "High", 187 "short": false 188 } 189 ], 190 191 "image_url": "http://my-website.com/path/to/image.jpg", 192 "thumb_url": "http://example.com/path/to/thumb.png" 193 } 194 ] 195 }` 196 197 resp, err = http.Post(url, "application/json", strings.NewReader(attachmentPayload)) 198 require.Nil(t, err) 199 assert.True(t, resp.StatusCode == http.StatusOK) 200 }) 201 202 t.Run("ChannelLockedWebhook", func(t *testing.T) { 203 channel, err := th.App.CreateChannel(&model.Channel{TeamId: th.BasicTeam.Id, Name: model.NewId(), DisplayName: model.NewId(), Type: model.CHANNEL_OPEN, CreatorId: th.BasicUser.Id}, true) 204 require.Nil(t, err) 205 206 hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, ChannelLocked: true}) 207 require.Nil(t, err) 208 209 url := ApiClient.Url + "/hooks/" + hook.Id 210 211 payload := "payload={\"text\": \"test text\"}" 212 resp, err2 := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload)) 213 require.Nil(t, err2) 214 assert.True(t, resp.StatusCode == http.StatusOK) 215 216 resp, err2 = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name))) 217 require.Nil(t, err2) 218 assert.True(t, resp.StatusCode == http.StatusOK) 219 220 resp, err2 = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", channel.Name))) 221 require.Nil(t, err2) 222 assert.True(t, resp.StatusCode == http.StatusForbidden) 223 }) 224 225 t.Run("DisableWebhooks", func(t *testing.T) { 226 th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = false }) 227 resp, err := http.Post(url, "application/json", strings.NewReader("{\"text\":\"this is a test\"}")) 228 require.Nil(t, err) 229 assert.True(t, resp.StatusCode == http.StatusNotImplemented) 230 }) 231 } 232 233 func TestCommandWebhooks(t *testing.T) { 234 th := Setup().InitBasic() 235 defer th.TearDown() 236 237 cmd, err := th.App.CreateCommand(&model.Command{ 238 CreatorId: th.BasicUser.Id, 239 TeamId: th.BasicTeam.Id, 240 URL: "http://nowhere.com", 241 Method: model.COMMAND_METHOD_POST, 242 Trigger: "delayed"}) 243 require.Nil(t, err) 244 245 args := &model.CommandArgs{ 246 TeamId: th.BasicTeam.Id, 247 UserId: th.BasicUser.Id, 248 ChannelId: th.BasicChannel.Id, 249 } 250 251 hook, err := th.App.CreateCommandWebhook(cmd.Id, args) 252 if err != nil { 253 t.Fatal(err) 254 } 255 256 if resp, _ := http.Post(ApiClient.Url+"/hooks/commands/123123123123", "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusNotFound { 257 t.Fatal("expected not-found for non-existent hook") 258 } 259 260 if resp, err := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"invalid`)); err != nil || resp.StatusCode != http.StatusBadRequest { 261 t.Fatal(err) 262 } 263 264 for i := 0; i < 5; i++ { 265 if resp, err := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); err != nil || resp.StatusCode != http.StatusOK { 266 t.Fatal(err) 267 } 268 } 269 270 if resp, _ := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusBadRequest { 271 t.Fatal("expected error for sixth usage") 272 } 273 }