github.com/adacta-ru/mattermost-server/v6@v6.0.0/app/slashcommands/command_test.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package slashcommands 5 6 import ( 7 "fmt" 8 "io" 9 "net/http" 10 "net/http/httptest" 11 "net/url" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18 19 "github.com/adacta-ru/mattermost-server/v6/model" 20 "github.com/adacta-ru/mattermost-server/v6/services/httpservice" 21 ) 22 23 type InfiniteReader struct { 24 Prefix string 25 } 26 27 func (r InfiniteReader) Read(p []byte) (n int, err error) { 28 for i := range p { 29 p[i] = 'a' 30 } 31 32 return len(p), nil 33 } 34 35 func TestMoveCommand(t *testing.T) { 36 th := setup(t) 37 defer th.tearDown() 38 39 sourceTeam := th.createTeam() 40 targetTeam := th.createTeam() 41 42 command := &model.Command{} 43 command.CreatorId = model.NewId() 44 command.Method = model.COMMAND_METHOD_POST 45 command.TeamId = sourceTeam.Id 46 command.URL = "http://nowhere.com/" 47 command.Trigger = "trigger1" 48 49 command, err := th.App.CreateCommand(command) 50 assert.Nil(t, err) 51 52 defer func() { 53 th.App.PermanentDeleteTeam(sourceTeam) 54 th.App.PermanentDeleteTeam(targetTeam) 55 }() 56 57 // Move a command and check the team is updated. 58 assert.Nil(t, th.App.MoveCommand(targetTeam, command)) 59 retrievedCommand, err := th.App.GetCommand(command.Id) 60 assert.Nil(t, err) 61 assert.EqualValues(t, targetTeam.Id, retrievedCommand.TeamId) 62 63 // Move it to the team it's already in. Nothing should change. 64 assert.Nil(t, th.App.MoveCommand(targetTeam, command)) 65 retrievedCommand, err = th.App.GetCommand(command.Id) 66 assert.Nil(t, err) 67 assert.EqualValues(t, targetTeam.Id, retrievedCommand.TeamId) 68 } 69 70 func TestCreateCommandPost(t *testing.T) { 71 th := setup(t).initBasic() 72 defer th.tearDown() 73 74 post := &model.Post{ 75 ChannelId: th.BasicChannel.Id, 76 UserId: th.BasicUser.Id, 77 Type: model.POST_SYSTEM_GENERIC, 78 } 79 80 resp := &model.CommandResponse{ 81 Text: "some message", 82 } 83 84 skipSlackParsing := false 85 _, err := th.App.CreateCommandPost(post, th.BasicTeam.Id, resp, skipSlackParsing) 86 require.NotNil(t, err) 87 require.Equal(t, err.Id, "api.context.invalid_param.app_error") 88 } 89 90 func TestExecuteCommand(t *testing.T) { 91 th := setup(t).initBasic() 92 defer th.tearDown() 93 94 t.Run("valid tests with different whitespace characters", func(t *testing.T) { 95 TestCases := map[string]string{ 96 "/code happy path": " happy path", 97 "/code\nnewline path": " newline path", 98 "/code\n/nDouble newline path": " /nDouble newline path", 99 "/code double space": " double space", 100 "/code\ttab": " tab", 101 } 102 103 for TestCase, result := range TestCases { 104 args := &model.CommandArgs{ 105 Command: TestCase, 106 TeamId: th.BasicTeam.Id, 107 ChannelId: th.BasicChannel.Id, 108 UserId: th.BasicUser.Id, 109 T: func(s string, args ...interface{}) string { return s }, 110 } 111 resp, err := th.App.ExecuteCommand(args) 112 require.Nil(t, err) 113 require.NotNil(t, resp) 114 115 assert.Equal(t, resp.Text, result) 116 } 117 }) 118 119 t.Run("missing slash character", func(t *testing.T) { 120 argsMissingSlashCharacter := &model.CommandArgs{ 121 Command: "missing leading slash character", 122 T: func(s string, args ...interface{}) string { return s }, 123 } 124 _, err := th.App.ExecuteCommand(argsMissingSlashCharacter) 125 require.Equal(t, "api.command.execute_command.format.app_error", err.Id) 126 }) 127 128 t.Run("empty", func(t *testing.T) { 129 argsMissingSlashCharacter := &model.CommandArgs{ 130 Command: "", 131 T: func(s string, args ...interface{}) string { return s }, 132 } 133 _, err := th.App.ExecuteCommand(argsMissingSlashCharacter) 134 require.Equal(t, "api.command.execute_command.format.app_error", err.Id) 135 }) 136 } 137 138 func TestHandleCommandResponsePost(t *testing.T) { 139 th := setup(t).initBasic() 140 defer th.tearDown() 141 142 command := &model.Command{} 143 args := &model.CommandArgs{ 144 ChannelId: th.BasicChannel.Id, 145 TeamId: th.BasicTeam.Id, 146 UserId: th.BasicUser.Id, 147 RootId: "", 148 ParentId: "", 149 } 150 151 resp := &model.CommandResponse{ 152 Type: model.POST_DEFAULT, 153 ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, 154 Props: model.StringInterface{"some_key": "some value"}, 155 Text: "some message", 156 } 157 158 builtIn := true 159 160 post, err := th.App.HandleCommandResponsePost(command, args, resp, builtIn) 161 assert.Nil(t, err) 162 assert.Equal(t, args.ChannelId, post.ChannelId) 163 assert.Equal(t, args.RootId, post.RootId) 164 assert.Equal(t, args.ParentId, post.ParentId) 165 assert.Equal(t, args.UserId, post.UserId) 166 assert.Equal(t, resp.Type, post.Type) 167 assert.Equal(t, resp.Props, post.GetProps()) 168 assert.Equal(t, resp.Text, post.Message) 169 assert.Nil(t, post.GetProp("override_icon_url")) 170 assert.Nil(t, post.GetProp("override_username")) 171 assert.Nil(t, post.GetProp("from_webhook")) 172 173 // Command is not built in, so it is a bot command. 174 builtIn = false 175 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 176 assert.Equal(t, "true", post.GetProp("from_webhook")) 177 178 builtIn = true 179 180 // Channel id is specified by response, it should override the command args value. 181 channel := th.CreateChannel(th.BasicTeam) 182 resp.ChannelId = channel.Id 183 th.addUserToChannel(th.BasicUser, channel) 184 185 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 186 assert.Nil(t, err) 187 assert.Equal(t, resp.ChannelId, post.ChannelId) 188 assert.NotEqual(t, args.ChannelId, post.ChannelId) 189 190 // Override username config is turned off. No override should occur. 191 *th.App.Config().ServiceSettings.EnablePostUsernameOverride = false 192 resp.ChannelId = "" 193 command.Username = "Command username" 194 resp.Username = "Response username" 195 196 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 197 assert.Nil(t, err) 198 assert.Nil(t, post.GetProp("override_username")) 199 200 *th.App.Config().ServiceSettings.EnablePostUsernameOverride = true 201 202 // Override username config is turned on. Override username through command property. 203 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 204 assert.Nil(t, err) 205 assert.Equal(t, command.Username, post.GetProp("override_username")) 206 assert.Equal(t, "true", post.GetProp("from_webhook")) 207 208 command.Username = "" 209 210 // Override username through response property. 211 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 212 assert.Nil(t, err) 213 assert.Equal(t, resp.Username, post.GetProp("override_username")) 214 assert.Equal(t, "true", post.GetProp("from_webhook")) 215 216 *th.App.Config().ServiceSettings.EnablePostUsernameOverride = false 217 218 // Override icon url config is turned off. No override should occur. 219 *th.App.Config().ServiceSettings.EnablePostIconOverride = false 220 command.IconURL = "Command icon url" 221 resp.IconURL = "Response icon url" 222 223 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 224 assert.Nil(t, err) 225 assert.Nil(t, post.GetProp("override_icon_url")) 226 227 *th.App.Config().ServiceSettings.EnablePostIconOverride = true 228 229 // Override icon url config is turned on. Override icon url through command property. 230 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 231 assert.Nil(t, err) 232 assert.Equal(t, command.IconURL, post.GetProp("override_icon_url")) 233 assert.Equal(t, "true", post.GetProp("from_webhook")) 234 235 command.IconURL = "" 236 237 // Override icon url through response property. 238 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 239 assert.Nil(t, err) 240 assert.Equal(t, resp.IconURL, post.GetProp("override_icon_url")) 241 assert.Equal(t, "true", post.GetProp("from_webhook")) 242 243 // Test Slack text conversion. 244 resp.Text = "<!channel>" 245 246 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 247 assert.Nil(t, err) 248 assert.Equal(t, "@channel", post.Message) 249 assert.Equal(t, "true", post.GetProp("from_webhook")) 250 251 // Test Slack attachments text conversion. 252 resp.Attachments = []*model.SlackAttachment{ 253 { 254 Text: "<!here>", 255 }, 256 } 257 258 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 259 assert.Nil(t, err) 260 assert.Equal(t, "@channel", post.Message) 261 if assert.Len(t, post.Attachments(), 1) { 262 assert.Equal(t, "@here", post.Attachments()[0].Text) 263 } 264 assert.Equal(t, "true", post.GetProp("from_webhook")) 265 266 channel = th.createPrivateChannel(th.BasicTeam) 267 resp.ChannelId = channel.Id 268 args.UserId = th.BasicUser2.Id 269 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 270 271 require.NotNil(t, err) 272 require.Equal(t, err.Id, "api.command.command_post.forbidden.app_error") 273 274 // Test that /code text is not converted with the Slack text conversion. 275 command.Trigger = "code" 276 resp.ChannelId = "" 277 resp.Text = "<test.com|test website>" 278 resp.Attachments = []*model.SlackAttachment{ 279 { 280 Text: "<!here>", 281 }, 282 } 283 284 // set and unset SkipSlackParsing here seems the nicest way as no separate response objects are created for every testcase. 285 resp.SkipSlackParsing = true 286 post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn) 287 resp.SkipSlackParsing = false 288 289 assert.Nil(t, err) 290 assert.Equal(t, resp.Text, post.Message, "/code text should not be converted to Slack links") 291 assert.Equal(t, "<!here>", resp.Attachments[0].Text) 292 } 293 294 func TestHandleCommandResponse(t *testing.T) { 295 th := setup(t).initBasic() 296 defer th.tearDown() 297 298 command := &model.Command{} 299 300 args := &model.CommandArgs{ 301 Command: "/invite username", 302 UserId: th.BasicUser.Id, 303 ChannelId: th.BasicChannel.Id, 304 } 305 306 resp := &model.CommandResponse{ 307 Text: "message 1", 308 Type: model.POST_SYSTEM_GENERIC, 309 } 310 311 builtIn := true 312 313 _, err := th.App.HandleCommandResponse(command, args, resp, builtIn) 314 require.NotNil(t, err) 315 require.Equal(t, err.Id, "api.command.execute_command.create_post_failed.app_error") 316 317 resp = &model.CommandResponse{ 318 Text: "message 1", 319 } 320 321 _, err = th.App.HandleCommandResponse(command, args, resp, builtIn) 322 assert.Nil(t, err) 323 324 resp = &model.CommandResponse{ 325 Text: "message 1", 326 ExtraResponses: []*model.CommandResponse{ 327 { 328 Text: "message 2", 329 }, 330 { 331 Type: model.POST_SYSTEM_GENERIC, 332 Text: "message 3", 333 }, 334 }, 335 } 336 337 _, err = th.App.HandleCommandResponse(command, args, resp, builtIn) 338 require.NotNil(t, err) 339 require.Equal(t, err.Id, "api.command.execute_command.create_post_failed.app_error") 340 341 resp = &model.CommandResponse{ 342 ExtraResponses: []*model.CommandResponse{ 343 {}, 344 {}, 345 }, 346 } 347 348 _, err = th.App.HandleCommandResponse(command, args, resp, builtIn) 349 assert.Nil(t, err) 350 } 351 352 func TestDoCommandRequest(t *testing.T) { 353 th := setup(t) 354 defer th.tearDown() 355 356 th.App.UpdateConfig(func(cfg *model.Config) { 357 cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewString("127.0.0.1") 358 cfg.ServiceSettings.EnableCommands = model.NewBool(true) 359 }) 360 361 t.Run("with a valid text response", func(t *testing.T) { 362 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 363 io.Copy(w, strings.NewReader("Hello, World!")) 364 })) 365 defer server.Close() 366 367 _, resp, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{}) 368 require.Nil(t, err) 369 370 assert.NotNil(t, resp) 371 assert.Equal(t, "Hello, World!", resp.Text) 372 }) 373 374 t.Run("with a valid json response", func(t *testing.T) { 375 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 376 w.Header().Add("Content-Type", "application/json") 377 378 io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`)) 379 })) 380 defer server.Close() 381 382 _, resp, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{}) 383 require.Nil(t, err) 384 385 assert.NotNil(t, resp) 386 assert.Equal(t, "Hello, World!", resp.Text) 387 }) 388 389 t.Run("with a large text response", func(t *testing.T) { 390 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 391 io.Copy(w, InfiniteReader{}) 392 })) 393 defer server.Close() 394 395 // Since we limit the length of the response, no error will be returned and resp.Text will be a finite string 396 397 _, resp, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{}) 398 require.Nil(t, err) 399 require.NotNil(t, resp) 400 }) 401 402 t.Run("with a large, valid json response", func(t *testing.T) { 403 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 404 w.Header().Add("Content-Type", "application/json") 405 406 io.Copy(w, io.MultiReader(strings.NewReader(`{"text": "`), InfiniteReader{}, strings.NewReader(`"}`))) 407 })) 408 defer server.Close() 409 410 _, _, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{}) 411 require.NotNil(t, err) 412 require.Equal(t, "api.command.execute_command.failed.app_error", err.Id) 413 }) 414 415 t.Run("with a large, invalid json response", func(t *testing.T) { 416 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 417 w.Header().Add("Content-Type", "application/json") 418 419 io.Copy(w, InfiniteReader{}) 420 })) 421 defer server.Close() 422 423 _, _, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{}) 424 require.NotNil(t, err) 425 require.Equal(t, "api.command.execute_command.failed.app_error", err.Id) 426 }) 427 428 t.Run("with a slow response", func(t *testing.T) { 429 done := make(chan bool) 430 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 431 <-done 432 io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`)) 433 })) 434 defer server.Close() 435 436 th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = 100 * time.Millisecond 437 defer func() { 438 th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = httpservice.RequestTimeout 439 }() 440 441 _, _, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{}) 442 require.NotNil(t, err) 443 require.Equal(t, "api.command.execute_command.failed.app_error", err.Id) 444 close(done) 445 }) 446 } 447 448 func TestMentionsToTeamMembers(t *testing.T) { 449 th := setup(t).initBasic() 450 defer th.tearDown() 451 452 otherTeam := th.createTeam() 453 otherUser := th.createUser() 454 th.linkUserToTeam(otherUser, otherTeam) 455 456 fixture := []struct { 457 message string 458 inTeam string 459 expectedMap model.UserMentionMap 460 }{ 461 { 462 "", 463 th.BasicTeam.Id, 464 model.UserMentionMap{}, 465 }, 466 { 467 "/trigger", 468 th.BasicTeam.Id, 469 model.UserMentionMap{}, 470 }, 471 { 472 "/trigger 0 mentions", 473 th.BasicTeam.Id, 474 model.UserMentionMap{}, 475 }, 476 { 477 fmt.Sprintf("/trigger 1 valid user @%s", th.BasicUser.Username), 478 th.BasicTeam.Id, 479 model.UserMentionMap{th.BasicUser.Username: th.BasicUser.Id}, 480 }, 481 { 482 fmt.Sprintf("/trigger 2 valid users @%s @%s", 483 th.BasicUser.Username, th.BasicUser2.Username, 484 ), 485 th.BasicTeam.Id, 486 model.UserMentionMap{ 487 th.BasicUser.Username: th.BasicUser.Id, 488 th.BasicUser2.Username: th.BasicUser2.Id, 489 }, 490 }, 491 { 492 fmt.Sprintf("/trigger 1 user from another team @%s", otherUser.Username), 493 th.BasicTeam.Id, 494 model.UserMentionMap{}, 495 }, 496 { 497 fmt.Sprintf("/trigger 2 valid users + 1 from another team @%s @%s @%s", 498 th.BasicUser.Username, th.BasicUser2.Username, otherUser.Username, 499 ), 500 th.BasicTeam.Id, 501 model.UserMentionMap{ 502 th.BasicUser.Username: th.BasicUser.Id, 503 th.BasicUser2.Username: th.BasicUser2.Id, 504 }, 505 }, 506 { 507 fmt.Sprintf("/trigger a valid channel ~%s", th.BasicChannel.Name), 508 th.BasicTeam.Id, 509 model.UserMentionMap{}, 510 }, 511 { 512 fmt.Sprintf("/trigger channel and mentions ~%s @%s", 513 th.BasicChannel.Name, th.BasicUser.Username), 514 th.BasicTeam.Id, 515 model.UserMentionMap{th.BasicUser.Username: th.BasicUser.Id}, 516 }, 517 { 518 fmt.Sprintf("/trigger repeated users @%s @%s @%s", 519 th.BasicUser.Username, th.BasicUser2.Username, th.BasicUser.Username), 520 th.BasicTeam.Id, 521 model.UserMentionMap{ 522 th.BasicUser.Username: th.BasicUser.Id, 523 th.BasicUser2.Username: th.BasicUser2.Id, 524 }, 525 }, 526 } 527 528 for _, data := range fixture { 529 actualMap := th.App.MentionsToTeamMembers(data.message, data.inTeam) 530 require.Equal(t, actualMap, data.expectedMap) 531 } 532 } 533 534 func TestMentionsToPublicChannels(t *testing.T) { 535 th := setup(t).initBasic() 536 defer th.tearDown() 537 538 otherPublicChannel := th.CreateChannel(th.BasicTeam) 539 privateChannel := th.createPrivateChannel(th.BasicTeam) 540 541 fixture := []struct { 542 message string 543 inTeam string 544 expectedMap model.ChannelMentionMap 545 }{ 546 { 547 "", 548 th.BasicTeam.Id, 549 model.ChannelMentionMap{}, 550 }, 551 { 552 "/trigger", 553 th.BasicTeam.Id, 554 model.ChannelMentionMap{}, 555 }, 556 { 557 "/trigger 0 mentions", 558 th.BasicTeam.Id, 559 model.ChannelMentionMap{}, 560 }, 561 { 562 fmt.Sprintf("/trigger 1 public channel ~%s", th.BasicChannel.Name), 563 th.BasicTeam.Id, 564 model.ChannelMentionMap{th.BasicChannel.Name: th.BasicChannel.Id}, 565 }, 566 { 567 fmt.Sprintf("/trigger 2 public channels ~%s ~%s", 568 th.BasicChannel.Name, otherPublicChannel.Name, 569 ), 570 th.BasicTeam.Id, 571 model.ChannelMentionMap{ 572 th.BasicChannel.Name: th.BasicChannel.Id, 573 otherPublicChannel.Name: otherPublicChannel.Id, 574 }, 575 }, 576 { 577 fmt.Sprintf("/trigger 1 private channel ~%s", privateChannel.Name), 578 th.BasicTeam.Id, 579 model.ChannelMentionMap{}, 580 }, 581 { 582 fmt.Sprintf("/trigger 2 public channel + 1 private ~%s ~%s ~%s", 583 th.BasicChannel.Name, otherPublicChannel.Name, privateChannel.Name, 584 ), 585 th.BasicTeam.Id, 586 model.ChannelMentionMap{ 587 th.BasicChannel.Name: th.BasicChannel.Id, 588 otherPublicChannel.Name: otherPublicChannel.Id, 589 }, 590 }, 591 { 592 fmt.Sprintf("/trigger a valid user @%s", th.BasicUser.Username), 593 th.BasicTeam.Id, 594 model.ChannelMentionMap{}, 595 }, 596 { 597 fmt.Sprintf("/trigger channel and mentions ~%s @%s", 598 th.BasicChannel.Name, th.BasicUser.Username), 599 th.BasicTeam.Id, 600 model.ChannelMentionMap{th.BasicChannel.Name: th.BasicChannel.Id}, 601 }, 602 { 603 fmt.Sprintf("/trigger repeated channels ~%s ~%s ~%s", 604 th.BasicChannel.Name, otherPublicChannel.Name, th.BasicChannel.Name), 605 th.BasicTeam.Id, 606 model.ChannelMentionMap{ 607 th.BasicChannel.Name: th.BasicChannel.Id, 608 otherPublicChannel.Name: otherPublicChannel.Id, 609 }, 610 }, 611 } 612 613 for _, data := range fixture { 614 actualMap := th.App.MentionsToPublicChannels(data.message, data.inTeam) 615 require.Equal(t, actualMap, data.expectedMap) 616 } 617 }