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