github.com/vnforks/kid@v5.11.1+incompatible/app/webhook_test.go (about) 1 // Copyright (c) 2017-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/model" 17 "github.com/mattermost/mattermost-server/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 assert := assert.New(t) 124 125 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = tc.EnableIncomingHooks }) 126 th.App.UpdateConfig(func(cfg *model.Config) { 127 *cfg.ServiceSettings.EnablePostUsernameOverride = tc.EnablePostUsernameOverride 128 }) 129 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostIconOverride = tc.EnablePostIconOverride }) 130 131 createdHook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &tc.IncomingWebhook) 132 if tc.ExpectedError && err == nil { 133 t.Fatal("should have failed") 134 } else if !tc.ExpectedError && err != nil { 135 t.Fatalf("should not have failed: %v", err.Error()) 136 } 137 if createdHook != nil { 138 defer th.App.DeleteIncomingWebhook(createdHook.Id) 139 } 140 if tc.ExpectedIncomingWebhook == nil { 141 assert.Nil(createdHook, "expected nil webhook") 142 } else if assert.NotNil(createdHook, "expected non-nil webhook") { 143 assert.Equal(tc.ExpectedIncomingWebhook.DisplayName, createdHook.DisplayName) 144 assert.Equal(tc.ExpectedIncomingWebhook.Description, createdHook.Description) 145 assert.Equal(tc.ExpectedIncomingWebhook.ChannelId, createdHook.ChannelId) 146 assert.Equal(tc.ExpectedIncomingWebhook.Username, createdHook.Username) 147 assert.Equal(tc.ExpectedIncomingWebhook.IconURL, createdHook.IconURL) 148 } 149 }) 150 } 151 } 152 153 func TestUpdateIncomingWebhook(t *testing.T) { 154 th := Setup(t).InitBasic() 155 defer th.TearDown() 156 157 type TestCase struct { 158 EnableIncomingHooks bool 159 EnablePostUsernameOverride bool 160 EnablePostIconOverride bool 161 IncomingWebhook model.IncomingWebhook 162 163 ExpectedError bool 164 ExpectedIncomingWebhook *model.IncomingWebhook 165 } 166 167 for name, tc := range map[string]TestCase{ 168 "webhooks not enabled": { 169 EnableIncomingHooks: false, 170 EnablePostUsernameOverride: false, 171 EnablePostIconOverride: false, 172 IncomingWebhook: model.IncomingWebhook{ 173 DisplayName: "title", 174 Description: "description", 175 ChannelId: th.BasicChannel.Id, 176 }, 177 178 ExpectedError: true, 179 ExpectedIncomingWebhook: nil, 180 }, 181 "valid: username and post icon url ignored, since override not enabled": { 182 EnableIncomingHooks: true, 183 EnablePostUsernameOverride: false, 184 EnablePostIconOverride: false, 185 IncomingWebhook: model.IncomingWebhook{ 186 DisplayName: "title", 187 Description: "description", 188 ChannelId: th.BasicChannel.Id, 189 Username: ":invalid and ignored:", 190 IconURL: "ignored", 191 }, 192 193 ExpectedError: false, 194 ExpectedIncomingWebhook: &model.IncomingWebhook{ 195 DisplayName: "title", 196 Description: "description", 197 ChannelId: th.BasicChannel.Id, 198 }, 199 }, 200 "invalid username, override enabled": { 201 EnableIncomingHooks: true, 202 EnablePostUsernameOverride: true, 203 EnablePostIconOverride: false, 204 IncomingWebhook: model.IncomingWebhook{ 205 DisplayName: "title", 206 Description: "description", 207 ChannelId: th.BasicChannel.Id, 208 Username: ":invalid:", 209 }, 210 211 ExpectedError: true, 212 ExpectedIncomingWebhook: nil, 213 }, 214 "valid, no username or post icon url provided": { 215 EnableIncomingHooks: true, 216 EnablePostUsernameOverride: true, 217 EnablePostIconOverride: true, 218 IncomingWebhook: model.IncomingWebhook{ 219 DisplayName: "title", 220 Description: "description", 221 ChannelId: th.BasicChannel.Id, 222 }, 223 224 ExpectedError: false, 225 ExpectedIncomingWebhook: &model.IncomingWebhook{ 226 DisplayName: "title", 227 Description: "description", 228 ChannelId: th.BasicChannel.Id, 229 }, 230 }, 231 "valid, with username and post icon": { 232 EnableIncomingHooks: true, 233 EnablePostUsernameOverride: true, 234 EnablePostIconOverride: true, 235 IncomingWebhook: model.IncomingWebhook{ 236 DisplayName: "title", 237 Description: "description", 238 ChannelId: th.BasicChannel.Id, 239 Username: "valid", 240 IconURL: "http://example.com/icon", 241 }, 242 243 ExpectedError: false, 244 ExpectedIncomingWebhook: &model.IncomingWebhook{ 245 DisplayName: "title", 246 Description: "description", 247 ChannelId: th.BasicChannel.Id, 248 Username: "valid", 249 IconURL: "http://example.com/icon", 250 }, 251 }, 252 } { 253 t.Run(name, func(t *testing.T) { 254 assert := assert.New(t) 255 256 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true }) 257 258 hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ 259 ChannelId: th.BasicChannel.Id, 260 }) 261 if err != nil { 262 t.Fatal(err.Error()) 263 } 264 defer th.App.DeleteIncomingWebhook(hook.Id) 265 266 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = tc.EnableIncomingHooks }) 267 th.App.UpdateConfig(func(cfg *model.Config) { 268 *cfg.ServiceSettings.EnablePostUsernameOverride = tc.EnablePostUsernameOverride 269 }) 270 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostIconOverride = tc.EnablePostIconOverride }) 271 272 updatedHook, err := th.App.UpdateIncomingWebhook(hook, &tc.IncomingWebhook) 273 if tc.ExpectedError && err == nil { 274 t.Fatal("should have failed") 275 } else if !tc.ExpectedError && err != nil { 276 t.Fatalf("should not have failed: %v", err.Error()) 277 } 278 if tc.ExpectedIncomingWebhook == nil { 279 assert.Nil(updatedHook, "expected nil webhook") 280 } else if assert.NotNil(updatedHook, "expected non-nil webhook") { 281 assert.Equal(tc.ExpectedIncomingWebhook.DisplayName, updatedHook.DisplayName) 282 assert.Equal(tc.ExpectedIncomingWebhook.Description, updatedHook.Description) 283 assert.Equal(tc.ExpectedIncomingWebhook.ChannelId, updatedHook.ChannelId) 284 assert.Equal(tc.ExpectedIncomingWebhook.Username, updatedHook.Username) 285 assert.Equal(tc.ExpectedIncomingWebhook.IconURL, updatedHook.IconURL) 286 } 287 }) 288 } 289 } 290 291 func TestCreateWebhookPost(t *testing.T) { 292 th := Setup(t).InitBasic() 293 defer th.TearDown() 294 295 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true }) 296 297 hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id}) 298 if err != nil { 299 t.Fatal(err.Error()) 300 } 301 defer th.App.DeleteIncomingWebhook(hook.Id) 302 303 post, err := th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", model.StringInterface{ 304 "attachments": []*model.SlackAttachment{ 305 { 306 Text: "text", 307 }, 308 }, 309 "webhook_display_name": hook.DisplayName, 310 }, model.POST_SLACK_ATTACHMENT, "") 311 if err != nil { 312 t.Fatal(err.Error()) 313 } 314 315 for _, k := range []string{"from_webhook", "attachments", "webhook_display_name"} { 316 if _, ok := post.Props[k]; !ok { 317 t.Log("missing one props: " + k) 318 t.Fatal(k) 319 } 320 } 321 322 _, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", nil, model.POST_SYSTEM_GENERIC, "") 323 if err == nil { 324 t.Fatal("should have failed - bad post type") 325 } 326 327 expectedText := "`<>|<>|`" 328 post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{ 329 "attachments": []*model.SlackAttachment{ 330 { 331 Text: "text", 332 }, 333 }, 334 "webhook_display_name": hook.DisplayName, 335 }, model.POST_SLACK_ATTACHMENT, "") 336 if err != nil { 337 t.Fatal(err.Error()) 338 } 339 assert.Equal(t, expectedText, post.Message) 340 341 expectedText = "< | \n|\n>" 342 post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{ 343 "attachments": []*model.SlackAttachment{ 344 { 345 Text: "text", 346 }, 347 }, 348 "webhook_display_name": hook.DisplayName, 349 }, model.POST_SLACK_ATTACHMENT, "") 350 if err != nil { 351 t.Fatal(err.Error()) 352 } 353 assert.Equal(t, expectedText, post.Message) 354 355 expectedText = `commit bc95839e4a430ace453e8b209a3723c000c1729a 356 Author: foo <foo@example.org> 357 Date: Thu Mar 1 19:46:54 2018 +0300 358 359 commit message 2 360 361 test | 1 + 362 1 file changed, 1 insertion(+) 363 364 commit 5df78b7139b543997838071cd912e375d8bd69b2 365 Author: foo <foo@example.org> 366 Date: Thu Mar 1 19:46:48 2018 +0300 367 368 commit message 1 369 370 test | 3 +++ 371 1 file changed, 3 insertions(+)` 372 post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{ 373 "attachments": []*model.SlackAttachment{ 374 { 375 Text: "text", 376 }, 377 }, 378 "webhook_display_name": hook.DisplayName, 379 }, model.POST_SLACK_ATTACHMENT, "") 380 if err != nil { 381 t.Fatal(err.Error()) 382 } 383 assert.Equal(t, expectedText, post.Message) 384 } 385 386 func TestSplitWebhookPost(t *testing.T) { 387 type TestCase struct { 388 Post *model.Post 389 Expected []*model.Post 390 } 391 392 maxPostSize := 10000 393 394 for name, tc := range map[string]TestCase{ 395 "LongPost": { 396 Post: &model.Post{ 397 Message: strings.Repeat("本", maxPostSize*3/2), 398 }, 399 Expected: []*model.Post{ 400 { 401 Message: strings.Repeat("本", maxPostSize), 402 }, 403 { 404 Message: strings.Repeat("本", maxPostSize/2), 405 }, 406 }, 407 }, 408 "LongPostAndMultipleAttachments": { 409 Post: &model.Post{ 410 Message: strings.Repeat("本", maxPostSize*3/2), 411 Props: map[string]interface{}{ 412 "attachments": []*model.SlackAttachment{ 413 &model.SlackAttachment{ 414 Text: strings.Repeat("本", 1000), 415 }, 416 &model.SlackAttachment{ 417 Text: strings.Repeat("本", 2000), 418 }, 419 &model.SlackAttachment{ 420 Text: strings.Repeat("本", model.POST_PROPS_MAX_USER_RUNES-1000), 421 }, 422 }, 423 }, 424 }, 425 Expected: []*model.Post{ 426 { 427 Message: strings.Repeat("本", maxPostSize), 428 }, 429 { 430 Message: strings.Repeat("本", maxPostSize/2), 431 Props: map[string]interface{}{ 432 "attachments": []*model.SlackAttachment{ 433 &model.SlackAttachment{ 434 Text: strings.Repeat("本", 1000), 435 }, 436 &model.SlackAttachment{ 437 Text: strings.Repeat("本", 2000), 438 }, 439 }, 440 }, 441 }, 442 { 443 Props: map[string]interface{}{ 444 "attachments": []*model.SlackAttachment{ 445 &model.SlackAttachment{ 446 Text: strings.Repeat("本", model.POST_PROPS_MAX_USER_RUNES-1000), 447 }, 448 }, 449 }, 450 }, 451 }, 452 }, 453 "UnsplittableProps": { 454 Post: &model.Post{ 455 Message: "foo", 456 Props: map[string]interface{}{ 457 "foo": strings.Repeat("x", model.POST_PROPS_MAX_USER_RUNES*2), 458 }, 459 }, 460 }, 461 } { 462 t.Run(name, func(t *testing.T) { 463 splits, err := SplitWebhookPost(tc.Post, maxPostSize) 464 if tc.Expected == nil { 465 require.NotNil(t, err) 466 } else { 467 require.Nil(t, err) 468 } 469 assert.Equal(t, len(tc.Expected), len(splits)) 470 for i, split := range splits { 471 if i < len(tc.Expected) { 472 assert.Equal(t, tc.Expected[i].Message, split.Message) 473 assert.Equal(t, tc.Expected[i].Props["attachments"], split.Props["attachments"]) 474 } 475 } 476 }) 477 } 478 } 479 480 func TestCreateOutGoingWebhookWithUsernameAndIconURL(t *testing.T) { 481 th := Setup(t).InitBasic() 482 defer th.TearDown() 483 484 outgoingWebhook := model.OutgoingWebhook{ 485 ChannelId: th.BasicChannel.Id, 486 TeamId: th.BasicChannel.TeamId, 487 CallbackURLs: []string{"http://nowhere.com"}, 488 Username: "some-user-name", 489 IconURL: "http://some-icon/", 490 DisplayName: "some-display-name", 491 Description: "some-description", 492 CreatorId: th.BasicUser.Id, 493 } 494 495 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true }) 496 497 createdHook, err := th.App.CreateOutgoingWebhook(&outgoingWebhook) 498 499 if err != nil { 500 t.Fatalf("should not have failed: %v", err.Error()) 501 } 502 503 assert.NotNil(t, createdHook, "should not be null") 504 505 assert.Equal(t, createdHook.ChannelId, outgoingWebhook.ChannelId) 506 assert.Equal(t, createdHook.TeamId, outgoingWebhook.TeamId) 507 assert.Equal(t, createdHook.CallbackURLs, outgoingWebhook.CallbackURLs) 508 assert.Equal(t, createdHook.Username, outgoingWebhook.Username) 509 assert.Equal(t, createdHook.IconURL, outgoingWebhook.IconURL) 510 assert.Equal(t, createdHook.DisplayName, outgoingWebhook.DisplayName) 511 assert.Equal(t, createdHook.Description, outgoingWebhook.Description) 512 513 } 514 515 func TestTriggerOutGoingWebhookWithUsernameAndIconURL(t *testing.T) { 516 517 getPayload := func(hook *model.OutgoingWebhook, th *TestHelper, channel *model.Channel) *model.OutgoingWebhookPayload { 518 return &model.OutgoingWebhookPayload{ 519 Token: hook.Token, 520 TeamId: hook.TeamId, 521 TeamDomain: th.BasicTeam.Name, 522 ChannelId: channel.Id, 523 ChannelName: channel.Name, 524 Timestamp: th.BasicPost.CreateAt, 525 UserId: th.BasicPost.UserId, 526 UserName: th.BasicUser.Username, 527 PostId: th.BasicPost.Id, 528 Text: th.BasicPost.Message, 529 TriggerWord: "Abracadabra", 530 FileIds: strings.Join(th.BasicPost.FileIds, ","), 531 } 532 } 533 534 waitUntilWebhookResposeIsCreatedAsPost := func(channel *model.Channel, th *TestHelper, t *testing.T, createdPost chan *model.Post) { 535 go func() { 536 for i := 0; i < 5; i++ { 537 time.Sleep(time.Second) 538 posts, _ := th.App.GetPosts(channel.Id, 0, 5) 539 if len(posts.Posts) > 0 { 540 for _, post := range posts.Posts { 541 createdPost <- post 542 return 543 } 544 } 545 } 546 }() 547 } 548 549 type TestCaseOutgoing struct { 550 EnablePostUsernameOverride bool 551 EnablePostIconOverride bool 552 ExpectedUsername string 553 ExpectedIconUrl string 554 WebhookResponse *model.OutgoingWebhookResponse 555 } 556 557 createOutgoingWebhook := func(channel *model.Channel, testCallBackUrl string, th *TestHelper) (*model.OutgoingWebhook, *model.AppError) { 558 outgoingWebhook := model.OutgoingWebhook{ 559 ChannelId: channel.Id, 560 TeamId: channel.TeamId, 561 CallbackURLs: []string{testCallBackUrl}, 562 Username: "some-user-name", 563 IconURL: "http://some-icon/", 564 DisplayName: "some-display-name", 565 Description: "some-description", 566 CreatorId: th.BasicUser.Id, 567 TriggerWords: []string{"Abracadabra"}, 568 ContentType: "application/json", 569 } 570 571 return th.App.CreateOutgoingWebhook(&outgoingWebhook) 572 } 573 574 getTestCases := func() map[string]TestCaseOutgoing { 575 576 webHookResponse := "sample response text from test server" 577 testCasesOutgoing := map[string]TestCaseOutgoing{ 578 579 "Should override username and Icon": { 580 EnablePostUsernameOverride: true, 581 EnablePostIconOverride: true, 582 ExpectedUsername: "some-user-name", 583 ExpectedIconUrl: "http://some-icon/", 584 }, 585 "Should not override username and Icon": { 586 EnablePostUsernameOverride: false, 587 EnablePostIconOverride: false, 588 }, 589 "Should not override username and Icon if the webhook response already has it": { 590 EnablePostUsernameOverride: true, 591 EnablePostIconOverride: true, 592 ExpectedUsername: "webhookuser", 593 ExpectedIconUrl: "http://webhok/icon", 594 WebhookResponse: &model.OutgoingWebhookResponse{Text: &webHookResponse, Username: "webhookuser", IconURL: "http://webhok/icon"}, 595 }, 596 } 597 return testCasesOutgoing 598 } 599 600 th := Setup(t).InitBasic() 601 defer th.TearDown() 602 603 th.App.UpdateConfig(func(cfg *model.Config) { 604 *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" 605 }) 606 createdPost := make(chan *model.Post) 607 608 for name, testCase := range getTestCases() { 609 t.Run(name, func(t *testing.T) { 610 611 th.App.UpdateConfig(func(cfg *model.Config) { 612 *cfg.ServiceSettings.EnableOutgoingWebhooks = true 613 *cfg.ServiceSettings.EnablePostUsernameOverride = testCase.EnablePostUsernameOverride 614 *cfg.ServiceSettings.EnablePostIconOverride = testCase.EnablePostIconOverride 615 }) 616 617 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 618 if testCase.WebhookResponse != nil { 619 w.Write([]byte(testCase.WebhookResponse.ToJson())) 620 } else { 621 w.Write([]byte(`{"text": "sample response text from test server"}`)) 622 } 623 })) 624 defer ts.Close() 625 626 channel := th.CreateChannel(th.BasicTeam) 627 hook, _ := createOutgoingWebhook(channel, ts.URL, th) 628 payload := getPayload(hook, th, channel) 629 630 th.App.TriggerWebhook(payload, hook, th.BasicPost, channel) 631 632 waitUntilWebhookResposeIsCreatedAsPost(channel, th, t, createdPost) 633 634 select { 635 case webhookPost := <-createdPost: 636 assert.Equal(t, webhookPost.Message, "sample response text from test server") 637 assert.Equal(t, webhookPost.Props["from_webhook"], "true") 638 if testCase.ExpectedIconUrl != "" { 639 assert.Equal(t, webhookPost.Props["override_icon_url"], testCase.ExpectedIconUrl) 640 } else { 641 assert.Nil(t, webhookPost.Props["override_icon_url"]) 642 } 643 644 if testCase.ExpectedUsername != "" { 645 assert.Equal(t, webhookPost.Props["override_username"], testCase.ExpectedUsername) 646 } else { 647 assert.Nil(t, webhookPost.Props["override_username"]) 648 } 649 case <-time.After(5 * time.Second): 650 t.Fatal("Timeout, webhook response not created as post") 651 } 652 653 }) 654 } 655 656 } 657 658 type InfiniteReader struct { 659 Prefix string 660 } 661 662 func (r InfiniteReader) Read(p []byte) (n int, err error) { 663 for i := range p { 664 p[i] = 'a' 665 } 666 667 return len(p), nil 668 } 669 670 func TestDoOutgoingWebhookRequest(t *testing.T) { 671 th := Setup(t).InitBasic() 672 defer th.TearDown() 673 674 th.App.UpdateConfig(func(cfg *model.Config) { 675 cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewString("127.0.0.1") 676 *cfg.ServiceSettings.EnableOutgoingWebhooks = true 677 }) 678 679 t.Run("with a valid response", func(t *testing.T) { 680 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 681 io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`)) 682 })) 683 defer server.Close() 684 685 resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json") 686 require.Nil(t, err) 687 688 assert.NotNil(t, resp) 689 assert.NotNil(t, resp.Text) 690 assert.Equal(t, "Hello, World!", *resp.Text) 691 }) 692 693 t.Run("with an invalid response", func(t *testing.T) { 694 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 695 io.Copy(w, strings.NewReader("aaaaaaaa")) 696 })) 697 defer server.Close() 698 699 _, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json") 700 require.NotNil(t, err) 701 require.IsType(t, &json.SyntaxError{}, err) 702 }) 703 704 t.Run("with a large, valid response", func(t *testing.T) { 705 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 706 io.Copy(w, io.MultiReader(strings.NewReader(`{"text": "`), InfiniteReader{}, strings.NewReader(`"}`))) 707 })) 708 defer server.Close() 709 710 _, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json") 711 require.NotNil(t, err) 712 require.Equal(t, io.ErrUnexpectedEOF, err) 713 }) 714 715 t.Run("with a large, invalid response", func(t *testing.T) { 716 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 717 io.Copy(w, InfiniteReader{}) 718 })) 719 defer server.Close() 720 721 _, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json") 722 require.NotNil(t, err) 723 require.IsType(t, &json.SyntaxError{}, err) 724 }) 725 726 t.Run("with a slow response", func(t *testing.T) { 727 timeout := 100 * time.Millisecond 728 729 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 730 time.Sleep(timeout + time.Millisecond) 731 io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`)) 732 })) 733 defer server.Close() 734 735 th.App.HTTPService.(*httpservice.HTTPServiceImpl).RequestTimeout = timeout 736 defer func() { 737 th.App.HTTPService.(*httpservice.HTTPServiceImpl).RequestTimeout = httpservice.RequestTimeout 738 }() 739 740 _, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json") 741 require.NotNil(t, err) 742 require.IsType(t, &url.Error{}, err) 743 }) 744 }