mattermost-community-enterp.../channels/api4/post_test.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

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