From 4e7633c4dcd6bf4f41ba81cc2a9ea27ac7b90f51 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 7 Nov 2024 16:48:33 +0800 Subject: [PATCH] Add Channel bookmarks to loadtest (#828) * Add Channel bookmarks to loadtest * update dependency to reflect changes in client4.go * fixes after model.NewString, model.NewBool and others were removed? * missing replacement of model.New... * add parameter to cfg.Sanitize * use model.NewPointer * feedback review * feedback review 2 * feedback review 3 * upload 4 files for posts and drafts --- api/agent_client_test.go | 2 +- cmd/ltctl/collect.go | 2 +- deployment/terraform/cloudwatchlogs.go | 4 +- deployment/terraform/create.go | 6 +- deployment/terraform/strings.go | 1 + go.mod | 8 +- go.sum | 12 +- loadtest/control/actions.go | 16 +- loadtest/control/gencontroller/actions.go | 6 +- loadtest/control/simulcontroller/actions.go | 12 +- loadtest/control/simulcontroller/bookmarks.go | 155 ++++++++++++++++++ .../control/simulcontroller/controller.go | 24 +++ loadtest/control/utils.go | 47 +++++- loadtest/store/memstore/store.go | 81 +++++++++ loadtest/store/memstore/store_test.go | 4 +- loadtest/store/store.go | 16 ++ loadtest/user/user.go | 15 ++ loadtest/user/userentity/actions.go | 10 +- loadtest/user/userentity/bookmarks.go | 73 +++++++++ loadtest/user/userentity/utils.go | 14 +- logger/logger.go | 8 +- 21 files changed, 468 insertions(+), 48 deletions(-) create mode 100644 loadtest/control/simulcontroller/bookmarks.go create mode 100644 loadtest/user/userentity/bookmarks.go diff --git a/api/agent_client_test.go b/api/agent_client_test.go index d1132df2b..067f94cff 100644 --- a/api/agent_client_test.go +++ b/api/agent_client_test.go @@ -33,7 +33,7 @@ func createFakeMMServer() *httptest.Server { case "/api/v4/config": mmCfg := model.Config{} mmCfg.SetDefaults() - mmCfg.TeamSettings.MaxUsersPerTeam = model.NewInt(10000) + mmCfg.TeamSettings.MaxUsersPerTeam = model.NewPointer(10000) json.NewEncoder(w).Encode(mmCfg) case "/api/v4/emoji": json.NewEncoder(w).Encode(&model.Emoji{}) diff --git a/cmd/ltctl/collect.go b/cmd/ltctl/collect.go index 0bd6a8f77..b5f6fb760 100644 --- a/cmd/ltctl/collect.go +++ b/cmd/ltctl/collect.go @@ -220,7 +220,7 @@ func collect(config deployment.Config, deploymentId string, outputName string) e if err := json.Unmarshal(input, &cfg); err != nil { return nil, fmt.Errorf("failed to unmarshal MM configuration: %w", err) } - cfg.Sanitize() + cfg.Sanitize(nil) sanitizedCfg, err := json.MarshalIndent(cfg, "", " ") if err != nil { return nil, fmt.Errorf("failed to sanitize MM configuration: %w", err) diff --git a/deployment/terraform/cloudwatchlogs.go b/deployment/terraform/cloudwatchlogs.go index dd79d351f..25052dbd4 100644 --- a/deployment/terraform/cloudwatchlogs.go +++ b/deployment/terraform/cloudwatchlogs.go @@ -136,8 +136,8 @@ func (t *Terraform) createCloudWatchLogsPolicy() error { docJsonStr := string(docJsonBytes) input := cloudwatchlogs.PutResourcePolicyInput{ - PolicyName: model.NewString("lt-cloudwatch-log-policy"), - PolicyDocument: model.NewString(docJsonStr), + PolicyName: model.NewPointer("lt-cloudwatch-log-policy"), + PolicyDocument: model.NewPointer(docJsonStr), } if _, err := cwclient.PutResourcePolicy(context.Background(), &input); err != nil { return fmt.Errorf("failed to create CloudWatchLogs policy; it can be manually created by running `aws logs put-resource-policy --policy-name lt-cloudwatch-log-policy --policy-document %q`; the next `deployment create` should work when such a policy is present in the AWS account; original error: %w", docJsonStr, err) diff --git a/deployment/terraform/create.go b/deployment/terraform/create.go index dc3f02c42..bff3c3556 100644 --- a/deployment/terraform/create.go +++ b/deployment/terraform/create.go @@ -1066,10 +1066,10 @@ func (t *Terraform) updateAppConfig(siteURL string, sshc *ssh.Client, jobServerE } if t.output.HasRedis() { - cfg.CacheSettings.CacheType = model.NewString(model.CacheTypeRedis) + cfg.CacheSettings.CacheType = model.NewPointer(model.CacheTypeRedis) redisEndpoint := net.JoinHostPort(t.output.RedisServer.Address, strconv.Itoa(t.output.RedisServer.Port)) - cfg.CacheSettings.RedisAddress = model.NewString(redisEndpoint) - cfg.CacheSettings.RedisDB = model.NewInt(0) + cfg.CacheSettings.RedisAddress = model.NewPointer(redisEndpoint) + cfg.CacheSettings.RedisDB = model.NewPointer(0) } if t.config.MattermostConfigPatchFile != "" { diff --git a/deployment/terraform/strings.go b/deployment/terraform/strings.go index 4d1c728e6..3adb103ba 100644 --- a/deployment/terraform/strings.go +++ b/deployment/terraform/strings.go @@ -21,6 +21,7 @@ Group=ubuntu LimitNOFILE=49152 Environment=MM_FEATUREFLAGS_POSTPRIORITY=true Environment=MM_FEATUREFLAGS_WEBSOCKETEVENTSCOPE=true +Environment=MM_FEATUREFLAGS_CHANNELBOOKMARKS=true Environment=MM_SERVICEENVIRONMENT=%s [Install] diff --git a/go.mod b/go.mod index 5ad686c52..ff2dc97e3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mattermost/mattermost-load-test-ng -go 1.22 +go 1.22.0 toolchain go1.22.8 @@ -32,8 +32,8 @@ require ( github.com/gliderlabs/ssh v0.1.1 github.com/grafana/alloy/syntax v0.1.0 github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a - github.com/mattermost/mattermost/server/public v0.1.7-0.20240806035841-540febd866aa - github.com/mattermost/mattermost/server/v8 v8.0.0-20240726090344-5547504c1d68 + github.com/mattermost/mattermost/server/public v0.1.8-0.20241015185928-63c97f5a6d8f + github.com/mattermost/mattermost/server/v8 v8.0.0-20241015185928-63c97f5a6d8f github.com/opensearch-project/opensearch-go/v4 v4.1.0 github.com/pelletier/go-toml/v2 v2.2.2 github.com/vmihailenco/msgpack/v5 v5.4.1 @@ -141,7 +141,7 @@ require ( github.com/yudai/pp v2.0.1+incompatible // indirect github.com/yuin/goldmark v1.7.4 // indirect golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f // indirect diff --git a/go.sum b/go.sum index 1b42ab622..a19232db0 100644 --- a/go.sum +++ b/go.sum @@ -270,10 +270,10 @@ github.com/mattermost/ldap v3.0.4+incompatible h1:SOeNnz+JNR+foQ3yHkYqijb9MLPhXN github.com/mattermost/ldap v3.0.4+incompatible/go.mod h1:b4reDCcGpBxJ4WX0f224KFY+OR0npin7or7EFpeIko4= github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4= github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc= -github.com/mattermost/mattermost/server/public v0.1.7-0.20240806035841-540febd866aa h1:KB8BE35ITSzVaCBoMj3NP9WHiJi/PLJD0Hgw5N9QpNM= -github.com/mattermost/mattermost/server/public v0.1.7-0.20240806035841-540febd866aa/go.mod h1:Dm5uf3z8ckDOKYD1cbnb1Uqm/G9WYIaouSP/HnH+Rbs= -github.com/mattermost/mattermost/server/v8 v8.0.0-20240726090344-5547504c1d68 h1:5EoqvfOGhU+b0E2h4XdMxtCReyGZVBIWBiYYk6WTEoU= -github.com/mattermost/mattermost/server/v8 v8.0.0-20240726090344-5547504c1d68/go.mod h1:bCPUN2odATk4v9Gox88gBphLhnrJfEvqNQtaFVVVso4= +github.com/mattermost/mattermost/server/public v0.1.8-0.20241015185928-63c97f5a6d8f h1:NUAf56HZHFLayAyCqHxeVLmxJUN9xw3Qxc9m3ghy+Xw= +github.com/mattermost/mattermost/server/public v0.1.8-0.20241015185928-63c97f5a6d8f/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk= +github.com/mattermost/mattermost/server/v8 v8.0.0-20241015185928-63c97f5a6d8f h1:h4T89Qkb3kddGnRN7xQAuB+fDaL3OgUgc5YPmssmzj8= +github.com/mattermost/mattermost/server/v8 v8.0.0-20241015185928-63c97f5a6d8f/go.mod h1:zwMZYGK4/xDy/kyepVBqXhM58YMTCRlanv/uKrZzJdw= github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw= github.com/mattermost/morph v1.1.0/go.mod h1:gD+EaqX2UMyyuzmF4PFh4r33XneQ8Nzi+0E8nXjMa3A= github.com/mattermost/squirrel v0.4.0 h1:azf9LZ+8JUTAvwt/njB1utkPqWQ6e7Rje2ya5N0P2i4= @@ -586,8 +586,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/loadtest/control/actions.go b/loadtest/control/actions.go index f7526dcea..2c80c5032 100644 --- a/loadtest/control/actions.go +++ b/loadtest/control/actions.go @@ -255,8 +255,8 @@ func CreateAckPost(u user.User) UserActionResponse { CreateAt: time.Now().UnixMilli(), Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ - Priority: model.NewString(model.PostPriorityUrgent), - RequestedAck: model.NewBool(true), + Priority: model.NewPointer(model.PostPriorityUrgent), + RequestedAck: model.NewPointer(true), }, }, }) @@ -346,9 +346,9 @@ func CreatePersistentNotificationPost(u user.User) UserActionResponse { CreateAt: time.Now().UnixMilli(), Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ - Priority: model.NewString(model.PostPriorityUrgent), - RequestedAck: model.NewBool(false), - PersistentNotifications: model.NewBool(true), + Priority: model.NewPointer(model.PostPriorityUrgent), + RequestedAck: model.NewPointer(false), + PersistentNotifications: model.NewPointer(true), }, }, }) @@ -1032,6 +1032,12 @@ func DraftsEnabled(u user.User) (bool, UserActionResponse) { return allow, UserActionResponse{} } +func ChannelBookmarkEnabled(u user.User) (bool, UserActionResponse) { + allow := u.Store().FeatureFlags()["ChannelBookmarks"] + + return allow, UserActionResponse{} +} + // MessageExport simulates the given user performing // a compliance message export func MessageExport(u user.User) UserActionResponse { diff --git a/loadtest/control/gencontroller/actions.go b/loadtest/control/gencontroller/actions.go index c3bf4c503..b03767018 100644 --- a/loadtest/control/gencontroller/actions.go +++ b/loadtest/control/gencontroller/actions.go @@ -258,9 +258,9 @@ func (c *GenController) createPost(u user.User) (res control.UserActionResponse) if isUrgent { post.Metadata = &model.PostMetadata{} post.Metadata.Priority = &model.PostPriority{ - Priority: model.NewString("urgent"), - RequestedAck: model.NewBool(false), - PersistentNotifications: model.NewBool(false), + Priority: model.NewPointer("urgent"), + RequestedAck: model.NewPointer(false), + PersistentNotifications: model.NewPointer(false), } } diff --git a/loadtest/control/simulcontroller/actions.go b/loadtest/control/simulcontroller/actions.go index 19056559a..c1ad450b5 100644 --- a/loadtest/control/simulcontroller/actions.go +++ b/loadtest/control/simulcontroller/actions.go @@ -541,6 +541,12 @@ func switchChannel(u user.User) control.UserActionResponse { return control.UserActionResponse{Err: control.NewUserError(err)} } + if u.Store().FeatureFlags()["ChannelBookmarks"] { + if err := u.GetChannelBookmarks(channel.Id, 0); err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + } + if u.Store().FeatureFlags()["WebSocketEventScope"] { if err := u.UpdateActiveChannel(channel.Id); err != nil { mlog.Warn("Failed to update active channel", mlog.String("channel_id", channel.Id)) @@ -796,9 +802,9 @@ func (c *SimulController) createPost(u user.User) control.UserActionResponse { if isUrgent { post.Metadata = &model.PostMetadata{} post.Metadata.Priority = &model.PostPriority{ - Priority: model.NewString("urgent"), - RequestedAck: model.NewBool(false), - PersistentNotifications: model.NewBool(false), + Priority: model.NewPointer("urgent"), + RequestedAck: model.NewPointer(false), + PersistentNotifications: model.NewPointer(false), } } diff --git a/loadtest/control/simulcontroller/bookmarks.go b/loadtest/control/simulcontroller/bookmarks.go new file mode 100644 index 000000000..5ab2b546c --- /dev/null +++ b/loadtest/control/simulcontroller/bookmarks.go @@ -0,0 +1,155 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package simulcontroller + +import ( + "fmt" + "math/rand" + + "github.com/mattermost/mattermost-load-test-ng/loadtest/control" + "github.com/mattermost/mattermost-load-test-ng/loadtest/user" + "github.com/mattermost/mattermost/server/public/model" +) + +var ( + bookmarkNames = []string{"this is a file", "this is a link", "this is another file", "this is another link"} + bookmarkType = []model.ChannelBookmarkType{model.ChannelBookmarkLink, model.ChannelBookmarkFile} +) + +func (c *SimulController) addChannelBookmark(u user.User) control.UserActionResponse { + if ok, resp := control.ChannelBookmarkEnabled(u); resp.Err != nil { + return resp + } else if !ok { + return control.UserActionResponse{Info: "channel bookmarks not enabled"} + } + + channel, err := u.Store().CurrentChannel() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + emoji := "" + // 10% of the times bookmarks will have an emoji assigned. + // https://mattermost.atlassian.net/browse/MM-61131 + if rand.Float64() < 0.1 { + emoji = control.RandomEmoji() + } + + bookmark := &model.ChannelBookmark{ + ChannelId: channel.Id, + DisplayName: control.PickRandomString(bookmarkNames), + Emoji: emoji, + Type: bookmarkType[rand.Intn(len(bookmarkType))], + } + + if bookmark.Type == model.ChannelBookmarkFile { + control.AttachFileToBookmark(u, bookmark) + } else { + bookmark.LinkUrl = control.RandomLink() + } + + err = u.AddChannelBookmark(bookmark) + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + return control.UserActionResponse{Info: fmt.Sprintf("bookmark created in channel id %v", channel.Id)} +} + +func (c *SimulController) updateBookmark(u user.User) control.UserActionResponse { + if ok, resp := control.ChannelBookmarkEnabled(u); resp.Err != nil { + return resp + } else if !ok { + return control.UserActionResponse{Info: "channel bookmarks not enabled"} + } + + channel, err := u.Store().CurrentChannel() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + currentBookmarks := u.Store().ChannelBookmarks(channel.Id) + if len(currentBookmarks) == 0 { + return control.UserActionResponse{Info: fmt.Sprintf("channel id %v does not have bookmarks to update", channel.Id)} + } + + // here we update + bookmark := currentBookmarks[rand.Intn(len(currentBookmarks))] + bookmarkWithFileInfo := bookmark.Clone() + bookmarkWithFileInfo.DisplayName = control.PickRandomString(bookmarkNames) + + // 10% of the times bookmarks will have an emoji assigned. + // https://mattermost.atlassian.net/browse/MM-61131 + if bookmarkWithFileInfo.Emoji == "" && rand.Float64() < 0.1 { + bookmarkWithFileInfo.Emoji = control.RandomEmoji() + } + + if bookmarkWithFileInfo.Type == model.ChannelBookmarkFile { + control.AttachFileToBookmark(u, bookmarkWithFileInfo.ChannelBookmark) + } else { + bookmarkWithFileInfo.LinkUrl = control.RandomLink() + } + + err = u.UpdateChannelBookmark(bookmarkWithFileInfo) + + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + return control.UserActionResponse{Info: fmt.Sprintf("bookmark %v updated in channel id %v", bookmarkWithFileInfo.Id, channel.Id)} +} + +func (c *SimulController) deleteBookmark(u user.User) control.UserActionResponse { + if ok, resp := control.ChannelBookmarkEnabled(u); resp.Err != nil { + return resp + } else if !ok { + return control.UserActionResponse{Info: "channel bookmarks not enabled"} + } + + channel, err := u.Store().CurrentChannel() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + currentBookmarks := u.Store().ChannelBookmarks(channel.Id) + if len(currentBookmarks) == 0 { + return control.UserActionResponse{Info: "no channel bookmarks found"} + } + + bookmark := currentBookmarks[rand.Intn(len(currentBookmarks))] + err = u.DeleteChannelBookmark(bookmark.ChannelId, bookmark.Id) + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + return control.UserActionResponse{Info: fmt.Sprintf("bookmark id %v deleted in channel id %v", bookmark.Id, channel.Id)} +} + +func (c *SimulController) updateBookmarksSortOrder(u user.User) control.UserActionResponse { + if ok, resp := control.ChannelBookmarkEnabled(u); resp.Err != nil { + return resp + } else if !ok { + return control.UserActionResponse{Info: "channel bookmarks not enabled"} + } + + channel, err := u.Store().CurrentChannel() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + currentBookmarks := u.Store().ChannelBookmarks(channel.Id) + if len(currentBookmarks) <= 1 { + return control.UserActionResponse{Info: "not enough channel bookmarks to sort"} + } + + bookmark := currentBookmarks[rand.Intn(len(currentBookmarks))] + newIndex := rand.Int63n(int64(len(currentBookmarks))) + err = u.UpdateChannelBookmarkSortOrder(channel.Id, bookmark.Id, newIndex) + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + return control.UserActionResponse{Info: fmt.Sprintf("bookmark id %v in channel id %v sorted at index %d", bookmark.Id, channel.Id, newIndex)} + +} diff --git a/loadtest/control/simulcontroller/controller.go b/loadtest/control/simulcontroller/controller.go index 666b05dc1..56c8f2a91 100644 --- a/loadtest/control/simulcontroller/controller.go +++ b/loadtest/control/simulcontroller/controller.go @@ -264,6 +264,30 @@ func getActionList(c *SimulController) []userAction { frequency: 1.41, minServerVersion: control.MinSupportedVersion, // 7.7.0 }, + { + name: "AddChannelBookmark", + run: c.addChannelBookmark, + frequency: 0.0003, // https://mattermost.atlassian.net/browse/MM-61131 + minServerVersion: semver.MustParse("10.0.0"), + }, + { + name: "UpdateOrAddChannelBookark", + run: c.updateBookmark, + frequency: 0.0002, // https://mattermost.atlassian.net/browse/MM-61131 + minServerVersion: semver.MustParse("10.0.0"), + }, + { + name: "UpdateChannelBookarkSortOrder", + run: c.updateBookmarksSortOrder, + frequency: 0.0002, // https://mattermost.atlassian.net/browse/MM-61131 + minServerVersion: semver.MustParse("10.0.0"), + }, + { + name: "DeleteChannelBookark", + run: c.deleteBookmark, + frequency: 0.0001, // https://mattermost.atlassian.net/browse/MM-61131 + minServerVersion: semver.MustParse("10.0.0"), + }, // All actions are required to contain a valid minServerVersion: // - If the action is present in server versions equal or older than // control.MinSupportedVersion, use control.MinSupportedVersion. diff --git a/loadtest/control/utils.go b/loadtest/control/utils.go index b2e56d763..94010dec9 100644 --- a/loadtest/control/utils.go +++ b/loadtest/control/utils.go @@ -165,10 +165,16 @@ func RandomEmoji() string { // AddLink appends a link to a string to test the LinkPreview feature. func AddLink(input string) string { + link := RandomLink() + + return input + " " + link + " " +} + +func RandomLink() string { n := rand.Int() % len(links) link := links[n] - return input + " " + link + " " + return link } // SelectWeighted does a random weighted selection on a given slice of weights. @@ -265,7 +271,7 @@ func ParseServerVersion(versionString string) (semver.Version, error) { // AttachFilesToPost uploads at least one file on behalf of the user, attaching // all uploaded files to the post. func AttachFilesToPost(u user.User, post *model.Post) error { - fileIDs, err := _attachFilesToObj(u, post.ChannelId) + fileIDs, err := _attachFilesToObj(u, post.ChannelId, 4) if err != nil { return err } @@ -276,7 +282,7 @@ func AttachFilesToPost(u user.User, post *model.Post) error { // AttachFilesToDraft uploads at least one file on behalf of the user, attaching // all uploaded files to the draft. func AttachFilesToDraft(u user.User, draft *model.Draft) error { - fileIDs, err := _attachFilesToObj(u, draft.ChannelId) + fileIDs, err := _attachFilesToObj(u, draft.ChannelId, 4) if err != nil { return err } @@ -284,7 +290,17 @@ func AttachFilesToDraft(u user.User, draft *model.Draft) error { return nil } -func _attachFilesToObj(u user.User, channelID string) ([]string, error) { +func AttachFileToBookmark(u user.User, bookmark *model.ChannelBookmark) error { + fileIDs, err := _attachFilesToObj(u, bookmark.ChannelId, 1) + if err != nil { + return err + } + + bookmark.FileId = fileIDs[0] + return nil +} + +func _attachFilesToObj(u user.User, channelID string, maxToUpload int) ([]string, error) { type file struct { data []byte upload bool @@ -292,15 +308,34 @@ func _attachFilesToObj(u user.User, channelID string) ([]string, error) { filenames := []string{"test_upload.png", "test_upload.jpg", "test_upload.mp4", "test_upload.txt"} files := make(map[string]*file, len(filenames)) + // Randomly select how many files to upload, but ensure at least 1. + countToUpload := rand.Intn(maxToUpload) + if countToUpload < 1 { + countToUpload = 1 + } + + count := 0 + for _, filename := range filenames { + upload := rand.Intn(2) == 0 + if upload { + count += 1 + } + files[filename] = &file{ data: MustAsset(filename), - upload: rand.Intn(2) == 0, + upload: upload, + } + + if count == countToUpload { + break } } // We make sure at least one file gets uploaded. - files[filenames[rand.Intn(len(filenames))]].upload = true + if count == 0 { + files[filenames[rand.Intn(len(filenames))]].upload = true + } var wg sync.WaitGroup fileIdsChan := make(chan string, len(files)) diff --git a/loadtest/store/memstore/store.go b/loadtest/store/memstore/store.go index f114f7e28..138e9eb93 100644 --- a/loadtest/store/memstore/store.go +++ b/loadtest/store/memstore/store.go @@ -50,6 +50,7 @@ type MemStore struct { sidebarCategories map[string]map[string]*model.SidebarCategoryWithChannels drafts map[string]map[string]*model.Draft featureFlags map[string]bool + channelBookmarks map[string]*model.ChannelBookmarkWithFileInfo } // New returns a new instance of MemStore with the given config. @@ -125,6 +126,8 @@ func (s *MemStore) Clear() { s.sidebarCategories = map[string]map[string]*model.SidebarCategoryWithChannels{} clear(s.drafts) s.drafts = map[string]map[string]*model.Draft{} + clear(s.channelBookmarks) + s.channelBookmarks = map[string]*model.ChannelBookmarkWithFileInfo{} } func (s *MemStore) setupQueues(config *Config) error { @@ -1221,3 +1224,81 @@ func (s *MemStore) SetDrafts(teamId string, drafts []*model.Draft) error { return nil } + +// ChannelBookmarks returns all bookmarks for the specified channel. +func (s *MemStore) ChannelBookmarks(channelId string) []*model.ChannelBookmarkWithFileInfo { + s.lock.RLock() + defer s.lock.RUnlock() + + var bookmarks []*model.ChannelBookmarkWithFileInfo + for _, b := range s.channelBookmarks { + if b.ChannelId == channelId { + bookmarks = append(bookmarks, b) + } + } + return bookmarks +} + +// SetChannelBookmarks stores the given bookmarks. +func (s *MemStore) SetChannelBookmarks(bookmarks []*model.ChannelBookmarkWithFileInfo) error { + s.lock.Lock() + defer s.lock.Unlock() + + for _, bookmark := range bookmarks { + if bookmark == nil { + return errors.New("memstore: bookmark should not be nil") + } + s.channelBookmarks[bookmark.Id] = bookmark + } + + return nil +} + +// AddChannelBookmark stores the bookmark. +func (s *MemStore) AddChannelBookmark(bookmark *model.ChannelBookmarkWithFileInfo) error { + s.lock.Lock() + defer s.lock.Unlock() + + if bookmark == nil { + return errors.New("memstore: bookmark should not be nil") + } + + s.channelBookmarks[bookmark.Id] = bookmark + return nil +} + +// UpdateChannelBookmark updates a given bookmark. +func (s *MemStore) UpdateChannelBookmark(bookmark *model.ChannelBookmarkWithFileInfo) error { + s.lock.Lock() + defer s.lock.Unlock() + + if bookmark == nil { + return errors.New("memstore: bookmark should not be nil") + } + + if s.channelBookmarks[bookmark.Id] == nil { + return errors.New("memstore: bookmark not found") + } + + s.channelBookmarks[bookmark.Id] = bookmark + + return nil +} + +// DeleteChannelBookmark deletes a given bookmark. +func (s *MemStore) DeleteChannelBookmark(bookmarkId string) error { + s.lock.Lock() + defer s.lock.Unlock() + + if bookmarkId == "" { + return errors.New("memstore: bookmarkId should not be empty") + } + + if s.channelBookmarks[bookmarkId] == nil { + return errors.New("memstore: bookmark not found") + } + + delete(s.channelBookmarks, bookmarkId) + + return nil +} diff --git a/loadtest/store/memstore/store_test.go b/loadtest/store/memstore/store_test.go index 7599fb555..d1d54d63b 100644 --- a/loadtest/store/memstore/store_test.go +++ b/loadtest/store/memstore/store_test.go @@ -762,8 +762,8 @@ func TestPostsWithAckRequests(t *testing.T) { Message: "ack post", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ - Priority: model.NewString(model.PostPriorityUrgent), - RequestedAck: model.NewBool(true), + Priority: model.NewPointer(model.PostPriorityUrgent), + RequestedAck: model.NewPointer(true), }, }, } diff --git a/loadtest/store/store.go b/loadtest/store/store.go index 3f2a1b45a..98184a59d 100644 --- a/loadtest/store/store.go +++ b/loadtest/store/store.go @@ -146,6 +146,22 @@ type UserStore interface { // PostsWithAckRequests returns IDs of the posts that asked for acknowledgment. PostsWithAckRequests() ([]string, error) + + // Channel Bookmarks + // ChannelBookmarks returns all bookmarks for the specified channel. + ChannelBookmarks(channelId string) []*model.ChannelBookmarkWithFileInfo + + // SetChannelBookmarks stores the given bookmarks. + SetChannelBookmarks(bookmarks []*model.ChannelBookmarkWithFileInfo) error + + // AddChannelBookmark stores the bookmark. + AddChannelBookmark(bookmark *model.ChannelBookmarkWithFileInfo) error + + // UpdateChannelBookmark updates a given bookmark. + UpdateChannelBookmark(bookmark *model.ChannelBookmarkWithFileInfo) error + + // DeleteChannelBookmark deletes a given bookmarkId. + DeleteChannelBookmark(bookmarkId string) error } // MutableUserStore is a super-set of UserStore which, apart from providing diff --git a/loadtest/user/user.go b/loadtest/user/user.go index 157a3cf30..9e28de1ec 100644 --- a/loadtest/user/user.go +++ b/loadtest/user/user.go @@ -325,4 +325,19 @@ type User interface { // GraphQL GetInitialDataGQL() error GetChannelsAndChannelMembersGQL(teamID string, includeDeleted bool, channelsCursor, channelMembersCursor string) (string, string, error) + + // GetChannelBookmarks fetches bookmarks for the given channel since a specific timestamp. + GetChannelBookmarks(channelId string, since int64) error + + // AddChannelBookmark creates a bookmark + AddChannelBookmark(bookmark *model.ChannelBookmark) error + + // UpdateChannelBookmark updates a given bookmark. + UpdateChannelBookmark(bookmark *model.ChannelBookmarkWithFileInfo) error + + // DeleteChannelBookmark deletes a given bookmarkId from a given channelId. + DeleteChannelBookmark(channelId, bookmarkId string) error + + // UpdateChannelBookmarkSortOrder sets the new position of a bookmark for the given channel + UpdateChannelBookmarkSortOrder(channelId, bookmarkId string, sortOrder int64) error } diff --git a/loadtest/user/userentity/actions.go b/loadtest/user/userentity/actions.go index ead4b9614..0f4a43667 100644 --- a/loadtest/user/userentity/actions.go +++ b/loadtest/user/userentity/actions.go @@ -1394,7 +1394,15 @@ func (ue *UserEntity) GetPostThreadWithOpts(threadId, etag string, opts model.Ge if postList == nil || len(postList.Posts) == 0 { return nil, false, nil } - return postList.Order, postList.HasNext, ue.store.SetPosts(postListToSlice(postList)) + + var hasNext bool + if postList.HasNext != nil { + hasNext = *postList.HasNext + } else { + hasNext = false + } + + return postList.Order, hasNext, ue.store.SetPosts(postListToSlice(postList)) } // MarkAllThreadsInTeamAsRead marks all threads in the given team as read diff --git a/loadtest/user/userentity/bookmarks.go b/loadtest/user/userentity/bookmarks.go new file mode 100644 index 000000000..0fa4b3fa7 --- /dev/null +++ b/loadtest/user/userentity/bookmarks.go @@ -0,0 +1,73 @@ +package userentity + +import ( + "context" + + "github.com/mattermost/mattermost/server/public/model" +) + +// GetChannelBookmarks fetches bookmarks for the given channel since a specific timestamp. +func (ue *UserEntity) GetChannelBookmarks(channelId string, since int64) error { + bookmarks, _, err := ue.client.ListChannelBookmarksForChannel(context.Background(), channelId, since) + if err != nil { + return err + } + + return ue.store.SetChannelBookmarks(bookmarks) +} + +// AddChannelBookmark creates a bookmark on the given channel +func (ue *UserEntity) AddChannelBookmark(bookmark *model.ChannelBookmark) error { + bookmarkResp, _, err := ue.client.CreateChannelBookmark(context.Background(), bookmark) + if err != nil { + return err + } + + return ue.store.AddChannelBookmark(bookmarkResp) +} + +// UpdateChannelBookmark updates a given bookmark. +func (ue *UserEntity) UpdateChannelBookmark(bookmark *model.ChannelBookmarkWithFileInfo) error { + patch := &model.ChannelBookmarkPatch{ + FileId: &bookmark.FileId, + DisplayName: &bookmark.DisplayName, + SortOrder: &bookmark.SortOrder, + LinkUrl: &bookmark.LinkUrl, + ImageUrl: &bookmark.ImageUrl, + Emoji: &bookmark.Emoji, + } + + result, _, err := ue.client.UpdateChannelBookmark(context.Background(), bookmark.ChannelId, bookmark.Id, patch) + if err != nil { + return err + } + + if result.Deleted != nil { + bId := result.Deleted.Id + if err := ue.store.DeleteChannelBookmark(bId); err != nil { + return err + } + } + + return ue.store.UpdateChannelBookmark(result.Updated) +} + +// DeleteChannelBookmark deletes a given bookmarkId from a given channelId. +func (ue *UserEntity) DeleteChannelBookmark(channelId, bookmarkId string) error { + result, _, err := ue.client.DeleteChannelBookmark(context.Background(), channelId, bookmarkId) + if err != nil { + return err + } + + return ue.store.DeleteChannelBookmark(result.Id) +} + +// UpdateChannelBookmarkSortOrder sets the new position of a bookmark for the given channel +func (ue *UserEntity) UpdateChannelBookmarkSortOrder(channelId, bookmarkId string, sortOrder int64) error { + result, _, err := ue.client.UpdateChannelBookmarkSortOrder(context.Background(), channelId, bookmarkId, sortOrder) + if err != nil { + return err + } + + return ue.store.SetChannelBookmarks(result) +} diff --git a/loadtest/user/userentity/utils.go b/loadtest/user/userentity/utils.go index 154ed3062..d9c17d784 100644 --- a/loadtest/user/userentity/utils.go +++ b/loadtest/user/userentity/utils.go @@ -126,13 +126,13 @@ func convertToTypedTeams(userID string, input []gqlTeamMember) ([]*model.Team, [ AllowOpenInvite: team.AllowOpenInvite, } if team.GroupConstrained != nil { - mTeam.GroupConstrained = model.NewBool(*team.GroupConstrained) + mTeam.GroupConstrained = model.NewPointer(*team.GroupConstrained) } if team.SchemeId != nil { - mTeam.SchemeId = model.NewString(*team.SchemeId) + mTeam.SchemeId = model.NewPointer(*team.SchemeId) } if team.PolicyId != nil { - mTeam.PolicyID = model.NewString(*team.PolicyId) + mTeam.PolicyID = model.NewPointer(*team.PolicyId) } teams = append(teams, mTeam) @@ -172,16 +172,16 @@ func convertToTypedChannels(input []gqlChannel) ([]*model.Channel, string) { Props: c.Props, } if c.SchemeID != nil { - ch.SchemeId = model.NewString(*c.SchemeID) + ch.SchemeId = model.NewPointer(*c.SchemeID) } if c.GroupConstrained != nil { - ch.GroupConstrained = model.NewBool(*c.GroupConstrained) + ch.GroupConstrained = model.NewPointer(*c.GroupConstrained) } if c.Shared != nil { - ch.Shared = model.NewBool(*c.Shared) + ch.Shared = model.NewPointer(*c.Shared) } if c.PolicyID != nil { - ch.PolicyID = model.NewString(*c.PolicyID) + ch.PolicyID = model.NewPointer(*c.PolicyID) } cursor = c.Cursor channels = append(channels, ch) diff --git a/logger/logger.go b/logger/logger.go index 6629f56e2..8bf0bd121 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -29,10 +29,10 @@ func New(logSettings *Settings) *mlog.Logger { cfg, _ := config.MloggerConfigFromLoggerConfig(&model.LogSettings{ EnableConsole: &logSettings.EnableConsole, ConsoleJson: &logSettings.ConsoleJson, - ConsoleLevel: model.NewString(strings.ToLower(logSettings.ConsoleLevel)), + ConsoleLevel: model.NewPointer(strings.ToLower(logSettings.ConsoleLevel)), EnableFile: &logSettings.EnableFile, FileJson: &logSettings.FileJson, - FileLevel: model.NewString(strings.ToLower(logSettings.FileLevel)), + FileLevel: model.NewPointer(strings.ToLower(logSettings.FileLevel)), FileLocation: &logSettings.FileLocation, EnableColor: &logSettings.EnableColor, }, nil, func(filename string) string { @@ -48,10 +48,10 @@ func Init(logSettings *Settings) { cfg, _ := config.MloggerConfigFromLoggerConfig(&model.LogSettings{ EnableConsole: &logSettings.EnableConsole, ConsoleJson: &logSettings.ConsoleJson, - ConsoleLevel: model.NewString(strings.ToLower(logSettings.ConsoleLevel)), + ConsoleLevel: model.NewPointer(strings.ToLower(logSettings.ConsoleLevel)), EnableFile: &logSettings.EnableFile, FileJson: &logSettings.FileJson, - FileLevel: model.NewString(strings.ToLower(logSettings.FileLevel)), + FileLevel: model.NewPointer(strings.ToLower(logSettings.FileLevel)), FileLocation: &logSettings.FileLocation, EnableColor: &logSettings.EnableColor, }, nil, func(filename string) string {