github.com/haalcala/mattermost-server-change-repo@v0.0.0-20210713015153-16753fbeee5f/app/slashcommands/command_loadtest.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 "io" 8 "io/ioutil" 9 "net/http" 10 "path" 11 "regexp" 12 "strconv" 13 "strings" 14 15 goi18n "github.com/mattermost/go-i18n/i18n" 16 "github.com/pkg/errors" 17 18 "github.com/mattermost/mattermost-server/v5/app" 19 "github.com/mattermost/mattermost-server/v5/mlog" 20 "github.com/mattermost/mattermost-server/v5/model" 21 "github.com/mattermost/mattermost-server/v5/utils" 22 ) 23 24 var usage = `Mattermost testing commands to help configure the system 25 26 COMMANDS: 27 28 Setup - Creates a testing environment in current team. 29 /test setup [teams] [fuzz] <Num Channels> <Num Users> <NumPosts> 30 31 Example: 32 /test setup teams fuzz 10 20 50 33 34 Users - Add a specified number of random users with fuzz text to current team. 35 /test users [fuzz] <Min Users> <Max Users> 36 37 Example: 38 /test users fuzz 5 10 39 40 Channels - Add a specified number of random channels with fuzz text to current team. 41 /test channels [fuzz] <Min Channels> <Max Channels> 42 43 Example: 44 /test channels fuzz 5 10 45 46 ThreadedPost - create a large threaded post 47 /test threaded_post 48 49 Posts - Add some random posts with fuzz text to current channel. 50 /test posts [fuzz] <Min Posts> <Max Posts> <Max Images> 51 52 Example: 53 /test posts fuzz 5 10 3 54 55 Post - Add post to a channel as another user. 56 /test post u=@username p=passwd c=~channelname t=teamname "message" 57 58 Example: 59 /test post u=@user-1 p=user-1 c=~town-square t=ad-1 "message" 60 61 Url - Add a post containing the text from a given url to current channel. 62 /test url 63 64 Example: 65 /test http://www.example.com/sample_file.md 66 67 Json - Add a post using the JSON file as payload to the current channel. 68 /test json url 69 70 Example 71 /test json http://www.example.com/sample_body.json 72 73 ` 74 75 const ( 76 CmdTest = "test" 77 ) 78 79 var ( 80 userRE = regexp.MustCompile(`u=@([^\s]+)`) 81 passwdRE = regexp.MustCompile(`p=([^\s]+)`) 82 teamRE = regexp.MustCompile(`t=([^\s]+)`) 83 channelRE = regexp.MustCompile(`c=~([^\s]+)`) 84 messageRE = regexp.MustCompile(`"(.*)"`) 85 ) 86 87 type LoadTestProvider struct { 88 } 89 90 func init() { 91 app.RegisterCommandProvider(&LoadTestProvider{}) 92 } 93 94 func (*LoadTestProvider) GetTrigger() string { 95 return CmdTest 96 } 97 98 func (*LoadTestProvider) GetCommand(a *app.App, T goi18n.TranslateFunc) *model.Command { 99 if !*a.Config().ServiceSettings.EnableTesting { 100 return nil 101 } 102 return &model.Command{ 103 Trigger: CmdTest, 104 AutoComplete: false, 105 AutoCompleteDesc: "Debug Load Testing", 106 AutoCompleteHint: "help", 107 DisplayName: "test", 108 } 109 } 110 111 func (lt *LoadTestProvider) DoCommand(a *app.App, args *model.CommandArgs, message string) *model.CommandResponse { 112 commandResponse, err := lt.doCommand(a, args, message) 113 if err != nil { 114 mlog.Error("failed command /"+CmdTest, mlog.Err(err)) 115 } 116 117 return commandResponse 118 } 119 120 func (lt *LoadTestProvider) doCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 121 //This command is only available when EnableTesting is true 122 if !*a.Config().ServiceSettings.EnableTesting { 123 return &model.CommandResponse{}, nil 124 } 125 126 if strings.HasPrefix(message, "setup") { 127 return lt.SetupCommand(a, args, message) 128 } 129 130 if strings.HasPrefix(message, "users") { 131 return lt.UsersCommand(a, args, message) 132 } 133 134 if strings.HasPrefix(message, "activate_user") { 135 return lt.ActivateUserCommand(a, args, message) 136 } 137 138 if strings.HasPrefix(message, "deactivate_user") { 139 return lt.DeActivateUserCommand(a, args, message) 140 } 141 142 if strings.HasPrefix(message, "channels") { 143 return lt.ChannelsCommand(a, args, message) 144 } 145 146 if strings.HasPrefix(message, "posts") { 147 return lt.PostsCommand(a, args, message) 148 } 149 150 if strings.HasPrefix(message, "post") { 151 return lt.PostCommand(a, args, message) 152 } 153 154 if strings.HasPrefix(message, "threaded_post") { 155 return lt.ThreadedPostCommand(a, args, message) 156 } 157 158 if strings.HasPrefix(message, "url") { 159 return lt.UrlCommand(a, args, message) 160 } 161 162 if strings.HasPrefix(message, "json") { 163 return lt.JsonCommand(a, args, message) 164 } 165 166 return lt.HelpCommand(args, message), nil 167 } 168 169 func (*LoadTestProvider) HelpCommand(args *model.CommandArgs, message string) *model.CommandResponse { 170 return &model.CommandResponse{Text: usage, ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} 171 } 172 173 func (*LoadTestProvider) SetupCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 174 tokens := strings.Fields(strings.TrimPrefix(message, "setup")) 175 doTeams := contains(tokens, "teams") 176 doFuzz := contains(tokens, "fuzz") 177 178 numArgs := 0 179 if doTeams { 180 numArgs++ 181 } 182 if doFuzz { 183 numArgs++ 184 } 185 186 var numTeams int 187 var numChannels int 188 var numUsers int 189 var numPosts int 190 191 // Defaults 192 numTeams = 10 193 numChannels = 10 194 numUsers = 10 195 numPosts = 10 196 197 if doTeams { 198 if (len(tokens) - numArgs) >= 4 { 199 numTeams, _ = strconv.Atoi(tokens[numArgs+0]) 200 numChannels, _ = strconv.Atoi(tokens[numArgs+1]) 201 numUsers, _ = strconv.Atoi(tokens[numArgs+2]) 202 numPosts, _ = strconv.Atoi(tokens[numArgs+3]) 203 } 204 } else { 205 if (len(tokens) - numArgs) >= 3 { 206 numChannels, _ = strconv.Atoi(tokens[numArgs+0]) 207 numUsers, _ = strconv.Atoi(tokens[numArgs+1]) 208 numPosts, _ = strconv.Atoi(tokens[numArgs+2]) 209 } 210 } 211 client := model.NewAPIv4Client(args.SiteURL) 212 213 if doTeams { 214 if err := CreateBasicUser(a, client); err != nil { 215 return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 216 } 217 _, resp := client.Login(BTestUserEmail, BTestUserPassword) 218 if resp.Error != nil { 219 return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, resp.Error 220 } 221 environment, err := CreateTestEnvironmentWithTeams( 222 a, 223 client, 224 utils.Range{Begin: numTeams, End: numTeams}, 225 utils.Range{Begin: numChannels, End: numChannels}, 226 utils.Range{Begin: numUsers, End: numUsers}, 227 utils.Range{Begin: numPosts, End: numPosts}, 228 doFuzz) 229 if err != nil { 230 return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 231 } 232 233 mlog.Info("Testing environment created") 234 for i := 0; i < len(environment.Teams); i++ { 235 mlog.Info("Team Created: " + environment.Teams[i].Name) 236 mlog.Info("\t User to login: " + environment.Environments[i].Users[0].Email + ", " + UserPassword) 237 } 238 } else { 239 team, err := a.Srv().Store.Team().Get(args.TeamId) 240 if err != nil { 241 return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 242 } 243 244 CreateTestEnvironmentInTeam( 245 a, 246 client, 247 team, 248 utils.Range{Begin: numChannels, End: numChannels}, 249 utils.Range{Begin: numUsers, End: numUsers}, 250 utils.Range{Begin: numPosts, End: numPosts}, 251 doFuzz) 252 } 253 254 return &model.CommandResponse{Text: "Created environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 255 } 256 257 func (*LoadTestProvider) ActivateUserCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 258 user_id := strings.TrimSpace(strings.TrimPrefix(message, "activate_user")) 259 if err := a.UpdateUserActive(user_id, true); err != nil { 260 return &model.CommandResponse{Text: "Failed to activate user", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 261 } 262 263 return &model.CommandResponse{Text: "Activated user", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 264 } 265 266 func (*LoadTestProvider) DeActivateUserCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 267 user_id := strings.TrimSpace(strings.TrimPrefix(message, "deactivate_user")) 268 if err := a.UpdateUserActive(user_id, false); err != nil { 269 return &model.CommandResponse{Text: "Failed to deactivate user", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 270 } 271 272 return &model.CommandResponse{Text: "DeActivated user", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 273 } 274 275 func (*LoadTestProvider) UsersCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 276 cmd := strings.TrimSpace(strings.TrimPrefix(message, "users")) 277 278 doFuzz := false 279 if strings.Index(cmd, "fuzz") == 0 { 280 doFuzz = true 281 cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "fuzz")) 282 } 283 284 usersr, ok := parseRange(cmd, "") 285 if !ok { 286 usersr = utils.Range{Begin: 2, End: 5} 287 } 288 289 team, err := a.Srv().Store.Team().Get(args.TeamId) 290 if err != nil { 291 return &model.CommandResponse{Text: "Failed to add users", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 292 } 293 294 client := model.NewAPIv4Client(args.SiteURL) 295 userCreator := NewAutoUserCreator(a, client, team) 296 userCreator.Fuzzy = doFuzz 297 if _, err := userCreator.CreateTestUsers(usersr); err != nil { 298 return &model.CommandResponse{Text: "Failed to add users: " + err.Error(), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 299 } 300 301 return &model.CommandResponse{Text: "Added users", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 302 } 303 304 func (*LoadTestProvider) ChannelsCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 305 cmd := strings.TrimSpace(strings.TrimPrefix(message, "channels")) 306 307 doFuzz := false 308 if strings.Index(cmd, "fuzz") == 0 { 309 doFuzz = true 310 cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "fuzz")) 311 } 312 313 channelsr, ok := parseRange(cmd, "") 314 if !ok { 315 channelsr = utils.Range{Begin: 2, End: 5} 316 } 317 318 team, err := a.Srv().Store.Team().Get(args.TeamId) 319 if err != nil { 320 return &model.CommandResponse{Text: "Failed to add channels", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 321 } 322 323 channelCreator := NewAutoChannelCreator(a, team, args.UserId) 324 channelCreator.Fuzzy = doFuzz 325 if _, err := channelCreator.CreateTestChannels(channelsr); err != nil { 326 return &model.CommandResponse{Text: "Failed to create test channels: " + err.Error(), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 327 } 328 329 return &model.CommandResponse{Text: "Added channels", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 330 } 331 332 func (*LoadTestProvider) ThreadedPostCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 333 var usernames []string 334 options := &model.UserGetOptions{InTeamId: args.TeamId, Page: 0, PerPage: 1000} 335 if profileUsers, err := a.Srv().Store.User().GetProfiles(options); err == nil { 336 usernames = make([]string, len(profileUsers)) 337 i := 0 338 for _, userprof := range profileUsers { 339 usernames[i] = userprof.Username 340 i++ 341 } 342 } 343 344 testPoster := NewAutoPostCreator(a, args.ChannelId, args.UserId) 345 testPoster.Fuzzy = true 346 testPoster.Users = usernames 347 rpost, err2 := testPoster.CreateRandomPost() 348 if err2 != nil { 349 return &model.CommandResponse{Text: "Failed to create a post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err2 350 } 351 for i := 0; i < 1000; i++ { 352 testPoster.CreateRandomPostNested(rpost.Id, rpost.Id) 353 } 354 355 return &model.CommandResponse{Text: "Added threaded post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 356 } 357 358 func (*LoadTestProvider) PostsCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 359 cmd := strings.TrimSpace(strings.TrimPrefix(message, "posts")) 360 361 doFuzz := false 362 if strings.Index(cmd, "fuzz") == 0 { 363 doFuzz = true 364 cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "fuzz")) 365 } 366 367 postsr, ok := parseRange(cmd, "") 368 if !ok { 369 postsr = utils.Range{Begin: 20, End: 30} 370 } 371 372 tokens := strings.Fields(cmd) 373 rimages := utils.Range{Begin: 0, End: 0} 374 if len(tokens) >= 3 { 375 if numImages, err := strconv.Atoi(tokens[2]); err == nil { 376 rimages = utils.Range{Begin: numImages, End: numImages} 377 } 378 } 379 380 var usernames []string 381 options := &model.UserGetOptions{InTeamId: args.TeamId, Page: 0, PerPage: 1000} 382 if profileUsers, err := a.Srv().Store.User().GetProfiles(options); err == nil { 383 usernames = make([]string, len(profileUsers)) 384 i := 0 385 for _, userprof := range profileUsers { 386 usernames[i] = userprof.Username 387 i++ 388 } 389 } 390 391 testPoster := NewAutoPostCreator(a, args.ChannelId, args.UserId) 392 testPoster.Fuzzy = doFuzz 393 testPoster.Users = usernames 394 395 numImages := utils.RandIntFromRange(rimages) 396 numPosts := utils.RandIntFromRange(postsr) 397 for i := 0; i < numPosts; i++ { 398 testPoster.HasImage = (i < numImages) 399 _, err := testPoster.CreateRandomPost() 400 if err != nil { 401 return &model.CommandResponse{Text: "Failed to add posts", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 402 } 403 404 } 405 406 return &model.CommandResponse{Text: "Added posts", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 407 } 408 409 func getMatch(re *regexp.Regexp, text string) string { 410 if match := re.FindStringSubmatch(text); match != nil { 411 return match[1] 412 } 413 414 return "" 415 } 416 417 func (*LoadTestProvider) PostCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 418 textMessage := getMatch(messageRE, message) 419 if textMessage == "" { 420 return &model.CommandResponse{Text: "No message to post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 421 } 422 423 teamName := getMatch(teamRE, message) 424 team, err := a.GetTeamByName(teamName) 425 if err != nil { 426 return &model.CommandResponse{Text: "Failed to get a team", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 427 } 428 429 channelName := getMatch(channelRE, message) 430 channel, err := a.GetChannelByName(channelName, team.Id, true) 431 if err != nil { 432 return &model.CommandResponse{Text: "Failed to get a channel", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 433 } 434 435 passwd := getMatch(passwdRE, message) 436 username := getMatch(userRE, message) 437 user, err := a.GetUserByUsername(username) 438 if err != nil { 439 return &model.CommandResponse{Text: "Failed to get a user", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 440 } 441 442 client := model.NewAPIv4Client(args.SiteURL) 443 _, resp := client.LoginById(user.Id, passwd) 444 if resp.Error != nil { 445 return &model.CommandResponse{Text: "Failed to login a user", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, resp.Error 446 } 447 448 post := &model.Post{ 449 ChannelId: channel.Id, 450 Message: textMessage, 451 } 452 _, resp = client.CreatePost(post) 453 if resp.Error != nil { 454 return &model.CommandResponse{Text: "Failed to create a post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, resp.Error 455 } 456 457 return &model.CommandResponse{Text: "Added a post to " + channel.DisplayName, ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 458 } 459 460 func (*LoadTestProvider) UrlCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 461 url := strings.TrimSpace(strings.TrimPrefix(message, "url")) 462 if url == "" { 463 return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 464 } 465 466 // provide a shortcut to easily access tests stored in doc/developer/tests 467 if !strings.HasPrefix(url, "http") { 468 url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url 469 470 if path.Ext(url) == "" { 471 url += ".md" 472 } 473 } 474 475 r, err := http.Get(url) 476 if err != nil { 477 return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 478 } 479 defer func() { 480 io.Copy(ioutil.Discard, r.Body) 481 r.Body.Close() 482 }() 483 484 if r.StatusCode > 400 { 485 return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, errors.Errorf("unexpected status code %d", r.StatusCode) 486 } 487 488 bytes := make([]byte, 4000) 489 490 // break contents into 4000 byte posts 491 for { 492 length, err := r.Body.Read(bytes) 493 if err != nil && err != io.EOF { 494 return &model.CommandResponse{Text: "Encountered error reading file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 495 } 496 497 if length == 0 { 498 break 499 } 500 501 post := &model.Post{} 502 post.Message = string(bytes[:length]) 503 post.ChannelId = args.ChannelId 504 post.UserId = args.UserId 505 506 if _, err := a.CreatePostMissingChannel(post, false); err != nil { 507 return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 508 } 509 } 510 511 return &model.CommandResponse{Text: "Loaded data", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 512 } 513 514 func (*LoadTestProvider) JsonCommand(a *app.App, args *model.CommandArgs, message string) (*model.CommandResponse, error) { 515 url := strings.TrimSpace(strings.TrimPrefix(message, "json")) 516 if url == "" { 517 return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 518 } 519 520 // provide a shortcut to easily access tests stored in doc/developer/tests 521 if !strings.HasPrefix(url, "http") { 522 url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url 523 524 if path.Ext(url) == "" { 525 url += ".json" 526 } 527 } 528 529 r, err := http.Get(url) 530 if err != nil { 531 return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 532 } 533 534 if r.StatusCode > 400 { 535 return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, errors.Errorf("unexpected status code %d", r.StatusCode) 536 } 537 defer func() { 538 io.Copy(ioutil.Discard, r.Body) 539 r.Body.Close() 540 }() 541 542 post := model.PostFromJson(r.Body) 543 post.ChannelId = args.ChannelId 544 post.UserId = args.UserId 545 if post.Message == "" { 546 post.Message = message 547 } 548 549 if _, err := a.CreatePostMissingChannel(post, false); err != nil { 550 return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, err 551 } 552 553 return &model.CommandResponse{Text: "Loaded data", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}, nil 554 } 555 556 func parseRange(command string, cmd string) (utils.Range, bool) { 557 tokens := strings.Fields(strings.TrimPrefix(command, cmd)) 558 var begin int 559 var end int 560 var err1 error 561 var err2 error 562 switch { 563 case len(tokens) == 1: 564 begin, err1 = strconv.Atoi(tokens[0]) 565 end = begin 566 if err1 != nil { 567 return utils.Range{Begin: 0, End: 0}, false 568 } 569 case len(tokens) >= 2: 570 begin, err1 = strconv.Atoi(tokens[0]) 571 end, err2 = strconv.Atoi(tokens[1]) 572 if err1 != nil || err2 != nil { 573 return utils.Range{Begin: 0, End: 0}, false 574 } 575 default: 576 return utils.Range{Begin: 0, End: 0}, false 577 } 578 return utils.Range{Begin: begin, End: end}, true 579 } 580 581 func contains(items []string, token string) bool { 582 for _, elem := range items { 583 if elem == token { 584 return true 585 } 586 } 587 return false 588 }