Files
mostlymatter/server/channels/api4/post_test.go
Agniva De Sarker d8dbb6cc22 MM-56548: [AI assisted]Add support for incremental thread loading using UpdateAt timestamp (#30486)
Every time we load the RHS, we used to load the FULL thread always. Although
the actual ThreadViewer React component is virtualized, and the server side
API call is paginated, we still went through all the pages, to get the full
thread and passed it on to the ThreadViewer. This would be for first loads,
and subsequent loads of the same thread.

This was a bug originally, but then it was a necessity after we applied websocket event scope because
now we won't get emoji reactions of a thread if the user is not on the thread.

To fix that, we enhance the thread loading functionality by adding support for fetching
thread updates based on the UpdateAt timestamp. Now, for subsequent loads,
we only get the changed posts in a thread. The implementation:

- Adds new API parameters: fromUpdateAt and updatesOnly to the GetPostThread endpoint
- Updates database queries to support sorting and filtering by UpdateAt
- Implements thread state management to track the last update timestamp
- Adds client-side support to use incremental loading for improved performance
- Ensures proper validation for parameter combinations and error handling

This change enables more efficient thread loading, particularly for long threads
with frequent updates, by only fetching posts that have been updated since the
last view.

Caveats: For delta updates, the SQL query won't use the best index possible
because we have an index for (CreateAt, Id), but no index for (UpdateAt, Id).
However, from my tests, it is not as bad as it looks:

```
[loadtest] # EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM Posts WHERE Posts.DeleteAt = 0 AND Posts.RootId = 'qbr5gctu9iyg8c36hpcq6f3w8e' AND Posts.UpdateAt > 1623445795824 ORDER BY UpdateAt ASC, Id ASC LIMIT 61;
                                                                   QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=8.31..8.31 rows=1 width=216) (actual time=0.047..0.049 rows=0 loops=1)
   Buffers: shared hit=2
   ->  Sort  (cost=8.31..8.31 rows=1 width=216) (actual time=0.044..0.045 rows=0 loops=1)
         Sort Key: updateat, id
         Sort Method: quicksort  Memory: 25kB
         Buffers: shared hit=2
         ->  Index Scan using idx_posts_root_id_delete_at on posts  (cost=0.28..8.30 rows=1 width=216) (actual time=0.031..0.032 rows=0 loops=1)
               Index Cond: (((rootid)::text = 'qbr5gctu9iyg8c36hpcq6f3w8e'::text) AND (deleteat = 0))
               Filter: (updateat > '1623445795824'::bigint)
               Buffers: shared hit=2
 Planning:
   Buffers: shared hit=3
 Planning Time: 0.508 ms
 Execution Time: 0.106 ms
(14 rows)
```

We still get an index scan with index cond. Although there's a filter element, but atleast we get the whole thread with the index.
My thinking is that while the whole thread might be large, but after that, updates on a thread should be incremental.
Therefore, we should be okay without adding yet another index on the posts table.

This is just the first step in what could be potentially improved further.

1. We shouldn't even be loading the full thread always. But rather let the virtualized viewer
load more posts on demand.
2. If a post has been just reacted to, then we need not send the whole post down, but just the
reaction. This further saves bandwidth.

https://mattermost.atlassian.net/browse/MM-56548

TBD: Add load-test coverage to update the thread loading code

```release-note
NONE
```
---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2025-04-22 10:43:13 +05:30

5421 строка
196 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"
"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) {
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) {
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) {
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) {
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,
) {
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)
})
}
func TestCreatePostPublic(t *testing.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) {
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) {
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) {
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) {
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 := 0; i < len(fileIds); i++ {
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) {
// 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) {
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 := 0; i < len(fileIDs); i++ {
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) {
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) {
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) {
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)
experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalViewArchivedChannels = true })
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.ExperimentalViewArchivedChannels = experimentalViewArchivedChannels
})
// the endpoint should work fine when viewing archived channels is enabled
_, _, err = c.GetPostsForChannel(context.Background(), channel.Id, 0, 10, "", false, false)
require.NoError(t, err)
// the endpoint should return forbidden if viewing archived channels is disabled
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalViewArchivedChannels = false })
_, resp, err = c.GetPostsForChannel(context.Background(), channel.Id, 0, 10, "", false, false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}, "Should forbid to retrieve posts if the channel is archived and users are not allowed to view archived messages")
_, 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) {
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) {
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) {
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) {
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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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) {
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) {
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) {
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) {
t.Run("WithChannelIteration", func(t *testing.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) {
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) {
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.Close()
require.NoError(t, th.TestLogger.Flush())
}
func TestDeletePostEvent(t *testing.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) {
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) {
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) {
th := Setup(t).InitBasic()
defer th.TearDown()
experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels
})
}()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
})
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")
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
})
posts, _, err = client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
require.Len(t, posts.Order, 1, "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) {
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) {
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) {
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) {
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 := 0; i < 3; i++ {
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) {
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) {
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(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) {
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) {
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) {
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)
for k, v := range tc.notifyProps {
patch.NotifyProps[k] = v
}
// 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) {
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) {
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) {
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) {
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) {
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)
})
}