Files
mattermost/server/channels/app/webhook_test.go

875 lines
27 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
)
func TestCreateIncomingWebhookForChannel(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
type TestCase struct {
EnableIncomingHooks bool
EnablePostUsernameOverride bool
EnablePostIconOverride bool
IncomingWebhook model.IncomingWebhook
ExpectedError bool
ExpectedIncomingWebhook *model.IncomingWebhook
}
for name, tc := range map[string]TestCase{
"webhooks not enabled": {
EnableIncomingHooks: false,
EnablePostUsernameOverride: false,
EnablePostIconOverride: false,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
},
ExpectedError: true,
ExpectedIncomingWebhook: nil,
},
"valid: username and post icon url ignored, since override not enabled": {
EnableIncomingHooks: true,
EnablePostUsernameOverride: false,
EnablePostIconOverride: false,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
Username: ":invalid and ignored:",
IconURL: "ignored",
},
ExpectedError: false,
ExpectedIncomingWebhook: &model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
},
},
"invalid username, override enabled": {
EnableIncomingHooks: true,
EnablePostUsernameOverride: true,
EnablePostIconOverride: false,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
Username: ":invalid:",
},
ExpectedError: true,
ExpectedIncomingWebhook: nil,
},
"valid, no username or post icon url provided": {
EnableIncomingHooks: true,
EnablePostUsernameOverride: true,
EnablePostIconOverride: true,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
},
ExpectedError: false,
ExpectedIncomingWebhook: &model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
},
},
"valid, with username and post icon": {
EnableIncomingHooks: true,
EnablePostUsernameOverride: true,
EnablePostIconOverride: true,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
Username: "valid",
IconURL: "http://example.com/icon",
},
ExpectedError: false,
ExpectedIncomingWebhook: &model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
Username: "valid",
IconURL: "http://example.com/icon",
},
},
} {
t.Run(name, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = tc.EnableIncomingHooks })
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnablePostUsernameOverride = tc.EnablePostUsernameOverride
})
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostIconOverride = tc.EnablePostIconOverride })
createdHook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &tc.IncomingWebhook)
if tc.ExpectedError {
require.NotNil(t, err, "should have failed")
} else {
require.Nil(t, err, "should not have failed")
}
if createdHook != nil {
defer th.App.DeleteIncomingWebhook(createdHook.Id)
}
if tc.ExpectedIncomingWebhook == nil {
assert.Nil(t, createdHook, "expected nil webhook")
} else if assert.NotNil(t, createdHook, "expected non-nil webhook") {
assert.Equal(t, tc.ExpectedIncomingWebhook.DisplayName, createdHook.DisplayName)
assert.Equal(t, tc.ExpectedIncomingWebhook.Description, createdHook.Description)
assert.Equal(t, tc.ExpectedIncomingWebhook.ChannelId, createdHook.ChannelId)
assert.Equal(t, tc.ExpectedIncomingWebhook.Username, createdHook.Username)
assert.Equal(t, tc.ExpectedIncomingWebhook.IconURL, createdHook.IconURL)
}
})
}
}
func TestUpdateIncomingWebhook(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
type TestCase struct {
EnableIncomingHooks bool
EnablePostUsernameOverride bool
EnablePostIconOverride bool
IncomingWebhook model.IncomingWebhook
ExpectedError bool
ExpectedIncomingWebhook *model.IncomingWebhook
}
for name, tc := range map[string]TestCase{
"webhooks not enabled": {
EnableIncomingHooks: false,
EnablePostUsernameOverride: false,
EnablePostIconOverride: false,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
},
ExpectedError: true,
ExpectedIncomingWebhook: nil,
},
"valid: username and post icon url ignored, since override not enabled": {
EnableIncomingHooks: true,
EnablePostUsernameOverride: false,
EnablePostIconOverride: false,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
Username: ":invalid and ignored:",
IconURL: "ignored",
},
ExpectedError: false,
ExpectedIncomingWebhook: &model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
},
},
"invalid username, override enabled": {
EnableIncomingHooks: true,
EnablePostUsernameOverride: true,
EnablePostIconOverride: false,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
Username: ":invalid:",
},
ExpectedError: true,
ExpectedIncomingWebhook: nil,
},
"valid, no username or post icon url provided": {
EnableIncomingHooks: true,
EnablePostUsernameOverride: true,
EnablePostIconOverride: true,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
},
ExpectedError: false,
ExpectedIncomingWebhook: &model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
},
},
"valid, with username and post icon": {
EnableIncomingHooks: true,
EnablePostUsernameOverride: true,
EnablePostIconOverride: true,
IncomingWebhook: model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
Username: "valid",
IconURL: "http://example.com/icon",
},
ExpectedError: false,
ExpectedIncomingWebhook: &model.IncomingWebhook{
DisplayName: "title",
Description: "description",
ChannelId: th.BasicChannel.Id,
Username: "valid",
IconURL: "http://example.com/icon",
},
},
} {
t.Run(name, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{
ChannelId: th.BasicChannel.Id,
})
require.Nil(t, err)
defer th.App.DeleteIncomingWebhook(hook.Id)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = tc.EnableIncomingHooks })
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnablePostUsernameOverride = tc.EnablePostUsernameOverride
})
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostIconOverride = tc.EnablePostIconOverride })
updatedHook, err := th.App.UpdateIncomingWebhook(hook, &tc.IncomingWebhook)
if tc.ExpectedError {
require.NotNil(t, err, "should have failed")
} else {
require.Nil(t, err, "should not have failed")
}
if tc.ExpectedIncomingWebhook == nil {
assert.Nil(t, updatedHook, "expected nil webhook")
} else if assert.NotNil(t, updatedHook, "expected non-nil webhook") {
assert.Equal(t, tc.ExpectedIncomingWebhook.DisplayName, updatedHook.DisplayName)
assert.Equal(t, tc.ExpectedIncomingWebhook.Description, updatedHook.Description)
assert.Equal(t, tc.ExpectedIncomingWebhook.ChannelId, updatedHook.ChannelId)
assert.Equal(t, tc.ExpectedIncomingWebhook.Username, updatedHook.Username)
assert.Equal(t, tc.ExpectedIncomingWebhook.IconURL, updatedHook.IconURL)
}
})
}
}
func TestCreateWebhookPost(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
require.Nil(t, err)
defer th.App.DeleteIncomingWebhook(hook.Id)
post, err := th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "text",
},
},
"webhook_display_name": hook.DisplayName,
}, model.PostTypeSlackAttachment, "")
require.Nil(t, err)
assert.Contains(t, post.GetProps(), "from_webhook", "missing from_webhook prop")
assert.Contains(t, post.GetProps(), "attachments", "missing attachments prop")
assert.Contains(t, post.GetProps(), "webhook_display_name", "missing webhook_display_name prop")
_, err = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", nil, model.PostTypeSystemGeneric, "")
require.NotNil(t, err, "Should have failed - bad post type")
expectedText := "`<>|<>|`"
post, err = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "text",
},
},
"webhook_display_name": hook.DisplayName,
}, model.PostTypeSlackAttachment, "")
require.Nil(t, err)
assert.Equal(t, expectedText, post.Message)
expectedText = "< | \n|\n>"
post, err = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "text",
},
},
"webhook_display_name": hook.DisplayName,
}, model.PostTypeSlackAttachment, "")
require.Nil(t, err)
assert.Equal(t, expectedText, post.Message)
expectedText = `commit bc95839e4a430ace453e8b209a3723c000c1729a
Author: foo <foo@example.org>
Date: Thu Mar 1 19:46:54 2018 +0300
commit message 2
test | 1 +
1 file changed, 1 insertion(+)
commit 5df78b7139b543997838071cd912e375d8bd69b2
Author: foo <foo@example.org>
Date: Thu Mar 1 19:46:48 2018 +0300
commit message 1
test | 3 +++
1 file changed, 3 insertions(+)`
post, err = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Text: "text",
},
},
"webhook_display_name": hook.DisplayName,
}, model.PostTypeSlackAttachment, "")
require.Nil(t, err)
assert.Equal(t, expectedText, post.Message)
t.Run("should set webhook creator status to online", func(t *testing.T) {
testCluster := &testlib.FakeClusterInterface{}
th.Server.Platform().SetCluster(testCluster)
defer th.Server.Platform().SetCluster(nil)
testCluster.ClearMessages()
_, appErr := th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "text", "", "", "", model.StringInterface{}, model.PostTypeDefault, "")
require.Nil(t, appErr)
msgs := testCluster.GetMessages()
// The first message is ClusterEventInvalidateCacheForChannelByName so we skip it
ev, err1 := model.WebSocketEventFromJSON(bytes.NewReader(msgs[1].Data))
require.NoError(t, err1)
require.Equal(t, model.WebsocketEventPosted, ev.EventType())
assert.Equal(t, false, ev.GetData()["set_online"])
})
}
func TestCreateWebhookPostLinks(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
require.Nil(t, err)
defer th.App.DeleteIncomingWebhook(hook.Id)
for name, tc := range map[string]struct {
input string
expectedOutput string
}{
"if statement": {
input: "`if(status_int < QUERY_UNKNOWN || status_int >= QUERY_STATUS_MAX)`",
expectedOutput: "`if(status_int < QUERY_UNKNOWN || status_int >= QUERY_STATUS_MAX)`",
},
"angle bracket link": {
input: "<https://mattermost.com|Mattermost>",
expectedOutput: "[Mattermost](https://mattermost.com)",
},
} {
t.Run(name, func(t *testing.T) {
post, err := th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, tc.input, "", "", "", model.StringInterface{}, "", "")
require.Nil(t, err)
require.Equal(t, tc.expectedOutput, post.Message)
})
}
}
func TestSplitWebhookPost(t *testing.T) {
type TestCase struct {
Post *model.Post
Expected []*model.Post
}
maxPostSize := 10000
for name, tc := range map[string]TestCase{
"LongPost": {
Post: &model.Post{
Message: strings.Repeat("本", maxPostSize*3/2),
},
Expected: []*model.Post{
{
Message: strings.Repeat("本", maxPostSize),
},
{
Message: strings.Repeat("本", maxPostSize/2),
},
},
},
"LongPostAndMultipleAttachments": {
Post: &model.Post{
Message: strings.Repeat("本", maxPostSize*3/2),
Props: map[string]any{
"attachments": []*model.SlackAttachment{
{
Text: strings.Repeat("本", 1000),
},
{
Text: strings.Repeat("本", 2000),
},
{
Text: strings.Repeat("本", model.PostPropsMaxUserRunes-1000),
},
},
},
},
Expected: []*model.Post{
{
Message: strings.Repeat("本", maxPostSize),
},
{
Message: strings.Repeat("本", maxPostSize/2),
Props: map[string]any{
"attachments": []*model.SlackAttachment{
{
Text: strings.Repeat("本", 1000),
},
{
Text: strings.Repeat("本", 2000),
},
},
},
},
{
Props: map[string]any{
"attachments": []*model.SlackAttachment{
{
Text: strings.Repeat("本", model.PostPropsMaxUserRunes-1000),
},
},
},
},
},
},
"UnsplittableProps": {
Post: &model.Post{
Message: "foo",
Props: map[string]any{
"foo": strings.Repeat("x", model.PostPropsMaxUserRunes*2),
},
},
},
} {
t.Run(name, func(t *testing.T) {
splits, err := SplitWebhookPost(tc.Post, maxPostSize)
if tc.Expected == nil {
require.NotNil(t, err)
} else {
require.Nil(t, err)
}
assert.Equal(t, len(tc.Expected), len(splits))
for i, split := range splits {
if i < len(tc.Expected) {
assert.Equal(t, tc.Expected[i].Message, split.Message)
assert.Equal(t, tc.Expected[i].GetProp("attachments"), split.GetProp("attachments"))
}
}
})
}
}
func makePost(message int, attachments []int) *model.Post {
var props model.StringInterface
if len(attachments) > 0 {
sa := make([]*model.SlackAttachment, 0, len(attachments))
for _, a := range attachments {
attach := &model.SlackAttachment{
Text: strings.Repeat("那", a),
}
sa = append(sa, attach)
}
props = map[string]any{"attachments": sa}
}
post := &model.Post{
Message: strings.Repeat("那", message),
Props: props,
}
return post
}
func TestSplitWebhookPostAttachments(t *testing.T) {
maxPostSize := 10000
testCases := []struct {
name string
post *model.Post
expected []*model.Post
}{
{
// makePost(messageLength, []int{attachmentLength, ...})
name: "no split",
post: makePost(10, []int{100, 150, 200}),
expected: []*model.Post{makePost(10, []int{100, 150, 200})},
},
{
name: "split into 2",
post: makePost(maxPostSize-1, []int{model.PostPropsMaxUserRunes * 3 / 4, model.PostPropsMaxUserRunes * 1 / 4}),
expected: []*model.Post{
makePost(maxPostSize-1, []int{model.PostPropsMaxUserRunes * 3 / 4}),
makePost(0, []int{model.PostPropsMaxUserRunes * 1 / 4}),
},
},
{
name: "split into 3",
post: makePost(maxPostSize*3/2, []int{1000, 2000, model.PostPropsMaxUserRunes - 1000}),
expected: []*model.Post{
makePost(maxPostSize, nil),
makePost(maxPostSize/2, []int{1000, 2000}),
makePost(0, []int{model.PostPropsMaxUserRunes - 1000}),
},
},
{
name: "MM-24644 split into 3",
post: makePost(maxPostSize*3/2, []int{5150, 2000, model.PostPropsMaxUserRunes - 1000}),
expected: []*model.Post{
makePost(maxPostSize, nil),
makePost(maxPostSize/2, []int{5150, 2000}),
makePost(0, []int{model.PostPropsMaxUserRunes - 1000}),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
splits, err := SplitWebhookPost(tc.post, maxPostSize)
if tc.expected == nil {
require.NotNil(t, err)
} else {
require.Nil(t, err)
}
assert.Equal(t, len(tc.expected), len(splits))
for i, split := range splits {
if i < len(tc.expected) {
assert.Equal(t, tc.expected[i].Message, split.Message, i)
assert.Equal(t, tc.expected[i].GetProp("attachments"), split.GetProp("attachments"), i)
}
}
})
}
}
func TestCreateOutGoingWebhookWithUsernameAndIconURL(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
outgoingWebhook := model.OutgoingWebhook{
ChannelId: th.BasicChannel.Id,
TeamId: th.BasicChannel.TeamId,
CallbackURLs: []string{"http://nowhere.com"},
Username: "some-user-name",
IconURL: "http://some-icon/",
DisplayName: "some-display-name",
Description: "some-description",
CreatorId: th.BasicUser.Id,
}
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true })
createdHook, err := th.App.CreateOutgoingWebhook(&outgoingWebhook)
require.Nil(t, err)
assert.NotNil(t, createdHook, "should not be null")
assert.Equal(t, createdHook.ChannelId, outgoingWebhook.ChannelId)
assert.Equal(t, createdHook.TeamId, outgoingWebhook.TeamId)
assert.Equal(t, createdHook.CallbackURLs, outgoingWebhook.CallbackURLs)
assert.Equal(t, createdHook.Username, outgoingWebhook.Username)
assert.Equal(t, createdHook.IconURL, outgoingWebhook.IconURL)
assert.Equal(t, createdHook.DisplayName, outgoingWebhook.DisplayName)
assert.Equal(t, createdHook.Description, outgoingWebhook.Description)
}
func TestTriggerOutGoingWebhookWithUsernameAndIconURL(t *testing.T) {
getPayload := func(hook *model.OutgoingWebhook, th *TestHelper, channel *model.Channel) *model.OutgoingWebhookPayload {
return &model.OutgoingWebhookPayload{
Token: hook.Token,
TeamId: hook.TeamId,
TeamDomain: th.BasicTeam.Name,
ChannelId: channel.Id,
ChannelName: channel.Name,
Timestamp: th.BasicPost.CreateAt,
UserId: th.BasicPost.UserId,
UserName: th.BasicUser.Username,
PostId: th.BasicPost.Id,
Text: th.BasicPost.Message,
TriggerWord: "Abracadabra",
FileIds: strings.Join(th.BasicPost.FileIds, ","),
}
}
waitUntilWebhookResponseIsCreatedAsPost := func(channel *model.Channel, th *TestHelper, createdPost chan *model.Post) {
go func() {
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
posts, _ := th.App.GetPosts(channel.Id, 0, 5)
if len(posts.Posts) > 0 {
for _, post := range posts.Posts {
createdPost <- post
return
}
}
}
}()
}
type TestCaseOutgoing struct {
EnablePostUsernameOverride bool
EnablePostIconOverride bool
ExpectedUsername string
ExpectedIconURL string
WebhookResponse *model.OutgoingWebhookResponse
}
createOutgoingWebhook := func(channel *model.Channel, testCallBackURL string, th *TestHelper) (*model.OutgoingWebhook, *model.AppError) {
outgoingWebhook := model.OutgoingWebhook{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CallbackURLs: []string{testCallBackURL},
Username: "some-user-name",
IconURL: "http://some-icon/",
DisplayName: "some-display-name",
Description: "some-description",
CreatorId: th.BasicUser.Id,
TriggerWords: []string{"Abracadabra"},
ContentType: "application/json",
}
return th.App.CreateOutgoingWebhook(&outgoingWebhook)
}
getTestCases := func() map[string]TestCaseOutgoing {
webHookResponse := "sample response text from test server"
testCasesOutgoing := map[string]TestCaseOutgoing{
"Should override username and Icon": {
EnablePostUsernameOverride: true,
EnablePostIconOverride: true,
ExpectedUsername: "some-user-name",
ExpectedIconURL: "http://some-icon/",
},
"Should not override username and Icon": {
EnablePostUsernameOverride: false,
EnablePostIconOverride: false,
},
"Should not override username and Icon if the webhook response already has it": {
EnablePostUsernameOverride: true,
EnablePostIconOverride: true,
ExpectedUsername: "webhookuser",
ExpectedIconURL: "http://webhook/icon",
WebhookResponse: &model.OutgoingWebhookResponse{Text: &webHookResponse, Username: "webhookuser", IconURL: "http://webhook/icon"},
},
}
return testCasesOutgoing
}
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
})
createdPost := make(chan *model.Post)
for name, testCase := range getTestCases() {
t.Run(name, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableOutgoingWebhooks = true
*cfg.ServiceSettings.EnablePostUsernameOverride = testCase.EnablePostUsernameOverride
*cfg.ServiceSettings.EnablePostIconOverride = testCase.EnablePostIconOverride
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if testCase.WebhookResponse != nil {
js, jsonErr := json.Marshal(testCase.WebhookResponse)
require.NoError(t, jsonErr)
w.Write(js)
} else {
w.Write([]byte(`{"text": "sample response text from test server"}`))
}
}))
defer ts.Close()
channel := th.CreateChannel(th.Context, th.BasicTeam)
hook, _ := createOutgoingWebhook(channel, ts.URL, th)
payload := getPayload(hook, th, channel)
th.App.TriggerWebhook(th.Context, payload, hook, th.BasicPost, channel)
waitUntilWebhookResponseIsCreatedAsPost(channel, th, createdPost)
select {
case webhookPost := <-createdPost:
assert.Equal(t, webhookPost.Message, "sample response text from test server")
assert.Equal(t, webhookPost.GetProp("from_webhook"), "true")
if testCase.ExpectedIconURL != "" {
assert.Equal(t, webhookPost.GetProp("override_icon_url"), testCase.ExpectedIconURL)
} else {
assert.Nil(t, webhookPost.GetProp("override_icon_url"))
}
if testCase.ExpectedUsername != "" {
assert.Equal(t, webhookPost.GetProp("override_username"), testCase.ExpectedUsername)
} else {
assert.Nil(t, webhookPost.GetProp("override_username"))
}
case <-time.After(5 * time.Second):
require.Fail(t, "Timeout, webhook response not created as post")
}
})
}
}
type InfiniteReader struct {
Prefix string
}
func (r InfiniteReader) Read(p []byte) (n int, err error) {
for i := range p {
p[i] = 'a'
}
return len(p), nil
}
func TestDoOutgoingWebhookRequest(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewString("127.0.0.1")
*cfg.ServiceSettings.EnableOutgoingWebhooks = true
})
t.Run("with a valid response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
}))
defer server.Close()
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
require.NoError(t, err)
require.NotNil(t, resp)
assert.NotNil(t, resp.Text)
assert.Equal(t, "Hello, World!", *resp.Text)
})
t.Run("with an invalid response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, strings.NewReader("aaaaaaaa"))
}))
defer server.Close()
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
require.Error(t, err)
require.Equal(t, "api.unmarshal_error", err.(*model.AppError).Id)
})
t.Run("with a large, valid response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, io.MultiReader(strings.NewReader(`{"text": "`), InfiniteReader{}, strings.NewReader(`"}`)))
}))
defer server.Close()
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
require.Error(t, err)
require.Equal(t, "api.unmarshal_error", err.(*model.AppError).Id)
})
t.Run("with a large, invalid response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, InfiniteReader{})
}))
defer server.Close()
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
require.Error(t, err)
require.Equal(t, "api.unmarshal_error", err.(*model.AppError).Id)
})
t.Run("with a slow response", func(t *testing.T) {
releaseHandler := make(chan any)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Don't actually handle the response, allowing the app to timeout.
<-releaseHandler
}))
defer server.Close()
defer close(releaseHandler)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(1)
})
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
require.Error(t, err)
require.IsType(t, &url.Error{}, err)
})
t.Run("with a slow response, long timeout configured", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(1 * time.Second)
io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
}))
defer server.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(2)
})
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
require.NoError(t, err)
require.NotNil(t, resp)
assert.NotNil(t, resp.Text)
assert.Equal(t, "Hello, World!", *resp.Text)
})
t.Run("without response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
}))
defer server.Close()
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
require.NoError(t, err)
require.Nil(t, resp)
})
}