mattermost/server/channels/api4/post_test.go
Rajat Dabade c7f6efdfb0
Guest cannot add file to post without upload_file permission (#34538)
* Guest cannot add file to post without upload_file permission

* Move checks to api layer, addd checks in update patch post scheduled post

* Minor

* Linter fixes

* i18n translations

* removed the duplicated check from scheduled_post app layer

* Move scheduled post permission test from app layer to API layer

The permission check for updating scheduled posts belonging to other
users was moved from the app layer to the API layer in the PR. This
commit moves the corresponding test to the API layer to match.

* Move scheduled post delete permission check to API layer

Move the permission check for deleting scheduled posts from the app
layer to the API layer, consistent with update permission check.
Also enhance API tests to verify posts aren't modified after forbidden
operations.

* Fix inconsistent status code for non-existent scheduled post

Return StatusNotFound instead of StatusInternalServerError when a
scheduled post doesn't exist in UpdateScheduledPost, matching the
API layer behavior.

* Fix flaky TestAddUserToChannelCreatesChannelMemberHistoryRecord test

Use ElementsMatch instead of Equal to compare user ID slices since the
order returned from GetUsersInChannelDuring is not guaranteed.

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Jesse Hallam <jesse@mattermost.com>
2026-01-07 10:40:05 -04:00

6141 lines
219 KiB
Go

// 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"
)
// Helper to enable feature with license
func enableBurnOnReadFeature(th *TestHelper) {
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.EnableBurnOnRead = model.NewPointer(true)
})
}
func TestCreatePost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
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(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.RemovePermissionFromRole(t, 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("should prevent creating post with files when user lacks upload_file permission in target channel", func(t *testing.T) {
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), th.BasicChannel.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
defer func() {
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
}()
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Test post with file",
FileIds: model.StringArray{fileId},
}
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("should allow creating post with files when user has upload_file permission", func(t *testing.T) {
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), th.BasicChannel.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Test post with file",
FileIds: model.StringArray{fileId},
}
rpost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, rpost)
assert.Contains(t, rpost.FileIds, fileId)
})
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(t)
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(t)
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(t)
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(t)
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(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
client := th.Client
ctx := context.Background()
basicUser1 := th.BasicUser
basicUser2 := th.BasicUser2
basicUser3 := th.CreateUser(t)
// 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(t)
th.AddUserToChannel(t, th.TeamAdminUser, publicChannel)
defer th.LoginBasic(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 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(t)
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(t)
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(t)
client := th.Client
WebSocketClient := th.CreateConnectedWebSocketClient(t)
inChannelUser := th.CreateUser(t)
th.LinkUserToTeam(t, 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(t)
th.LinkUserToTeam(t, 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(t)
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(t)
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("should prevent updating post with files when user lacks upload_file permission in target channel", func(t *testing.T) {
postWithoutFiles, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "Post without files",
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), channel.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
defer func() {
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
}()
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: channel.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, updatedPost)
})
t.Run("should allow updating post with files when user has upload_file permission", func(t *testing.T) {
postWithoutFiles, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "Post without files",
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), channel.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: channel.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, updatedPost)
assert.Contains(t, updatedPost.FileIds, fileId)
})
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(t)
_, 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(t)
_, 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(t)
// 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(t)
// 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(t)
// 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(t)
// 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(t)
dmChannel := th.CreateDmChannel(t, 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(t)
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(t)
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(t)
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(t)
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(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.RemovePermissionFromRole(t, model.PermissionEditPost.Id, model.ChannelUserRoleId)
th.AddPermissionToRole(t, 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(t)
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(t)
client := th.Client
pinnedPost := th.CreatePinnedPost(t)
_, 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(t)
client := th.Client
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
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(t)
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(t)
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(t) // post6
post7 := th.CreatePost(t)
post8 := th.CreatePost(t)
th.CreatePost(t) // post9
post10 := th.CreatePost(t)
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(t)
th.CreatePostWithClient(t, 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(t)
th.CreateMessagePostNoClient(t, 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(t)
th.CreateMessagePostNoClient(t, 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(t, th.BasicUser2)
th.CreateMessagePostNoClient(t, 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(t)
gmChannel, _, err := th.Client.CreateGroupChannel(context.Background(), []string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id})
require.NoError(t, err)
th.CreateMessagePostNoClient(t, 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(t)
client := th.Client
user := th.BasicUser
team1 := th.BasicTeam
channel1 := th.BasicChannel
post1 := th.CreatePost(t)
channel2 := th.CreatePublicChannel(t)
post2 := th.CreatePostWithClient(t, 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(t)
post4 := th.CreatePostWithClient(t, 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(t, th.SystemAdminClient, model.ChannelTypePrivate)
post5 := th.CreatePostWithClient(t, 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(t, 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(t)
client := th.Client
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
post3 := th.CreatePost(t)
post4 := th.CreatePost(t)
post5 := th.CreatePost(t)
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(t)
th.CreatePost(t) // post7
post8 := th.CreatePost(t)
post9 := th.CreatePost(t)
post10 := th.CreatePost(t) // 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(t)
client := th.Client
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
post3 := th.CreatePost(t)
post4 := th.CreatePost(t)
post5 := th.CreatePost(t)
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(t)
th.CreatePost(t) // post7
post8 := th.CreatePost(t)
post9 := th.CreatePost(t)
post10 := th.CreatePost(t)
// 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(t)
client := th.Client
userId := th.BasicUser.Id
channelId := th.BasicChannel.Id
// 12 posts = 2 systems posts + 10 created posts below
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
post3 := th.CreatePost(t)
post4 := th.CreatePost(t)
post5 := th.CreatePost(t)
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(t)
post12, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: channelId,
Message: model.NewId(),
RootId: post4.Id,
})
require.NoError(t, err)
post13 := th.CreatePost(t)
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(t)
// 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(t, 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(t)
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(t)
post2 := th.CreatePost(t)
user := th.CreateUser(t)
_, 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(t)
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(t)
_, 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(t)
post2 := th.CreatePost(t)
user := th.CreateUser(t)
_, 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(t)
_testWebHubMembership(th, t)
})
t.Run("WithoutChannelIteration", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_testWebHubMembership(th, t)
})
}
func _testWebHubMembership(th *TestHelper, t *testing.T) {
t.Helper()
u1 := th.CreateUser(t)
th.LinkUserToTeam(t, u1, th.BasicTeam)
th.AddUserToChannel(t, u1, th.BasicChannel)
ch2 := th.CreatePrivateChannel(t)
u2 := th.CreateUser(t)
th.LinkUserToTeam(t, u2, th.BasicTeam)
th.AddUserToChannel(t, 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(t)
// Add u1 to ch2
th.AddUserToChannel(t, u1, ch2)
// Send post to ch2
th.CreatePostWithClient(t, th.Client, ch2)
// Remove u1 from ch2
th.RemoveUserFromChannel(t, u1, ch2)
// Send post to ch2
th.CreatePostWithClient(t, 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(t)
defer func() {
_, 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(t)
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(t)
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, th.SystemAdminUser, th.BasicChannel, false)
require.Nil(t, appErr)
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(t)
_, 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(t)
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(t, 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(t)
th.LoginBasic(t)
client := th.Client
message := "search for post1"
_ = th.CreateMessagePost(t, message)
message = "search for post2"
post2 := th.CreateMessagePost(t, message)
message = "#hashtag search for post3"
post3 := th.CreateMessagePost(t, message)
message = "hashtag for post4"
_ = th.CreateMessagePost(t, message)
archivedChannel := th.CreatePublicChannel(t)
_ = th.CreateMessagePostWithClient(t, th.Client, archivedChannel, "#hashtag for post3")
_, err := th.Client.DeleteChannel(context.Background(), archivedChannel.Id)
require.NoError(t, err)
otherTeam := th.CreateTeam(t)
channelInOtherTeam := th.CreateChannelWithClientAndTeam(t, th.Client, model.ChannelTypeOpen, otherTeam.Id)
_ = th.AddUserToChannel(t, th.BasicUser, channelInOtherTeam)
_ = th.CreateMessagePostWithClient(t, 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(t)
th.LoginBasic(t)
client := th.Client
message := "#sgtitlereview with space"
assert.NotNil(t, th.CreateMessagePost(t, message))
message = "#sgtitlereview\n with return"
assert.NotNil(t, th.CreateMessagePost(t, message))
message = "no hashtag"
assert.NotNil(t, th.CreateMessagePost(t, 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(t)
th.LoginBasic(t)
client := th.Client
channel := th.CreatePublicChannel(t)
message := "sgtitlereview with space"
_ = th.CreateMessagePost(t, message)
message = "sgtitlereview\n with return"
_ = th.CreateMessagePostWithClient(t, client, th.BasicChannel2, message)
message = "other message with no return"
_ = th.CreateMessagePostWithClient(t, client, th.BasicChannel2, message)
message = "other message with no return"
_ = th.CreateMessagePostWithClient(t, 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(t)
client := th.Client
th.LoginTeamAdmin(t)
user := th.CreateUser(t)
th.LinkUserToTeam(t, 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(t, message)
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic2(t)
message = "sgtitlereview\n with return"
_ = th.CreateMessagePostWithClient(t, 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(t, 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(t, 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(t)
th.LoginBasic(t)
client := th.Client
message := "sgtitlereview\n with return"
createDate := time.Date(2018, 8, 1, 5, 0, 0, 0, time.UTC)
_ = th.CreateMessagePostNoClient(t, 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(t, 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(t, 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(t)
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(t)
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(t, c1, "AAA", now)
p2 := th.CreateMessagePostNoClient(t, c1, "BBB", now+10)
th.CreateMessagePostNoClient(t, c1, "CCC", now+20)
pp1 := th.CreateMessagePostNoClient(t, th.BasicPrivateChannel, "Sssh!", now)
pp2 := th.CreateMessagePostNoClient(t, 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(t, u2)
th.CreateMessagePostNoClient(t, dc, "test1", now)
p := th.CreateMessagePostNoClient(t, dc, "test2", now+10)
require.NotNil(t, p)
th.CreateMessagePostNoClient(t, dc, "test3", now+20)
p1 := th.CreateMessagePostNoClient(t, 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(t, th.CreateUser(t))
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(t)
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(t)
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(t)
client := th.Client
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
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(t)
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(t)
_, 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(t)
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(t)
rpost := th.CreatePost(t)
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(t)
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(t)
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(t)
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.RemovePermissionFromRole(t, model.PermissionManagePrivateChannelMembers.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionManagePrivateChannelMembers.Id, model.ChannelUserRoleId)
th.RemovePermissionFromRole(t, 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(t)
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(t)
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(t)
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(t)
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(t)
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(t)
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(t)
restoredPost, response, err := th.SystemAdminClient.RestorePostVersion(context.Background(), createdPost.Id, editHistory[0].Id)
require.Error(t, err)
CheckForbiddenStatus(t, response)
require.Nil(t, restoredPost)
})
}
func TestRevealPost(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_BURNONREAD", "true")
t.Cleanup(func() {
os.Unsetenv("MM_FEATUREFLAGS_BURNONREAD")
})
th := SetupEnterprise(t).InitBasic(t)
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, th.BasicChannel)
// Helper to create burn-on-read post
createBurnOnReadPost := func(client *model.Client4, channel *model.Channel) *model.Post {
post := &model.Post{
ChannelId: channel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, createdPost)
return createdPost
}
// Helper to create and login second user
createSecondUser := func(channel *model.Channel) (*model.User, *model.Client4) {
user2 := th.CreateUser(t)
th.LinkUserToTeam(t, user2, th.BasicTeam)
if channel != nil {
th.AddUserToChannel(t, user2, channel)
}
client2 := th.CreateClient()
_, _, err := client2.Login(context.Background(), user2.Email, user2.Password)
require.NoError(t, err)
t.Cleanup(func() {
_, err = client2.Logout(context.Background())
require.NoError(t, err)
})
return user2, client2
}
t.Run("feature not enabled, should still allow reveal", func(t *testing.T) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(th.SystemAdminClient, th.BasicChannel)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.FeatureFlags.BurnOnRead = false
})
revealedPost, resp, err := th.Client.RevealPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, revealedPost)
require.Equal(t, post.Id, revealedPost.Id)
require.Equal(t, "burn on read message", revealedPost.Message)
require.NotNil(t, revealedPost.Metadata)
require.NotZero(t, revealedPost.Metadata.ExpireAt)
})
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
regularPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "regular message",
}
createdPost, resp, err := th.Client.CreatePost(context.Background(), regularPost)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, client2 := createSecondUser(th.BasicChannel)
revealedPost, resp, err := client2.RevealPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "app.reveal_post.not_burn_on_read.app_error")
require.Nil(t, revealedPost)
}, "reveal regular post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
revealedPost, resp, err := th.Client.RevealPost(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
require.Nil(t, revealedPost)
}, "reveal non-existing post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(client, th.BasicChannel)
revealedPost, resp, err := client.RevealPost(context.Background(), post.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Nil(t, revealedPost)
CheckErrorID(t, err, "api.post.reveal_post.cannot_reveal_own_post.app_error")
}, "try reveal own post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
revealedPost, resp, err := client.RevealPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, revealedPost)
require.Equal(t, post.Id, revealedPost.Id)
require.Equal(t, "burn on read message", revealedPost.Message)
require.NotNil(t, revealedPost.Metadata)
require.NotZero(t, revealedPost.Metadata.ExpireAt)
}, "reveal someone elses post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
_, client2 := createSecondUser(th.BasicChannel)
createdPost, resp, err := client2.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
// Manually expire the post
storePost, err := th.App.Srv().Store().Post().Get(th.Context, createdPost.Id, model.GetPostsOptions{}, "", th.App.Config().GetSanitizeOptions())
require.NoError(t, err)
require.Len(t, storePost.Posts, 1)
postToUpdate := storePost.Posts[createdPost.Id]
postToUpdate.AddProp(model.PostPropsExpireAt, model.GetMillis()-1000)
_, err = th.App.Srv().Store().Post().Overwrite(th.Context, postToUpdate)
require.NoError(t, err)
revealedPost, resp, err := client.RevealPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Nil(t, revealedPost)
CheckErrorID(t, err, "app.reveal_post.post_expired.app_error")
}, "reveal expired post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
user := th.BasicUser
if client == th.SystemAdminClient {
user = th.SystemAdminUser
}
// Create expired read receipt
readReceipt, err := th.App.Srv().Store().ReadReceipt().Save(th.Context, &model.ReadReceipt{
PostID: post.Id,
UserID: user.Id,
ExpireAt: model.GetMillis() - 1000,
})
require.NoError(t, err)
require.NotNil(t, readReceipt)
revealedPost, resp, err := client.RevealPost(context.Background(), post.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
require.Nil(t, revealedPost)
CheckErrorID(t, err, "app.post.get.app_error")
}, "reveal post with expired read receipt")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(nil)
privateChannel, resp, err := client2.CreateChannel(context.Background(), &model.Channel{
TeamId: th.BasicTeam.Id,
Type: model.ChannelTypePrivate,
Name: GenerateTestChannelName(),
DisplayName: "Private Channel",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
post := &model.Post{
ChannelId: privateChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := client2.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
revealedPost, resp, err := client.RevealPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
require.Nil(t, revealedPost)
}, "user without channel access")
}
func TestCreateBurnOnReadPost(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_BURNONREAD", "true")
t.Cleanup(func() {
os.Unsetenv("MM_FEATUREFLAGS_BURNONREAD")
})
th := SetupEnterprise(t).InitBasic(t)
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, th.BasicChannel)
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
createdPost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, createdPost)
require.Equal(t, model.PostTypeBurnOnRead, createdPost.Type)
}, "create burn on read post")
t.Run("reveal burn on read post and verify in channel posts", func(t *testing.T) {
enableBurnOnReadFeature(th)
// Create burn-on-read post with basic user
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := th.Client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, createdPost)
// Create websocket client for system admin to receive reveal event
wsClient := th.CreateConnectedWebSocketClientWithClient(t, th.SystemAdminClient)
// Get posts for channel with system admin client - verify post is not revealed by default
posts, resp, err := th.SystemAdminClient.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, posts)
require.NotNil(t, posts.Posts[createdPost.Id])
unrevealedPost := posts.Posts[createdPost.Id]
require.Equal(t, "", unrevealedPost.Message)
// Check if the metadata is empty
require.Equal(t, model.PostMetadata{}, *unrevealedPost.Metadata)
// Reveal the post with system admin client
revealedPost, resp, err := th.SystemAdminClient.RevealPost(context.Background(), createdPost.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, revealedPost)
require.Equal(t, "burn on read message", revealedPost.Message)
require.NotNil(t, revealedPost.Metadata)
require.NotZero(t, revealedPost.Metadata.ExpireAt)
// Verify websocket client receives the reveal event
var eventPost model.Post
require.Eventually(t, func() bool {
select {
case event := <-wsClient.EventChannel:
if event.EventType() == model.WebsocketEventPostRevealed {
eventPostJSON, ok := event.GetData()["post"].(string)
if !ok {
return false
}
err = json.Unmarshal([]byte(eventPostJSON), &eventPost)
if err != nil {
return false
}
return eventPost.Id == createdPost.Id
}
default:
return false
}
return false
}, 5*time.Second, 100*time.Millisecond, "should have received post_revealed websocket event")
require.Equal(t, createdPost.Id, eventPost.Id)
require.Equal(t, "burn on read message", eventPost.Message)
require.NotNil(t, eventPost.Metadata)
require.NotZero(t, eventPost.Metadata.ExpireAt)
// Get the single post - verify it's revealed
singlePost, resp, err := th.SystemAdminClient.GetPost(context.Background(), createdPost.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, singlePost)
require.Equal(t, "burn on read message", singlePost.Message)
require.NotNil(t, singlePost.Metadata)
require.NotZero(t, singlePost.Metadata.ExpireAt)
// Query for posts in channel again - verify this time it's revealed
postsAfterReveal, resp, err := th.SystemAdminClient.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, postsAfterReveal)
require.NotNil(t, postsAfterReveal.Posts[createdPost.Id])
revealedPostInChannel := postsAfterReveal.Posts[createdPost.Id]
require.Equal(t, "burn on read message", revealedPostInChannel.Message)
require.NotNil(t, revealedPostInChannel.Metadata)
require.NotZero(t, revealedPostInChannel.Metadata.ExpireAt)
})
t.Run("Create post send back pending post ID for post creator", func(t *testing.T) {
post := &model.Post{
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
PendingPostId: model.NewId(),
}
createdPost, response, err := th.Client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, response)
require.NotNil(t, createdPost)
require.Equal(t, post.PendingPostId, createdPost.PendingPostId)
})
}
func TestBurnPost(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_BURNONREAD", "true")
t.Cleanup(func() {
os.Unsetenv("MM_FEATUREFLAGS_BURNONREAD")
})
th := SetupEnterprise(t).InitBasic(t)
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, th.BasicChannel)
// Helper to create burn-on-read post
createBurnOnReadPost := func(client *model.Client4, channel *model.Channel) *model.Post {
post := &model.Post{
ChannelId: channel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, createdPost)
return createdPost
}
// Helper to create and login second user
createSecondUser := func(channel *model.Channel) (*model.User, *model.Client4) {
user2 := th.CreateUser(t)
th.LinkUserToTeam(t, user2, th.BasicTeam)
if channel != nil {
th.AddUserToChannel(t, user2, channel)
}
client2 := th.CreateClient()
_, _, err := client2.Login(context.Background(), user2.Email, user2.Password)
require.NoError(t, err)
t.Cleanup(func() {
_, err = client2.Logout(context.Background())
require.NoError(t, err)
})
return user2, client2
}
t.Run("feature not enabled, burn post allowed", func(t *testing.T) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(th.SystemAdminClient, th.BasicChannel)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.EnableBurnOnRead = model.NewPointer(false)
})
_, resp, err := th.Client.RevealPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
resp, err = th.Client.BurnPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
})
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
regularPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "regular message",
}
createdPost, resp, err := th.Client.CreatePost(context.Background(), regularPost)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
resp, err = client.BurnPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "app.burn_post.not_burn_on_read.app_error")
}, "burn regular post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
resp, err := client.BurnPost(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}, "burn non-existing post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(client, th.BasicChannel)
resp, err := client.BurnPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
// Verify post is permanently deleted
_, resp, err = client.GetPost(context.Background(), post.Id, "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}, "author burns own post - permanently deleted")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
resp, err := client.BurnPost(context.Background(), post.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "app.burn_post.not_revealed.app_error")
}, "non-author burns post without read receipt")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
// Create websocket client to receive burn event
wsClient := th.CreateConnectedWebSocketClientWithClient(t, client)
// Create expired read receipt
revealedPost, resp, err := client.RevealPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, revealedPost)
resp, err = client.BurnPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
userID := th.BasicUser.Id
if client == th.SystemAdminClient {
userID = th.SystemAdminUser.Id
}
// Verify receipt ExpireAt is unchanged (no-op)
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, post.Id, userID)
require.NoError(t, err)
require.LessOrEqual(t, receipt.ExpireAt, revealedPost.Metadata.ExpireAt)
// Verify websocket client receives the burn event
var eventPostID string
require.Eventually(t, func() bool {
select {
case event := <-wsClient.EventChannel:
if event.EventType() == model.WebsocketEventPostBurned {
var ok bool
eventPostID, ok = event.GetData()["post_id"].(string)
if !ok {
return false
}
return eventPostID == post.Id
}
default:
return false
}
return false
}, 5*time.Second, 100*time.Millisecond, "should have received post_burned websocket event")
require.Equal(t, post.Id, eventPostID)
}, "non-author burns post with expired read receipt")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(nil)
privateChannel, resp, err := client2.CreateChannel(context.Background(), &model.Channel{
TeamId: th.BasicTeam.Id,
Type: model.ChannelTypePrivate,
Name: GenerateTestChannelName(),
DisplayName: "Private Channel",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
post := &model.Post{
ChannelId: privateChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := client2.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
resp, err = client.BurnPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}, "user without channel access")
t.Run("unauthorized access", func(t *testing.T) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(th.Client, th.BasicChannel)
// Create unauthenticated client
unauthClient := th.CreateClient()
resp, err := unauthClient.BurnPost(context.Background(), post.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}