github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/webhook_test.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"encoding/json"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/mattermost/mattermost-server/v5/model"
    17  	"github.com/mattermost/mattermost-server/v5/services/httpservice"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  func TestCreateIncomingWebhookForChannel(t *testing.T) {
    23  	th := Setup(t).InitBasic()
    24  	defer th.TearDown()
    25  
    26  	type TestCase struct {
    27  		EnableIncomingHooks        bool
    28  		EnablePostUsernameOverride bool
    29  		EnablePostIconOverride     bool
    30  		IncomingWebhook            model.IncomingWebhook
    31  
    32  		ExpectedError           bool
    33  		ExpectedIncomingWebhook *model.IncomingWebhook
    34  	}
    35  
    36  	for name, tc := range map[string]TestCase{
    37  		"webhooks not enabled": {
    38  			EnableIncomingHooks:        false,
    39  			EnablePostUsernameOverride: false,
    40  			EnablePostIconOverride:     false,
    41  			IncomingWebhook: model.IncomingWebhook{
    42  				DisplayName: "title",
    43  				Description: "description",
    44  				ChannelId:   th.BasicChannel.Id,
    45  			},
    46  
    47  			ExpectedError:           true,
    48  			ExpectedIncomingWebhook: nil,
    49  		},
    50  		"valid: username and post icon url ignored, since override not enabled": {
    51  			EnableIncomingHooks:        true,
    52  			EnablePostUsernameOverride: false,
    53  			EnablePostIconOverride:     false,
    54  			IncomingWebhook: model.IncomingWebhook{
    55  				DisplayName: "title",
    56  				Description: "description",
    57  				ChannelId:   th.BasicChannel.Id,
    58  				Username:    ":invalid and ignored:",
    59  				IconURL:     "ignored",
    60  			},
    61  
    62  			ExpectedError: false,
    63  			ExpectedIncomingWebhook: &model.IncomingWebhook{
    64  				DisplayName: "title",
    65  				Description: "description",
    66  				ChannelId:   th.BasicChannel.Id,
    67  			},
    68  		},
    69  		"invalid username, override enabled": {
    70  			EnableIncomingHooks:        true,
    71  			EnablePostUsernameOverride: true,
    72  			EnablePostIconOverride:     false,
    73  			IncomingWebhook: model.IncomingWebhook{
    74  				DisplayName: "title",
    75  				Description: "description",
    76  				ChannelId:   th.BasicChannel.Id,
    77  				Username:    ":invalid:",
    78  			},
    79  
    80  			ExpectedError:           true,
    81  			ExpectedIncomingWebhook: nil,
    82  		},
    83  		"valid, no username or post icon url provided": {
    84  			EnableIncomingHooks:        true,
    85  			EnablePostUsernameOverride: true,
    86  			EnablePostIconOverride:     true,
    87  			IncomingWebhook: model.IncomingWebhook{
    88  				DisplayName: "title",
    89  				Description: "description",
    90  				ChannelId:   th.BasicChannel.Id,
    91  			},
    92  
    93  			ExpectedError: false,
    94  			ExpectedIncomingWebhook: &model.IncomingWebhook{
    95  				DisplayName: "title",
    96  				Description: "description",
    97  				ChannelId:   th.BasicChannel.Id,
    98  			},
    99  		},
   100  		"valid, with username and post icon": {
   101  			EnableIncomingHooks:        true,
   102  			EnablePostUsernameOverride: true,
   103  			EnablePostIconOverride:     true,
   104  			IncomingWebhook: model.IncomingWebhook{
   105  				DisplayName: "title",
   106  				Description: "description",
   107  				ChannelId:   th.BasicChannel.Id,
   108  				Username:    "valid",
   109  				IconURL:     "http://example.com/icon",
   110  			},
   111  
   112  			ExpectedError: false,
   113  			ExpectedIncomingWebhook: &model.IncomingWebhook{
   114  				DisplayName: "title",
   115  				Description: "description",
   116  				ChannelId:   th.BasicChannel.Id,
   117  				Username:    "valid",
   118  				IconURL:     "http://example.com/icon",
   119  			},
   120  		},
   121  	} {
   122  		t.Run(name, func(t *testing.T) {
   123  			th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = tc.EnableIncomingHooks })
   124  			th.App.UpdateConfig(func(cfg *model.Config) {
   125  				*cfg.ServiceSettings.EnablePostUsernameOverride = tc.EnablePostUsernameOverride
   126  			})
   127  			th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostIconOverride = tc.EnablePostIconOverride })
   128  
   129  			createdHook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &tc.IncomingWebhook)
   130  			if tc.ExpectedError {
   131  				require.NotNil(t, err, "should have failed")
   132  			} else {
   133  				require.Nil(t, err, "should not have failed")
   134  			}
   135  			if createdHook != nil {
   136  				defer th.App.DeleteIncomingWebhook(createdHook.Id)
   137  			}
   138  			if tc.ExpectedIncomingWebhook == nil {
   139  				assert.Nil(t, createdHook, "expected nil webhook")
   140  			} else if assert.NotNil(t, createdHook, "expected non-nil webhook") {
   141  				assert.Equal(t, tc.ExpectedIncomingWebhook.DisplayName, createdHook.DisplayName)
   142  				assert.Equal(t, tc.ExpectedIncomingWebhook.Description, createdHook.Description)
   143  				assert.Equal(t, tc.ExpectedIncomingWebhook.ChannelId, createdHook.ChannelId)
   144  				assert.Equal(t, tc.ExpectedIncomingWebhook.Username, createdHook.Username)
   145  				assert.Equal(t, tc.ExpectedIncomingWebhook.IconURL, createdHook.IconURL)
   146  			}
   147  		})
   148  	}
   149  }
   150  
   151  func TestUpdateIncomingWebhook(t *testing.T) {
   152  	th := Setup(t).InitBasic()
   153  	defer th.TearDown()
   154  
   155  	type TestCase struct {
   156  		EnableIncomingHooks        bool
   157  		EnablePostUsernameOverride bool
   158  		EnablePostIconOverride     bool
   159  		IncomingWebhook            model.IncomingWebhook
   160  
   161  		ExpectedError           bool
   162  		ExpectedIncomingWebhook *model.IncomingWebhook
   163  	}
   164  
   165  	for name, tc := range map[string]TestCase{
   166  		"webhooks not enabled": {
   167  			EnableIncomingHooks:        false,
   168  			EnablePostUsernameOverride: false,
   169  			EnablePostIconOverride:     false,
   170  			IncomingWebhook: model.IncomingWebhook{
   171  				DisplayName: "title",
   172  				Description: "description",
   173  				ChannelId:   th.BasicChannel.Id,
   174  			},
   175  
   176  			ExpectedError:           true,
   177  			ExpectedIncomingWebhook: nil,
   178  		},
   179  		"valid: username and post icon url ignored, since override not enabled": {
   180  			EnableIncomingHooks:        true,
   181  			EnablePostUsernameOverride: false,
   182  			EnablePostIconOverride:     false,
   183  			IncomingWebhook: model.IncomingWebhook{
   184  				DisplayName: "title",
   185  				Description: "description",
   186  				ChannelId:   th.BasicChannel.Id,
   187  				Username:    ":invalid and ignored:",
   188  				IconURL:     "ignored",
   189  			},
   190  
   191  			ExpectedError: false,
   192  			ExpectedIncomingWebhook: &model.IncomingWebhook{
   193  				DisplayName: "title",
   194  				Description: "description",
   195  				ChannelId:   th.BasicChannel.Id,
   196  			},
   197  		},
   198  		"invalid username, override enabled": {
   199  			EnableIncomingHooks:        true,
   200  			EnablePostUsernameOverride: true,
   201  			EnablePostIconOverride:     false,
   202  			IncomingWebhook: model.IncomingWebhook{
   203  				DisplayName: "title",
   204  				Description: "description",
   205  				ChannelId:   th.BasicChannel.Id,
   206  				Username:    ":invalid:",
   207  			},
   208  
   209  			ExpectedError:           true,
   210  			ExpectedIncomingWebhook: nil,
   211  		},
   212  		"valid, no username or post icon url provided": {
   213  			EnableIncomingHooks:        true,
   214  			EnablePostUsernameOverride: true,
   215  			EnablePostIconOverride:     true,
   216  			IncomingWebhook: model.IncomingWebhook{
   217  				DisplayName: "title",
   218  				Description: "description",
   219  				ChannelId:   th.BasicChannel.Id,
   220  			},
   221  
   222  			ExpectedError: false,
   223  			ExpectedIncomingWebhook: &model.IncomingWebhook{
   224  				DisplayName: "title",
   225  				Description: "description",
   226  				ChannelId:   th.BasicChannel.Id,
   227  			},
   228  		},
   229  		"valid, with username and post icon": {
   230  			EnableIncomingHooks:        true,
   231  			EnablePostUsernameOverride: true,
   232  			EnablePostIconOverride:     true,
   233  			IncomingWebhook: model.IncomingWebhook{
   234  				DisplayName: "title",
   235  				Description: "description",
   236  				ChannelId:   th.BasicChannel.Id,
   237  				Username:    "valid",
   238  				IconURL:     "http://example.com/icon",
   239  			},
   240  
   241  			ExpectedError: false,
   242  			ExpectedIncomingWebhook: &model.IncomingWebhook{
   243  				DisplayName: "title",
   244  				Description: "description",
   245  				ChannelId:   th.BasicChannel.Id,
   246  				Username:    "valid",
   247  				IconURL:     "http://example.com/icon",
   248  			},
   249  		},
   250  	} {
   251  		t.Run(name, func(t *testing.T) {
   252  			th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
   253  
   254  			hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{
   255  				ChannelId: th.BasicChannel.Id,
   256  			})
   257  			require.Nil(t, err)
   258  			defer th.App.DeleteIncomingWebhook(hook.Id)
   259  
   260  			th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = tc.EnableIncomingHooks })
   261  			th.App.UpdateConfig(func(cfg *model.Config) {
   262  				*cfg.ServiceSettings.EnablePostUsernameOverride = tc.EnablePostUsernameOverride
   263  			})
   264  			th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostIconOverride = tc.EnablePostIconOverride })
   265  
   266  			updatedHook, err := th.App.UpdateIncomingWebhook(hook, &tc.IncomingWebhook)
   267  			if tc.ExpectedError {
   268  				require.NotNil(t, err, "should have failed")
   269  			} else {
   270  				require.Nil(t, err, "should not have failed")
   271  			}
   272  			if tc.ExpectedIncomingWebhook == nil {
   273  				assert.Nil(t, updatedHook, "expected nil webhook")
   274  			} else if assert.NotNil(t, updatedHook, "expected non-nil webhook") {
   275  				assert.Equal(t, tc.ExpectedIncomingWebhook.DisplayName, updatedHook.DisplayName)
   276  				assert.Equal(t, tc.ExpectedIncomingWebhook.Description, updatedHook.Description)
   277  				assert.Equal(t, tc.ExpectedIncomingWebhook.ChannelId, updatedHook.ChannelId)
   278  				assert.Equal(t, tc.ExpectedIncomingWebhook.Username, updatedHook.Username)
   279  				assert.Equal(t, tc.ExpectedIncomingWebhook.IconURL, updatedHook.IconURL)
   280  			}
   281  		})
   282  	}
   283  }
   284  
   285  func TestCreateWebhookPost(t *testing.T) {
   286  	th := Setup(t).InitBasic()
   287  	defer th.TearDown()
   288  
   289  	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
   290  
   291  	hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
   292  	require.Nil(t, err)
   293  	defer th.App.DeleteIncomingWebhook(hook.Id)
   294  
   295  	post, err := th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", model.StringInterface{
   296  		"attachments": []*model.SlackAttachment{
   297  			{
   298  				Text: "text",
   299  			},
   300  		},
   301  		"webhook_display_name": hook.DisplayName,
   302  	}, model.POST_SLACK_ATTACHMENT, "")
   303  	require.Nil(t, err)
   304  
   305  	assert.Contains(t, post.GetProps(), "from_webhook", "missing from_webhook prop")
   306  	assert.Contains(t, post.GetProps(), "attachments", "missing attachments prop")
   307  	assert.Contains(t, post.GetProps(), "webhook_display_name", "missing webhook_display_name prop")
   308  
   309  	_, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", nil, model.POST_SYSTEM_GENERIC, "")
   310  	require.NotNil(t, err, "Should have failed - bad post type")
   311  
   312  	expectedText := "`<>|<>|`"
   313  	post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
   314  		"attachments": []*model.SlackAttachment{
   315  			{
   316  				Text: "text",
   317  			},
   318  		},
   319  		"webhook_display_name": hook.DisplayName,
   320  	}, model.POST_SLACK_ATTACHMENT, "")
   321  	require.Nil(t, err)
   322  	assert.Equal(t, expectedText, post.Message)
   323  
   324  	expectedText = "< | \n|\n>"
   325  	post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
   326  		"attachments": []*model.SlackAttachment{
   327  			{
   328  				Text: "text",
   329  			},
   330  		},
   331  		"webhook_display_name": hook.DisplayName,
   332  	}, model.POST_SLACK_ATTACHMENT, "")
   333  	require.Nil(t, err)
   334  	assert.Equal(t, expectedText, post.Message)
   335  
   336  	expectedText = `commit bc95839e4a430ace453e8b209a3723c000c1729a
   337  Author: foo <foo@example.org>
   338  Date:   Thu Mar 1 19:46:54 2018 +0300
   339  
   340      commit message 2
   341  
   342    test | 1 +
   343   1 file changed, 1 insertion(+)
   344  
   345  commit 5df78b7139b543997838071cd912e375d8bd69b2
   346  Author: foo <foo@example.org>
   347  Date:   Thu Mar 1 19:46:48 2018 +0300
   348  
   349      commit message 1
   350  
   351   test | 3 +++
   352   1 file changed, 3 insertions(+)`
   353  	post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
   354  		"attachments": []*model.SlackAttachment{
   355  			{
   356  				Text: "text",
   357  			},
   358  		},
   359  		"webhook_display_name": hook.DisplayName,
   360  	}, model.POST_SLACK_ATTACHMENT, "")
   361  	require.Nil(t, err)
   362  	assert.Equal(t, expectedText, post.Message)
   363  }
   364  
   365  func TestSplitWebhookPost(t *testing.T) {
   366  	type TestCase struct {
   367  		Post     *model.Post
   368  		Expected []*model.Post
   369  	}
   370  
   371  	maxPostSize := 10000
   372  
   373  	for name, tc := range map[string]TestCase{
   374  		"LongPost": {
   375  			Post: &model.Post{
   376  				Message: strings.Repeat("本", maxPostSize*3/2),
   377  			},
   378  			Expected: []*model.Post{
   379  				{
   380  					Message: strings.Repeat("本", maxPostSize),
   381  				},
   382  				{
   383  					Message: strings.Repeat("本", maxPostSize/2),
   384  				},
   385  			},
   386  		},
   387  		"LongPostAndMultipleAttachments": {
   388  			Post: &model.Post{
   389  				Message: strings.Repeat("本", maxPostSize*3/2),
   390  				Props: map[string]interface{}{
   391  					"attachments": []*model.SlackAttachment{
   392  						{
   393  							Text: strings.Repeat("本", 1000),
   394  						},
   395  						{
   396  							Text: strings.Repeat("本", 2000),
   397  						},
   398  						{
   399  							Text: strings.Repeat("本", model.POST_PROPS_MAX_USER_RUNES-1000),
   400  						},
   401  					},
   402  				},
   403  			},
   404  			Expected: []*model.Post{
   405  				{
   406  					Message: strings.Repeat("本", maxPostSize),
   407  				},
   408  				{
   409  					Message: strings.Repeat("本", maxPostSize/2),
   410  					Props: map[string]interface{}{
   411  						"attachments": []*model.SlackAttachment{
   412  							{
   413  								Text: strings.Repeat("本", 1000),
   414  							},
   415  							{
   416  								Text: strings.Repeat("本", 2000),
   417  							},
   418  						},
   419  					},
   420  				},
   421  				{
   422  					Props: map[string]interface{}{
   423  						"attachments": []*model.SlackAttachment{
   424  							{
   425  								Text: strings.Repeat("本", model.POST_PROPS_MAX_USER_RUNES-1000),
   426  							},
   427  						},
   428  					},
   429  				},
   430  			},
   431  		},
   432  		"UnsplittableProps": {
   433  			Post: &model.Post{
   434  				Message: "foo",
   435  				Props: map[string]interface{}{
   436  					"foo": strings.Repeat("x", model.POST_PROPS_MAX_USER_RUNES*2),
   437  				},
   438  			},
   439  		},
   440  	} {
   441  		t.Run(name, func(t *testing.T) {
   442  			splits, err := SplitWebhookPost(tc.Post, maxPostSize)
   443  			if tc.Expected == nil {
   444  				require.NotNil(t, err)
   445  			} else {
   446  				require.Nil(t, err)
   447  			}
   448  			assert.Equal(t, len(tc.Expected), len(splits))
   449  			for i, split := range splits {
   450  				if i < len(tc.Expected) {
   451  					assert.Equal(t, tc.Expected[i].Message, split.Message)
   452  					assert.Equal(t, tc.Expected[i].GetProp("attachments"), split.GetProp("attachments"))
   453  				}
   454  			}
   455  		})
   456  	}
   457  }
   458  
   459  func makePost(message int, attachments []int) *model.Post {
   460  	var props model.StringInterface
   461  	if len(attachments) > 0 {
   462  		sa := make([]*model.SlackAttachment, 0, len(attachments))
   463  		for _, a := range attachments {
   464  			attach := &model.SlackAttachment{
   465  				Text: strings.Repeat("那", a),
   466  			}
   467  			sa = append(sa, attach)
   468  		}
   469  		props = map[string]interface{}{"attachments": sa}
   470  	}
   471  	post := &model.Post{
   472  		Message: strings.Repeat("那", message),
   473  		Props:   props,
   474  	}
   475  	return post
   476  }
   477  
   478  func TestSplitWebhookPostAttachments(t *testing.T) {
   479  	maxPostSize := 10000
   480  	testCases := []struct {
   481  		name     string
   482  		post     *model.Post
   483  		expected []*model.Post
   484  	}{
   485  		{
   486  			// makePost(messageLength, []int{attachmentLength, ...})
   487  			name:     "no split",
   488  			post:     makePost(10, []int{100, 150, 200}),
   489  			expected: []*model.Post{makePost(10, []int{100, 150, 200})},
   490  		},
   491  		{
   492  			name: "split into 2",
   493  			post: makePost(maxPostSize-1, []int{model.POST_PROPS_MAX_USER_RUNES * 3 / 4, model.POST_PROPS_MAX_USER_RUNES * 1 / 4}),
   494  			expected: []*model.Post{
   495  				makePost(maxPostSize-1, []int{model.POST_PROPS_MAX_USER_RUNES * 3 / 4}),
   496  				makePost(0, []int{model.POST_PROPS_MAX_USER_RUNES * 1 / 4}),
   497  			},
   498  		},
   499  		{
   500  			name: "split into 3",
   501  			post: makePost(maxPostSize*3/2, []int{1000, 2000, model.POST_PROPS_MAX_USER_RUNES - 1000}),
   502  			expected: []*model.Post{
   503  				makePost(maxPostSize, nil),
   504  				makePost(maxPostSize/2, []int{1000, 2000}),
   505  				makePost(0, []int{model.POST_PROPS_MAX_USER_RUNES - 1000}),
   506  			},
   507  		},
   508  		{
   509  			name: "MM-24644 split into 3",
   510  			post: makePost(maxPostSize*3/2, []int{5150, 2000, model.POST_PROPS_MAX_USER_RUNES - 1000}),
   511  			expected: []*model.Post{
   512  				makePost(maxPostSize, nil),
   513  				makePost(maxPostSize/2, []int{5150}),
   514  				makePost(0, []int{2000}),
   515  				makePost(0, []int{model.POST_PROPS_MAX_USER_RUNES - 1000}),
   516  			},
   517  		},
   518  	}
   519  
   520  	for _, tc := range testCases {
   521  		t.Run(tc.name, func(t *testing.T) {
   522  			splits, err := SplitWebhookPost(tc.post, maxPostSize)
   523  			if tc.expected == nil {
   524  				require.NotNil(t, err)
   525  			} else {
   526  				require.Nil(t, err)
   527  			}
   528  			assert.Equal(t, len(tc.expected), len(splits))
   529  			for i, split := range splits {
   530  				if i < len(tc.expected) {
   531  					assert.Equal(t, tc.expected[i].Message, split.Message, i)
   532  					assert.Equal(t, tc.expected[i].GetProp("attachments"), split.GetProp("attachments"), i)
   533  				}
   534  			}
   535  		})
   536  	}
   537  }
   538  
   539  func TestCreateOutGoingWebhookWithUsernameAndIconURL(t *testing.T) {
   540  	th := Setup(t).InitBasic()
   541  	defer th.TearDown()
   542  
   543  	outgoingWebhook := model.OutgoingWebhook{
   544  		ChannelId:    th.BasicChannel.Id,
   545  		TeamId:       th.BasicChannel.TeamId,
   546  		CallbackURLs: []string{"http://nowhere.com"},
   547  		Username:     "some-user-name",
   548  		IconURL:      "http://some-icon/",
   549  		DisplayName:  "some-display-name",
   550  		Description:  "some-description",
   551  		CreatorId:    th.BasicUser.Id,
   552  	}
   553  
   554  	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true })
   555  
   556  	createdHook, err := th.App.CreateOutgoingWebhook(&outgoingWebhook)
   557  	require.Nil(t, err)
   558  
   559  	assert.NotNil(t, createdHook, "should not be null")
   560  
   561  	assert.Equal(t, createdHook.ChannelId, outgoingWebhook.ChannelId)
   562  	assert.Equal(t, createdHook.TeamId, outgoingWebhook.TeamId)
   563  	assert.Equal(t, createdHook.CallbackURLs, outgoingWebhook.CallbackURLs)
   564  	assert.Equal(t, createdHook.Username, outgoingWebhook.Username)
   565  	assert.Equal(t, createdHook.IconURL, outgoingWebhook.IconURL)
   566  	assert.Equal(t, createdHook.DisplayName, outgoingWebhook.DisplayName)
   567  	assert.Equal(t, createdHook.Description, outgoingWebhook.Description)
   568  
   569  }
   570  
   571  func TestTriggerOutGoingWebhookWithUsernameAndIconURL(t *testing.T) {
   572  
   573  	getPayload := func(hook *model.OutgoingWebhook, th *TestHelper, channel *model.Channel) *model.OutgoingWebhookPayload {
   574  		return &model.OutgoingWebhookPayload{
   575  			Token:       hook.Token,
   576  			TeamId:      hook.TeamId,
   577  			TeamDomain:  th.BasicTeam.Name,
   578  			ChannelId:   channel.Id,
   579  			ChannelName: channel.Name,
   580  			Timestamp:   th.BasicPost.CreateAt,
   581  			UserId:      th.BasicPost.UserId,
   582  			UserName:    th.BasicUser.Username,
   583  			PostId:      th.BasicPost.Id,
   584  			Text:        th.BasicPost.Message,
   585  			TriggerWord: "Abracadabra",
   586  			FileIds:     strings.Join(th.BasicPost.FileIds, ","),
   587  		}
   588  	}
   589  
   590  	waitUntilWebhookResposeIsCreatedAsPost := func(channel *model.Channel, th *TestHelper, t *testing.T, createdPost chan *model.Post) {
   591  		go func() {
   592  			for i := 0; i < 5; i++ {
   593  				time.Sleep(time.Second)
   594  				posts, _ := th.App.GetPosts(channel.Id, 0, 5)
   595  				if len(posts.Posts) > 0 {
   596  					for _, post := range posts.Posts {
   597  						createdPost <- post
   598  						return
   599  					}
   600  				}
   601  			}
   602  		}()
   603  	}
   604  
   605  	type TestCaseOutgoing struct {
   606  		EnablePostUsernameOverride bool
   607  		EnablePostIconOverride     bool
   608  		ExpectedUsername           string
   609  		ExpectedIconUrl            string
   610  		WebhookResponse            *model.OutgoingWebhookResponse
   611  	}
   612  
   613  	createOutgoingWebhook := func(channel *model.Channel, testCallBackUrl string, th *TestHelper) (*model.OutgoingWebhook, *model.AppError) {
   614  		outgoingWebhook := model.OutgoingWebhook{
   615  			ChannelId:    channel.Id,
   616  			TeamId:       channel.TeamId,
   617  			CallbackURLs: []string{testCallBackUrl},
   618  			Username:     "some-user-name",
   619  			IconURL:      "http://some-icon/",
   620  			DisplayName:  "some-display-name",
   621  			Description:  "some-description",
   622  			CreatorId:    th.BasicUser.Id,
   623  			TriggerWords: []string{"Abracadabra"},
   624  			ContentType:  "application/json",
   625  		}
   626  
   627  		return th.App.CreateOutgoingWebhook(&outgoingWebhook)
   628  	}
   629  
   630  	getTestCases := func() map[string]TestCaseOutgoing {
   631  
   632  		webHookResponse := "sample response text from test server"
   633  		testCasesOutgoing := map[string]TestCaseOutgoing{
   634  
   635  			"Should override username and Icon": {
   636  				EnablePostUsernameOverride: true,
   637  				EnablePostIconOverride:     true,
   638  				ExpectedUsername:           "some-user-name",
   639  				ExpectedIconUrl:            "http://some-icon/",
   640  			},
   641  			"Should not override username and Icon": {
   642  				EnablePostUsernameOverride: false,
   643  				EnablePostIconOverride:     false,
   644  			},
   645  			"Should not override username and Icon if the webhook response already has it": {
   646  				EnablePostUsernameOverride: true,
   647  				EnablePostIconOverride:     true,
   648  				ExpectedUsername:           "webhookuser",
   649  				ExpectedIconUrl:            "http://webhok/icon",
   650  				WebhookResponse:            &model.OutgoingWebhookResponse{Text: &webHookResponse, Username: "webhookuser", IconURL: "http://webhok/icon"},
   651  			},
   652  		}
   653  		return testCasesOutgoing
   654  	}
   655  
   656  	th := Setup(t).InitBasic()
   657  	defer th.TearDown()
   658  
   659  	th.App.UpdateConfig(func(cfg *model.Config) {
   660  		*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
   661  	})
   662  	createdPost := make(chan *model.Post)
   663  
   664  	for name, testCase := range getTestCases() {
   665  		t.Run(name, func(t *testing.T) {
   666  
   667  			th.App.UpdateConfig(func(cfg *model.Config) {
   668  				*cfg.ServiceSettings.EnableOutgoingWebhooks = true
   669  				*cfg.ServiceSettings.EnablePostUsernameOverride = testCase.EnablePostUsernameOverride
   670  				*cfg.ServiceSettings.EnablePostIconOverride = testCase.EnablePostIconOverride
   671  			})
   672  
   673  			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   674  				if testCase.WebhookResponse != nil {
   675  					w.Write([]byte(testCase.WebhookResponse.ToJson()))
   676  				} else {
   677  					w.Write([]byte(`{"text": "sample response text from test server"}`))
   678  				}
   679  			}))
   680  			defer ts.Close()
   681  
   682  			channel := th.CreateChannel(th.BasicTeam)
   683  			hook, _ := createOutgoingWebhook(channel, ts.URL, th)
   684  			payload := getPayload(hook, th, channel)
   685  
   686  			th.App.TriggerWebhook(payload, hook, th.BasicPost, channel)
   687  
   688  			waitUntilWebhookResposeIsCreatedAsPost(channel, th, t, createdPost)
   689  
   690  			select {
   691  			case webhookPost := <-createdPost:
   692  				assert.Equal(t, webhookPost.Message, "sample response text from test server")
   693  				assert.Equal(t, webhookPost.GetProp("from_webhook"), "true")
   694  				if testCase.ExpectedIconUrl != "" {
   695  					assert.Equal(t, webhookPost.GetProp("override_icon_url"), testCase.ExpectedIconUrl)
   696  				} else {
   697  					assert.Nil(t, webhookPost.GetProp("override_icon_url"))
   698  				}
   699  
   700  				if testCase.ExpectedUsername != "" {
   701  					assert.Equal(t, webhookPost.GetProp("override_username"), testCase.ExpectedUsername)
   702  				} else {
   703  					assert.Nil(t, webhookPost.GetProp("override_username"))
   704  				}
   705  			case <-time.After(5 * time.Second):
   706  				require.Fail(t, "Timeout, webhook response not created as post")
   707  			}
   708  
   709  		})
   710  	}
   711  
   712  }
   713  
   714  type InfiniteReader struct {
   715  	Prefix string
   716  }
   717  
   718  func (r InfiniteReader) Read(p []byte) (n int, err error) {
   719  	for i := range p {
   720  		p[i] = 'a'
   721  	}
   722  
   723  	return len(p), nil
   724  }
   725  
   726  func TestDoOutgoingWebhookRequest(t *testing.T) {
   727  	th := Setup(t)
   728  	defer th.TearDown()
   729  
   730  	th.App.UpdateConfig(func(cfg *model.Config) {
   731  		cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewString("127.0.0.1")
   732  		*cfg.ServiceSettings.EnableOutgoingWebhooks = true
   733  	})
   734  
   735  	t.Run("with a valid response", func(t *testing.T) {
   736  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   737  			io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
   738  		}))
   739  		defer server.Close()
   740  
   741  		resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
   742  		require.Nil(t, err)
   743  
   744  		assert.NotNil(t, resp)
   745  		assert.NotNil(t, resp.Text)
   746  		assert.Equal(t, "Hello, World!", *resp.Text)
   747  	})
   748  
   749  	t.Run("with an invalid response", func(t *testing.T) {
   750  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   751  			io.Copy(w, strings.NewReader("aaaaaaaa"))
   752  		}))
   753  		defer server.Close()
   754  
   755  		_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
   756  		require.NotNil(t, err)
   757  		require.IsType(t, &json.SyntaxError{}, err)
   758  	})
   759  
   760  	t.Run("with a large, valid response", func(t *testing.T) {
   761  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   762  			io.Copy(w, io.MultiReader(strings.NewReader(`{"text": "`), InfiniteReader{}, strings.NewReader(`"}`)))
   763  		}))
   764  		defer server.Close()
   765  
   766  		_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
   767  		require.NotNil(t, err)
   768  		require.Equal(t, io.ErrUnexpectedEOF, err)
   769  	})
   770  
   771  	t.Run("with a large, invalid response", func(t *testing.T) {
   772  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   773  			io.Copy(w, InfiniteReader{})
   774  		}))
   775  		defer server.Close()
   776  
   777  		_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
   778  		require.NotNil(t, err)
   779  		require.IsType(t, &json.SyntaxError{}, err)
   780  	})
   781  
   782  	t.Run("with a slow response", func(t *testing.T) {
   783  		releaseHandler := make(chan interface{})
   784  
   785  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   786  			// Don't actually handle the response, allowing the app to timeout.
   787  			<-releaseHandler
   788  		}))
   789  		defer server.Close()
   790  		defer close(releaseHandler)
   791  
   792  		th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = 500 * time.Millisecond
   793  		defer func() {
   794  			th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = httpservice.RequestTimeout
   795  		}()
   796  
   797  		_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
   798  		require.NotNil(t, err)
   799  		require.IsType(t, &url.Error{}, err)
   800  	})
   801  
   802  	t.Run("without response", func(t *testing.T) {
   803  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   804  		}))
   805  		defer server.Close()
   806  
   807  		resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
   808  		require.Nil(t, err)
   809  		require.Nil(t, resp)
   810  	})
   811  }