* Content-Type is optional mime.ParseMimeType returns and "no media type" error if the passed string is empty. Given that the Content-Type header is optional we shouldn't return an error in that case, so we're fixing that allowing the users to call the webhook without passing that header * Include webhook id in the error message Given that the number of webhooks could be big the user could need the id to check which one of the multiple webhooks are failing so include the id aids in that part
304 строки
12 KiB
Go
304 строки
12 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
|
)
|
|
|
|
func TestIncomingWebhook(t *testing.T) {
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
if !*th.App.Config().ServiceSettings.EnableIncomingWebhooks {
|
|
_, err := http.Post(ApiClient.Url+"/hooks/123", "", strings.NewReader("123"))
|
|
assert.NotNil(t, err, "should have errored - webhooks turned off")
|
|
return
|
|
}
|
|
|
|
hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
|
|
require.Nil(t, err)
|
|
|
|
url := ApiClient.Url + "/hooks/" + hook.Id
|
|
|
|
tooLongText := ""
|
|
for i := 0; i < 8200; i++ {
|
|
tooLongText += "a"
|
|
}
|
|
|
|
t.Run("WebhookBasics", func(t *testing.T) {
|
|
payload := "payload={\"text\": \"test text\"}"
|
|
resp, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload))
|
|
require.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
payload = "payload={\"text\": \"\"}"
|
|
resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload))
|
|
require.Nil(t, err)
|
|
assert.NotEqual(t, http.StatusOK, resp.StatusCode, "should have errored - no text post")
|
|
|
|
payload = "payload={\"text\": \"test text\", \"channel\": \"junk\"}"
|
|
resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload))
|
|
require.Nil(t, err)
|
|
assert.NotEqual(t, http.StatusOK, resp.StatusCode, "should have errored - bad channel")
|
|
|
|
payload = "payload={\"text\": \"test text\"}"
|
|
resp, err = http.Post(ApiClient.Url+"/hooks/abc123", "application/x-www-form-urlencoded", strings.NewReader(payload))
|
|
require.Nil(t, err)
|
|
assert.NotEqual(t, http.StatusOK, resp.StatusCode, "should have errored - bad hook")
|
|
|
|
resp, err = http.Post(url, "application/json", strings.NewReader("{\"text\":\"this is a test\"}"))
|
|
require.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
text := `this is a \"test\"
|
|
that contains a newline and a tab`
|
|
resp, err = http.Post(url, "application/json", strings.NewReader("{\"text\":\""+text+"\"}"))
|
|
require.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name)))
|
|
require.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"#%s\"}", th.BasicChannel.Name)))
|
|
require.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"@%s\"}", th.BasicUser.Username)))
|
|
require.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("payload={\"text\":\"this is a test\"}"))
|
|
require.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("payload={\"text\":\""+text+"\"}"))
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "AppLicaTion/x-www-Form-urlencoded", strings.NewReader("payload={\"text\":\""+text+"\"}"))
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/x-www-form-urlencoded;charset=utf-8", strings.NewReader("payload={\"text\":\""+text+"\"}"))
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/x-www-form-urlencoded; charset=utf-8", strings.NewReader("payload={\"text\":\""+text+"\"}"))
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/x-www-form-urlencoded wrongtext", strings.NewReader("payload={\"text\":\""+text+"\"}"))
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/json", strings.NewReader("{\"text\":\""+tooLongText+"\"}"))
|
|
require.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("{\"text\":\""+tooLongText+"\"}"))
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "application/json", strings.NewReader("payload={\"text\":\""+text+"\"}"))
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
|
|
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--"
|
|
resp, err = http.Post(ApiClient.Url+"/hooks/"+hook.Id, "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW", strings.NewReader(payloadMultiPart))
|
|
require.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "mimetype/wrong", strings.NewReader("payload={\"text\":\""+text+"\"}"))
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
|
|
resp, err = http.Post(url, "", strings.NewReader("{\"text\":\""+text+"\"}"))
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("WebhookExperimentalReadOnly", func(t *testing.T) {
|
|
th.App.SetLicense(model.NewTestLicense())
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true })
|
|
|
|
// Read only default channel should fail.
|
|
resp, err := http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL)))
|
|
require.Nil(t, err)
|
|
assert.True(t, resp.StatusCode != http.StatusOK)
|
|
|
|
// None-default channel should still work.
|
|
resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name)))
|
|
require.Nil(t, err)
|
|
assert.True(t, resp.StatusCode == http.StatusOK)
|
|
|
|
// System-Admin Owned Hook
|
|
adminHook, err := th.App.CreateIncomingWebhookForChannel(th.SystemAdminUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
|
|
require.Nil(t, err)
|
|
adminUrl := ApiClient.Url + "/hooks/" + adminHook.Id
|
|
|
|
resp, err = http.Post(adminUrl, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL)))
|
|
require.Nil(t, err)
|
|
assert.True(t, resp.StatusCode == http.StatusOK)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = false })
|
|
})
|
|
|
|
t.Run("WebhookAttachments", func(t *testing.T) {
|
|
attachmentPayload := `{
|
|
"text": "this is a test",
|
|
"attachments": [
|
|
{
|
|
"fallback": "Required plain-text summary of the attachment.",
|
|
|
|
"color": "#36a64f",
|
|
|
|
"pretext": "Optional text that appears above the attachment block",
|
|
|
|
"author_name": "Bobby Tables",
|
|
"author_link": "http://flickr.com/bobby/",
|
|
"author_icon": "http://flickr.com/icons/bobby.jpg",
|
|
|
|
"title": "Slack API Documentation",
|
|
"title_link": "https://api.slack.com/",
|
|
|
|
"text": "Optional text that appears within the attachment",
|
|
|
|
"fields": [
|
|
{
|
|
"title": "Priority",
|
|
"value": "High",
|
|
"short": false
|
|
}
|
|
],
|
|
|
|
"image_url": "http://my-website.com/path/to/image.jpg",
|
|
"thumb_url": "http://example.com/path/to/thumb.png"
|
|
}
|
|
]
|
|
}`
|
|
|
|
resp, err := http.Post(url, "application/json", strings.NewReader(attachmentPayload))
|
|
require.Nil(t, err)
|
|
assert.True(t, resp.StatusCode == http.StatusOK)
|
|
|
|
attachmentPayload = `{
|
|
"text": "this is a test",
|
|
"attachments": [
|
|
{
|
|
"fallback": "Required plain-text summary of the attachment.",
|
|
|
|
"color": "#36a64f",
|
|
|
|
"pretext": "Optional text that appears above the attachment block",
|
|
|
|
"author_name": "Bobby Tables",
|
|
"author_link": "http://flickr.com/bobby/",
|
|
"author_icon": "http://flickr.com/icons/bobby.jpg",
|
|
|
|
"title": "Slack API Documentation",
|
|
"title_link": "https://api.slack.com/",
|
|
|
|
"text": "` + tooLongText + `",
|
|
|
|
"fields": [
|
|
{
|
|
"title": "Priority",
|
|
"value": "High",
|
|
"short": false
|
|
}
|
|
],
|
|
|
|
"image_url": "http://my-website.com/path/to/image.jpg",
|
|
"thumb_url": "http://example.com/path/to/thumb.png"
|
|
}
|
|
]
|
|
}`
|
|
|
|
resp, err = http.Post(url, "application/json", strings.NewReader(attachmentPayload))
|
|
require.Nil(t, err)
|
|
assert.True(t, resp.StatusCode == http.StatusOK)
|
|
})
|
|
|
|
t.Run("ChannelLockedWebhook", func(t *testing.T) {
|
|
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)
|
|
require.Nil(t, err)
|
|
|
|
hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, ChannelLocked: true})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, hook)
|
|
|
|
apiHookUrl := ApiClient.Url + "/hooks/" + hook.Id
|
|
|
|
payload := "payload={\"text\": \"test text\"}"
|
|
resp, err2 := http.Post(apiHookUrl, "application/x-www-form-urlencoded", strings.NewReader(payload))
|
|
require.Nil(t, err2)
|
|
assert.True(t, resp.StatusCode == http.StatusOK)
|
|
|
|
resp, err2 = http.Post(apiHookUrl, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name)))
|
|
require.Nil(t, err2)
|
|
assert.True(t, resp.StatusCode == http.StatusOK)
|
|
|
|
resp, err2 = http.Post(apiHookUrl, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", channel.Name)))
|
|
require.Nil(t, err2)
|
|
assert.True(t, resp.StatusCode == http.StatusForbidden)
|
|
})
|
|
|
|
t.Run("DisableWebhooks", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = false })
|
|
resp, err := http.Post(url, "application/json", strings.NewReader("{\"text\":\"this is a test\"}"))
|
|
require.Nil(t, err)
|
|
assert.True(t, resp.StatusCode == http.StatusNotImplemented)
|
|
})
|
|
}
|
|
|
|
func TestCommandWebhooks(t *testing.T) {
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
cmd, appErr := th.App.CreateCommand(&model.Command{
|
|
CreatorId: th.BasicUser.Id,
|
|
TeamId: th.BasicTeam.Id,
|
|
URL: "http://nowhere.com",
|
|
Method: model.COMMAND_METHOD_POST,
|
|
Trigger: "delayed"})
|
|
require.Nil(t, appErr)
|
|
|
|
args := &model.CommandArgs{
|
|
TeamId: th.BasicTeam.Id,
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
}
|
|
|
|
hook, appErr := th.App.CreateCommandWebhook(cmd.Id, args)
|
|
require.Nil(t, appErr)
|
|
|
|
resp, err := http.Post(ApiClient.Url+"/hooks/commands/123123123123", "application/json", bytes.NewBufferString(`{"text":"this is a test"}`))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode, "expected not-found for non-existent hook")
|
|
|
|
resp, err = http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"invalid`))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
|
|
for i := 0; i < 5; i++ {
|
|
response, appErr2 := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`))
|
|
require.Nil(t, appErr2)
|
|
require.Equal(t, http.StatusOK, response.StatusCode)
|
|
}
|
|
|
|
resp, _ = http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`))
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
}
|