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