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 }