// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package api4 import ( "bytes" "context" "encoding/json" "errors" "fmt" "maps" "net/http" "net/http/httptest" "net/url" "os" "reflect" "sort" "strings" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/app" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" "github.com/mattermost/mattermost/server/v8/channels/testlib" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" ) func TestCreatePost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client basicPost := func() *model.Post { p := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "#hashtag a" + model.NewId() + "a", DeleteAt: 101, } p.AddProp(model.PropsAddChannelMember, "no good") return p } post := basicPost() rootPost, resp2, err2 := client.CreatePost(context.Background(), post) require.NoError(t, err2) CheckCreatedStatus(t, resp2) require.NotNil(t, rootPost) require.Equal(t, post.Message, rootPost.Message, "message didn't match") require.Equal(t, "#hashtag", rootPost.Hashtags, "hashtag didn't match") require.Empty(t, rootPost.FileIds) require.Equal(t, 0, int(rootPost.EditAt), "newly created post shouldn't have EditAt set") require.Nil(t, rootPost.GetProp(model.PropsAddChannelMember), "newly created post shouldn't have Props['add_channel_member'] set") require.Equal(t, 0, int(rootPost.DeleteAt), "newly created post shouldn't have DeleteAt set") post = basicPost() post.RootId = rootPost.Id childPost, resp2, err2 := client.CreatePost(context.Background(), post) require.NoError(t, err2) CheckCreatedStatus(t, resp2) require.NotNil(t, childPost) t.Run("with file uploaded by same user", func(t *testing.T) { fileResp, resp, err := client.UploadFile(context.Background(), []byte("data"), th.BasicChannel.Id, "test") require.NoError(t, err) CheckCreatedStatus(t, resp) fileId := fileResp.FileInfos[0].Id postWithFiles, resp, err := client.CreatePost(context.Background(), &model.Post{ ChannelId: th.BasicChannel.Id, Message: "with files", FileIds: model.StringArray{fileId}, }) require.NoError(t, err) CheckCreatedStatus(t, resp) assert.Equal(t, model.StringArray{fileId}, postWithFiles.FileIds) actualPostWithFiles, resp, err := client.GetPost(context.Background(), postWithFiles.Id, "") require.NoError(t, err) CheckOKStatus(t, resp) assert.Equal(t, model.StringArray{fileId}, actualPostWithFiles.FileIds) }) t.Run("with file uploaded by different user", func(t *testing.T) { fileResp, resp, err := th.SystemAdminClient.UploadFile(context.Background(), []byte("data"), th.BasicChannel.Id, "test") require.NoError(t, err) CheckCreatedStatus(t, resp) fileId := fileResp.FileInfos[0].Id postWithFiles, resp, err := client.CreatePost(context.Background(), &model.Post{ ChannelId: th.BasicChannel.Id, Message: "with files", FileIds: model.StringArray{fileId}, }) require.NoError(t, err) CheckCreatedStatus(t, resp) assert.Empty(t, postWithFiles.FileIds) actualPostWithFiles, resp, err := client.GetPost(context.Background(), postWithFiles.Id, "") require.NoError(t, err) CheckOKStatus(t, resp) assert.Empty(t, actualPostWithFiles.FileIds) }) t.Run("with file uploaded by nouser", func(t *testing.T) { fileInfo, appErr := th.App.UploadFile(th.Context, []byte("data"), th.BasicChannel.Id, "test") require.Nil(t, appErr) fileId := fileInfo.Id postWithFiles, resp, err := client.CreatePost(context.Background(), &model.Post{ ChannelId: th.BasicChannel.Id, Message: "with files", FileIds: model.StringArray{fileId}, }) require.NoError(t, err) CheckCreatedStatus(t, resp) assert.Equal(t, model.StringArray{fileId}, postWithFiles.FileIds) actualPostWithFiles, resp, err := client.GetPost(context.Background(), postWithFiles.Id, "") require.NoError(t, err) CheckOKStatus(t, resp) assert.Equal(t, model.StringArray{fileId}, actualPostWithFiles.FileIds) }) t.Run("Create posts without the USE_CHANNEL_MENTIONS Permission - returns ephemeral message with mentions and no ephemeral message without mentions", func(t *testing.T) { wsClient := th.CreateConnectedWebSocketClient(t) defaultPerms := th.SaveDefaultRolePermissions() defer th.RestoreDefaultRolePermissions(defaultPerms) th.RemovePermissionFromRole(model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId) post := basicPost() post.RootId = rootPost.Id post.Message = "a post with no channel mentions" rPost, resp, err := client.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, resp) require.NotNil(t, rPost) // Message with no channel mentions should result in no ephemeral message timeout := time.After(5 * time.Second) waiting := true for waiting { select { case event := <-wsClient.EventChannel: require.NotEqual(t, model.WebsocketEventEphemeralMessage, event.EventType(), "should not have ephemeral message event") case <-timeout: waiting = false } } post.Message = "a post with @channel" rPost, resp, err = client.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, resp) require.NotNil(t, rPost) post.Message = "a post with @all" rPost, resp, err = client.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, resp) require.NotNil(t, rPost) post.Message = "a post with @here" rPost, resp, err = client.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, resp) require.NotNil(t, rPost) timeout = time.After(5 * time.Second) expectedEvents := 3 // 3 Posts created with @ mentions should result in 3 websocket events gotEvents := 0 for gotEvents < expectedEvents { select { case event := <-wsClient.EventChannel: if event.EventType() == model.WebsocketEventEphemeralMessage { gotEvents++ } case <-timeout: require.Fail(t, fmt.Sprintf("Got %d ephemeral messages, expected: %d", gotEvents, expectedEvents)) } } }) t.Run("err with integrations-reserved props", func(t *testing.T) { originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableHardenedMode = true }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting }) rpost, postResp, postErr := client.CreatePost(context.Background(), &model.Post{ ChannelId: th.BasicChannel.Id, Message: "with props", Props: model.StringInterface{model.PostPropsFromWebhook: "true"}, }) require.Error(t, postErr) CheckBadRequestStatus(t, postResp) assert.Nil(t, rpost) }) t.Run("invalid post type", func(t *testing.T) { post := basicPost() post.Type = model.PostTypeSystemGeneric rpost, resp, err := client.CreatePost(context.Background(), post) require.Error(t, err) CheckBadRequestStatus(t, resp) assert.Nil(t, rpost) }) t.Run("invalid rootId type", func(t *testing.T) { post := basicPost() post.RootId = "junk" rpost, resp, err := client.CreatePost(context.Background(), post) require.Error(t, err) CheckBadRequestStatus(t, resp) assert.Nil(t, rpost) }) t.Run("RootId points to child post", func(t *testing.T) { post := basicPost() post.RootId = childPost.Id rpost, resp, err := client.CreatePost(context.Background(), post) require.Error(t, err) CheckBadRequestStatus(t, resp) assert.Nil(t, rpost) }) t.Run("invalid ChannelId", func(t *testing.T) { post := basicPost() post.ChannelId = "junk" rpost, resp, err := client.CreatePost(context.Background(), post) require.Error(t, err) CheckForbiddenStatus(t, resp) assert.Nil(t, rpost) }) t.Run("invalid ChannelId", func(t *testing.T) { post := basicPost() post.ChannelId = model.NewId() rpost, resp, err := client.CreatePost(context.Background(), post) require.Error(t, err) CheckForbiddenStatus(t, resp) assert.Nil(t, rpost) }) t.Run("invalid payload", func(t *testing.T) { r, err := client.DoAPIPost(context.Background(), "/posts", "garbage") require.Error(t, err) require.Equal(t, http.StatusBadRequest, r.StatusCode) }) t.Run("not logged in", func(t *testing.T) { resp, err := client.Logout(context.Background()) require.NoError(t, err) CheckOKStatus(t, resp) post := basicPost() rpost, resp, err := client.CreatePost(context.Background(), post) require.Error(t, err) CheckUnauthorizedStatus(t, resp) assert.Nil(t, rpost) }) t.Run("CreateAt should match the one provided in the request", func(t *testing.T) { post := basicPost() post.CreateAt = 123 rpost, resp, err := th.SystemAdminClient.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, resp) assert.Equal(t, post.CreateAt, rpost.CreateAt, "create at should match") }) t.Run("Should not be able to define the RemoteId of a post from the API", func(t *testing.T) { newPost := &model.Post{ RemoteId: model.NewPointer(model.NewId()), ChannelId: th.BasicChannel.Id, Message: "post content " + model.NewId(), DeleteAt: 0, } respPost, resp, err := th.SystemAdminClient.CreatePost(context.Background(), newPost) require.NoError(t, err) CheckCreatedStatus(t, resp) require.Zero(t, *respPost.RemoteId) createdPost, appErr := th.App.GetSinglePost(th.Context, respPost.Id, false) require.Nil(t, appErr) require.Zero(t, *createdPost.RemoteId) }) } func TestCreatePostForPriority(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostPriority = true *cfg.ServiceSettings.AllowPersistentNotifications = true }) t.Run("should return forbidden when post-priority is disabled", func(t *testing.T) { originalPrioritySetting := *th.App.Config().ServiceSettings.PostPriority th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostPriority = false }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostPriority = originalPrioritySetting }) post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer("urgent"), }, }} _, resp, err := client.CreatePost(context.Background(), post) require.Error(t, err) CheckForbiddenStatus(t, resp) }) t.Run("should return badRequest when priority is set for reply post", func(t *testing.T) { rootPost := &model.Post{ChannelId: th.BasicChannel.Id, Message: "root"} post, resp, err := client.CreatePost(context.Background(), rootPost) require.NoError(t, err) CheckCreatedStatus(t, resp) replyPost := &model.Post{RootId: post.Id, ChannelId: th.BasicChannel.Id, Message: "reply", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer("urgent"), }, }} _, resp, err = client.CreatePost(context.Background(), replyPost) require.Error(t, err) CheckBadRequestStatus(t, resp) }) t.Run("should return statusNotImplemented when min. pro. license not available", func(t *testing.T) { appErr := th.App.Srv().RemoveLicense() require.Nil(t, appErr) defer th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) // for Acknowledment p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer("urgent"), RequestedAck: model.NewPointer(true), }, }} _, resp, err := client.CreatePost(context.Background(), p1) require.Error(t, err) CheckNotImplementedStatus(t, resp) // for Persistent Notification p2 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer("urgent"), PersistentNotifications: model.NewPointer(true), }, }} _, resp, err = client.CreatePost(context.Background(), p2) require.Error(t, err) CheckNotImplementedStatus(t, resp) }) t.Run("should return forbidden when persistent notification not enabled", func(t *testing.T) { originalSetting := *th.App.Config().ServiceSettings.AllowPersistentNotifications th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowPersistentNotifications = false }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowPersistentNotifications = originalSetting }) p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer("urgent"), PersistentNotifications: model.NewPointer(true), }, }} _, resp, err := client.CreatePost(context.Background(), p1) require.Error(t, err) CheckForbiddenStatus(t, resp) }) t.Run("should return badRequest when post is not urgent for persistent notification", func(t *testing.T) { p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer("important"), PersistentNotifications: model.NewPointer(true), }, }} _, resp, err := client.CreatePost(context.Background(), p1) require.Error(t, err) CheckBadRequestStatus(t, resp) }) t.Run("should return forbidden when persistent notification is disabled for guest users", func(t *testing.T) { originalSetting := *th.App.Config().ServiceSettings.AllowPersistentNotificationsForGuests th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowPersistentNotificationsForGuests = false }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowPersistentNotificationsForGuests = originalSetting }) appErr := th.App.DemoteUserToGuest(th.Context, th.BasicUser) require.Nil(t, appErr) defer func() { appErr = th.App.PromoteGuestToUser(th.Context, th.BasicUser, th.SystemAdminUser.Id) require.Nil(t, appErr) }() p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer("urgent"), PersistentNotifications: model.NewPointer(true), }, }} _, resp, err := client.CreatePost(context.Background(), p1) require.Error(t, err) CheckForbiddenStatus(t, resp) }) t.Run("should create priority post", func(t *testing.T) { p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer("important"), }, }} _, resp, err := client.CreatePost(context.Background(), p1) require.NoError(t, err) CheckCreatedStatus(t, resp) }) t.Run("should create acknowledge post", func(t *testing.T) { p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer(""), RequestedAck: model.NewPointer(true), }, }} _, resp, err := client.CreatePost(context.Background(), p1) require.NoError(t, err) CheckCreatedStatus(t, resp) }) t.Run("should create persistent notification post", func(t *testing.T) { p1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test @" + th.BasicUser2.Username, Metadata: &model.PostMetadata{ Priority: &model.PostPriority{ Priority: model.NewPointer("urgent"), RequestedAck: model.NewPointer(false), PersistentNotifications: model.NewPointer(true), }, }} _, resp, err := client.CreatePost(context.Background(), p1) require.NoError(t, err) CheckCreatedStatus(t, resp) }) } func TestCreatePostWithOAuthClient(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() originalOAuthSetting := *th.App.Config().ServiceSettings.EnableOAuthServiceProvider th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = originalOAuthSetting }) oAuthApp, appErr := th.App.CreateOAuthApp(&model.OAuthApp{ CreatorId: th.SystemAdminUser.Id, Name: "name", CallbackUrls: []string{"http://test.com"}, Homepage: "http://test.com", }) require.Nil(t, appErr, "should create an OAuthApp") session, appErr := th.App.CreateSession(th.Context, &model.Session{ UserId: th.BasicUser.Id, Token: "token", IsOAuth: true, Props: model.StringMap{model.SessionPropOAuthAppID: oAuthApp.Id}, }) require.Nil(t, appErr, "should create a session") post, _, err := th.Client.CreatePost(context.Background(), &model.Post{ ChannelId: th.BasicPost.ChannelId, Message: "test message", }) require.NoError(t, err) assert.NotContains(t, post.GetProps(), model.PostPropsFromOAuthApp, fmt.Sprintf("contains %s prop when not using OAuth client", model.PostPropsOverrideUsername)) client := th.CreateClient() client.SetOAuthToken(session.Token) post, _, err = client.CreatePost(context.Background(), &model.Post{ ChannelId: th.BasicPost.ChannelId, Message: "test message", }) require.NoError(t, err) assert.Contains(t, post.GetProps(), model.PostPropsFromOAuthApp, fmt.Sprintf("missing %s prop when using OAuth client", model.PostPropsOverrideUsername)) t.Run("allow username and icon overrides", func(t *testing.T) { originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableHardenedMode = true }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting }) post, _, err = client.CreatePost(context.Background(), &model.Post{ ChannelId: th.BasicChannel.Id, Message: "test message", Props: model.StringInterface{model.PostPropsOverrideUsername: "newUsernameValue", model.PostPropsOverrideIconURL: "iconUrlOverrideValue"}, }) require.NoError(t, err) assert.Contains(t, post.GetProps(), model.PostPropsOverrideUsername, fmt.Sprintf("missing %s prop when using OAuth client", model.PostPropsOverrideUsername)) assert.Contains(t, post.GetProps(), model.PostPropsOverrideIconURL, fmt.Sprintf("missing %s prop when using OAuth client", model.PostPropsOverrideIconURL)) }) } func TestCreatePostEphemeral(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.SystemAdminClient ephemeralPost := &model.PostEphemeral{ UserID: th.BasicUser2.Id, Post: &model.Post{ChannelId: th.BasicChannel.Id, Message: "a" + model.NewId() + "a", Props: model.StringInterface{model.PropsAddChannelMember: "no good"}}, } rpost, resp, err := client.CreatePostEphemeral(context.Background(), ephemeralPost) require.NoError(t, err) CheckCreatedStatus(t, resp) require.Equal(t, ephemeralPost.Post.Message, rpost.Message, "message didn't match") require.Equal(t, 0, int(rpost.EditAt), "newly created ephemeral post shouldn't have EditAt set") r, err := client.DoAPIPost(context.Background(), "/posts/ephemeral", "garbage") require.Error(t, err) require.Equal(t, http.StatusBadRequest, r.StatusCode) _, err = client.Logout(context.Background()) require.NoError(t, err) _, resp, err = client.CreatePostEphemeral(context.Background(), ephemeralPost) require.Error(t, err) CheckUnauthorizedStatus(t, resp) client = th.Client _, resp, err = client.CreatePostEphemeral(context.Background(), ephemeralPost) require.Error(t, err) CheckForbiddenStatus(t, resp) } func testCreatePostWithOutgoingHook( t *testing.T, hookContentType, expectedContentType, message, triggerWord string, fileIds []string, triggerWhen int, commentPostType bool, ) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() user := th.SystemAdminUser team := th.BasicTeam channel := th.BasicChannel enableOutgoingWebhooks := *th.App.Config().ServiceSettings.EnableOutgoingWebhooks allowedUntrustedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections defer func() { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingWebhooks }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = allowedUntrustedInternalConnections }) }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1" }) var hook *model.OutgoingWebhook var post *model.Post // Create a test server that is the target of the outgoing webhook. It will // validate the webhook body fields and write to the success channel on // success/failure. success := make(chan bool) wait := make(chan bool, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-wait requestContentType := r.Header.Get("Content-Type") if requestContentType != expectedContentType { t.Logf("Content-Type is %s, should be %s", requestContentType, expectedContentType) success <- false return } expectedPayload := &model.OutgoingWebhookPayload{ Token: hook.Token, TeamId: hook.TeamId, TeamDomain: team.Name, ChannelId: post.ChannelId, ChannelName: channel.Name, Timestamp: post.CreateAt, UserId: post.UserId, UserName: user.Username, PostId: post.Id, Text: post.Message, TriggerWord: triggerWord, FileIds: strings.Join(post.FileIds, ","), } // depending on the Content-Type, we expect to find a JSON or form encoded payload if requestContentType == "application/json" { decoder := json.NewDecoder(r.Body) o := &model.OutgoingWebhookPayload{} err := decoder.Decode(&o) if err != nil { th.TestLogger.Warn("Error decoding body", mlog.Err(err)) } if !reflect.DeepEqual(expectedPayload, o) { t.Logf("JSON payload is %+v, should be %+v", o, expectedPayload) success <- false return } } else { err := r.ParseForm() if err != nil { t.Logf("Error parsing form: %q", err) success <- false return } expectedFormValues, _ := url.ParseQuery(expectedPayload.ToFormValues()) if !reflect.DeepEqual(expectedFormValues, r.Form) { t.Logf("Form values are: %q\n, should be: %q\n", r.Form, expectedFormValues) success <- false return } } respPostType := "" // if is empty or post will do a normal post. if commentPostType { respPostType = model.OutgoingHookResponseTypeComment } outGoingHookResponse := &model.OutgoingWebhookResponse{ Text: model.NewPointer("some test text"), Username: "TestCommandServer", IconURL: "https://mattermost.com/wp-content/uploads/2022/02/icon.png", Type: "custom_as", ResponseType: respPostType, } hookJSON, jsonErr := json.Marshal(outGoingHookResponse) require.NoError(t, jsonErr) _, err := w.Write(hookJSON) require.NoError(t, err) success <- true })) defer ts.Close() // create an outgoing webhook, passing it the test server URL var triggerWords []string if triggerWord != "" { triggerWords = []string{triggerWord} } hook = &model.OutgoingWebhook{ ChannelId: channel.Id, TeamId: team.Id, ContentType: hookContentType, TriggerWords: triggerWords, TriggerWhen: triggerWhen, CallbackURLs: []string{ts.URL}, } hook, _, err := th.SystemAdminClient.CreateOutgoingWebhook(context.Background(), hook) require.NoError(t, err) // create a post to trigger the webhook post = &model.Post{ ChannelId: channel.Id, Message: message, FileIds: fileIds, } post, _, err = th.SystemAdminClient.CreatePost(context.Background(), post) require.NoError(t, err) wait <- true // We wait for the test server to write to the success channel and we make // the test fail if that doesn't happen before the timeout. select { case ok := <-success: require.True(t, ok, "Test server did send an invalid webhook.") case <-time.After(2 * time.Second): require.FailNow(t, "Timeout, test server did not send the webhook.") } if commentPostType { time.Sleep(time.Millisecond * 100) postList, _, err := th.SystemAdminClient.GetPostThread(context.Background(), post.Id, "", false) require.NoError(t, err) require.Equal(t, post.Id, postList.Order[0], "wrong order") _, ok := postList.Posts[post.Id] require.True(t, ok, "should have had post") require.Len(t, postList.Posts, 2, "should have 2 posts") } } func TestCreatePostWithOutgoingHook_form_urlencoded(t *testing.T) { t.Run("Case 1", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "triggerword", []string{"file_id_1"}, app.TriggerwordsExactMatch, false) }) t.Run("Case 2", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1"}, app.TriggerwordsStartsWith, false) }) t.Run("Case 3", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "", "", []string{"file_id_1"}, app.TriggerwordsExactMatch, false) }) t.Run("Case 4", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "", "", []string{"file_id_1"}, app.TriggerwordsStartsWith, false) }) t.Run("Case 5", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "triggerword", []string{"file_id_1"}, app.TriggerwordsExactMatch, true) }) t.Run("Case 6", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1"}, app.TriggerwordsStartsWith, true) }) } func TestCreatePostWithOutgoingHook_json(t *testing.T) { t.Run("Case 1", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerword lorem ipsum", "triggerword", []string{"file_id_1, file_id_2"}, app.TriggerwordsExactMatch, false) }) t.Run("Case 2", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1, file_id_2"}, app.TriggerwordsStartsWith, false) }) t.Run("Case 3", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerword lorem ipsum", "", []string{"file_id_1"}, app.TriggerwordsExactMatch, false) }) t.Run("Case 4", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerwordaaazzz lorem ipsum", "", []string{"file_id_1"}, app.TriggerwordsStartsWith, false) }) t.Run("Case 5", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerword lorem ipsum", "triggerword", []string{"file_id_1, file_id_2"}, app.TriggerwordsExactMatch, true) }) t.Run("Case 6", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerwordaaazzz lorem ipsum", "", []string{"file_id_1"}, app.TriggerwordsStartsWith, true) }) } // hooks created before we added the ContentType field should be considered as // application/x-www-form-urlencoded func TestCreatePostWithOutgoingHook_no_content_type(t *testing.T) { t.Run("Case 1", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "triggerword", []string{"file_id_1"}, app.TriggerwordsExactMatch, false) }) t.Run("Case 2", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1"}, app.TriggerwordsStartsWith, false) }) t.Run("Case 3", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "", []string{"file_id_1, file_id_2"}, app.TriggerwordsExactMatch, false) }) t.Run("Case 4", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "", []string{"file_id_1, file_id_2"}, app.TriggerwordsStartsWith, false) }) t.Run("Case 5", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "triggerword", []string{"file_id_1"}, app.TriggerwordsExactMatch, true) }) t.Run("Case 6", func(t *testing.T) { testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "", []string{"file_id_1, file_id_2"}, app.TriggerwordsExactMatch, true) }) } func TestMoveThread(t *testing.T) { os.Setenv("MM_FEATUREFLAGS_MOVETHREADSENABLED", "true") defer os.Unsetenv("MM_FEATUREFLAGS_MOVETHREADSENABLED") th := SetupEnterprise(t).InitBasic() defer th.TearDown() th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise)) client := th.Client ctx := context.Background() basicUser1 := th.BasicUser basicUser2 := th.BasicUser2 basicUser3 := th.CreateUser() // Helper function to create a new public channel to move the post to createPublicChannel := func(teamId, name, displayName string) *model.Channel { channel, resp, err := client.CreateChannel(ctx, &model.Channel{ TeamId: teamId, Name: name, DisplayName: displayName, Type: model.ChannelTypeOpen, }) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, channel) return channel } // Create a new private channel to move the post to privateChannel, resp, err := client.CreateChannel(ctx, &model.Channel{ TeamId: th.BasicTeam.Id, Name: "test-private-channel", DisplayName: "Test Private Channel", Type: model.ChannelTypePrivate, }) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, privateChannel) // Create a new direct message channel to move the post to dmChannel, resp, err := client.CreateDirectChannel(ctx, basicUser1.Id, basicUser2.Id) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, dmChannel) // Create a new group message channel to move the post to gmChannel, resp, err := client.CreateGroupChannel(ctx, []string{basicUser1.Id, basicUser2.Id, basicUser3.Id}) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, gmChannel) t.Run("Move to public channel", func(t *testing.T) { // Create a public channel publicChannel := createPublicChannel(th.BasicTeam.Id, "test-public-channel", "Test Public Channel") // Create a new post to move post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "test post", } newPost, resp, err := client.CreatePost(ctx, post) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, newPost) // Move the post to the public channel moveThreadParams := &model.MoveThreadParams{ ChannelId: publicChannel.Id, } resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) // Check that the post was moved to the public channel posts, resp, err := client.GetPostsForChannel(ctx, publicChannel.Id, 0, 100, "", true, false) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, posts) // There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved require.Equal(t, 2, len(posts.Posts)) require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message) }) t.Run("Move to private channel", func(t *testing.T) { // Create a new post to move post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "test post", } newPost, resp, err := client.CreatePost(ctx, post) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, newPost) // Move the post to the private channel moveThreadParams := &model.MoveThreadParams{ ChannelId: privateChannel.Id, } resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) // Check that the post was moved to the private channel posts, resp, err := client.GetPostsForChannel(ctx, privateChannel.Id, 0, 100, "", true, false) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, posts) // There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved require.Equal(t, 2, len(posts.Posts)) require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message) }) t.Run("Move to direct message channel", func(t *testing.T) { // Create a new post to move post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "test post", } newPost, resp, err := client.CreatePost(ctx, post) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, newPost) // Move the post to the direct message channel moveThreadParams := &model.MoveThreadParams{ ChannelId: dmChannel.Id, } resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) // Check that the post was moved to the direct message channel posts, resp, err := client.GetPostsForChannel(ctx, dmChannel.Id, 0, 100, "", true, false) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, posts) // There should be 1 post, the post we moved require.Equal(t, 1, len(posts.Posts)) require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message) }) t.Run("Move to group message channel", func(t *testing.T) { // Create a new post to move post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "test post", } newPost, resp, err := client.CreatePost(ctx, post) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, newPost) // Move the post to the group message channel moveThreadParams := &model.MoveThreadParams{ ChannelId: gmChannel.Id, } resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) // Check that the post was moved to the group message channel posts, resp, err := client.GetPostsForChannel(ctx, gmChannel.Id, 0, 100, "", true, false) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, posts) // There should be 1 post, the post we moved require.Equal(t, 1, len(posts.Posts)) require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message) }) t.Run("Move thread with more than one post", func(t *testing.T) { // Create a new public channel to move the post to pChannel, resp, err := client.CreateChannel(ctx, &model.Channel{ TeamId: th.BasicTeam.Id, Name: "test-public-channel2", DisplayName: "Test Public Channel", Type: model.ChannelTypeOpen, }) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, pChannel) // Create a new post to use as the root post rootPost := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "root post", } rootPost, resp, err = client.CreatePost(ctx, rootPost) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, rootPost) // Create a new post to move post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "test post", RootId: rootPost.Id, } newPost, resp, err := client.CreatePost(ctx, post) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, newPost) // Create another post in the thread post = &model.Post{ ChannelId: th.BasicChannel.Id, Message: "test post 2", RootId: rootPost.Id, } newPost2, resp, err := client.CreatePost(ctx, post) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, newPost2) // Move the thread to the public channel moveThreadParams := &model.MoveThreadParams{ ChannelId: pChannel.Id, } resp, err = client.MoveThread(ctx, rootPost.Id, moveThreadParams) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) // Check that the thread was moved to the public channel posts, resp, err := client.GetPostsForChannel(ctx, pChannel.Id, 0, 100, "", false, false) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, posts) require.Equal(t, "This thread was moved from another channel", posts.Posts[posts.Order[0]].Message) require.Equal(t, newPost2.Message, posts.Posts[posts.Order[1]].Message) require.Equal(t, newPost.Message, posts.Posts[posts.Order[2]].Message) require.Equal(t, rootPost.Message, posts.Posts[posts.Order[3]].Message) }) t.Run("Move thread when permitted role is channel admin", func(t *testing.T) { // Create public channel publicChannel := createPublicChannel(th.BasicTeam.Id, "test-public-channel-admin", "Test Public Channel Admin") // Set permitted role as channel admin enabled := true th.App.UpdateConfig(func(cfg *model.Config) { cfg.WranglerSettings = model.WranglerSettings{ MoveThreadToAnotherTeamEnable: &enabled, PermittedWranglerRoles: []string{model.PermissionsChannelAdmin}, } }) defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.WranglerSettings = model.WranglerSettings{} }) // Login as channel admin and add to channel th.LoginTeamAdmin() th.AddUserToChannel(th.TeamAdminUser, publicChannel) defer th.LoginBasic() // Create a new post to move post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "test post", } newPost, resp, err := client.CreatePost(ctx, post) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, newPost) // Move the post to the public channel moveThreadParams := &model.MoveThreadParams{ ChannelId: publicChannel.Id, } resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) // Check that the post was moved to the public channel posts, resp, err := client.GetPostsForChannel(ctx, publicChannel.Id, 0, 100, "", true, false) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, posts) // There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved require.Equal(t, 2, len(posts.Posts)) require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message) }) t.Run("check permissions limited by AllowedEmailDomain", func(t *testing.T) { th.App.UpdateConfig(func(c *model.Config) { c.WranglerSettings.AllowedEmailDomain = []string{"foo.com", "bar.com"} }) t.Cleanup(func() { th.App.UpdateConfig(func(c *model.Config) { c.WranglerSettings.AllowedEmailDomain = make([]string, 0) }) }) // Create a public channel publicChannel := createPublicChannel(th.BasicTeam.Id, "test-public-channel-allowed-email-domain", "Test Public Channel") // Create a new post to move post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "test post", } newPost, resp, err := client.CreatePost(ctx, post) require.NoError(t, err) CheckCreatedStatus(t, resp) require.NotNil(t, newPost) // Move the post to the public channel as a user without the configured domain moveThreadParams := &model.MoveThreadParams{ ChannelId: publicChannel.Id, } resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams) require.Error(t, err) CheckForbiddenStatus(t, resp) // Change the email domain to match the configured setting th.BasicUser.Email = "basicuser@foo.com" _, resp, err = client.UpdateUser(ctx, th.BasicUser) require.NoError(t, err) CheckOKStatus(t, resp) resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams) require.NoError(t, err) CheckOKStatus(t, resp) // Check that the post was moved to the public channel posts, resp, err := client.GetPostsForChannel(ctx, publicChannel.Id, 0, 100, "", true, false) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, posts) // There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved require.Equal(t, 2, len(posts.Posts)) require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message) }) } func TestCreatePostPublic(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "#hashtag a" + model.NewId() + "a"} user := model.User{Email: th.GenerateTestEmail(), Nickname: "Joram Wilander", Password: "hello1", Username: GenerateTestUsername(), Roles: model.SystemUserRoleId} ruser, _, err := client.CreateUser(context.Background(), &user) require.NoError(t, err) _, _, err = client.Login(context.Background(), user.Email, user.Password) require.NoError(t, err) _, resp, err := client.CreatePost(context.Background(), post) require.Error(t, err) CheckForbiddenStatus(t, resp) _, appErr := th.App.UpdateUserRoles(th.Context, ruser.Id, model.SystemUserRoleId+" "+model.SystemPostAllPublicRoleId, false) require.Nil(t, appErr) appErr = th.App.Srv().InvalidateAllCaches() require.Nil(t, appErr) _, _, err = client.Login(context.Background(), user.Email, user.Password) require.NoError(t, err) _, _, err = client.CreatePost(context.Background(), post) require.NoError(t, err) post.ChannelId = th.BasicPrivateChannel.Id _, resp, err = client.CreatePost(context.Background(), post) require.Error(t, err) CheckForbiddenStatus(t, resp) _, appErr = th.App.UpdateUserRoles(th.Context, ruser.Id, model.SystemUserRoleId, false) require.Nil(t, appErr) _, appErr = th.App.JoinUserToTeam(th.Context, th.BasicTeam, ruser, "") require.Nil(t, appErr) _, appErr = th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, ruser.Id, model.TeamUserRoleId+" "+model.TeamPostAllPublicRoleId) require.Nil(t, appErr) appErr = th.App.Srv().InvalidateAllCaches() require.Nil(t, appErr) _, _, err = client.Login(context.Background(), user.Email, user.Password) require.NoError(t, err) post.ChannelId = th.BasicPrivateChannel.Id _, resp, err = client.CreatePost(context.Background(), post) require.Error(t, err) CheckForbiddenStatus(t, resp) post.ChannelId = th.BasicChannel.Id _, _, err = client.CreatePost(context.Background(), post) require.NoError(t, err) } func TestCreatePostAll(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "#hashtag a" + model.NewId() + "a"} user := model.User{Email: th.GenerateTestEmail(), Nickname: "Joram Wilander", Password: "hello1", Username: GenerateTestUsername(), Roles: model.SystemUserRoleId} directChannel, _ := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id) ruser, _, err := client.CreateUser(context.Background(), &user) require.NoError(t, err) _, _, err = client.Login(context.Background(), user.Email, user.Password) require.NoError(t, err) _, resp, err := client.CreatePost(context.Background(), post) require.Error(t, err) CheckForbiddenStatus(t, resp) _, appErr := th.App.UpdateUserRoles(th.Context, ruser.Id, model.SystemUserRoleId+" "+model.SystemPostAllRoleId, false) require.Nil(t, appErr) appErr = th.App.Srv().InvalidateAllCaches() require.Nil(t, appErr) _, _, err = client.Login(context.Background(), user.Email, user.Password) require.NoError(t, err) _, _, err = client.CreatePost(context.Background(), post) require.NoError(t, err) post.ChannelId = th.BasicPrivateChannel.Id _, _, err = client.CreatePost(context.Background(), post) require.NoError(t, err) post.ChannelId = directChannel.Id _, _, err = client.CreatePost(context.Background(), post) require.NoError(t, err) _, appErr = th.App.UpdateUserRoles(th.Context, ruser.Id, model.SystemUserRoleId, false) require.Nil(t, appErr) _, appErr = th.App.JoinUserToTeam(th.Context, th.BasicTeam, ruser, "") require.Nil(t, appErr) _, appErr = th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, ruser.Id, model.TeamUserRoleId+" "+model.TeamPostAllRoleId) require.Nil(t, appErr) appErr = th.App.Srv().InvalidateAllCaches() require.Nil(t, appErr) _, _, err = client.Login(context.Background(), user.Email, user.Password) require.NoError(t, err) post.ChannelId = th.BasicPrivateChannel.Id _, _, err = client.CreatePost(context.Background(), post) require.NoError(t, err) post.ChannelId = th.BasicChannel.Id _, _, err = client.CreatePost(context.Background(), post) require.NoError(t, err) post.ChannelId = directChannel.Id _, resp, err = client.CreatePost(context.Background(), post) require.Error(t, err) CheckForbiddenStatus(t, resp) } func TestCreatePostSendOutOfChannelMentions(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client WebSocketClient := th.CreateConnectedWebSocketClient(t) inChannelUser := th.CreateUser() th.LinkUserToTeam(inChannelUser, th.BasicTeam) _, appErr := th.App.AddUserToChannel(th.Context, inChannelUser, th.BasicChannel, false) require.Nil(t, appErr) post1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "@" + inChannelUser.Username} _, resp, err := client.CreatePost(context.Background(), post1) require.NoError(t, err) CheckCreatedStatus(t, resp) timeout := time.After(5 * time.Second) waiting := true for waiting { select { case event := <-WebSocketClient.EventChannel: require.NotEqual(t, model.WebsocketEventEphemeralMessage, event.EventType(), "should not have ephemeral message event") case <-timeout: waiting = false } } outOfChannelUser := th.CreateUser() th.LinkUserToTeam(outOfChannelUser, th.BasicTeam) post2 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "@" + outOfChannelUser.Username} _, resp, err = client.CreatePost(context.Background(), post2) require.NoError(t, err) CheckCreatedStatus(t, resp) timeout = time.After(5 * time.Second) waiting = true for waiting { select { case event := <-WebSocketClient.EventChannel: if event.EventType() != model.WebsocketEventEphemeralMessage { // Ignore any other events continue } var wpost model.Post err := json.Unmarshal([]byte(event.GetData()["post"].(string)), &wpost) require.NoError(t, err) acm, ok := wpost.GetProp(model.PropsAddChannelMember).(map[string]any) require.True(t, ok, "should have received ephemeral post with 'add_channel_member' in props") require.True(t, acm["post_id"] != nil, "should not be nil") require.True(t, acm["user_ids"] != nil, "should not be nil") require.True(t, acm["usernames"] != nil, "should not be nil") waiting = false case <-timeout: require.FailNow(t, "timed out waiting for ephemeral message event") } } } func TestCreatePostCheckOnlineStatus(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() api, err := Init(th.Server) require.NoError(t, err) session, _ := th.App.GetSession(th.Client.AuthToken) cli := th.CreateClient() _, _, err = cli.Login(context.Background(), th.BasicUser2.Username, th.BasicUser2.Password) require.NoError(t, err) wsClient := th.CreateConnectedWebSocketClientWithClient(t, cli) waitForEvent := func(isSetOnline bool) { timeout := time.After(5 * time.Second) for { select { case ev := <-wsClient.EventChannel: if ev.EventType() == model.WebsocketEventPosted { assert.True(t, ev.GetData()["set_online"].(bool) == isSetOnline) return } case <-timeout: // We just skip the test instead of failing because waiting for more than 5 seconds // to get a response does not make sense, and it will unnecessarily slow down // the tests further in an already congested CI environment. t.Skip("timed out waiting for event") } } } handler := api.APIHandler(createPost) resp := httptest.NewRecorder() post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "some message", } postJSON, jsonErr := json.Marshal(post) require.NoError(t, jsonErr) req := httptest.NewRequest("POST", "/api/v4/posts?set_online=false", bytes.NewReader(postJSON)) req.Header.Set(model.HeaderAuth, "Bearer "+session.Token) handler.ServeHTTP(resp, req) assert.Equal(t, http.StatusCreated, resp.Code) waitForEvent(false) _, appErr := th.App.GetStatus(th.BasicUser.Id) require.NotNil(t, appErr) assert.Equal(t, "app.status.get.missing.app_error", appErr.Id) postJSON, jsonErr = json.Marshal(post) require.NoError(t, jsonErr) req = httptest.NewRequest("POST", "/api/v4/posts", bytes.NewReader(postJSON)) req.Header.Set(model.HeaderAuth, "Bearer "+session.Token) handler.ServeHTTP(resp, req) assert.Equal(t, http.StatusCreated, resp.Code) waitForEvent(true) st, appErr := th.App.GetStatus(th.BasicUser.Id) require.Nil(t, appErr) assert.Equal(t, "online", st.Status) } func TestUpdatePost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client channel := th.BasicChannel th.App.Srv().SetLicense(model.NewTestLicense()) fileIds := make([]string, 3) data, err2 := testutils.ReadTestFile("test.png") require.NoError(t, err2) for i := range fileIds { fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) fileIds[i] = fileResp.FileInfos[0].Id } rpost, appErr := th.App.CreatePost(th.Context, &model.Post{ UserId: th.BasicUser.Id, ChannelId: channel.Id, Message: "zz" + model.NewId() + "a", FileIds: fileIds, }, channel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) assert.Equal(t, rpost.Message, rpost.Message, "full name didn't match") assert.EqualValues(t, 0, rpost.EditAt, "Newly created post shouldn't have EditAt set") assert.Equal(t, model.StringArray(fileIds), rpost.FileIds, "FileIds should have been set") t.Run("new message, invalid props", func(t *testing.T) { msg1 := "#hashtag a" + model.NewId() + " update post again" rpost.Message = msg1 rpost.AddProp(model.PropsAddChannelMember, "no good") rrupost, _, err := client.UpdatePost(context.Background(), rpost.Id, rpost) require.NoError(t, err) assert.Equal(t, msg1, rrupost.Message, "failed to update message") assert.Equal(t, "#hashtag", rrupost.Hashtags, "failed to update hashtags") assert.Nil(t, rrupost.GetProp(model.PropsAddChannelMember), "failed to sanitize Props['add_channel_member'], should be nil") actual, _, err := client.GetPost(context.Background(), rpost.Id, "") require.NoError(t, err) assert.Equal(t, msg1, actual.Message, "failed to update message") assert.Equal(t, "#hashtag", actual.Hashtags, "failed to update hashtags") assert.Nil(t, actual.GetProp(model.PropsAddChannelMember), "failed to sanitize Props['add_channel_member'], should be nil") }) t.Run("join/leave post", func(t *testing.T) { var rpost2 *model.Post rpost2, appErr = th.App.CreatePost(th.Context, &model.Post{ ChannelId: channel.Id, Message: "zz" + model.NewId() + "a", Type: model.PostTypeJoinLeave, UserId: th.BasicUser.Id, }, channel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) up2 := &model.Post{ Id: rpost2.Id, ChannelId: channel.Id, Message: "zz" + model.NewId() + " update post 2", } _, resp, err := client.UpdatePost(context.Background(), rpost2.Id, up2) require.Error(t, err) CheckBadRequestStatus(t, resp) }) rpost3, appErr := th.App.CreatePost(th.Context, &model.Post{ ChannelId: channel.Id, Message: "zz" + model.NewId() + "a", UserId: th.BasicUser.Id, }, channel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) t.Run("add slack attachments", func(t *testing.T) { up4 := &model.Post{ Id: rpost3.Id, ChannelId: channel.Id, Message: "zz" + model.NewId() + " update post 3", } up4.AddProp(model.PostPropsAttachments, []model.SlackAttachment{ { Text: "Hello World", }, }) rrupost3, _, err := client.UpdatePost(context.Background(), rpost3.Id, up4) require.NoError(t, err) assert.NotEqual(t, rpost3.EditAt, rrupost3.EditAt) assert.NotEqual(t, rpost3.Attachments(), rrupost3.Attachments()) }) t.Run("change message, but post too old", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostEditTimeLimit = 1 }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostEditTimeLimit = -1 }) rpost4, appErr := th.App.CreatePost(th.Context, &model.Post{ ChannelId: channel.Id, Message: "zz" + model.NewId() + "a", UserId: th.BasicUser.Id, CreateAt: model.GetMillis() - 2000, }, channel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) up4 := &model.Post{ Id: rpost4.Id, ChannelId: channel.Id, Message: "zz" + model.NewId() + " update post 4", } _, resp, err := client.UpdatePost(context.Background(), rpost4.Id, up4) require.Error(t, err, "should fail on update old post") CheckBadRequestStatus(t, resp) }) t.Run("err with integrations-reserved props", func(t *testing.T) { originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableHardenedMode = true }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting }) _, resp, err := client.UpdatePost(context.Background(), rpost.Id, &model.Post{ ChannelId: th.BasicChannel.Id, Message: "with props", Props: model.StringInterface{model.PostPropsFromWebhook: "true"}, }) require.Error(t, err) CheckBadRequestStatus(t, resp) }) t.Run("logged out", func(t *testing.T) { _, err := client.Logout(context.Background()) require.NoError(t, err) _, resp, err := client.UpdatePost(context.Background(), rpost.Id, rpost) require.Error(t, err) CheckUnauthorizedStatus(t, resp) }) t.Run("different user", func(t *testing.T) { th.LoginBasic2() _, resp, err := client.UpdatePost(context.Background(), rpost.Id, rpost) require.Error(t, err) CheckForbiddenStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) }) t.Run("different user, but team admin", func(t *testing.T) { th.LoginTeamAdmin() _, resp, err := client.UpdatePost(context.Background(), rpost.Id, rpost) require.Error(t, err) CheckForbiddenStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) }) t.Run("different user, but system admin", func(t *testing.T) { _, _, err := th.SystemAdminClient.UpdatePost(context.Background(), rpost.Id, rpost) require.NoError(t, err) }) t.Run("should be able to add new files", func(t *testing.T) { th.LoginBasic() // create new file fileResponse, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse.FileInfos)) fileInfo := fileResponse.FileInfos[0] // create new post post, appErr := th.App.CreatePost(th.Context, &model.Post{ UserId: th.BasicUser.Id, ChannelId: channel.Id, Message: "zz" + model.NewId() + "a", }, channel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) require.NotNil(t, post) // update post with new file post.FileIds = []string{fileInfo.Id} _, _, err = client.UpdatePost(context.Background(), post.Id, post) require.NoError(t, err) updatedPost, _, err := client.GetPost(context.Background(), post.Id, "") require.NoError(t, err) require.Equal(t, post.Id, updatedPost.Id) require.Equal(t, 1, len(updatedPost.FileIds)) require.Equal(t, fileInfo.Id, updatedPost.FileIds[0]) // verify file is attached to the post fetchedFileInfo, _, err := client.GetFileInfo(context.Background(), fileInfo.Id) require.NoError(t, err) require.Equal(t, fileInfo.Id, fetchedFileInfo.Id) require.Equal(t, post.Id, fetchedFileInfo.PostId) }) t.Run("should be able to remove files", func(t *testing.T) { th.LoginBasic() // create new file fileResponse, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse.FileInfos)) fileInfo := fileResponse.FileInfos[0] // create new post post, appErr := th.App.CreatePost(th.Context, &model.Post{ UserId: th.BasicUser.Id, ChannelId: channel.Id, Message: "zz" + model.NewId() + "a", FileIds: []string{fileInfo.Id}, }, channel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) require.NotNil(t, post) require.Equal(t, 1, len(post.FileIds)) // remove files from post post.FileIds = []string{} _, _, err = client.UpdatePost(context.Background(), post.Id, post) require.NoError(t, err) updatedPost, _, err := client.GetPost(context.Background(), post.Id, "") require.NoError(t, err) require.Equal(t, post.Id, updatedPost.Id) require.Equal(t, 0, len(updatedPost.FileIds)) // verify file is removed from the post postFileInfos, err := th.App.Srv().Store().FileInfo().GetForPost(post.Id, true, true, false) require.NoError(t, err) require.Equal(t, 1, len(postFileInfos)) require.Equal(t, fileInfo.Id, postFileInfos[0].Id) require.Greater(t, postFileInfos[0].DeleteAt, int64(0)) }) t.Run("post files remain unchanged when fileIds is nil", func(t *testing.T) { th.LoginBasic() // create new file fileResponse, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse.FileInfos)) fileInfo := fileResponse.FileInfos[0] // create new post post, appErr := th.App.CreatePost(th.Context, &model.Post{ UserId: th.BasicUser.Id, ChannelId: channel.Id, Message: "zz" + model.NewId() + "a", FileIds: []string{fileInfo.Id}, }, channel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) require.NotNil(t, post) require.Equal(t, 1, len(post.FileIds)) // update post without specifying fileIds post.FileIds = nil post.Message = "updated message" _, _, err = client.UpdatePost(context.Background(), post.Id, post) require.NoError(t, err) updatedPost, _, err := client.GetPost(context.Background(), post.Id, "") require.NoError(t, err) require.Equal(t, post.Id, updatedPost.Id) require.Equal(t, 1, len(updatedPost.FileIds)) require.Equal(t, fileInfo.Id, updatedPost.FileIds[0]) require.Equal(t, "updated message", updatedPost.Message) // verify file is still part of the post postFileInfos, err := th.App.Srv().Store().FileInfo().GetForPost(post.Id, true, false, false) require.NoError(t, err) require.Equal(t, 1, len(postFileInfos)) require.Equal(t, fileInfo.Id, postFileInfos[0].Id) require.Equal(t, int64(0), postFileInfos[0].DeleteAt) }) t.Run("should be able to add and remove files simultaneously", func(t *testing.T) { th.LoginBasic() // create new file fileResponse1, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse1.FileInfos)) fileInfo1 := fileResponse1.FileInfos[0] fileResponse2, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse2.FileInfos)) fileInfo2 := fileResponse2.FileInfos[0] // create new post post, appErr := th.App.CreatePost(th.Context, &model.Post{ UserId: th.BasicUser.Id, ChannelId: channel.Id, Message: "zz" + model.NewId() + "a", FileIds: model.StringArray{fileInfo1.Id, fileInfo2.Id}, }, channel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) require.NotNil(t, post) require.Equal(t, 2, len(post.FileIds)) // update post with new file fileResponse3, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse3.FileInfos)) fileInfo3 := fileResponse3.FileInfos[0] fileResponse4, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse4.FileInfos)) fileInfo4 := fileResponse4.FileInfos[0] post.FileIds = []string{fileInfo3.Id, fileInfo4.Id} _, _, err = client.UpdatePost(context.Background(), post.Id, post) require.NoError(t, err) updatedPost, _, err := client.GetPost(context.Background(), post.Id, "") require.NoError(t, err) require.Equal(t, post.Id, updatedPost.Id) require.Equal(t, 2, len(updatedPost.FileIds)) require.Contains(t, updatedPost.FileIds, fileInfo3.Id) require.Contains(t, updatedPost.FileIds, fileInfo4.Id) postFiles, err := th.App.Srv().Store().FileInfo().GetForPost(post.Id, true, true, false) require.NoError(t, err) require.Equal(t, 4, len(postFiles)) for _, postFile := range postFiles { if postFile.Id == fileInfo1.Id || postFile.Id == fileInfo2.Id { require.Greater(t, postFile.DeleteAt, int64(0)) } if postFile.Id == fileInfo3.Id || postFile.Id == fileInfo4.Id { require.Equal(t, postFile.PostId, post.Id) } } }) } func TestUpdateOthersPostInDirectMessageChannel(t *testing.T) { mainHelper.Parallel(t) // This test checks that a sysadmin with the "EDIT_OTHERS_POSTS" permission can edit someone else's post in a // channel without a team (DM/GM). This indirectly checks for the proper cascading all the way to system-wide roles // on the user object of permissions based on a post in a channel with no team ID. th := Setup(t).InitBasic() defer th.TearDown() dmChannel := th.CreateDmChannel(th.SystemAdminUser) post := &model.Post{ Message: "asd", ChannelId: dmChannel.Id, PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), UserId: th.BasicUser.Id, CreateAt: 0, } post, _, err := th.Client.CreatePost(context.Background(), post) require.NoError(t, err) post.Message = "changed" _, _, err = th.SystemAdminClient.UpdatePost(context.Background(), post.Id, post) require.NoError(t, err) } func TestPatchPost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client channel := th.BasicChannel th.App.Srv().SetLicense(model.NewTestLicense()) fileIDs := make([]string, 3) data, err2 := testutils.ReadTestFile("test.png") require.NoError(t, err2) for i := range fileIDs { fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) fileIDs[i] = fileResp.FileInfos[0].Id } sort.Strings(fileIDs) post := &model.Post{ ChannelId: channel.Id, IsPinned: true, Message: "#hashtag a message", Props: model.StringInterface{"channel_header": "old_header"}, FileIds: fileIDs[0:2], HasReactions: true, } post, _, err := client.CreatePost(context.Background(), post) require.NoError(t, err) var rpost *model.Post t.Run("new message, props, files, HasReactions bit", func(t *testing.T) { patch := &model.PostPatch{} patch.IsPinned = model.NewPointer(false) patch.Message = model.NewPointer("#otherhashtag other message") patch.Props = &model.StringInterface{"channel_header": "new_header"} patchFileIds := model.StringArray(fileIDs) // one extra file patch.FileIds = &patchFileIds patch.HasReactions = model.NewPointer(false) rpost, _, err = client.PatchPost(context.Background(), post.Id, patch) require.NoError(t, err) assert.False(t, rpost.IsPinned, "IsPinned did not update properly") assert.Equal(t, "#otherhashtag other message", rpost.Message, "Message did not update properly") assert.Equal(t, *patch.Props, rpost.GetProps(), "Props did not update properly") assert.Equal(t, "#otherhashtag", rpost.Hashtags, "Message did not update properly") assert.Equal(t, model.StringArray(fileIDs), rpost.FileIds, "FileIds should not update") assert.False(t, rpost.HasReactions, "HasReactions did not update properly") }) t.Run("add slack attachments", func(t *testing.T) { patch2 := &model.PostPatch{} attachments := []model.SlackAttachment{ { Text: "Hello World", }, } patch2.Props = &model.StringInterface{model.PostPropsAttachments: attachments} var rpost2 *model.Post rpost2, _, err = client.PatchPost(context.Background(), post.Id, patch2) require.NoError(t, err) assert.NotEmpty(t, rpost2.GetProp(model.PostPropsAttachments)) assert.NotEqual(t, rpost.EditAt, rpost2.EditAt) }) t.Run("invalid requests", func(t *testing.T) { var origEnableDeveloper bool th.App.UpdateConfig(func(cfg *model.Config) { origEnableDeveloper = *cfg.ServiceSettings.EnableDeveloper *cfg.ServiceSettings.EnableDeveloper = true }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableDeveloper = origEnableDeveloper }) var r *http.Response r, err = client.DoAPIPut(context.Background(), "/posts/"+post.Id+"/patch", "garbage") require.EqualError(t, err, "Invalid or missing post in request body., invalid character 'g' looking for beginning of value") require.Equal(t, http.StatusBadRequest, r.StatusCode, "wrong status code") var resp *model.Response patch := &model.PostPatch{} _, resp, err = client.PatchPost(context.Background(), "junk", patch) require.Error(t, err) CheckBadRequestStatus(t, resp) }) t.Run("unknown post", func(t *testing.T) { var resp *model.Response patch := &model.PostPatch{} _, resp, err = client.PatchPost(context.Background(), GenerateTestID(), patch) require.Error(t, err) CheckForbiddenStatus(t, resp) }) t.Run("logged out", func(t *testing.T) { _, err = client.Logout(context.Background()) require.NoError(t, err) patch := &model.PostPatch{} _, resp, err := client.PatchPost(context.Background(), post.Id, patch) require.Error(t, err) CheckUnauthorizedStatus(t, resp) }) t.Run("different user", func(t *testing.T) { th.LoginBasic2() patch := &model.PostPatch{} _, resp, err := client.PatchPost(context.Background(), post.Id, patch) require.Error(t, err) CheckForbiddenStatus(t, resp) }) t.Run("different user, but team admin", func(t *testing.T) { th.LoginTeamAdmin() patch := &model.PostPatch{} _, resp, err := client.PatchPost(context.Background(), post.Id, patch) require.Error(t, err) CheckForbiddenStatus(t, resp) }) t.Run("different user, but system admin", func(t *testing.T) { patch := &model.PostPatch{} _, _, err := th.SystemAdminClient.PatchPost(context.Background(), post.Id, patch) require.NoError(t, err) }) t.Run("edit others posts permission can function independently of edit own post", func(t *testing.T) { th.LoginBasic2() patch := &model.PostPatch{} _, resp, err := client.PatchPost(context.Background(), post.Id, patch) require.Error(t, err) CheckForbiddenStatus(t, resp) // Add permission to edit others' defaultPerms := th.SaveDefaultRolePermissions() defer th.RestoreDefaultRolePermissions(defaultPerms) th.RemovePermissionFromRole(model.PermissionEditPost.Id, model.ChannelUserRoleId) th.AddPermissionToRole(model.PermissionEditOthersPosts.Id, model.ChannelUserRoleId) _, _, err = client.PatchPost(context.Background(), post.Id, patch) require.NoError(t, err) }) t.Run("time limit expired", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostEditTimeLimit = 1 }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostEditTimeLimit = -1 }) post2 := &model.Post{ ChannelId: channel.Id, Message: "#hashtag a message", CreateAt: model.GetMillis() - 2000, } post2, _, err := th.SystemAdminClient.CreatePost(context.Background(), post2) require.NoError(t, err) patch2 := &model.PostPatch{ Message: model.NewPointer("new message"), } _, resp, err := th.SystemAdminClient.PatchPost(context.Background(), post2.Id, patch2) require.Error(t, err) CheckBadRequestStatus(t, resp) require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id, "should be time limit error") }) t.Run("err with integrations-reserved props", func(t *testing.T) { originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableHardenedMode = true }) defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting }) post := &model.Post{ ChannelId: channel.Id, Message: "#hashtag a message", CreateAt: model.GetMillis() - 2000, } post, _, createErr := th.SystemAdminClient.CreatePost(context.Background(), post) require.NoError(t, createErr) patch := &model.PostPatch{} patch.Props = &model.StringInterface{model.PostPropsFromWebhook: "true"} _, patchResp, patchErr := client.PatchPost(context.Background(), post.Id, patch) require.Error(t, patchErr) CheckBadRequestStatus(t, patchResp) }) t.Run("should be able to add new files", func(t *testing.T) { post, _, err := client.CreatePost(context.Background(), &model.Post{ ChannelId: channel.Id, Message: "#hashtag a message", CreateAt: model.GetMillis() - 2000, }) require.NoError(t, err) fileResponse, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse.FileInfos)) fileInfo := fileResponse.FileInfos[0] patch := &model.PostPatch{ FileIds: &model.StringArray{fileInfo.Id}, } _, _, err = client.PatchPost(context.Background(), post.Id, patch) require.NoError(t, err) patchedPost, _, err := client.GetPost(context.Background(), post.Id, "") require.NoError(t, err) require.Equal(t, 1, len(patchedPost.FileIds)) require.Equal(t, fileInfo.Id, patchedPost.FileIds[0]) }) t.Run("should be able to remove some files", func(t *testing.T) { fileResponse1, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse1.FileInfos)) fileInfo1 := fileResponse1.FileInfos[0] fileResponse2, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse2.FileInfos)) fileInfo2 := fileResponse2.FileInfos[0] post, _, err := client.CreatePost(context.Background(), &model.Post{ ChannelId: channel.Id, Message: "#hashtag a message", CreateAt: model.GetMillis() - 2000, FileIds: model.StringArray{fileInfo1.Id, fileInfo2.Id}, }) require.NoError(t, err) require.Equal(t, 2, len(post.FileIds)) patch := &model.PostPatch{ FileIds: &model.StringArray{fileInfo2.Id}, } _, _, err = client.PatchPost(context.Background(), post.Id, patch) require.NoError(t, err) patchedPost, _, err := client.GetPost(context.Background(), post.Id, "") require.NoError(t, err) require.Equal(t, 1, len(patchedPost.FileIds)) require.Equal(t, fileInfo2.Id, patchedPost.FileIds[0]) }) t.Run("should be able to remove all files", func(t *testing.T) { fileResponse1, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse1.FileInfos)) fileInfo1 := fileResponse1.FileInfos[0] fileResponse2, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse2.FileInfos)) fileInfo2 := fileResponse2.FileInfos[0] post, _, err := client.CreatePost(context.Background(), &model.Post{ ChannelId: channel.Id, Message: "#hashtag a message", CreateAt: model.GetMillis() - 2000, FileIds: model.StringArray{fileInfo1.Id, fileInfo2.Id}, }) require.NoError(t, err) require.Equal(t, 2, len(post.FileIds)) patch := &model.PostPatch{ FileIds: &model.StringArray{}, } _, _, err = client.PatchPost(context.Background(), post.Id, patch) require.NoError(t, err) patchedPost, _, err := client.GetPost(context.Background(), post.Id, "") require.NoError(t, err) require.Equal(t, 0, len(patchedPost.FileIds)) }) t.Run("post files remain unchanged when fileIds is nil", func(t *testing.T) { fileResponse1, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse1.FileInfos)) fileInfo1 := fileResponse1.FileInfos[0] fileResponse2, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png") require.NoError(t, err) require.Equal(t, 1, len(fileResponse2.FileInfos)) fileInfo2 := fileResponse2.FileInfos[0] post, _, err := client.CreatePost(context.Background(), &model.Post{ ChannelId: channel.Id, Message: "#hashtag a message", CreateAt: model.GetMillis() - 2000, FileIds: model.StringArray{fileInfo1.Id, fileInfo2.Id}, }) require.NoError(t, err) require.Equal(t, 2, len(post.FileIds)) patch := &model.PostPatch{ FileIds: nil, } _, _, err = client.PatchPost(context.Background(), post.Id, patch) require.NoError(t, err) patchedPost, _, err := client.GetPost(context.Background(), post.Id, "") require.NoError(t, err) require.Equal(t, 2, len(patchedPost.FileIds)) require.Contains(t, patchedPost.FileIds, fileInfo1.Id) require.Contains(t, patchedPost.FileIds, fileInfo2.Id) }) } func TestPinPost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post := th.BasicPost _, err := client.PinPost(context.Background(), post.Id) require.NoError(t, err) rpost, appErr := th.App.GetSinglePost(th.Context, post.Id, false) require.Nil(t, appErr) require.True(t, rpost.IsPinned, "failed to pin post") resp, err := client.PinPost(context.Background(), "junk") require.Error(t, err) CheckBadRequestStatus(t, resp) resp, err = client.PinPost(context.Background(), GenerateTestID()) require.Error(t, err) CheckForbiddenStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) resp, err = client.PinPost(context.Background(), post.Id) require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, err = th.SystemAdminClient.PinPost(context.Background(), post.Id) require.NoError(t, err) } func TestUnpinPost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client pinnedPost := th.CreatePinnedPost() _, err := client.UnpinPost(context.Background(), pinnedPost.Id) require.NoError(t, err) rpost, appErr := th.App.GetSinglePost(th.Context, pinnedPost.Id, false) require.Nil(t, appErr) require.False(t, rpost.IsPinned) resp, err := client.UnpinPost(context.Background(), "junk") require.Error(t, err) CheckBadRequestStatus(t, resp) resp, err = client.UnpinPost(context.Background(), GenerateTestID()) require.Error(t, err) CheckForbiddenStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) resp, err = client.UnpinPost(context.Background(), pinnedPost.Id) require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, err = th.SystemAdminClient.UnpinPost(context.Background(), pinnedPost.Id) require.NoError(t, err) } func TestGetPostsForChannel(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post1 := th.CreatePost() post2 := th.CreatePost() post3 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a", RootId: post1.Id} post3, _, _ = client.CreatePost(context.Background(), post3) time.Sleep(300 * time.Millisecond) since := model.GetMillis() time.Sleep(300 * time.Millisecond) post4 := th.CreatePost() th.TestForAllClients(t, func(t *testing.T, c *model.Client4) { posts, resp, err := c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 60, "", false, false) require.NoError(t, err) require.Equal(t, post4.Id, posts.Order[0], "wrong order") require.Equal(t, post3.Id, posts.Order[1], "wrong order") require.Equal(t, post2.Id, posts.Order[2], "wrong order") require.Equal(t, post1.Id, posts.Order[3], "wrong order") require.Nil(t, posts.HasNext, "HasNext should not be returned") posts, resp, _ = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 3, resp.Etag, false, false) CheckEtag(t, posts, resp) posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "wrong number returned") _, ok := posts.Posts[post3.Id] require.True(t, ok, "missing comment") _, ok = posts.Posts[post1.Id] require.True(t, ok, "missing root post") posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 1, 1, "", false, false) require.NoError(t, err) require.Equal(t, post3.Id, posts.Order[0], "wrong order") posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 10000, 10000, "", false, false) require.NoError(t, err) require.Empty(t, posts.Order, "should be no posts") }) post5 := th.CreatePost() th.TestForAllClients(t, func(t *testing.T, c *model.Client4) { posts, _, err := c.GetPostsSince(context.Background(), th.BasicChannel.Id, since, false) require.NoError(t, err) require.Len(t, posts.Posts, 2, "should return 2 posts") // "since" query to return empty NextPostId and PrevPostId require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") found := make([]bool, 2) for _, p := range posts.Posts { require.LessOrEqual(t, since, p.CreateAt, "bad create at for post returned") if p.Id == post4.Id { found[0] = true } else if p.Id == post5.Id { found[1] = true } } for _, f := range found { require.True(t, f, "missing post") } _, resp, err := c.GetPostsForChannel(context.Background(), "", 0, 60, "", false, false) require.Error(t, err) CheckBadRequestStatus(t, resp) _, resp, err = c.GetPostsForChannel(context.Background(), "junk", 0, 60, "", false, false) require.Error(t, err) CheckBadRequestStatus(t, resp) }) _, resp, err := client.GetPostsForChannel(context.Background(), model.NewId(), 0, 60, "", false, false) require.Error(t, err) CheckNotFoundStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) _, resp, err = client.GetPostsForChannel(context.Background(), model.NewId(), 0, 60, "", false, false) require.Error(t, err) CheckUnauthorizedStatus(t, resp) // more tests for next_post_id, prev_post_id, and order // There are 12 posts composed of first 2 system messages and 10 created posts _, _, err = client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password) require.NoError(t, err) th.CreatePost() // post6 post7 := th.CreatePost() post8 := th.CreatePost() th.CreatePost() // post9 post10 := th.CreatePost() var posts *model.PostList th.TestForAllClients(t, func(t *testing.T, c *model.Client4) { // get the system post IDs posted before the created posts above posts, _, err = c.GetPostsBefore(context.Background(), th.BasicChannel.Id, post1.Id, 0, 2, "", false, false) require.NoError(t, err) systemPostId1 := posts.Order[1] // similar to '/posts' posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 60, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 12, "expected 12 posts") require.Equal(t, post10.Id, posts.Order[0], "posts not in order") require.Equal(t, systemPostId1, posts.Order[11], "posts not in order") require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") // similar to '/posts?per_page=3' posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "expected 3 posts") require.Equal(t, post10.Id, posts.Order[0], "posts not in order") require.Equal(t, post8.Id, posts.Order[2], "should return 3 posts and match order") require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, post7.Id, posts.PrevPostId, "should return post7.Id as PrevPostId") // similar to '/posts?per_page=3&page=1' posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 1, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "expected 3 posts") require.Equal(t, post7.Id, posts.Order[0], "posts not in order") require.Equal(t, post5.Id, posts.Order[2], "posts not in order") require.Equal(t, post8.Id, posts.NextPostId, "should return post8.Id as NextPostId") require.Equal(t, post4.Id, posts.PrevPostId, "should return post4.Id as PrevPostId") // similar to '/posts?per_page=3&page=2' posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 2, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "expected 3 posts") require.Equal(t, post4.Id, posts.Order[0], "posts not in order") require.Equal(t, post2.Id, posts.Order[2], "should return 3 posts and match order") require.Equal(t, post5.Id, posts.NextPostId, "should return post5.Id as NextPostId") require.Equal(t, post1.Id, posts.PrevPostId, "should return post1.Id as PrevPostId") // similar to '/posts?per_page=3&page=3' posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 3, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "expected 3 posts") require.Equal(t, post1.Id, posts.Order[0], "posts not in order") require.Equal(t, systemPostId1, posts.Order[2], "should return 3 posts and match order") require.Equal(t, post2.Id, posts.NextPostId, "should return post2.Id as NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") // similar to '/posts?per_page=3&page=4' posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 4, 3, "", false, false) require.NoError(t, err) require.Empty(t, posts.Order, "should return 0 post") require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") }) th.TestForAllClients(t, func(t *testing.T, c *model.Client4) { channel := th.CreatePublicChannel() th.CreatePostWithClient(th.SystemAdminClient, channel) _, err = th.SystemAdminClient.DeleteChannel(context.Background(), channel.Id) require.NoError(t, err) _, _, err = c.GetPostsForChannel(context.Background(), channel.Id, 0, 10, "", false, false) require.NoError(t, err) }, "Should allow retrieving posts if the channel is archived") _, err = client.DeletePost(context.Background(), post10.Id) require.NoError(t, err) _, err = client.DeletePost(context.Background(), post8.Id) require.NoError(t, err) // include deleted posts for non-admin users. _, resp, err = client.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", false, true) require.Error(t, err) CheckForbiddenStatus(t, resp) th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) { // include deleted posts for admin users. posts, resp, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", false, true) require.NoError(t, err) CheckOKStatus(t, resp) require.Len(t, posts.Order, 12, "expected 12 posts") // not include deleted posts for admin users. posts, resp, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", false, false) require.NoError(t, err) CheckOKStatus(t, resp) require.Len(t, posts.Order, 10, "expected 10 posts") // System admin can access public channel without being member adminPublicChannel := th.CreatePublicChannel() th.CreateMessagePostNoClient(adminPublicChannel, "admin channel post", model.GetMillis()) posts, resp, err = c.GetPostsForChannel(context.Background(), adminPublicChannel.Id, 0, 100, "", false, false) require.NoError(t, err) CheckOKStatus(t, resp) require.NotEmpty(t, posts.Order) // System admin can access private channel without being member privateChannel := th.CreatePrivateChannel() th.CreateMessagePostNoClient(privateChannel, "private channel post", model.GetMillis()) posts, resp, err = c.GetPostsForChannel(context.Background(), privateChannel.Id, 0, 100, "", false, false) require.NoError(t, err) CheckOKStatus(t, resp) require.NotEmpty(t, posts.Order) // System admin can access direct messages without being member dmChannel := th.CreateDmChannel(th.BasicUser2) th.CreateMessagePostNoClient(dmChannel, "test1", model.GetMillis()) posts, resp, err = c.GetPostsForChannel(context.Background(), dmChannel.Id, 0, 100, "", false, false) require.NoError(t, err) CheckOKStatus(t, resp) require.NotEmpty(t, posts.Order) // System admin can access group messages without being member user3 := th.CreateUser() gmChannel, _, err := th.Client.CreateGroupChannel(context.Background(), []string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id}) require.NoError(t, err) th.CreateMessagePostNoClient(gmChannel, "test2", model.GetMillis()) posts, resp, err = c.GetPostsForChannel(context.Background(), gmChannel.Id, 0, 100, "", false, false) require.NoError(t, err) CheckOKStatus(t, resp) require.NotEmpty(t, posts.Order) }) } func TestGetFlaggedPostsForUser(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client user := th.BasicUser team1 := th.BasicTeam channel1 := th.BasicChannel post1 := th.CreatePost() channel2 := th.CreatePublicChannel() post2 := th.CreatePostWithClient(client, channel2) preference := model.Preference{ UserId: user.Id, Category: model.PreferenceCategoryFlaggedPost, Name: post1.Id, Value: "true", } _, err := client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference}) require.NoError(t, err) preference.Name = post2.Id _, err = client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference}) require.NoError(t, err) opl := model.NewPostList() opl.AddPost(post1) opl.AddOrder(post1.Id) rpl, _, err := client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 0, 10) require.NoError(t, err) require.Len(t, rpl.Posts, 1, "should have returned 1 post") require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched") rpl, _, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 0, 1) require.NoError(t, err) require.Len(t, rpl.Posts, 1, "should have returned 1 post") rpl, _, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 1, 1) require.NoError(t, err) require.Empty(t, rpl.Posts) rpl, _, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, GenerateTestID(), 0, 10) require.NoError(t, err) require.Empty(t, rpl.Posts) rpl, _, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, "junk", 0, 10) require.Error(t, err) require.Nil(t, rpl) opl.AddPost(post2) opl.AddOrder(post2.Id) rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 0, 10) require.NoError(t, err) require.Len(t, rpl.Posts, 2, "should have returned 2 posts") require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched") rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 0, 1) require.NoError(t, err) require.Len(t, rpl.Posts, 1, "should have returned 1 post") rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 1, 1) require.NoError(t, err) require.Len(t, rpl.Posts, 1, "should have returned 1 post") rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 1000, 10) require.NoError(t, err) require.Empty(t, rpl.Posts) rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, GenerateTestID(), 0, 10) require.NoError(t, err) require.Empty(t, rpl.Posts) rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, "junk", 0, 10) require.Error(t, err) require.Nil(t, rpl) channel3 := th.CreatePrivateChannel() post4 := th.CreatePostWithClient(client, channel3) preference.Name = post4.Id _, err = client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference}) require.NoError(t, err) opl.AddPost(post4) opl.AddOrder(post4.Id) rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10) require.NoError(t, err) require.Len(t, rpl.Posts, 3, "should have returned 3 posts") require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched") rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 2) require.NoError(t, err) require.Len(t, rpl.Posts, 2, "should have returned 2 posts") rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 2, 2) require.NoError(t, err) require.Len(t, rpl.Posts, 1, "should have returned 1 post") rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 1000, 10) require.NoError(t, err) require.Empty(t, rpl.Posts) channel4 := th.CreateChannelWithClient(th.SystemAdminClient, model.ChannelTypePrivate) post5 := th.CreatePostWithClient(th.SystemAdminClient, channel4) preference.Name = post5.Id resp, err := client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference}) require.Error(t, err) CheckForbiddenStatus(t, resp) rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10) require.NoError(t, err) require.Len(t, rpl.Posts, 3, "should have returned 3 posts") require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched") th.AddUserToChannel(user, channel4) _, err = client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference}) require.NoError(t, err) rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10) require.NoError(t, err) opl.AddPost(post5) opl.AddOrder(post5.Id) require.Len(t, rpl.Posts, 4, "should have returned 4 posts") require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched") appErr := th.App.RemoveUserFromChannel(th.Context, user.Id, "", channel4) assert.Nil(t, appErr, "unable to remove user from channel") rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10) require.NoError(t, err) opl2 := model.NewPostList() opl2.AddPost(post1) opl2.AddOrder(post1.Id) opl2.AddPost(post2) opl2.AddOrder(post2.Id) opl2.AddPost(post4) opl2.AddOrder(post4.Id) require.Len(t, rpl.Posts, 3, "should have returned 3 posts") require.Equal(t, opl2.Posts, rpl.Posts, "posts should have matched") _, resp, err = client.GetFlaggedPostsForUser(context.Background(), "junk", 0, 10) require.Error(t, err) CheckBadRequestStatus(t, resp) _, resp, err = client.GetFlaggedPostsForUser(context.Background(), GenerateTestID(), 0, 10) require.Error(t, err) CheckForbiddenStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) _, resp, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 0, 10) require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, resp, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 0, 10) require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, resp, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10) require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, _, err = th.SystemAdminClient.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 0, 10) require.NoError(t, err) _, _, err = th.SystemAdminClient.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 0, 10) require.NoError(t, err) _, _, err = th.SystemAdminClient.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10) require.NoError(t, err) mockStore := mocks.Store{} mockPostStore := mocks.PostStore{} mockPostStore.On("GetFlaggedPosts", mock.AnythingOfType("string"), mock.AnythingOfType("int"), mock.AnythingOfType("int")).Return(nil, errors.New("some-error")) mockPostStore.On("ClearCaches").Return() mockStore.On("Team").Return(th.App.Srv().Store().Team()) mockStore.On("Channel").Return(th.App.Srv().Store().Channel()) mockStore.On("User").Return(th.App.Srv().Store().User()) mockStore.On("Scheme").Return(th.App.Srv().Store().Scheme()) mockStore.On("Post").Return(&mockPostStore) mockStore.On("FileInfo").Return(th.App.Srv().Store().FileInfo()) mockStore.On("Webhook").Return(th.App.Srv().Store().Webhook()) mockStore.On("System").Return(th.App.Srv().Store().System()) mockStore.On("License").Return(th.App.Srv().Store().License()) mockStore.On("Role").Return(th.App.Srv().Store().Role()) mockStore.On("Close").Return(nil) // Playbooks DB job requires a plugin mock pluginStore := mocks.PluginStore{} pluginStore.On("List", mock.Anything, mock.Anything, mock.Anything).Return([]string{}, nil) mockStore.On("Plugin").Return(&pluginStore) th.App.Srv().SetStore(&mockStore) _, resp, err = th.SystemAdminClient.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10) require.Error(t, err) CheckInternalErrorStatus(t, resp) } func TestGetPostsBefore(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post1 := th.CreatePost() post2 := th.CreatePost() post3 := th.CreatePost() post4 := th.CreatePost() post5 := th.CreatePost() posts, _, err := client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post3.Id, 0, 100, "", false, false) require.NoError(t, err) found := make([]bool, 2) for _, p := range posts.Posts { if p.Id == post1.Id { found[0] = true } else if p.Id == post2.Id { found[1] = true } require.NotEqual(t, post4.Id, p.Id, "returned posts after") require.NotEqual(t, post5.Id, p.Id, "returned posts after") } for _, f := range found { require.True(t, f, "missing post") } require.Equal(t, post3.Id, posts.NextPostId, "should match NextPostId") require.Equal(t, "", posts.PrevPostId, "should match empty PrevPostId") posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post4.Id, 1, 1, "", false, false) require.NoError(t, err) require.Len(t, posts.Posts, 1, "too many posts returned") require.Equal(t, post2.Id, posts.Order[0], "should match returned post") require.Equal(t, post3.Id, posts.NextPostId, "should match NextPostId") require.Equal(t, post1.Id, posts.PrevPostId, "should match PrevPostId") _, resp, err := client.GetPostsBefore(context.Background(), th.BasicChannel.Id, "junk", 1, 1, "", false, false) require.Error(t, err) CheckBadRequestStatus(t, resp) posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post5.Id, 0, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Posts, 3, "should match length of posts returned") require.Equal(t, post4.Id, posts.Order[0], "should match returned post") require.Equal(t, post2.Id, posts.Order[2], "should match returned post") require.Equal(t, post5.Id, posts.NextPostId, "should match NextPostId") require.Equal(t, post1.Id, posts.PrevPostId, "should match PrevPostId") // get the system post IDs posted before the created posts above posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post1.Id, 0, 2, "", false, false) require.NoError(t, err) systemPostId2 := posts.Order[0] systemPostId1 := posts.Order[1] posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post5.Id, 1, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Posts, 3, "should match length of posts returned") require.Equal(t, post1.Id, posts.Order[0], "should match returned post") require.Equal(t, systemPostId2, posts.Order[1], "should match returned post") require.Equal(t, systemPostId1, posts.Order[2], "should match returned post") require.Equal(t, post2.Id, posts.NextPostId, "should match NextPostId") require.Equal(t, "", posts.PrevPostId, "should return empty PrevPostId") // more tests for next_post_id, prev_post_id, and order // There are 12 posts composed of first 2 system messages and 10 created posts post6 := th.CreatePost() th.CreatePost() // post7 post8 := th.CreatePost() post9 := th.CreatePost() post10 := th.CreatePost() // post10 // similar to '/posts?before=post9' posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 0, 60, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 10, "expected 10 posts") require.Equal(t, post8.Id, posts.Order[0], "posts not in order") require.Equal(t, systemPostId1, posts.Order[9], "posts not in order") require.Equal(t, post9.Id, posts.NextPostId, "should return post9.Id as NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") // similar to '/posts?before=post9&per_page=3' posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 0, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "expected 3 posts") require.Equal(t, post8.Id, posts.Order[0], "posts not in order") require.Equal(t, post6.Id, posts.Order[2], "should return 3 posts and match order") require.Equal(t, post9.Id, posts.NextPostId, "should return post9.Id as NextPostId") require.Equal(t, post5.Id, posts.PrevPostId, "should return post5.Id as PrevPostId") // similar to '/posts?before=post9&per_page=3&page=1' posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 1, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "expected 3 posts") require.Equal(t, post5.Id, posts.Order[0], "posts not in order") require.Equal(t, post3.Id, posts.Order[2], "posts not in order") require.Equal(t, post6.Id, posts.NextPostId, "should return post6.Id as NextPostId") require.Equal(t, post2.Id, posts.PrevPostId, "should return post2.Id as PrevPostId") // similar to '/posts?before=post9&per_page=3&page=2' posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 2, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "expected 3 posts") require.Equal(t, post2.Id, posts.Order[0], "posts not in order") require.Equal(t, systemPostId2, posts.Order[2], "posts not in order") require.Equal(t, post3.Id, posts.NextPostId, "should return post3.Id as NextPostId") require.Equal(t, systemPostId1, posts.PrevPostId, "should return systemPostId1 as PrevPostId") // similar to '/posts?before=post1&per_page=3' posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post1.Id, 0, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 2, "expected 2 posts") require.Equal(t, systemPostId2, posts.Order[0], "posts not in order") require.Equal(t, systemPostId1, posts.Order[1], "posts not in order") require.Equal(t, post1.Id, posts.NextPostId, "should return post1.Id as NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") // similar to '/posts?before=systemPostId1' posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, systemPostId1, 0, 60, "", false, false) require.NoError(t, err) require.Empty(t, posts.Order, "should return 0 post") require.Equal(t, systemPostId1, posts.NextPostId, "should return systemPostId1 as NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") // similar to '/posts?before=systemPostId1&per_page=60&page=1' posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, systemPostId1, 1, 60, "", false, false) require.NoError(t, err) require.Empty(t, posts.Order, "should return 0 posts") require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") // similar to '/posts?before=non-existent-post' nonExistentPostId := model.NewId() posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, nonExistentPostId, 0, 60, "", false, false) require.NoError(t, err) require.Empty(t, posts.Order, "should return 0 post") require.Equal(t, nonExistentPostId, posts.NextPostId, "should return nonExistentPostId as NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") _, err = client.DeletePost(context.Background(), post9.Id) require.NoError(t, err) _, err = client.DeletePost(context.Background(), post8.Id) require.NoError(t, err) // include deleted posts for non-admin users. _, resp, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 0, 60, "", false, true) require.Error(t, err) CheckForbiddenStatus(t, resp) th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) { // include deleted posts for admin users. posts, resp, err = c.GetPostsBefore(context.Background(), th.BasicChannel.Id, post10.Id, 0, 60, "", false, true) require.NoError(t, err) CheckOKStatus(t, resp) require.Len(t, posts.Order, 11, "expected 11 posts") // not include deleted posts for admin users. posts, resp, err = c.GetPostsBefore(context.Background(), th.BasicChannel.Id, post10.Id, 0, 60, "", false, false) require.NoError(t, err) CheckOKStatus(t, resp) require.Len(t, posts.Order, 9, "expected 9 posts") }) } func TestGetPostsAfter(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post1 := th.CreatePost() post2 := th.CreatePost() post3 := th.CreatePost() post4 := th.CreatePost() post5 := th.CreatePost() posts, _, err := client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post3.Id, 0, 100, "", false, false) require.NoError(t, err) found := make([]bool, 2) for _, p := range posts.Posts { if p.Id == post4.Id { found[0] = true } else if p.Id == post5.Id { found[1] = true } require.NotEqual(t, post1.Id, p.Id, "returned posts before") require.NotEqual(t, post2.Id, p.Id, "returned posts before") } for _, f := range found { require.True(t, f, "missing post") } require.Equal(t, "", posts.NextPostId, "should match empty NextPostId") require.Equal(t, post3.Id, posts.PrevPostId, "should match PrevPostId") posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 1, 1, "", false, false) require.NoError(t, err) require.Len(t, posts.Posts, 1, "too many posts returned") require.Equal(t, post4.Id, posts.Order[0], "should match returned post") require.Equal(t, post5.Id, posts.NextPostId, "should match NextPostId") require.Equal(t, post3.Id, posts.PrevPostId, "should match PrevPostId") _, resp, err := client.GetPostsAfter(context.Background(), th.BasicChannel.Id, "junk", 1, 1, "", false, false) require.Error(t, err) CheckBadRequestStatus(t, resp) posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 0, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Posts, 3, "should match length of posts returned") require.Equal(t, post4.Id, posts.Order[0], "should match returned post") require.Equal(t, post2.Id, posts.Order[2], "should match returned post") require.Equal(t, post5.Id, posts.NextPostId, "should match NextPostId") require.Equal(t, post1.Id, posts.PrevPostId, "should match PrevPostId") posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 1, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Posts, 1, "should match length of posts returned") require.Equal(t, post5.Id, posts.Order[0], "should match returned post") require.Equal(t, "", posts.NextPostId, "should match NextPostId") require.Equal(t, post4.Id, posts.PrevPostId, "should match PrevPostId") // more tests for next_post_id, prev_post_id, and order // There are 12 posts composed of first 2 system messages and 10 created posts post6 := th.CreatePost() th.CreatePost() // post7 post8 := th.CreatePost() post9 := th.CreatePost() post10 := th.CreatePost() // similar to '/posts?after=post2' posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 0, 60, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 8, "expected 8 posts") require.Equal(t, post10.Id, posts.Order[0], "should match order") require.Equal(t, post3.Id, posts.Order[7], "should match order") require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, post2.Id, posts.PrevPostId, "should return post2.Id as PrevPostId") // similar to '/posts?after=post2&per_page=3' posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 0, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "expected 3 posts") require.Equal(t, post5.Id, posts.Order[0], "should match order") require.Equal(t, post3.Id, posts.Order[2], "should return 3 posts and match order") require.Equal(t, post6.Id, posts.NextPostId, "should return post6.Id as NextPostId") require.Equal(t, post2.Id, posts.PrevPostId, "should return post2.Id as PrevPostId") // similar to '/posts?after=post2&per_page=3&page=1' posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 1, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 3, "expected 3 posts") require.Equal(t, post8.Id, posts.Order[0], "should match order") require.Equal(t, post6.Id, posts.Order[2], "should match order") require.Equal(t, post9.Id, posts.NextPostId, "should return post9.Id as NextPostId") require.Equal(t, post5.Id, posts.PrevPostId, "should return post5.Id as PrevPostId") // similar to '/posts?after=post2&per_page=3&page=2' posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 2, 3, "", false, false) require.NoError(t, err) require.Len(t, posts.Order, 2, "expected 2 posts") require.Equal(t, post10.Id, posts.Order[0], "should match order") require.Equal(t, post9.Id, posts.Order[1], "should match order") require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, post8.Id, posts.PrevPostId, "should return post8.Id as PrevPostId") // similar to '/posts?after=post10' posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post10.Id, 0, 60, "", false, false) require.NoError(t, err) require.Empty(t, posts.Order, "should return 0 post") require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, post10.Id, posts.PrevPostId, "should return post10.Id as PrevPostId") // similar to '/posts?after=post10&page=1' posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post10.Id, 1, 60, "", false, false) require.NoError(t, err) require.Empty(t, posts.Order, "should return 0 post") require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId") // similar to '/posts?after=non-existent-post' nonExistentPostId := model.NewId() posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, nonExistentPostId, 0, 60, "", false, false) require.NoError(t, err) require.Empty(t, posts.Order, "should return 0 post") require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId") require.Equal(t, nonExistentPostId, posts.PrevPostId, "should return nonExistentPostId as PrevPostId") _, err = client.DeletePost(context.Background(), post10.Id) require.NoError(t, err) _, err = client.DeletePost(context.Background(), post9.Id) require.NoError(t, err) // include deleted posts for non-admin users. _, resp, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 0, 60, "", false, true) require.Error(t, err) CheckForbiddenStatus(t, resp) th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) { // include deleted posts for admin users. posts, resp, err = c.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 0, 60, "", false, true) require.NoError(t, err) CheckOKStatus(t, resp) require.Len(t, posts.Order, 9, "expected 9 posts") // not include deleted posts for admin users. posts, resp, err = c.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 0, 60, "", false, false) require.NoError(t, err) CheckOKStatus(t, resp) require.Len(t, posts.Order, 7, "expected 7 posts") }) } func TestGetPostsForChannelAroundLastUnread(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client userId := th.BasicUser.Id channelId := th.BasicChannel.Id // 12 posts = 2 systems posts + 10 created posts below post1 := th.CreatePost() post2 := th.CreatePost() post3 := th.CreatePost() post4 := th.CreatePost() post5 := th.CreatePost() replyPost := &model.Post{ChannelId: channelId, Message: model.NewId(), RootId: post4.Id} post6, _, err := client.CreatePost(context.Background(), replyPost) require.NoError(t, err) post7, _, err := client.CreatePost(context.Background(), replyPost) require.NoError(t, err) post8, _, err := client.CreatePost(context.Background(), replyPost) require.NoError(t, err) post9, _, err := client.CreatePost(context.Background(), replyPost) require.NoError(t, err) post10, _, err := client.CreatePost(context.Background(), replyPost) require.NoError(t, err) postIdNames := map[string]string{ post1.Id: "post1", post2.Id: "post2", post3.Id: "post3", post4.Id: "post4", post5.Id: "post5", post6.Id: "post6 (reply to post4)", post7.Id: "post7 (reply to post4)", post8.Id: "post8 (reply to post4)", post9.Id: "post9 (reply to post4)", post10.Id: "post10 (reply to post4)", } namePost := func(postId string) string { name, ok := postIdNames[postId] if ok { return name } return fmt.Sprintf("unknown (%s)", postId) } namePosts := func(postIds []string) []string { namedPostIds := make([]string, 0, len(postIds)) for _, postId := range postIds { namedPostIds = append(namedPostIds, namePost(postId)) } return namedPostIds } namePostsMap := func(posts map[string]*model.Post) []string { namedPostIds := make([]string, 0, len(posts)) for postId := range posts { namedPostIds = append(namedPostIds, namePost(postId)) } sort.Strings(namedPostIds) return namedPostIds } assertPostList := func(t *testing.T, expected, actual *model.PostList) { t.Helper() require.Equal(t, namePosts(expected.Order), namePosts(actual.Order), "unexpected post order") require.Equal(t, namePostsMap(expected.Posts), namePostsMap(actual.Posts), "unexpected posts") require.Equal(t, namePost(expected.NextPostId), namePost(actual.NextPostId), "unexpected next post id") require.Equal(t, namePost(expected.PrevPostId), namePost(actual.PrevPostId), "unexpected prev post id") } // Setting limit_after to zero should fail with a 400 BadRequest. posts, resp, err := client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 20, 0, false) require.Error(t, err) CheckErrorID(t, err, "api.context.invalid_url_param.app_error") require.Equal(t, http.StatusBadRequest, resp.StatusCode) require.Nil(t, posts) // All returned posts are all read by the user, since it's created by the user itself. posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 20, 20, false) require.NoError(t, err) require.Len(t, posts.Order, 12, "Should return 12 posts only since there's no unread post") // Set channel member's last viewed to 0. // All returned posts are latest posts as if all previous posts were already read by the user. channelMember, err := th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId) require.NoError(t, err) channelMember.LastViewedAt = 0 _, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember) require.NoError(t, err) th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId) posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 20, 20, false) require.NoError(t, err) require.Len(t, posts.Order, 12, "Should return 12 posts only since there's no unread post") // get the first system post generated before the created posts above posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post1.Id, 0, 2, "", false, false) require.NoError(t, err) systemPost0 := posts.Posts[posts.Order[0]] postIdNames[systemPost0.Id] = "system post 0" systemPost1 := posts.Posts[posts.Order[1]] postIdNames[systemPost1.Id] = "system post 1" // Set channel member's last viewed before post1. channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId) require.NoError(t, err) channelMember.LastViewedAt = post1.CreateAt - 1 _, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember) require.NoError(t, err) th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId) posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 3, 3, false) require.NoError(t, err) assertPostList(t, &model.PostList{ Order: []string{post3.Id, post2.Id, post1.Id, systemPost0.Id, systemPost1.Id}, Posts: map[string]*model.Post{ systemPost0.Id: systemPost0, systemPost1.Id: systemPost1, post1.Id: post1, post2.Id: post2, post3.Id: post3, }, NextPostId: post4.Id, PrevPostId: "", }, posts) // Set channel member's last viewed before post6. channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId) require.NoError(t, err) channelMember.LastViewedAt = post6.CreateAt - 1 _, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember) require.NoError(t, err) th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId) posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 3, 3, false) require.NoError(t, err) assertPostList(t, &model.PostList{ Order: []string{post8.Id, post7.Id, post6.Id, post5.Id, post4.Id, post3.Id}, Posts: map[string]*model.Post{ post3.Id: post3, post4.Id: post4, post5.Id: post5, post6.Id: post6, post7.Id: post7, post8.Id: post8, post9.Id: post9, post10.Id: post10, }, NextPostId: post9.Id, PrevPostId: post2.Id, }, posts) // Set channel member's last viewed before post10. channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId) require.NoError(t, err) channelMember.LastViewedAt = post10.CreateAt - 1 _, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember) require.NoError(t, err) th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId) posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 3, 3, false) require.NoError(t, err) assertPostList(t, &model.PostList{ Order: []string{post10.Id, post9.Id, post8.Id, post7.Id}, Posts: map[string]*model.Post{ post4.Id: post4, post6.Id: post6, post7.Id: post7, post8.Id: post8, post9.Id: post9, post10.Id: post10, }, NextPostId: "", PrevPostId: post6.Id, }, posts) // Set channel member's last viewed equal to post10. channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId) require.NoError(t, err) channelMember.LastViewedAt = post10.CreateAt _, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember) require.NoError(t, err) th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId) posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 3, 3, false) require.NoError(t, err) assertPostList(t, &model.PostList{ Order: []string{post10.Id, post9.Id, post8.Id}, Posts: map[string]*model.Post{ post4.Id: post4, post6.Id: post6, post7.Id: post7, post8.Id: post8, post9.Id: post9, post10.Id: post10, }, NextPostId: "", PrevPostId: post7.Id, }, posts) // Set channel member's last viewed to just before a new reply to a previous thread, not // otherwise in the requested window. post11 := th.CreatePost() post12, _, err := client.CreatePost(context.Background(), &model.Post{ ChannelId: channelId, Message: model.NewId(), RootId: post4.Id, }) require.NoError(t, err) post13 := th.CreatePost() postIdNames[post11.Id] = "post11" postIdNames[post12.Id] = "post12 (reply to post4)" postIdNames[post13.Id] = "post13" channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId) require.NoError(t, err) channelMember.LastViewedAt = post12.CreateAt - 1 _, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember) require.NoError(t, err) th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId) posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 1, 2, false) require.NoError(t, err) assertPostList(t, &model.PostList{ Order: []string{post13.Id, post12.Id, post11.Id}, Posts: map[string]*model.Post{ post4.Id: post4, post6.Id: post6, post7.Id: post7, post8.Id: post8, post9.Id: post9, post10.Id: post10, post11.Id: post11, post12.Id: post12, post13.Id: post13, }, NextPostId: "", PrevPostId: post10.Id, }, posts) } func TestGetPost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() // TODO: migrate this entirely to the subtest's client // once the other methods are migrated too. client := th.Client var privatePost *model.Post th.TestForAllClients(t, func(t *testing.T, c *model.Client4) { t.Helper() post, resp, err := c.GetPost(context.Background(), th.BasicPost.Id, "") require.NoError(t, err) require.Equal(t, th.BasicPost.Id, post.Id, "post ids don't match") post, resp, err = c.GetPost(context.Background(), th.BasicPost.Id, resp.Etag) require.NoError(t, err) CheckEtag(t, post, resp) _, resp, err = c.GetPost(context.Background(), "", "") require.Error(t, err) CheckNotFoundStatus(t, resp) _, resp, err = c.GetPost(context.Background(), "junk", "") require.Error(t, err) CheckBadRequestStatus(t, resp) _, resp, err = c.GetPost(context.Background(), model.NewId(), "") require.Error(t, err) CheckNotFoundStatus(t, resp) _, err = client.RemoveUserFromChannel(context.Background(), th.BasicChannel.Id, th.BasicUser.Id) require.NoError(t, err) t.Cleanup(func() { // Add the user back to the channel _, _, err = client.AddChannelMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id) require.NoError(t, err) }) // Channel is public, should be able to read post _, _, err = c.GetPost(context.Background(), th.BasicPost.Id, "") require.NoError(t, err) privatePost = th.CreatePostWithClient(client, th.BasicPrivateChannel) _, _, err = c.GetPost(context.Background(), privatePost.Id, "") require.NoError(t, err) }) _, err := client.RemoveUserFromChannel(context.Background(), th.BasicPrivateChannel.Id, th.BasicUser.Id) require.NoError(t, err) // Channel is private, should not be able to read post _, resp, err := client.GetPost(context.Background(), privatePost.Id, "") require.Error(t, err) CheckForbiddenStatus(t, resp) // But local client should. _, _, err = th.LocalClient.GetPost(context.Background(), privatePost.Id, "") require.NoError(t, err) // Delete post _, err = th.SystemAdminClient.DeletePost(context.Background(), th.BasicPost.Id) require.NoError(t, err) // Normal client should get 404 when trying to access deleted post normally _, resp, err = client.GetPost(context.Background(), th.BasicPost.Id, "") require.Error(t, err) CheckNotFoundStatus(t, resp) // Normal client should get unauthorized when trying to access deleted post _, resp, err = client.GetPostIncludeDeleted(context.Background(), th.BasicPost.Id, "") require.Error(t, err) CheckForbiddenStatus(t, resp) // System client should get 404 when trying to access deleted post normally _, resp, err = th.SystemAdminClient.GetPost(context.Background(), th.BasicPost.Id, "") require.Error(t, err) CheckNotFoundStatus(t, resp) // System client should be able to access deleted post with include_deleted param post, _, err := th.SystemAdminClient.GetPostIncludeDeleted(context.Background(), th.BasicPost.Id, "") require.NoError(t, err) require.Equal(t, th.BasicPost.Id, post.Id) _, err = client.Logout(context.Background()) require.NoError(t, err) // Normal client should get unauthorized, but local client should get 404. _, resp, err = client.GetPost(context.Background(), model.NewId(), "") require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, resp, err = th.LocalClient.GetPost(context.Background(), model.NewId(), "") require.Error(t, err) CheckNotFoundStatus(t, resp) } func TestDeletePost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client t.Run("Post not found", func(t *testing.T) { resp, err := client.DeletePost(context.Background(), "") require.Error(t, err) CheckNotFoundStatus(t, resp) }) t.Run("Post doesn't exist", func(t *testing.T) { resp, err := client.DeletePost(context.Background(), "junk") require.Error(t, err) CheckBadRequestStatus(t, resp) }) t.Run("No permissions to delete a post", func(t *testing.T) { resp, err := client.DeletePost(context.Background(), th.BasicPost.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) }) t.Run("Try to delete a post across different user roles", func(t *testing.T) { _, _, err := client.Login(context.Background(), th.TeamAdminUser.Email, th.TeamAdminUser.Password) require.NoError(t, err) _, cErr := client.DeletePost(context.Background(), th.BasicPost.Id) require.NoError(t, cErr) post := th.CreatePost() post2 := th.CreatePost() user := th.CreateUser() _, err = client.Logout(context.Background()) require.NoError(t, err) _, _, err = client.Login(context.Background(), user.Email, user.Password) require.NoError(t, err) resp, err := client.DeletePost(context.Background(), post.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) resp, err = client.DeletePost(context.Background(), model.NewId()) require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, err = th.SystemAdminClient.DeletePost(context.Background(), post.Id) require.NoError(t, err) _, err = th.LocalClient.DeletePost(context.Background(), post2.Id) require.NoError(t, err) }) } func TestPermanentDeletePost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client enableAPIPostDeletion := *th.App.Config().ServiceSettings.EnableAPIPostDeletion defer func() { th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableAPIPostDeletion = &enableAPIPostDeletion }) }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIPostDeletion = false }) t.Run("Post not found", func(t *testing.T) { resp, err := client.PermanentDeletePost(context.Background(), "") require.Error(t, err) CheckNotFoundStatus(t, resp) }) t.Run("Post doesn't exist", func(t *testing.T) { resp, err := client.PermanentDeletePost(context.Background(), "junk") require.Error(t, err) CheckBadRequestStatus(t, resp) }) t.Run("Permanent deletion not available through API if EnableAPIPostDeletion is not set", func(t *testing.T) { resp, err := th.SystemAdminClient.PermanentDeletePost(context.Background(), th.BasicPost.Id) require.Error(t, err) CheckNotImplementedStatus(t, resp) }) t.Run("Permanent deletion available through local mode even if EnableAPIPostDeletion is not set", func(t *testing.T) { post := th.CreatePost() _, err := th.LocalClient.PermanentDeletePost(context.Background(), post.Id) require.NoError(t, err) }) t.Run("No permissions to permanently delete a post", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIPostDeletion = true }) resp, err := client.PermanentDeletePost(context.Background(), th.BasicPost.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) }) t.Run("Try to permanently delete a post across different user roles", func(t *testing.T) { _, _, err := client.Login(context.Background(), th.TeamAdminUser.Email, th.TeamAdminUser.Password) require.NoError(t, err) resp, err := client.PermanentDeletePost(context.Background(), th.BasicPost.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) post := th.CreatePost() post2 := th.CreatePost() user := th.CreateUser() _, err = client.Logout(context.Background()) require.NoError(t, err) _, _, err = client.Login(context.Background(), user.Email, user.Password) require.NoError(t, err) resp, err = client.PermanentDeletePost(context.Background(), post.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) resp, err = client.PermanentDeletePost(context.Background(), post.Id) require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, err = th.SystemAdminClient.PermanentDeletePost(context.Background(), post.Id) require.NoError(t, err) _, err = th.LocalClient.PermanentDeletePost(context.Background(), post2.Id) require.NoError(t, err) }) } func TestWebHubMembership(t *testing.T) { mainHelper.Parallel(t) t.Run("WithChannelIteration", func(t *testing.T) { mainHelper.Parallel(t) th := SetupConfig(t, func(cfg *model.Config) { *cfg.ServiceSettings.EnableWebHubChannelIteration = true }).InitBasic() defer th.TearDown() _testWebHubMembership(th, t) }) t.Run("WithoutChannelIteration", func(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() _testWebHubMembership(th, t) }) } func _testWebHubMembership(th *TestHelper, t *testing.T) { t.Helper() u1 := th.CreateUser() th.LinkUserToTeam(u1, th.BasicTeam) th.AddUserToChannel(u1, th.BasicChannel) ch2 := th.CreatePrivateChannel() u2 := th.CreateUser() th.LinkUserToTeam(u2, th.BasicTeam) th.AddUserToChannel(u2, ch2) quitChan := make(chan struct{}) var wg sync.WaitGroup wg.Add(3) for _, obj := range []struct { testName string user *model.User }{ { testName: "basicUser", user: th.BasicUser, }, { testName: "u1", user: u1, }, { testName: "u2", user: u2, }, } { cli := th.CreateClient() _, _, err := cli.Login(context.Background(), obj.user.Username, obj.user.Password) require.NoError(t, err) wsClient := th.CreateConnectedWebSocketClientWithClient(t, cli) go func(testName string) { defer wg.Done() var cnt int for { select { case event := <-wsClient.EventChannel: if event.EventType() == model.WebsocketEventPosted { var post model.Post err := json.Unmarshal([]byte(event.GetData()["post"].(string)), &post) require.NoError(t, err) cnt++ // Cases: // Post to basicChannel should go to u1 and basicUser. // Add u1 to ch2. // Post to ch2 should go to u1, u2 and basicUser. // Remove u1 from ch2. // Post to ch2 should go to u2 and basicUser. switch testName { case "basicUser": if cnt == 1 { assert.Equal(t, th.BasicChannel.Id, post.ChannelId) } else if cnt == 2 { assert.Equal(t, ch2.Id, post.ChannelId) } else if cnt == 3 { // After removing, there will be a "removed from channel post" assert.Equal(t, ch2.Id, post.ChannelId) } else if cnt == 4 { assert.Equal(t, ch2.Id, post.ChannelId) } else { assert.Fail(t, "more than 4 messages arrived for basicUser") } case "u1": // First msg should be from basicChannel if cnt == 1 { assert.Equal(t, th.BasicChannel.Id, post.ChannelId) } else if cnt == 2 { // second should be from ch2 assert.Equal(t, ch2.Id, post.ChannelId) } else { assert.Fail(t, "more than 2 messages arrived for u1") } case "u2": if cnt == 1 { assert.Equal(t, ch2.Id, post.ChannelId) } else if cnt == 2 { // After removing, there will be a "removed from channel post" assert.Equal(t, ch2.Id, post.ChannelId) } else if cnt == 3 { assert.Equal(t, ch2.Id, post.ChannelId) } else { assert.Fail(t, "more than 3 messages arrived for u2") } } } case <-quitChan: return } } }(obj.testName) } // Will send to basic channel th.CreatePost() // Add u1 to ch2 th.AddUserToChannel(u1, ch2) // Send post to ch2 th.CreatePostWithClient(th.Client, ch2) // Remove u1 from ch2 th.RemoveUserFromChannel(u1, ch2) // Send post to ch2 th.CreatePostWithClient(th.Client, ch2) // It is possible to create a signalling mechanism from the goroutines // after all events are received, but we also want to verify that no additional // events are being sent. time.Sleep(2 * time.Second) close(quitChan) wg.Wait() } func TestWebHubCloseConnOnDBFail(t *testing.T) { mainHelper.Parallel(t) th := SetupConfig(t, func(cfg *model.Config) { *cfg.ServiceSettings.EnableWebHubChannelIteration = true }).InitBasic() defer func() { th.TearDown() _, err := th.Server.Store().GetInternalMasterDB().Exec(`ALTER TABLE dummy RENAME to ChannelMembers`) require.NoError(t, err) // Asserting that the error message is present in the log testlib.AssertLog(t, th.LogBuffer, mlog.LvlError.Name, "Error while registering to hub") }() cli := th.CreateClient() _, _, err := cli.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password) require.NoError(t, err) _, err = th.Server.Store().GetInternalMasterDB().Exec(`ALTER TABLE ChannelMembers RENAME to dummy`) require.NoError(t, err) wsClient, err := th.CreateWebSocketClientWithClient(cli) require.NoError(t, err) wsClient.Listen() select { case <-wsClient.EventChannel: // event channel should be closed on failure case <-time.After(5 * time.Second): require.FailNow(t, "timed out waiting for event") } wsClient.Close() require.NoError(t, th.TestLogger.Flush()) } func TestDeletePostEvent(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() WebSocketClient := th.CreateConnectedWebSocketClient(t) _, err := th.SystemAdminClient.DeletePost(context.Background(), th.BasicPost.Id) require.NoError(t, err) var received, exit bool for !received && !exit { select { case event := <-WebSocketClient.EventChannel: if event.EventType() == model.WebsocketEventPostDeleted { var post model.Post err := json.Unmarshal([]byte(event.GetData()["post"].(string)), &post) require.NoError(t, err) received = true } case <-time.After(5 * time.Second): exit = true } } require.True(t, received) } func TestDeletePostMessage(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() th.LinkUserToTeam(th.SystemAdminUser, th.BasicTeam) _, appErr := th.App.AddUserToChannel(th.Context, th.SystemAdminUser, th.BasicChannel, false) require.Nil(t, appErr) defer th.TearDown() testCases := []struct { description string client *model.Client4 delete_by any }{ {"Do not send delete_by to regular user", th.Client, nil}, {"Send delete_by to system admin user", th.SystemAdminClient, th.SystemAdminUser.Id}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { wsClient := th.CreateConnectedWebSocketClientWithClient(t, tc.client) post := th.CreatePost() _, err := th.SystemAdminClient.DeletePost(context.Background(), post.Id) require.NoError(t, err) timeout := time.After(5 * time.Second) for { select { case ev := <-wsClient.EventChannel: if ev.EventType() == model.WebsocketEventPostDeleted { assert.Equal(t, tc.delete_by, ev.GetData()["delete_by"]) return } case <-timeout: // We just skip the test instead of failing because waiting for more than 5 seconds // to get a response does not make sense, and it will unnecessarily slow down // the tests further in an already congested CI environment. t.Skip("timed out waiting for event") } } }) } } func TestGetPostThread(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a", RootId: th.BasicPost.Id} post, _, _ = client.CreatePost(context.Background(), post) list, resp, err := client.GetPostThread(context.Background(), th.BasicPost.Id, "", false) require.NoError(t, err) var list2 *model.PostList list2, resp, _ = client.GetPostThread(context.Background(), th.BasicPost.Id, resp.Etag, false) CheckEtag(t, list2, resp) require.Equal(t, th.BasicPost.Id, list.Order[0], "wrong order") _, ok := list.Posts[th.BasicPost.Id] require.True(t, ok, "should have had post") _, ok = list.Posts[post.Id] require.True(t, ok, "should have had post") _, resp, err = client.GetPostThread(context.Background(), "junk", "", false) require.Error(t, err) CheckBadRequestStatus(t, resp) _, resp, err = client.GetPostThread(context.Background(), model.NewId(), "", false) require.Error(t, err) CheckNotFoundStatus(t, resp) _, err = client.RemoveUserFromChannel(context.Background(), th.BasicChannel.Id, th.BasicUser.Id) require.NoError(t, err) // Channel is public, should be able to read post _, _, err = client.GetPostThread(context.Background(), th.BasicPost.Id, "", false) require.NoError(t, err) privatePost := th.CreatePostWithClient(client, th.BasicPrivateChannel) _, _, err = client.GetPostThread(context.Background(), privatePost.Id, "", false) require.NoError(t, err) _, err = client.RemoveUserFromChannel(context.Background(), th.BasicPrivateChannel.Id, th.BasicUser.Id) require.NoError(t, err) // Channel is private, should not be able to read post _, resp, err = client.GetPostThread(context.Background(), privatePost.Id, "", false) require.Error(t, err) CheckForbiddenStatus(t, resp) // Test the new query parameters - updatesOnly, fromUpdateAt // Sending some bad params _, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{ UpdatesOnly: true, // updatesOnly is true but fromUpdateAt is not set }) require.Error(t, err) CheckBadRequestStatus(t, resp) // Test error when both fromUpdateAt and fromCreateAt are set _, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{ FromUpdateAt: 12345, FromCreateAt: 12345, }) require.Error(t, err) CheckBadRequestStatus(t, resp) // Test error when updatesOnly is used with direction="up" _, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{ UpdatesOnly: true, FromUpdateAt: 12345, Direction: "up", }) require.Error(t, err) CheckBadRequestStatus(t, resp) // Test valid parameters // This should work with proper parameters _, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{ UpdatesOnly: true, FromUpdateAt: 12345, Direction: "down", }) require.NoError(t, err) CheckOKStatus(t, resp) list, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{ UpdatesOnly: true, Direction: "down", FromUpdateAt: post.UpdateAt, }) require.NoError(t, err) CheckOKStatus(t, resp) assert.Len(t, list.Order, 1) assert.Len(t, list.Posts, 1) require.Equal(t, th.BasicPost.Id, list.Order[0], "wrong order") // Test with just fromUpdateAt parameter _, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{ FromUpdateAt: 12345, }) require.NoError(t, err) CheckOKStatus(t, resp) // Sending other bad params unrelated to the new changes _, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{ CollapsedThreads: true, FromPost: "something", PerPage: 10, }) require.Error(t, err) CheckBadRequestStatus(t, resp) _, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{ CollapsedThreads: true, Direction: "sideways", }) require.Error(t, err) CheckBadRequestStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) _, resp, err = client.GetPostThread(context.Background(), model.NewId(), "", false) require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, _, err = th.SystemAdminClient.GetPostThread(context.Background(), th.BasicPost.Id, "", false) require.NoError(t, err) } func TestSearchPosts(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() th.LoginBasic() client := th.Client message := "search for post1" _ = th.CreateMessagePost(message) message = "search for post2" post2 := th.CreateMessagePost(message) message = "#hashtag search for post3" post3 := th.CreateMessagePost(message) message = "hashtag for post4" _ = th.CreateMessagePost(message) archivedChannel := th.CreatePublicChannel() _ = th.CreateMessagePostWithClient(th.Client, archivedChannel, "#hashtag for post3") _, err := th.Client.DeleteChannel(context.Background(), archivedChannel.Id) require.NoError(t, err) otherTeam := th.CreateTeam() channelInOtherTeam := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, otherTeam.Id) _ = th.AddUserToChannel(th.BasicUser, channelInOtherTeam) _ = th.CreateMessagePostWithClient(th.Client, channelInOtherTeam, "search for post 5") terms := "search" isOrSearch := false timezoneOffset := 5 searchParams := model.SearchParameter{ Terms: &terms, IsOrSearch: &isOrSearch, TimeZoneOffset: &timezoneOffset, } allTeamsPosts, _, err := client.SearchPostsWithParams(context.Background(), "", &searchParams) require.NoError(t, err) require.Len(t, allTeamsPosts.Order, 4, "wrong search along multiple teams") terms = "search" isOrSearch = false timezoneOffset = 5 searchParams = model.SearchParameter{ Terms: &terms, IsOrSearch: &isOrSearch, TimeZoneOffset: &timezoneOffset, } posts, _, err := client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams) require.NoError(t, err) require.Len(t, posts.Order, 3, "wrong search") terms = "search" page := 0 perPage := 2 searchParams = model.SearchParameter{ Terms: &terms, IsOrSearch: &isOrSearch, TimeZoneOffset: &timezoneOffset, Page: &page, PerPage: &perPage, } posts2, _, err := client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams) require.NoError(t, err) // We don't support paging for DB search yet, modify this when we do. require.Len(t, posts2.Order, 3, "Wrong number of posts") assert.Equal(t, posts.Order[0], posts2.Order[0]) assert.Equal(t, posts.Order[1], posts2.Order[1]) page = 1 searchParams = model.SearchParameter{ Terms: &terms, IsOrSearch: &isOrSearch, TimeZoneOffset: &timezoneOffset, Page: &page, PerPage: &perPage, } posts2, _, err = client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams) require.NoError(t, err) // We don't support paging for DB search yet, modify this when we do. require.Empty(t, posts2.Order, "Wrong number of posts") posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "search", false) require.NoError(t, err) require.Len(t, posts.Order, 3, "wrong search") posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "post2", false) require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts") require.Equal(t, post2.Id, posts.Order[0], "wrong search") posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "#hashtag", false) require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts") require.Equal(t, post3.Id, posts.Order[0], "wrong search") terms = "#hashtag" includeDeletedChannels := true searchParams = model.SearchParameter{ Terms: &terms, IsOrSearch: &isOrSearch, TimeZoneOffset: &timezoneOffset, IncludeDeletedChannels: &includeDeletedChannels, } posts, _, err = client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams) require.NoError(t, err) require.Len(t, posts.Order, 2, "wrong search") // Archived channels are always included now, so this should return the same result posts, _, err = client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams) require.NoError(t, err) require.Len(t, posts.Order, 2, "wrong search") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "*", false) require.Empty(t, posts.Order, "searching for just * shouldn't return any results") posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "post1 post2", true) require.NoError(t, err) require.Len(t, posts.Order, 2, "wrong search results") _, resp, err := client.SearchPosts(context.Background(), "junk", "#sgtitlereview", false) require.Error(t, err) CheckBadRequestStatus(t, resp) _, resp, err = client.SearchPosts(context.Background(), model.NewId(), "#sgtitlereview", false) require.Error(t, err) CheckForbiddenStatus(t, resp) _, resp, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "", false) require.Error(t, err) CheckBadRequestStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) _, resp, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "#sgtitlereview", false) require.Error(t, err) CheckUnauthorizedStatus(t, resp) } func TestSearchHashtagPosts(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() th.LoginBasic() client := th.Client message := "#sgtitlereview with space" assert.NotNil(t, th.CreateMessagePost(message)) message = "#sgtitlereview\n with return" assert.NotNil(t, th.CreateMessagePost(message)) message = "no hashtag" assert.NotNil(t, th.CreateMessagePost(message)) posts, _, err := client.SearchPosts(context.Background(), th.BasicTeam.Id, "#sgtitlereview", false) require.NoError(t, err) require.Len(t, posts.Order, 2, "wrong search results") _, err = client.Logout(context.Background()) require.NoError(t, err) _, resp, err := client.SearchPosts(context.Background(), th.BasicTeam.Id, "#sgtitlereview", false) require.Error(t, err) CheckUnauthorizedStatus(t, resp) } func TestSearchPostsInChannel(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() th.LoginBasic() client := th.Client channel := th.CreatePublicChannel() message := "sgtitlereview with space" _ = th.CreateMessagePost(message) message = "sgtitlereview\n with return" _ = th.CreateMessagePostWithClient(client, th.BasicChannel2, message) message = "other message with no return" _ = th.CreateMessagePostWithClient(client, th.BasicChannel2, message) message = "other message with no return" _ = th.CreateMessagePostWithClient(client, channel, message) posts, _, _ := client.SearchPosts(context.Background(), th.BasicTeam.Id, "channel:", false) require.Empty(t, posts.Order, "wrong number of posts for search 'channel:'") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "in:", false) require.Empty(t, posts.Order, "wrong number of posts for search 'in:'") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "channel:"+th.BasicChannel.Name, false) require.Lenf(t, posts.Order, 2, "wrong number of posts returned for search 'channel:%v'", th.BasicChannel.Name) posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "in:"+th.BasicChannel2.Name, false) require.Lenf(t, posts.Order, 2, "wrong number of posts returned for search 'in:%v'", th.BasicChannel2.Name) posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "channel:"+th.BasicChannel2.Name, false) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'channel:%v'", th.BasicChannel2.Name) posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "ChAnNeL:"+th.BasicChannel2.Name, false) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'ChAnNeL:%v'", th.BasicChannel2.Name) posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "sgtitlereview", false) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'sgtitlereview'") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "sgtitlereview channel:"+th.BasicChannel.Name, false) require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'sgtitlereview channel:%v'", th.BasicChannel.Name) posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "sgtitlereview in: "+th.BasicChannel2.Name, false) require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'sgtitlereview in: %v'", th.BasicChannel2.Name) posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "sgtitlereview channel: "+th.BasicChannel2.Name, false) require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'sgtitlereview channel: %v'", th.BasicChannel2.Name) posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "channel: "+th.BasicChannel2.Name+" channel: "+channel.Name, false) require.Lenf(t, posts.Order, 3, "wrong number of posts for 'channel: %v channel: %v'", th.BasicChannel2.Name, channel.Name) } func TestSearchPostsFromUser(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client th.LoginTeamAdmin() user := th.CreateUser() th.LinkUserToTeam(user, th.BasicTeam) _, appErr := th.App.AddUserToChannel(th.Context, user, th.BasicChannel, false) require.Nil(t, appErr) _, appErr = th.App.AddUserToChannel(th.Context, user, th.BasicChannel2, false) require.Nil(t, appErr) message := "sgtitlereview with space" _ = th.CreateMessagePost(message) _, err := client.Logout(context.Background()) require.NoError(t, err) th.LoginBasic2() message = "sgtitlereview\n with return" _ = th.CreateMessagePostWithClient(client, th.BasicChannel2, message) posts, _, err := client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.TeamAdminUser.Username, false) require.NoError(t, err) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v'", th.TeamAdminUser.Username) posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) require.NoError(t, err) require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'from: %v", th.BasicUser2.Username) posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" sgtitlereview", false) require.NoError(t, err) require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'from: %v'", th.BasicUser2.Username) message = "hullo" _ = th.CreateMessagePost(message) posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" in:"+th.BasicChannel.Name, false) require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts for search 'from: %v in:", th.BasicUser2.Username, th.BasicChannel.Name) _, _, err = client.Login(context.Background(), user.Email, user.Password) require.NoError(t, err) // wait for the join/leave messages to be created for user3 since they're done asynchronously time.Sleep(100 * time.Millisecond) posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) require.NoError(t, err) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v'", th.BasicUser2.Username) posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username, false) require.NoError(t, err) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v from: %v'", th.BasicUser2.Username, user.Username) posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name, false) require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts") message = "coconut" _ = th.CreateMessagePostWithClient(client, th.BasicChannel2, message) posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name+" coconut", false) require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts") } func TestSearchPostsWithDateFlags(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() th.LoginBasic() client := th.Client message := "sgtitlereview\n with return" createDate := time.Date(2018, 8, 1, 5, 0, 0, 0, time.UTC) _ = th.CreateMessagePostNoClient(th.BasicChannel, message, utils.MillisFromTime(createDate)) message = "other message with no return" createDate = time.Date(2018, 8, 2, 5, 0, 0, 0, time.UTC) _ = th.CreateMessagePostNoClient(th.BasicChannel, message, utils.MillisFromTime(createDate)) message = "other message with no return" createDate = time.Date(2018, 8, 3, 5, 0, 0, 0, time.UTC) _ = th.CreateMessagePostNoClient(th.BasicChannel, message, utils.MillisFromTime(createDate)) posts, _, _ := client.SearchPosts(context.Background(), th.BasicTeam.Id, "return", false) require.Len(t, posts.Order, 3, "wrong number of posts") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "on:", false) require.Empty(t, posts.Order, "wrong number of posts") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "after:", false) require.Empty(t, posts.Order, "wrong number of posts") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "before:", false) require.Empty(t, posts.Order, "wrong number of posts") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "on:2018-08-01", false) require.Len(t, posts.Order, 1, "wrong number of posts") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "after:2018-08-01", false) resultCount := 0 for _, post := range posts.Posts { if post.UserId == th.BasicUser.Id { resultCount = resultCount + 1 } } require.Equal(t, 2, resultCount, "wrong number of posts") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "before:2018-08-02", false) require.Len(t, posts.Order, 1, "wrong number of posts") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "before:2018-08-03 after:2018-08-02", false) require.Empty(t, posts.Order, "wrong number of posts") posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "before:2018-08-03 after:2018-08-01", false) require.Len(t, posts.Order, 1, "wrong number of posts") } func TestGetFileInfosForPost(t *testing.T) { t.Skip("MM-46902") th := Setup(t).InitBasic() defer th.TearDown() client := th.Client fileIds := make([]string, 3) data, err := testutils.ReadTestFile("test.png") require.NoError(t, err) for i := range 3 { fileResp, _, _ := client.UploadFile(context.Background(), data, th.BasicChannel.Id, "test.png") fileIds[i] = fileResp.FileInfos[0].Id } post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a", FileIds: fileIds} post, _, _ = client.CreatePost(context.Background(), post) infos, resp, err := client.GetFileInfosForPost(context.Background(), post.Id, "") require.NoError(t, err) require.Len(t, infos, 3, "missing file infos") found := false for _, info := range infos { if info.Id == fileIds[0] { found = true } } require.True(t, found, "missing file info") infos, resp, _ = client.GetFileInfosForPost(context.Background(), post.Id, resp.Etag) CheckEtag(t, infos, resp) infos, _, err = client.GetFileInfosForPost(context.Background(), th.BasicPost.Id, "") require.NoError(t, err) require.Empty(t, infos, "should have no file infos") _, resp, err = client.GetFileInfosForPost(context.Background(), "junk", "") require.Error(t, err) CheckBadRequestStatus(t, resp) _, resp, err = client.GetFileInfosForPost(context.Background(), model.NewId(), "") require.Error(t, err) CheckForbiddenStatus(t, resp) // Delete post _, err = th.SystemAdminClient.DeletePost(context.Background(), post.Id) require.NoError(t, err) // Normal client should get 404 when trying to access deleted post normally _, resp, err = client.GetFileInfosForPost(context.Background(), post.Id, "") require.Error(t, err) CheckNotFoundStatus(t, resp) // Normal client should get unauthorized when trying to access deleted post _, resp, err = client.GetFileInfosForPostIncludeDeleted(context.Background(), post.Id, "") require.Error(t, err) CheckForbiddenStatus(t, resp) // System client should get 404 when trying to access deleted post normally _, resp, err = th.SystemAdminClient.GetFileInfosForPost(context.Background(), post.Id, "") require.Error(t, err) CheckNotFoundStatus(t, resp) // System client should be able to access deleted post with include_deleted param infos, _, err = th.SystemAdminClient.GetFileInfosForPostIncludeDeleted(context.Background(), post.Id, "") require.NoError(t, err) require.Len(t, infos, 3, "missing file infos") found = false for _, info := range infos { if info.Id == fileIds[0] { found = true } } require.True(t, found, "missing file info") _, err = client.Logout(context.Background()) require.NoError(t, err) _, resp, err = client.GetFileInfosForPost(context.Background(), model.NewId(), "") require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, _, err = th.SystemAdminClient.GetFileInfosForPost(context.Background(), th.BasicPost.Id, "") require.NoError(t, err) } func TestSetChannelUnread(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() u1 := th.BasicUser u2 := th.BasicUser2 s2, _ := th.App.GetSession(th.Client.AuthToken) _, _, err := th.Client.Login(context.Background(), u1.Email, u1.Password) require.NoError(t, err) c1 := th.BasicChannel c1toc2 := &model.ChannelView{ChannelId: th.BasicChannel2.Id, PrevChannelId: c1.Id} now := utils.MillisFromTime(time.Now()) th.CreateMessagePostNoClient(c1, "AAA", now) p2 := th.CreateMessagePostNoClient(c1, "BBB", now+10) th.CreateMessagePostNoClient(c1, "CCC", now+20) pp1 := th.CreateMessagePostNoClient(th.BasicPrivateChannel, "Sssh!", now) pp2 := th.CreateMessagePostNoClient(th.BasicPrivateChannel, "You Sssh!", now+10) require.NotNil(t, pp1) require.NotNil(t, pp2) // Ensure that post have been read unread, appErr := th.App.GetChannelUnread(th.Context, c1.Id, u1.Id) require.Nil(t, appErr) require.Equal(t, int64(4), unread.MsgCount) unread, appErr = th.App.GetChannelUnread(th.Context, c1.Id, u2.Id) require.Nil(t, appErr) require.Equal(t, int64(4), unread.MsgCount) _, appErr = th.App.ViewChannel(th.Context, c1toc2, u2.Id, s2.Id, false) require.Nil(t, appErr) unread, appErr = th.App.GetChannelUnread(th.Context, c1.Id, u2.Id) require.Nil(t, appErr) require.Equal(t, int64(0), unread.MsgCount) t.Run("Unread last one", func(t *testing.T) { var r *model.Response r, err = th.Client.SetPostUnread(context.Background(), u1.Id, p2.Id, true) require.NoError(t, err) CheckOKStatus(t, r) unread, appErr := th.App.GetChannelUnread(th.Context, c1.Id, u1.Id) require.Nil(t, appErr) assert.Equal(t, int64(2), unread.MsgCount) }) t.Run("Unread on a direct channel", func(t *testing.T) { dc := th.CreateDmChannel(u2) th.CreateMessagePostNoClient(dc, "test1", now) p := th.CreateMessagePostNoClient(dc, "test2", now+10) require.NotNil(t, p) th.CreateMessagePostNoClient(dc, "test3", now+20) p1 := th.CreateMessagePostNoClient(dc, "test4", now+30) require.NotNil(t, p1) // Ensure that post have been read unread, err := th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(4), unread.MsgCount) cv := &model.ChannelView{ChannelId: dc.Id} _, appErr := th.App.ViewChannel(th.Context, cv, u1.Id, s2.Id, false) require.Nil(t, appErr) unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(0), unread.MsgCount) r, _ := th.Client.SetPostUnread(context.Background(), u1.Id, p.Id, false) assert.Equal(t, 200, r.StatusCode) unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(3), unread.MsgCount) // Ensure that post have been read _, appErr = th.App.ViewChannel(th.Context, cv, u1.Id, s2.Id, false) require.Nil(t, appErr) unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(0), unread.MsgCount) r, _ = th.Client.SetPostUnread(context.Background(), u1.Id, p1.Id, false) assert.Equal(t, 200, r.StatusCode) unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(1), unread.MsgCount) }) t.Run("Unread on a direct channel in a thread", func(t *testing.T) { dc := th.CreateDmChannel(th.CreateUser()) rootPost, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: u1.Id, CreateAt: now, ChannelId: dc.Id, Message: "root"}, dc, model.CreatePostFlags{}) require.Nil(t, appErr) _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 10, ChannelId: dc.Id, Message: "reply 1"}, dc, model.CreatePostFlags{}) require.Nil(t, appErr) reply2, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 20, ChannelId: dc.Id, Message: "reply 2"}, dc, model.CreatePostFlags{}) require.Nil(t, appErr) _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 30, ChannelId: dc.Id, Message: "reply 3"}, dc, model.CreatePostFlags{}) require.Nil(t, appErr) // Ensure that post have been read unread, err := th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(4), unread.MsgCount) require.Equal(t, int64(1), unread.MsgCountRoot) cv := &model.ChannelView{ChannelId: dc.Id} _, appErr = th.App.ViewChannel(th.Context, cv, u1.Id, s2.Id, false) require.Nil(t, appErr) unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(0), unread.MsgCount) require.Equal(t, int64(0), unread.MsgCountRoot) r, _ := th.Client.SetPostUnread(context.Background(), u1.Id, rootPost.Id, false) assert.Equal(t, 200, r.StatusCode) unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(4), unread.MsgCount) require.Equal(t, int64(1), unread.MsgCountRoot) // Ensure that post have been read _, appErr = th.App.ViewChannel(th.Context, cv, u1.Id, s2.Id, false) require.Nil(t, appErr) unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(0), unread.MsgCount) require.Equal(t, int64(0), unread.MsgCountRoot) r, _ = th.Client.SetPostUnread(context.Background(), u1.Id, reply2.Id, false) assert.Equal(t, 200, r.StatusCode) unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id) require.Nil(t, err) require.Equal(t, int64(2), unread.MsgCount) require.Equal(t, int64(0), unread.MsgCountRoot) }) t.Run("Unread on a private channel", func(t *testing.T) { r, _ := th.Client.SetPostUnread(context.Background(), u1.Id, pp2.Id, true) assert.Equal(t, 200, r.StatusCode) unread, appErr := th.App.GetChannelUnread(th.Context, th.BasicPrivateChannel.Id, u1.Id) require.Nil(t, appErr) assert.Equal(t, int64(1), unread.MsgCount) r, _ = th.Client.SetPostUnread(context.Background(), u1.Id, pp1.Id, true) assert.Equal(t, 200, r.StatusCode) unread, appErr = th.App.GetChannelUnread(th.Context, th.BasicPrivateChannel.Id, u1.Id) require.Nil(t, appErr) assert.Equal(t, int64(2), unread.MsgCount) }) t.Run("Can't unread an imaginary post", func(t *testing.T) { r, _ := th.Client.SetPostUnread(context.Background(), u1.Id, "invalid4ofngungryquinj976y", true) assert.Equal(t, http.StatusForbidden, r.StatusCode) }) // let's create another user to test permissions u3 := th.CreateUser() c3 := th.CreateClient() _, _, err = c3.Login(context.Background(), u3.Email, u3.Password) require.NoError(t, err) t.Run("Can't unread channels you don't belong to", func(t *testing.T) { r, _ := c3.SetPostUnread(context.Background(), u3.Id, pp1.Id, true) assert.Equal(t, http.StatusForbidden, r.StatusCode) }) t.Run("Can't unread users you don't have permission to edit", func(t *testing.T) { r, _ := c3.SetPostUnread(context.Background(), u1.Id, pp1.Id, true) assert.Equal(t, http.StatusForbidden, r.StatusCode) }) t.Run("Can't unread if user is not logged in", func(t *testing.T) { _, err := th.Client.Logout(context.Background()) require.NoError(t, err) response, err := th.Client.SetPostUnread(context.Background(), u1.Id, p2.Id, true) require.Error(t, err) CheckUnauthorizedStatus(t, response) }) } func TestSetPostUnreadWithoutCollapsedThreads(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn }) // user2: first root mention @user1 // - user1: hello // - user2: mention @u1 // - user1: another reply // - user2: another mention @u1 // user1: a root post // user2: Another root mention @u1 user1Mention := " @" + th.BasicUser.Username rootPost1, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "first root mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{}) require.Nil(t, appErr) _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hello"}, th.BasicChannel, model.CreatePostFlags{}) require.Nil(t, appErr) replyPost1, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{}) require.Nil(t, appErr) _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another reply"}, th.BasicChannel, model.CreatePostFlags{}) require.Nil(t, appErr) _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{}) require.Nil(t, appErr) _, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "a root post"}, th.BasicChannel, model.CreatePostFlags{}) require.Nil(t, appErr) _, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another root mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{}) require.Nil(t, appErr) t.Run("Mark reply post as unread", func(t *testing.T) { userWSClient := th.CreateConnectedWebSocketClient(t) _, err := th.Client.SetPostUnread(context.Background(), th.BasicUser.Id, replyPost1.Id, false) require.NoError(t, err) channelUnread, appErr := th.App.GetChannelUnread(th.Context, th.BasicChannel.Id, th.BasicUser.Id) require.Nil(t, appErr) require.Equal(t, int64(3), channelUnread.MentionCount) // MentionCountRoot should be zero so that supported clients don't show a mention badge for the channel require.Equal(t, int64(0), channelUnread.MentionCountRoot) require.Equal(t, int64(5), channelUnread.MsgCount) // MentionCountRoot should be zero so that supported clients don't show the channel as unread require.Equal(t, channelUnread.MsgCountRoot, int64(0)) // test websocket event for marking post as unread var caught bool var exit bool var data map[string]any for { select { case ev := <-userWSClient.EventChannel: if ev.EventType() == model.WebsocketEventPostUnread { caught = true data = ev.GetData() } case <-time.After(5 * time.Second): exit = true } if exit { break } } require.Truef(t, caught, "User should have received %s event", model.WebsocketEventPostUnread) msgCount, ok := data["msg_count"] require.True(t, ok) require.EqualValues(t, 3, msgCount) mentionCount, ok := data["mention_count"] require.True(t, ok) require.EqualValues(t, 3, mentionCount) threadMembership, appErr := th.App.GetThreadMembershipForUser(th.BasicUser.Id, rootPost1.Id) require.Nil(t, appErr) thread, appErr := th.App.GetThreadForUser(th.Context, threadMembership, false) require.Nil(t, appErr) require.Equal(t, int64(2), thread.UnreadMentions) require.Equal(t, int64(3), thread.UnreadReplies) }) t.Run("Mark root post as unread", func(t *testing.T) { _, err := th.Client.SetPostUnread(context.Background(), th.BasicUser.Id, rootPost1.Id, false) require.NoError(t, err) channelUnread, appErr := th.App.GetChannelUnread(th.Context, th.BasicChannel.Id, th.BasicUser.Id) require.Nil(t, appErr) require.Equal(t, int64(4), channelUnread.MentionCount) require.Equal(t, int64(2), channelUnread.MentionCountRoot) require.Equal(t, int64(7), channelUnread.MsgCount) require.Equal(t, int64(3), channelUnread.MsgCountRoot) }) } func TestGetPostsByIds(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post1 := th.CreatePost() post2 := th.CreatePost() posts, response, err := client.GetPostsByIds(context.Background(), []string{post1.Id, post2.Id}) require.NoError(t, err) CheckOKStatus(t, response) require.Len(t, posts, 2, "wrong number returned") require.Equal(t, posts[0].Id, post2.Id) require.Equal(t, posts[1].Id, post1.Id) _, response, err = client.GetPostsByIds(context.Background(), []string{}) require.Error(t, err) CheckBadRequestStatus(t, response) _, response, err = client.GetPostsByIds(context.Background(), []string{"abc123"}) require.Error(t, err) CheckNotFoundStatus(t, response) } func TestGetEditHistoryForPost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "new message", UserId: th.BasicUser.Id, } rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, err) time.Sleep(1 * time.Millisecond) t.Run("unedited post", func(t *testing.T) { history, resp, err := client.GetEditHistoryForPost(context.Background(), rpost.Id) require.Error(t, err) CheckNotFoundStatus(t, resp) require.Len(t, history, 0) }) // update the post message patch := &model.PostPatch{ Message: model.NewPointer("new message edited"), } // Patch the post _, response1, err1 := client.PatchPost(context.Background(), rpost.Id, patch) require.NoError(t, err1) CheckOKStatus(t, response1) // update the post message again patch = &model.PostPatch{ Message: model.NewPointer("new message edited again"), } _, response2, err2 := client.PatchPost(context.Background(), rpost.Id, patch) require.NoError(t, err2) CheckOKStatus(t, response2) t.Run("update history correctly", func(t *testing.T) { history, response3, err3 := client.GetEditHistoryForPost(context.Background(), rpost.Id) require.NoError(t, err3) CheckOKStatus(t, response3) require.Len(t, history, 2) require.Equal(t, "new message edited", history[0].Message) require.Equal(t, "new message", history[1].Message) }) t.Run("logged out", func(t *testing.T) { _, err := client.Logout(context.Background()) require.NoError(t, err) _, resp, err := client.GetEditHistoryForPost(context.Background(), rpost.Id) require.Error(t, err) CheckUnauthorizedStatus(t, resp) }) t.Run("different user", func(t *testing.T) { th.LoginBasic2() _, resp, err := client.GetEditHistoryForPost(context.Background(), rpost.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) }) t.Run("edit history includes file metadata", func(t *testing.T) { th.LoginBasic() fileInfo1, appErr := th.App.UploadFile(th.Context, []byte("data"), th.BasicChannel.Id, "test") require.Nil(t, appErr) fileInfo2, appErr := th.App.UploadFile(th.Context, []byte("data"), th.BasicChannel.Id, "test") require.Nil(t, appErr) post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "new message", UserId: th.BasicUser.Id, FileIds: []string{fileInfo1.Id, fileInfo2.Id}, } createdPost, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) require.Contains(t, createdPost.FileIds, fileInfo1.Id) require.Contains(t, createdPost.FileIds, fileInfo2.Id) patch = &model.PostPatch{ Message: model.NewPointer("new message 1"), } _, response, err := client.PatchPost(context.Background(), createdPost.Id, patch) require.NoError(t, err) CheckOKStatus(t, response) patch = &model.PostPatch{ Message: model.NewPointer("new message 2"), } _, response, err = client.PatchPost(context.Background(), createdPost.Id, patch) require.NoError(t, err) CheckOKStatus(t, response) patch = &model.PostPatch{ Message: model.NewPointer("new message 3"), } _, response, err = client.PatchPost(context.Background(), createdPost.Id, patch) require.NoError(t, err) CheckOKStatus(t, response) editHistory, resp, err := client.GetEditHistoryForPost(context.Background(), createdPost.Id) require.NoError(t, err) CheckOKStatus(t, resp) for _, editHistoryItem := range editHistory { require.Len(t, editHistoryItem.FileIds, 2) require.Contains(t, editHistoryItem.FileIds, fileInfo1.Id) require.Contains(t, editHistoryItem.FileIds, fileInfo2.Id) } }) } func TestCreatePostNotificationsWithCRT(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() rpost := th.CreatePost() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn }) testCases := []struct { name string post *model.Post notifyProps model.StringMap mentions bool followers bool }{ { name: "When default is NONE, comments is NEVER, desktop threads is ALL, and has no mentions", post: &model.Post{ ChannelId: th.BasicChannel.Id, Message: "reply", UserId: th.BasicUser2.Id, RootId: rpost.Id, }, notifyProps: model.StringMap{ model.DesktopNotifyProp: model.UserNotifyNone, model.CommentsNotifyProp: model.CommentsNotifyNever, model.DesktopThreadsNotifyProp: model.UserNotifyAll, }, mentions: false, followers: false, }, { name: "When default is NONE, comments is NEVER, desktop threads is ALL, and has mentions", post: &model.Post{ ChannelId: th.BasicChannel.Id, Message: "mention @" + th.BasicUser.Username, UserId: th.BasicUser2.Id, RootId: rpost.Id, }, notifyProps: model.StringMap{ model.DesktopNotifyProp: model.UserNotifyNone, model.CommentsNotifyProp: model.CommentsNotifyNever, model.DesktopThreadsNotifyProp: model.UserNotifyAll, }, mentions: true, followers: false, }, { name: "When default is MENTION, comments is NEVER, desktop threads is ALL, and has no mentions", post: &model.Post{ ChannelId: th.BasicChannel.Id, Message: "reply", UserId: th.BasicUser2.Id, RootId: rpost.Id, }, notifyProps: model.StringMap{ model.DesktopNotifyProp: model.UserNotifyMention, model.CommentsNotifyProp: model.CommentsNotifyNever, model.DesktopThreadsNotifyProp: model.UserNotifyAll, }, mentions: false, followers: true, }, { name: "When default is MENTION, comments is ANY, desktop threads is MENTION, and has no mentions", post: &model.Post{ ChannelId: th.BasicChannel.Id, Message: "reply", UserId: th.BasicUser2.Id, RootId: rpost.Id, }, notifyProps: model.StringMap{ model.DesktopNotifyProp: model.UserNotifyMention, model.CommentsNotifyProp: model.CommentsNotifyAny, model.DesktopThreadsNotifyProp: model.UserNotifyMention, }, mentions: false, followers: false, }, { name: "When default is MENTION, comments is NEVER, desktop threads is MENTION, and has mentions", post: &model.Post{ ChannelId: th.BasicChannel.Id, Message: "reply @" + th.BasicUser.Username, UserId: th.BasicUser2.Id, RootId: rpost.Id, }, notifyProps: model.StringMap{ model.DesktopNotifyProp: model.UserNotifyMention, model.CommentsNotifyProp: model.CommentsNotifyNever, model.DesktopThreadsNotifyProp: model.UserNotifyMention, }, mentions: true, followers: true, }, } // reset the cache so that channel member notify props includes all users th.App.Srv().Store().Channel().ClearCaches() for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { userWSClient := th.CreateConnectedWebSocketClient(t) patch := &model.UserPatch{} patch.NotifyProps = model.CopyStringMap(th.BasicUser.NotifyProps) maps.Copy(patch.NotifyProps, tc.notifyProps) // update user's notify props _, _, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, patch) require.NoError(t, err) // post a reply on the thread _, appErr := th.App.CreatePostAsUser(th.Context, tc.post, th.Context.Session().Id, false) require.Nil(t, appErr) var caught bool func() { for { select { case ev := <-userWSClient.EventChannel: if ev.EventType() == model.WebsocketEventPosted { caught = true data := ev.GetData() users, ok := data["mentions"] require.Equal(t, tc.mentions, ok) if ok { require.EqualValues(t, "[\""+th.BasicUser.Id+"\"]", users) } users, ok = data["followers"] require.Equal(t, tc.followers, ok) if ok { require.EqualValues(t, "[\""+th.BasicUser.Id+"\"]", users) } } case <-time.After(5 * time.Second): return } } }() require.Truef(t, caught, "User should have received %s event", model.WebsocketEventPosted) }) } } func TestGetPostStripActionIntegrations(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "with slack attachment action", } post.AddProp(model.PostPropsAttachments, []*model.SlackAttachment{ { Text: "Slack Attachment Text", Fields: []*model.SlackAttachmentField{ { Title: "Test Field", Value: "test value", Short: true, }, }, Actions: []*model.PostAction{ { Type: model.PostActionTypeButton, Name: "test-name", Integration: &model.PostActionIntegration{ URL: "https://test.test/action", Context: map[string]any{ "test-ctx": "some-value", }, }, }, }, }, }) rpost, resp, err2 := client.CreatePost(context.Background(), post) require.NoError(t, err2) CheckCreatedStatus(t, resp) actualPost, _, err := client.GetPost(context.Background(), rpost.Id, "") require.NoError(t, err) attachments, _ := actualPost.Props[model.PostPropsAttachments].([]any) require.Equal(t, 1, len(attachments)) att, _ := attachments[0].(map[string]any) require.NotNil(t, att) actions, _ := att["actions"].([]any) require.Equal(t, 1, len(actions)) action, _ := actions[0].(map[string]any) require.NotNil(t, action) // integration must be omitted require.Nil(t, action["integration"]) } func TestPostReminder(t *testing.T) { t.Skip("MM-60329") th := Setup(t).InitBasic() defer th.TearDown() client := th.Client userWSClient := th.CreateConnectedWebSocketClient(t) targetTime := time.Now().UTC().Unix() resp, err := client.SetPostReminder(context.Background(), &model.PostReminder{ TargetTime: targetTime, PostId: th.BasicPost.Id, UserId: th.BasicUser.Id, }) require.NoError(t, err) CheckOKStatus(t, resp) post, _, err := client.GetPost(context.Background(), th.BasicPost.Id, "") require.NoError(t, err) user, _, err := client.GetUser(context.Background(), post.UserId, "") require.NoError(t, err) var caught bool func() { for { select { case ev := <-userWSClient.EventChannel: if ev.EventType() == model.WebsocketEventEphemeralMessage { caught = true data := ev.GetData() post, ok := data["post"].(string) require.True(t, ok) var parsedPost model.Post err := json.Unmarshal([]byte(post), &parsedPost) require.NoError(t, err) assert.Equal(t, model.PostTypeEphemeral, parsedPost.Type) assert.Equal(t, th.BasicUser.Id, parsedPost.UserId) assert.Equal(t, th.BasicPost.Id, parsedPost.RootId) require.Equal(t, float64(targetTime), parsedPost.GetProp("target_time").(float64)) require.Equal(t, th.BasicPost.Id, parsedPost.GetProp("post_id").(string)) require.Equal(t, user.Username, parsedPost.GetProp("username").(string)) require.Equal(t, th.BasicTeam.Name, parsedPost.GetProp("team_name").(string)) return } case <-time.After(5 * time.Second): return } } }() require.Truef(t, caught, "User should have received %s event", model.WebsocketEventEphemeralMessage) } func TestPostGetInfo(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() defaultPerms := th.SaveDefaultRolePermissions() defer th.RestoreDefaultRolePermissions(defaultPerms) th.RemovePermissionFromRole(model.PermissionManagePrivateChannelMembers.Id, model.SystemUserRoleId) th.RemovePermissionFromRole(model.PermissionManagePrivateChannelMembers.Id, model.ChannelUserRoleId) th.RemovePermissionFromRole(model.PermissionManagePrivateChannelMembers.Id, model.TeamUserRoleId) client := th.Client sysadminClient := th.SystemAdminClient _, _, err := sysadminClient.AddTeamMember(context.Background(), th.BasicTeam.Id, th.SystemAdminUser.Id) require.NoError(t, err) openChannel, _, err := client.CreateChannel(context.Background(), &model.Channel{TeamId: th.BasicTeam.Id, Type: model.ChannelTypeOpen, Name: "open-channel", DisplayName: "Open Channel"}) require.NoError(t, err) _, _, err = sysadminClient.AddChannelMember(context.Background(), openChannel.Id, th.SystemAdminUser.Id) require.NoError(t, err) openPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: openChannel.Id}) require.NoError(t, err) privateChannel, _, err := sysadminClient.CreateChannel(context.Background(), &model.Channel{TeamId: th.BasicTeam.Id, Type: model.ChannelTypePrivate, Name: "private-channel", DisplayName: "Private Channel"}) require.NoError(t, err) privatePost, _, err := sysadminClient.CreatePost(context.Background(), &model.Post{ChannelId: privateChannel.Id}) require.NoError(t, err) privateChannelBasicUser, _, err := client.CreateChannel(context.Background(), &model.Channel{TeamId: th.BasicTeam.Id, Type: model.ChannelTypePrivate, Name: "private-channel-basic-user", DisplayName: "Private Channel - Basic User"}) require.NoError(t, err) privatePostBasicUser, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: privateChannelBasicUser.Id}) require.NoError(t, err) user3 := th.CreateUser() gmChannel, _, err := client.CreateGroupChannel(context.Background(), []string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id}) require.NoError(t, err) gmPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: gmChannel.Id}) require.NoError(t, err) dmChannel, _, err := client.CreateDirectChannel(context.Background(), th.BasicUser.Id, th.BasicUser2.Id) require.NoError(t, err) dmPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: dmChannel.Id}) require.NoError(t, err) openTeam, _, err := sysadminClient.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "open-team", DisplayName: "Open Team", AllowOpenInvite: true}) require.NoError(t, err) openTeamOpenChannel, _, err := sysadminClient.CreateChannel(context.Background(), &model.Channel{TeamId: openTeam.Id, Type: model.ChannelTypeOpen, Name: "open-team-open-channel", DisplayName: "Open Team - Open Channel"}) require.NoError(t, err) openTeamOpenPost, _, err := sysadminClient.CreatePost(context.Background(), &model.Post{ChannelId: openTeamOpenChannel.Id}) require.NoError(t, err) // Alt team is a team without the sysadmin in it. altOpenTeam, _, err := client.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "alt-open-team", DisplayName: "Alt Open Team", AllowOpenInvite: true}) require.NoError(t, err) altOpenTeamOpenChannel, _, err := client.CreateChannel(context.Background(), &model.Channel{TeamId: altOpenTeam.Id, Type: model.ChannelTypeOpen, Name: "alt-open-team-open-channel", DisplayName: "Open Team - Open Channel"}) require.NoError(t, err) altOpenTeamOpenPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: altOpenTeamOpenChannel.Id}) require.NoError(t, err) inviteTeam, _, err := sysadminClient.CreateTeam(context.Background(), &model.Team{Type: model.TeamInvite, Name: "invite-team", DisplayName: "Invite Team"}) require.NoError(t, err) inviteTeamOpenChannel, _, err := sysadminClient.CreateChannel(context.Background(), &model.Channel{TeamId: inviteTeam.Id, Type: model.ChannelTypeOpen, Name: "invite-team-open-channel", DisplayName: "Invite Team - Open Channel"}) require.NoError(t, err) inviteTeamOpenPost, _, err := sysadminClient.CreatePost(context.Background(), &model.Post{ChannelId: inviteTeamOpenChannel.Id}) require.NoError(t, err) testCases := []struct { name string team *model.Team hasJoinedTeam bool channel *model.Channel hasJoinedChannel bool post *model.Post client *model.Client4 hasAccess bool }{ // Open channel - Current Team { name: "Open post - Current team - Basic user", team: th.BasicTeam, hasJoinedTeam: true, channel: openChannel, hasJoinedChannel: true, post: openPost, client: client, hasAccess: true, }, { name: "Open post - Current team - Sysadmin user", team: th.BasicTeam, hasJoinedTeam: true, channel: openChannel, hasJoinedChannel: true, post: openPost, client: sysadminClient, hasAccess: true, }, // Private channel - Current Team { name: "Private post by sysadmin - Current team - Basic user", team: th.BasicTeam, channel: privateChannel, post: privatePost, client: client, hasAccess: false, }, { name: "Private post by sysadmin - Current team - Sysadmin user", team: th.BasicTeam, hasJoinedTeam: true, channel: privateChannel, hasJoinedChannel: true, post: privatePost, client: sysadminClient, hasAccess: true, }, { name: "Private post by basic user - Current team - Basic user", team: th.BasicTeam, hasJoinedTeam: true, channel: privateChannelBasicUser, hasJoinedChannel: true, post: privatePostBasicUser, client: client, hasAccess: true, }, { name: "Private post by basic user - Current team - Sysadmin user", team: th.BasicTeam, hasJoinedTeam: true, channel: privateChannelBasicUser, hasJoinedChannel: false, post: privatePostBasicUser, client: sysadminClient, hasAccess: true, }, // GM channel { name: "GM post - Current team - Basic user", team: nil, channel: gmChannel, hasJoinedChannel: true, post: gmPost, client: client, hasAccess: true, }, { name: "GM post - Current team - Sysadmin user", team: nil, channel: gmChannel, post: gmPost, client: sysadminClient, hasAccess: true, }, // DM channel { name: "DM post - Current team - Basic user", team: nil, channel: dmChannel, hasJoinedChannel: true, post: dmPost, client: client, hasAccess: true, }, { name: "DM post - Current team - Sysadmin user", team: nil, channel: dmChannel, post: dmPost, client: sysadminClient, hasAccess: true, }, // Open channel - Open Team { name: "Open post - Open team - Basic user", team: openTeam, hasJoinedTeam: false, channel: openTeamOpenChannel, hasJoinedChannel: false, post: openTeamOpenPost, client: client, hasAccess: true, }, { name: "Open post - Open team - Sysadmin user", team: openTeam, hasJoinedTeam: true, channel: openTeamOpenChannel, hasJoinedChannel: true, post: openTeamOpenPost, client: sysadminClient, hasAccess: true, }, // Open channel - Alt Open Team { name: "Open post - Alt open team - Basic user", team: altOpenTeam, hasJoinedTeam: true, channel: altOpenTeamOpenChannel, hasJoinedChannel: true, post: altOpenTeamOpenPost, client: client, hasAccess: true, }, { name: "Open post - Alt open team - Sysadmin user", team: altOpenTeam, hasJoinedTeam: false, channel: altOpenTeamOpenChannel, hasJoinedChannel: false, post: altOpenTeamOpenPost, client: sysadminClient, hasAccess: true, }, // Open channel - Invite Team { name: "Open post - Invite team - Basic user", team: inviteTeam, channel: inviteTeamOpenChannel, post: inviteTeamOpenPost, client: client, hasAccess: false, }, { name: "Open post - Invite team - Sysadmin user", team: inviteTeam, hasJoinedTeam: true, channel: inviteTeamOpenChannel, hasJoinedChannel: true, post: inviteTeamOpenPost, client: sysadminClient, hasAccess: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { info, resp, err := tc.client.GetPostInfo(context.Background(), tc.post.Id) if !tc.hasAccess { require.Error(t, err) CheckNotFoundStatus(t, resp) return } require.NoError(t, err) CheckOKStatus(t, resp) require.Equal(t, tc.channel.Id, info.ChannelId) require.Equal(t, tc.channel.Type, info.ChannelType) require.Equal(t, tc.channel.DisplayName, info.ChannelDisplayName) require.Equal(t, tc.hasJoinedChannel, info.HasJoinedChannel) if tc.team != nil { teamType := "I" if tc.team.AllowOpenInvite { teamType = "O" } require.Equal(t, tc.team.Id, info.TeamId) require.Equal(t, teamType, info.TeamType) require.Equal(t, tc.team.DisplayName, info.TeamDisplayName) require.Equal(t, tc.hasJoinedTeam, info.HasJoinedTeam) } }) } } func TestAcknowledgePost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) client := th.Client post := th.BasicPost ack, _, err := client.AcknowledgePost(context.Background(), post.Id, th.BasicUser.Id) require.NoError(t, err) acks, appErr := th.App.GetAcknowledgementsForPost(post.Id) require.Nil(t, appErr) require.Len(t, acks, 1) require.Equal(t, acks[0], ack) _, resp, err := client.AcknowledgePost(context.Background(), "junk", th.BasicUser.Id) require.Error(t, err) CheckBadRequestStatus(t, resp) _, resp, err = client.AcknowledgePost(context.Background(), GenerateTestID(), th.BasicUser.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) _, resp, err = client.AcknowledgePost(context.Background(), post.Id, "junk") require.Error(t, err) CheckBadRequestStatus(t, resp) _, resp, err = client.AcknowledgePost(context.Background(), post.Id, th.BasicUser2.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) _, err = client.Logout(context.Background()) require.NoError(t, err) _, resp, err = client.AcknowledgePost(context.Background(), post.Id, th.BasicUser.Id) require.Error(t, err) CheckUnauthorizedStatus(t, resp) _, _, err = th.SystemAdminClient.AcknowledgePost(context.Background(), post.Id, th.SystemAdminUser.Id) require.NoError(t, err) } func TestUnacknowledgePost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) client := th.Client post := th.BasicPost ack, _, err := client.AcknowledgePost(context.Background(), post.Id, th.BasicUser.Id) require.NoError(t, err) acks, appErr := th.App.GetAcknowledgementsForPost(post.Id) require.Nil(t, appErr) require.Len(t, acks, 1) require.Equal(t, acks[0], ack) resp, err := client.UnacknowledgePost(context.Background(), "junk", th.BasicUser.Id) require.Error(t, err) CheckBadRequestStatus(t, resp) resp, err = client.UnacknowledgePost(context.Background(), GenerateTestID(), th.BasicUser.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) resp, err = client.UnacknowledgePost(context.Background(), post.Id, "junk") require.Error(t, err) CheckBadRequestStatus(t, resp) resp, err = client.UnacknowledgePost(context.Background(), post.Id, th.BasicUser2.Id) require.Error(t, err) CheckForbiddenStatus(t, resp) _, err = client.UnacknowledgePost(context.Background(), post.Id, th.BasicUser.Id) require.NoError(t, err) acks, appErr = th.App.GetAcknowledgementsForPost(post.Id) require.Nil(t, appErr) require.Len(t, acks, 0) _, err = client.Logout(context.Background()) require.NoError(t, err) resp, err = client.UnacknowledgePost(context.Background(), post.Id, th.BasicUser.Id) require.Error(t, err) CheckUnauthorizedStatus(t, resp) } func TestRestorePostVersion(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() client := th.Client t.Run("should restore post version successfully", func(t *testing.T) { post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "original message", UserId: th.BasicUser.Id, } createdPost, response, err := client.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, response) patch, response, err := client.PatchPost(context.Background(), createdPost.Id, &model.PostPatch{ Message: model.NewPointer("edited message 1"), }) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, "edited message 1", patch.Message) patch, response, err = client.PatchPost(context.Background(), createdPost.Id, &model.PostPatch{ Message: model.NewPointer("edited message 2"), }) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, "edited message 2", patch.Message) // verify edit history editHistory, response, err := client.GetEditHistoryForPost(context.Background(), createdPost.Id) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, 2, len(editHistory)) require.Equal(t, "edited message 1", editHistory[0].Message) require.Equal(t, "original message", editHistory[1].Message) // now we'll restore to the original version restoredPost, response, err := client.RestorePostVersion(context.Background(), createdPost.Id, editHistory[1].Id) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, "original message", restoredPost.Message) require.Equal(t, createdPost.Id, restoredPost.Id) // verify restored post fetchedPost, response, err := client.GetPost(context.Background(), createdPost.Id, "") require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, "original message", fetchedPost.Message) // verify edit history after restoring editHistory, response, err = client.GetEditHistoryForPost(context.Background(), createdPost.Id) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, 3, len(editHistory)) require.Equal(t, "edited message 2", editHistory[0].Message) require.Equal(t, "edited message 1", editHistory[1].Message) require.Equal(t, "original message", editHistory[2].Message) }) t.Run("should restore post version successfully with files", func(t *testing.T) { fileResp, _, err := client.UploadFile(context.Background(), []byte("data"), th.BasicChannel.Id, "test") require.NoError(t, err) fileId := fileResp.FileInfos[0].Id post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "original message", UserId: th.BasicUser.Id, FileIds: model.StringArray{fileId}, } createdPost, response, err := client.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, response) require.Equal(t, 1, len(createdPost.FileIds)) patch, response, err := client.PatchPost(context.Background(), createdPost.Id, &model.PostPatch{ Message: model.NewPointer("edited message 1"), FileIds: &model.StringArray{}, }) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, "edited message 1", patch.Message) require.Equal(t, 0, len(patch.FileIds)) // verify edit history editHistory, response, err := client.GetEditHistoryForPost(context.Background(), createdPost.Id) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, 1, len(editHistory)) require.Equal(t, "original message", editHistory[0].Message) require.Equal(t, 1, len(editHistory[0].FileIds)) // now we'll restore to the original version restoredPost, response, err := client.RestorePostVersion(context.Background(), createdPost.Id, editHistory[0].Id) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, "original message", restoredPost.Message) require.Equal(t, createdPost.Id, restoredPost.Id) require.Equal(t, 1, len(restoredPost.FileIds)) // verify restored post fetchedPost, response, err := client.GetPost(context.Background(), createdPost.Id, "") require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, "original message", fetchedPost.Message) require.Equal(t, 1, len(fetchedPost.FileIds)) // verify edit history after restoring editHistory, response, err = client.GetEditHistoryForPost(context.Background(), createdPost.Id) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, 2, len(editHistory)) require.Equal(t, "edited message 1", editHistory[0].Message) require.Equal(t, 0, len(editHistory[0].FileIds)) require.Equal(t, "original message", editHistory[1].Message) require.Equal(t, 1, len(editHistory[1].FileIds)) }) t.Run("should get error when trying to restore non existent post ori history ID", func(t *testing.T) { restoredPost, response, err := client.RestorePostVersion(context.Background(), model.NewId(), model.NewId()) require.Error(t, err) CheckForbiddenStatus(t, response) require.Nil(t, restoredPost) post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "original message", UserId: th.BasicUser.Id, } createdPost, response, err := client.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, response) restoredPost, response, err = client.RestorePostVersion(context.Background(), createdPost.Id, model.NewId()) require.Error(t, err) CheckForbiddenStatus(t, response) require.Nil(t, restoredPost) post2 := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "original message 2", UserId: th.BasicUser.Id, } createdPost, response, err = client.CreatePost(context.Background(), post2) require.NoError(t, err) CheckCreatedStatus(t, response) restoredPost, response, err = client.RestorePostVersion(context.Background(), createdPost.Id, post2.Id) require.Error(t, err) CheckNotFoundStatus(t, response) require.Nil(t, restoredPost) }) t.Run("user should not be able to restore someone else's post", func(t *testing.T) { post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "original message", UserId: th.BasicUser.Id, } createdPost, response, err := client.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, response) patch, response, err := client.PatchPost(context.Background(), createdPost.Id, &model.PostPatch{ Message: model.NewPointer("edited message 1"), }) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, "edited message 1", patch.Message) // verify edit history editHistory, response, err := client.GetEditHistoryForPost(context.Background(), createdPost.Id) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, 1, len(editHistory)) require.Equal(t, "original message", editHistory[0].Message) // now we'll restore to the original version th.LoginBasic2() restoredPost, response, err := th.Client.RestorePostVersion(context.Background(), createdPost.Id, editHistory[0].Id) require.Error(t, err) CheckForbiddenStatus(t, response) require.Nil(t, restoredPost) }) t.Run("system admin should not be able to restore someone else's post", func(t *testing.T) { th.LoginBasic() post := &model.Post{ ChannelId: th.BasicChannel.Id, Message: "original message", UserId: th.BasicUser.Id, } createdPost, response, err := th.Client.CreatePost(context.Background(), post) require.NoError(t, err) CheckCreatedStatus(t, response) patch, response, err := th.Client.PatchPost(context.Background(), createdPost.Id, &model.PostPatch{ Message: model.NewPointer("edited message 1"), }) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, "edited message 1", patch.Message) // verify edit history editHistory, response, err := th.Client.GetEditHistoryForPost(context.Background(), createdPost.Id) require.NoError(t, err) CheckOKStatus(t, response) require.Equal(t, 1, len(editHistory)) require.Equal(t, "original message", editHistory[0].Message) // now we'll restore to the original version th.LoginSystemAdmin() restoredPost, response, err := th.SystemAdminClient.RestorePostVersion(context.Background(), createdPost.Id, editHistory[0].Id) require.Error(t, err) CheckForbiddenStatus(t, response) require.Nil(t, restoredPost) }) }