mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* MM-69010: Validate incoming webhook user membership Incoming webhook creation/update did not verify that the assigned user_id had legitimate access to the target team or channel, allowing a team admin to attribute persisted posts to an arbitrary user. Validate that the assigned user can read the target channel and does not hold privileges the requester lacks at creation, re-check channel access when a hook is moved, and require a shared team before a webhook creates a direct message via an @username payload. * MM-69010: Add regression test for owner+channel update Verify that changing both the channel and the supplied user_id in a single update still validates against the retained owner, since the owner is immutable on update. --------- Co-authored-by: Mattermost Build <[email protected]> Co-authored-by: Hanzo Dev <[email protected]>
1273 lines
40 KiB
Go
1273 lines
40 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"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 TestHandleIncomingWebhookRootId(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
|
|
|
hook, appErr := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
|
|
require.Nil(t, appErr)
|
|
defer func() {
|
|
require.Nil(t, th.App.DeleteIncomingWebhook(hook.Id))
|
|
}()
|
|
|
|
root := th.CreatePost(t, th.BasicChannel)
|
|
reply := th.CreatePostReply(t, root)
|
|
otherChannel := th.CreateChannel(t, th.BasicTeam)
|
|
otherPost := th.CreatePost(t, otherChannel)
|
|
|
|
t.Run("creates reply in thread when root_id is the thread root", func(t *testing.T) {
|
|
err := th.App.HandleIncomingWebhook(th.Context, hook.Id, &model.IncomingWebhookRequest{
|
|
Text: "webhook thread reply",
|
|
RootId: root.Id,
|
|
})
|
|
require.Nil(t, err)
|
|
list, err2 := th.App.GetPosts(th.Context, th.BasicChannel.Id, 0, 5)
|
|
require.Nil(t, err2)
|
|
var found *model.Post
|
|
for _, p := range list.Posts {
|
|
if p.Message == "webhook thread reply" {
|
|
found = p
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, found)
|
|
assert.Equal(t, root.Id, found.RootId)
|
|
})
|
|
|
|
t.Run("rejects root_id pointing at a reply post", func(t *testing.T) {
|
|
err := th.App.HandleIncomingWebhook(th.Context, hook.Id, &model.IncomingWebhookRequest{
|
|
Text: "webhook via reply id",
|
|
RootId: reply.Id,
|
|
})
|
|
require.NotNil(t, err)
|
|
assert.Equal(t, "api.post.create_post.root_id.app_error", err.Id)
|
|
assert.Equal(t, http.StatusBadRequest, err.StatusCode)
|
|
})
|
|
|
|
t.Run("rejects non-existent root_id", func(t *testing.T) {
|
|
err := th.App.HandleIncomingWebhook(th.Context, hook.Id, &model.IncomingWebhookRequest{
|
|
Text: "missing root",
|
|
RootId: model.NewId(),
|
|
})
|
|
require.NotNil(t, err)
|
|
assert.Equal(t, "api.post.create_post.root_id.app_error", err.Id)
|
|
assert.Equal(t, http.StatusBadRequest, err.StatusCode)
|
|
})
|
|
|
|
t.Run("rejects root_id in a different channel", func(t *testing.T) {
|
|
err := th.App.HandleIncomingWebhook(th.Context, hook.Id, &model.IncomingWebhookRequest{
|
|
Text: "wrong channel",
|
|
RootId: otherPost.Id,
|
|
})
|
|
require.NotNil(t, err)
|
|
assert.Equal(t, "api.post.create_post.channel_root_id.app_error", err.Id)
|
|
assert.Equal(t, http.StatusBadRequest, err.StatusCode)
|
|
})
|
|
|
|
t.Run("rejects invalid root_id", func(t *testing.T) {
|
|
err := th.App.HandleIncomingWebhook(th.Context, hook.Id, &model.IncomingWebhookRequest{
|
|
Text: "bad id",
|
|
RootId: "not-a-valid-id",
|
|
})
|
|
require.NotNil(t, err)
|
|
assert.Equal(t, "api.context.invalid_param.app_error", err.Id)
|
|
assert.Equal(t, http.StatusBadRequest, err.StatusCode)
|
|
})
|
|
}
|
|
|
|
func TestHandleIncomingWebhookDirectMessage(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
|
|
|
hook, appErr := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, ChannelLocked: false})
|
|
require.Nil(t, appErr)
|
|
defer func() {
|
|
require.Nil(t, th.App.DeleteIncomingWebhook(hook.Id))
|
|
}()
|
|
|
|
t.Run("rejects DM to a user the owner shares no team with", func(t *testing.T) {
|
|
stranger := th.CreateUser(t)
|
|
err := th.App.HandleIncomingWebhook(th.Context, hook.Id, &model.IncomingWebhookRequest{
|
|
Text: "out of team dm",
|
|
ChannelName: "@" + stranger.Username,
|
|
})
|
|
require.NotNil(t, err)
|
|
assert.Equal(t, http.StatusForbidden, err.StatusCode)
|
|
})
|
|
|
|
t.Run("allows DM to a user the owner shares a team with", func(t *testing.T) {
|
|
err := th.App.HandleIncomingWebhook(th.Context, hook.Id, &model.IncomingWebhookRequest{
|
|
Text: "team dm",
|
|
ChannelName: "@" + th.BasicUser2.Username,
|
|
})
|
|
require.Nil(t, err)
|
|
})
|
|
}
|
|
|
|
func TestCreateIncomingWebhookForChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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, appErr := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &tc.IncomingWebhook)
|
|
if tc.ExpectedError {
|
|
require.NotNil(t, appErr, "should have failed")
|
|
} else {
|
|
require.Nil(t, appErr, "should not have failed")
|
|
}
|
|
if createdHook != nil {
|
|
defer func() {
|
|
appErr := th.App.DeleteIncomingWebhook(createdHook.Id)
|
|
require.Nil(t, appErr, "Error cleaning up webhook")
|
|
}()
|
|
}
|
|
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) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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, appErr := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{
|
|
ChannelId: th.BasicChannel.Id,
|
|
})
|
|
require.Nil(t, appErr)
|
|
defer func() {
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
|
appErr = th.App.DeleteIncomingWebhook(hook.Id)
|
|
require.Nil(t, appErr, "Error cleaning up webhook")
|
|
}()
|
|
|
|
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, appErr := th.App.UpdateIncomingWebhook(hook, &tc.IncomingWebhook)
|
|
if tc.ExpectedError {
|
|
require.NotNil(t, appErr, "should have failed")
|
|
} else {
|
|
require.Nil(t, appErr, "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) {
|
|
mainHelper.Parallel(t)
|
|
testCluster := &testlib.FakeClusterInterface{}
|
|
th := SetupWithClusterMock(t, testCluster).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
|
|
|
hook, appErr := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
|
|
require.Nil(t, appErr)
|
|
defer func() {
|
|
appErr = th.App.DeleteIncomingWebhook(hook.Id)
|
|
require.Nil(t, appErr, "Error cleaning up webhook")
|
|
}()
|
|
|
|
post, appErr := th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "",
|
|
model.StringInterface{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: "text",
|
|
},
|
|
},
|
|
model.PostPropsWebhookDisplayName: hook.DisplayName,
|
|
},
|
|
model.PostTypeMessageAttachment,
|
|
"", nil)
|
|
require.Nil(t, appErr)
|
|
|
|
assert.Contains(t, post.GetProps(), model.PostPropsFromWebhook, "missing from_webhook prop")
|
|
assert.Contains(t, post.GetProps(), model.PostPropsAttachments, "missing attachments prop")
|
|
assert.Contains(t, post.GetProps(), model.PostPropsWebhookDisplayName, "missing webhook_display_name prop")
|
|
|
|
_, appErr = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", nil, model.PostTypeSystemGeneric, "", nil)
|
|
require.NotNil(t, appErr, "Should have failed - bad post type")
|
|
|
|
expectedText := "`<>|<>|`"
|
|
post, appErr = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: "text",
|
|
},
|
|
},
|
|
model.PostPropsWebhookDisplayName: hook.DisplayName,
|
|
}, model.PostTypeMessageAttachment, "", nil)
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, expectedText, post.Message)
|
|
|
|
expectedText = "< | \n|\n>"
|
|
post, appErr = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: "text",
|
|
},
|
|
},
|
|
model.PostPropsWebhookDisplayName: hook.DisplayName,
|
|
}, model.PostTypeMessageAttachment, "", nil)
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, expectedText, post.Message)
|
|
|
|
expectedText = `commit bc95839e4a430ace453e8b209a3723c000c1729a
|
|
Author: foo <[email protected]>
|
|
Date: Thu Mar 1 19:46:54 2018 +0300
|
|
|
|
commit message 2
|
|
|
|
test | 1 +
|
|
1 file changed, 1 insertion(+)
|
|
|
|
commit 5df78b7139b543997838071cd912e375d8bd69b2
|
|
Author: foo <[email protected]>
|
|
Date: Thu Mar 1 19:46:48 2018 +0300
|
|
|
|
commit message 1
|
|
|
|
test | 3 +++
|
|
1 file changed, 3 insertions(+)`
|
|
post, appErr = th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: "text",
|
|
},
|
|
},
|
|
model.PostPropsWebhookDisplayName: hook.DisplayName,
|
|
}, model.PostTypeMessageAttachment, "", nil)
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, expectedText, post.Message)
|
|
|
|
t.Run("should set webhook creator status to online", func(t *testing.T) {
|
|
testCluster.ClearMessages()
|
|
_, appErr := th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "text", "", "", "", model.StringInterface{}, model.PostTypeDefault, "", nil)
|
|
require.Nil(t, appErr)
|
|
|
|
msgs := testCluster.SelectMessages(func(msg *model.ClusterMessage) bool {
|
|
event, err := model.WebSocketEventFromJSON(bytes.NewReader(msg.Data))
|
|
return err == nil && event.EventType() == model.WebsocketEventPosted
|
|
})
|
|
require.Len(t, msgs, 1)
|
|
// We know there will be no error from the filter condition.
|
|
event, _ := model.WebSocketEventFromJSON(bytes.NewReader(msgs[0].Data))
|
|
assert.Equal(t, false, event.GetData()["set_online"])
|
|
})
|
|
}
|
|
|
|
func TestCreateWebhookPostWithOverriddenIcon(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableIncomingWebhooks = true
|
|
*cfg.ServiceSettings.EnablePostIconOverride = true
|
|
})
|
|
|
|
hook, appErr := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
|
|
require.Nil(t, appErr)
|
|
|
|
t.Run("should set props based on icon_url", func(t *testing.T) {
|
|
post, appErr := th.App.CreateWebhookPost(
|
|
th.Context,
|
|
hook.UserId,
|
|
th.BasicChannel,
|
|
"test post",
|
|
"",
|
|
"https://example.com/icon.png",
|
|
"",
|
|
nil,
|
|
"",
|
|
"",
|
|
nil,
|
|
)
|
|
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, "https://example.com/icon.png", post.GetProp(model.PostPropsOverrideIconURL))
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{IsNewPost: true})
|
|
|
|
assert.Equal(t, "https://example.com/icon.png", clientPost.GetProp(model.PostPropsOverrideIconURL))
|
|
})
|
|
|
|
t.Run("should set props based on icon_emoji", func(t *testing.T) {
|
|
post, appErr := th.App.CreateWebhookPost(
|
|
th.Context,
|
|
hook.UserId,
|
|
th.BasicChannel,
|
|
"test post",
|
|
"",
|
|
"",
|
|
"smile",
|
|
nil,
|
|
"",
|
|
"",
|
|
nil,
|
|
)
|
|
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, "smile", post.GetProp(model.PostPropsOverrideIconEmoji))
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{IsNewPost: true})
|
|
|
|
assert.Equal(t, "/static/emoji/1f604.png", clientPost.GetProp(model.PostPropsOverrideIconURL))
|
|
})
|
|
|
|
t.Run("should set props based on icon_emoji (using a custom emoji)", func(t *testing.T) {
|
|
emoji := th.CreateEmoji(t)
|
|
|
|
post, appErr := th.App.CreateWebhookPost(
|
|
th.Context,
|
|
hook.UserId,
|
|
th.BasicChannel,
|
|
"test post",
|
|
"",
|
|
"",
|
|
emoji.Name,
|
|
nil,
|
|
"",
|
|
"",
|
|
nil,
|
|
)
|
|
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, emoji.Name, post.GetProp(model.PostPropsOverrideIconEmoji))
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{IsNewPost: true})
|
|
|
|
assert.Equal(t, fmt.Sprintf("/api/v4/emoji/%s/image", emoji.Id), clientPost.GetProp(model.PostPropsOverrideIconURL))
|
|
})
|
|
|
|
t.Run("should set props based on icon_emoji (with colons around emoji name)", func(t *testing.T) {
|
|
post, appErr := th.App.CreateWebhookPost(
|
|
th.Context,
|
|
hook.UserId,
|
|
th.BasicChannel,
|
|
"test post",
|
|
"",
|
|
"",
|
|
":smile:",
|
|
nil,
|
|
"",
|
|
"",
|
|
nil,
|
|
)
|
|
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, ":smile:", post.GetProp(model.PostPropsOverrideIconEmoji))
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{IsNewPost: true})
|
|
|
|
assert.Equal(t, "/static/emoji/1f604.png", clientPost.GetProp(model.PostPropsOverrideIconURL))
|
|
})
|
|
}
|
|
|
|
func TestCreateWebhookPostWithPriority(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
testCluster := &testlib.FakeClusterInterface{}
|
|
th := SetupWithClusterMock(t, testCluster).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
|
|
|
hook, appErr := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
|
|
require.Nil(t, appErr)
|
|
defer func() {
|
|
appErr := th.App.DeleteIncomingWebhook(hook.Id)
|
|
require.Nil(t, appErr, "Error cleaning up webhook")
|
|
}()
|
|
|
|
testConditions := []model.PostPriority{
|
|
{
|
|
Priority: new("high"),
|
|
RequestedAck: new(true),
|
|
PersistentNotifications: new(false),
|
|
},
|
|
{
|
|
Priority: new(""),
|
|
RequestedAck: new(true),
|
|
PersistentNotifications: new(false),
|
|
},
|
|
{
|
|
Priority: new("urgent"),
|
|
RequestedAck: new(false),
|
|
PersistentNotifications: new(true),
|
|
},
|
|
}
|
|
|
|
for _, conditions := range testConditions {
|
|
post, appErr := th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, "foo @"+th.BasicUser.Username, "user", "http://iconurl", "",
|
|
model.StringInterface{model.PostPropsWebhookDisplayName: hook.DisplayName},
|
|
model.PostTypeMessageAttachment,
|
|
"",
|
|
&conditions,
|
|
)
|
|
|
|
require.Nil(t, appErr)
|
|
|
|
assert.Equal(t, post.Message, "foo @"+th.BasicUser.Username)
|
|
assert.Contains(t, post.GetProps(), model.PostPropsFromWebhook, "missing from_webhook prop")
|
|
assert.Equal(t, *conditions.Priority, *post.GetPriority().Priority)
|
|
assert.Equal(t, *conditions.RequestedAck, *post.GetPriority().RequestedAck)
|
|
assert.Equal(t, *conditions.PersistentNotifications, *post.GetPriority().PersistentNotifications)
|
|
}
|
|
}
|
|
|
|
func TestCreateWebhookPostLinks(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
|
|
|
hook, appErr := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
|
|
require.Nil(t, appErr)
|
|
defer func() {
|
|
appErr := th.App.DeleteIncomingWebhook(hook.Id)
|
|
require.Nil(t, appErr, "Error cleaning up webhook")
|
|
}()
|
|
|
|
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, appErr := th.App.CreateWebhookPost(th.Context, hook.UserId, th.BasicChannel, tc.input, "", "", "", model.StringInterface{}, "", "", nil)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, tc.expectedOutput, post.Message)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSplitWebhookPost(t *testing.T) {
|
|
mainHelper.Parallel(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{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
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{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: strings.Repeat("本", 1000),
|
|
},
|
|
{
|
|
Text: strings.Repeat("本", 2000),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Props: map[string]any{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
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(model.PostPropsAttachments), split.GetProp(model.PostPropsAttachments))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func makePost(message int, attachments []int) *model.Post {
|
|
var props model.StringInterface
|
|
if len(attachments) > 0 {
|
|
sa := make([]*model.MessageAttachment, 0, len(attachments))
|
|
for _, a := range attachments {
|
|
attach := &model.MessageAttachment{
|
|
Text: strings.Repeat("那", a),
|
|
}
|
|
sa = append(sa, attach)
|
|
}
|
|
props = map[string]any{model.PostPropsAttachments: sa}
|
|
}
|
|
post := &model.Post{
|
|
Message: strings.Repeat("那", message),
|
|
Props: props,
|
|
}
|
|
return post
|
|
}
|
|
|
|
func TestSplitWebhookPostAttachments(t *testing.T) {
|
|
mainHelper.Parallel(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(model.PostPropsAttachments), split.GetProp(model.PostPropsAttachments), i)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateOutGoingWebhookWithUsernameAndIconURL(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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) {
|
|
mainHelper.Parallel(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 range 5 {
|
|
time.Sleep(time.Second)
|
|
posts, _ := th.App.GetPosts(th.Context, 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(t)
|
|
|
|
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)
|
|
_, err := w.Write(js)
|
|
require.NoError(t, err)
|
|
} else {
|
|
_, err := w.Write([]byte(`{"text": "sample response text from test server"}`))
|
|
require.NoError(t, err)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
channel := th.CreateChannel(t, 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(model.PostPropsFromWebhook), "true")
|
|
if testCase.ExpectedIconURL != "" {
|
|
assert.Equal(t, webhookPost.GetProp(model.PostPropsOverrideIconURL), testCase.ExpectedIconURL)
|
|
} else {
|
|
assert.Nil(t, webhookPost.GetProp(model.PostPropsOverrideIconURL))
|
|
}
|
|
|
|
if testCase.ExpectedUsername != "" {
|
|
assert.Equal(t, webhookPost.GetProp(model.PostPropsOverrideUsername), testCase.ExpectedUsername)
|
|
} else {
|
|
assert.Nil(t, webhookPost.GetProp(model.PostPropsOverrideUsername))
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
require.Fail(t, "Timeout, webhook response not created as post")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTriggerOutGoingWebhookWithMultipleURLs(t *testing.T) {
|
|
mainHelper.Parallel(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, ","),
|
|
}
|
|
}
|
|
|
|
createOutgoingWebhook := func(channel *model.Channel, testCallBackURLs []string, th *TestHelper) (*model.OutgoingWebhook, *model.AppError) {
|
|
outgoingWebhook := model.OutgoingWebhook{
|
|
ChannelId: channel.Id,
|
|
TeamId: channel.TeamId,
|
|
CallbackURLs: testCallBackURLs,
|
|
Username: "some-user-name",
|
|
IconURL: "http://some-website.com/assets/some-icon.png",
|
|
DisplayName: "some-display-name",
|
|
Description: "some-description",
|
|
CreatorId: th.BasicUser.Id,
|
|
TriggerWords: []string{"Abracadabra"},
|
|
ContentType: "application/json",
|
|
}
|
|
|
|
return th.App.CreateOutgoingWebhook(&outgoingWebhook)
|
|
}
|
|
|
|
chanTs1 := make(chan string, 1)
|
|
ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
chanTs1 <- "webhook received!"
|
|
}))
|
|
defer ts1.Close()
|
|
|
|
chanTs2 := make(chan string, 1)
|
|
ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
chanTs2 <- "webhook received!"
|
|
}))
|
|
defer ts2.Close()
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
|
})
|
|
|
|
for name, testCase := range map[string]struct {
|
|
CallBackURLs []string
|
|
}{
|
|
"One WebhookURL": {
|
|
CallBackURLs: []string{ts1.URL},
|
|
},
|
|
"Two WebhookURLs": {
|
|
CallBackURLs: []string{ts1.URL, ts2.URL},
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableOutgoingWebhooks = true
|
|
})
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
hook, _ := createOutgoingWebhook(channel, testCase.CallBackURLs, th)
|
|
payload := getPayload(hook, th, channel)
|
|
|
|
th.App.TriggerWebhook(th.Context, payload, hook, th.BasicPost, channel)
|
|
|
|
select {
|
|
case webhookResponse := <-chanTs1:
|
|
require.Equal(t, "webhook received!", webhookResponse)
|
|
|
|
case <-time.After(5 * time.Second):
|
|
require.Fail(t, "Timeout, webhook URL 1 response not received")
|
|
}
|
|
|
|
if len(testCase.CallBackURLs) > 1 {
|
|
select {
|
|
case webhookResponse := <-chanTs2:
|
|
require.Equal(t, "webhook received!", webhookResponse)
|
|
|
|
case <-time.After(5 * time.Second):
|
|
require.Fail(t, "Timeout, webhook URL 2 response not received")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.AllowedUntrustedInternalConnections = new("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) {
|
|
_, err := io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
|
|
require.NoError(t, err)
|
|
}))
|
|
defer server.Close()
|
|
|
|
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
|
|
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) {
|
|
_, err := io.Copy(w, strings.NewReader("aaaaaaaa"))
|
|
require.NoError(t, err)
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
|
|
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) {
|
|
// Don't check the error here as the client may disconnect after hitting
|
|
// the response size limit, causing a broken pipe error that we can't avoid
|
|
_, _ = io.Copy(w, io.MultiReader(strings.NewReader(`{"text": "`), InfiniteReader{}, strings.NewReader(`"}`)))
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
|
|
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) {
|
|
// Don't check the error here as the client may disconnect after hitting
|
|
// the response size limit, causing a broken pipe error that we can't avoid
|
|
_, _ = io.Copy(w, InfiniteReader{})
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
|
|
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 = new(int64(1))
|
|
})
|
|
|
|
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
|
|
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)
|
|
|
|
_, err := io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
|
|
require.NoError(t, err)
|
|
}))
|
|
defer server.Close()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = new(int64(2))
|
|
})
|
|
|
|
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
|
|
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", nil)
|
|
require.NoError(t, err)
|
|
require.Nil(t, resp)
|
|
})
|
|
|
|
t.Run("with auth token", func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, err := io.Copy(w, strings.NewReader(fmt.Sprintf(`{"text":"%s"}`, r.Header.Get("Authorization"))))
|
|
require.NoError(t, err)
|
|
}))
|
|
defer server.Close()
|
|
|
|
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", &model.OutgoingOAuthConnectionToken{
|
|
AccessToken: "test",
|
|
TokenType: "Bearer",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, `Bearer test`, *resp.Text)
|
|
})
|
|
}
|