Files
zeekay 8feb24ea1f One name for this fork, and it is ours
The server module named github.com/hanzoai/team/server/v8 — a path that
resolves to the retired workspace's repository, not this one — while
server/public still published github.com/mattermost/mattermost/server/public.
Both are github.com/hanzoteam/server/... now, which is where this code
actually lives.

That second half is what made the binary unbootable. Upstream's public path
stayed claimed by upstream, mattermost-plugin-ai imported it, and the build
linked public/model twice; public/plugin's gob.Register then ran twice under
one name and panicked at init, before main. Reverting the rename traded the
brand for a working binary. Rebranding the plugin alongside it needs no such
trade: the agents fork is github.com/hanzoteam/agents/v2 against our public
module, so no module named mattermost/mattermost is in the graph at all.

Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and
it is ours.
2026-08-28 23:35:36 -07:00

7944 lines
285 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"
"io"
"maps"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/server/public/plugin/plugintest/mock"
"github.com/hanzoteam/server/server/public/shared/mlog"
"github.com/hanzoteam/server/server/v8/channels/app"
"github.com/hanzoteam/server/server/v8/channels/store/storetest/mocks"
"github.com/hanzoteam/server/server/v8/channels/testlib"
"github.com/hanzoteam/server/server/v8/channels/utils"
"github.com/hanzoteam/server/server/v8/channels/utils/testutils"
)
// Helper to enable feature with license
func enableBurnOnReadFeature(th *TestHelper) {
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.EnableBurnOnRead = new(true)
})
}
// isolatedTeamChannelWithTeamScheme creates a fresh team (with its own team scheme) and a channel on that team.
// Permission edits target scheme-specific role names so parallel api4 tests do not mutate built-in channel_user,
// and upload_file is revoked on the team scheme's channel user role so MergeChannelHigherScopedPermissions does not
// reintroduce it from the higher scope (model.Role.MergeChannelHigherScopedPermissions).
func isolatedTeamChannelWithTeamScheme(t *testing.T, th *TestHelper) (*model.Channel, *model.Scheme) {
t.Helper()
require.NoError(t, th.App.SetPhase2PermissionsMigrationStatus(true))
team := th.CreateTeamWithClient(t, th.SystemAdminClient)
th.LinkUserToTeam(t, th.BasicUser, team)
scheme := th.SetupTeamScheme(t)
team.SchemeId = &scheme.Id
_, appErr := th.App.UpdateTeamScheme(team)
require.Nil(t, appErr)
ch := th.CreateChannelWithClientAndTeam(t, th.Client, model.ChannelTypeOpen, team.Id)
return ch, scheme
}
// isolatedNoSchemeChannelWithoutUploadFile creates a fresh channel inside th.BasicTeam (which has no team scheme)
// and gives th.BasicUser an isolated channel-member role on it that does not grant upload_file. This exercises the
// no-scheme permission-resolution path (i.e. the configuration the original test in PR #34538 was validating)
// without mutating the process-shared channel_user role, so the subtest is safe under ENABLE_FULLY_PARALLEL_TESTS.
//
// The returned channel has neither a team scheme nor a channel scheme. The user's channel membership is configured
// with SchemeUser/SchemeAdmin/SchemeGuest all false and ExplicitRoles set to a unique custom role that mirrors
// channel_user's default permissions minus upload_file. Because no scheme role is injected into the effective
// role set for that membership, the only way upload_file could be granted is via the higher-scoped team or system
// roles, and upload_file is PermissionScopeChannel — it is not granted by team_user, team_admin, system_user, etc.
//
// Returns the channel and a cleanup function that removes the test role.
func isolatedNoSchemeChannelWithoutUploadFile(t *testing.T, th *TestHelper) (*model.Channel, func()) {
t.Helper()
ch := th.CreateChannelWithClientAndTeam(t, th.SystemAdminClient, model.ChannelTypeOpen, th.BasicTeam.Id)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, ch, false)
require.Nil(t, appErr)
channelUserRole, appErr := th.App.GetRoleByName(th.Context, model.ChannelUserRoleId)
require.Nil(t, appErr)
perms := make([]string, 0, len(channelUserRole.Permissions))
for _, p := range channelUserRole.Permissions {
if p == model.PermissionUploadFile.Id {
continue
}
perms = append(perms, p)
}
roleName := "test_" + model.NewId()
role, appErr := th.App.CreateRole(&model.Role{
Name: roleName,
DisplayName: roleName,
Description: "isolated no-scheme channel-member role for TestCreatePost/TestUpdatePost",
Permissions: perms,
})
require.Nil(t, appErr)
// Replace the membership's role flags via the store so the built-in channel_user role is not injected.
// App.UpdateChannelMemberRoles refuses to leave a member with SchemeUser=false; the store accepts it.
member, appErr := th.App.GetChannelMember(th.Context, ch.Id, th.BasicUser.Id)
require.Nil(t, appErr)
member.SchemeUser = false
member.SchemeAdmin = false
member.SchemeGuest = false
member.ExplicitRoles = roleName
_, sErr := th.App.Srv().Store().Channel().UpdateMember(th.Context, member)
require.NoError(t, sErr)
// SessionHasPermissionToChannel reads through the cached GetAllChannelMembersForUser lookup.
// Store().Channel().UpdateMember does not invalidate that cache, so do it explicitly.
th.App.Srv().Platform().InvalidateChannelCacheForUser(th.BasicUser.Id)
cleanup := func() {
if _, err := th.App.DeleteRole(role.Id); err != nil {
t.Logf("failed to delete test role %s: %v", roleName, err)
}
}
return ch, cleanup
}
func TestCreatePost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
basicPost := func() *model.Post {
p := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "#hashtag a" + model.NewId() + "a",
DeleteAt: 101,
}
p.AddProp(model.PropsAddChannelMember, "no good")
return p
}
post := basicPost()
rootPost, resp2, err2 := client.CreatePost(context.Background(), post)
require.NoError(t, err2)
CheckCreatedStatus(t, resp2)
require.NotNil(t, rootPost)
require.Equal(t, post.Message, rootPost.Message, "message didn't match")
require.Equal(t, "#hashtag", rootPost.Hashtags, "hashtag didn't match")
require.Empty(t, rootPost.FileIds)
require.Equal(t, 0, int(rootPost.EditAt), "newly created post shouldn't have EditAt set")
require.Nil(t, rootPost.GetProp(model.PropsAddChannelMember), "newly created post shouldn't have Props['add_channel_member'] set")
require.Equal(t, 0, int(rootPost.DeleteAt), "newly created post shouldn't have DeleteAt set")
post = basicPost()
post.RootId = rootPost.Id
childPost, resp2, err2 := client.CreatePost(context.Background(), post)
require.NoError(t, err2)
CheckCreatedStatus(t, resp2)
require.NotNil(t, childPost)
t.Run("with file uploaded by same user", func(t *testing.T) {
fileResp, resp, err := client.UploadFile(context.Background(), []byte("data"), th.BasicChannel.Id, "test")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
postWithFiles, resp, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "with files",
FileIds: model.StringArray{fileId},
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
assert.Equal(t, model.StringArray{fileId}, postWithFiles.FileIds)
actualPostWithFiles, resp, err := client.GetPost(context.Background(), postWithFiles.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Equal(t, model.StringArray{fileId}, actualPostWithFiles.FileIds)
})
t.Run("with file uploaded by different user", func(t *testing.T) {
fileResp, resp, err := th.SystemAdminClient.UploadFile(context.Background(), []byte("data"), th.BasicChannel.Id, "test")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
postWithFiles, resp, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "with files",
FileIds: model.StringArray{fileId},
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
assert.Empty(t, postWithFiles.FileIds)
actualPostWithFiles, resp, err := client.GetPost(context.Background(), postWithFiles.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Empty(t, actualPostWithFiles.FileIds)
})
t.Run("with file uploaded by nouser", func(t *testing.T) {
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("data"), th.BasicChannel.Id, "test")
require.Nil(t, appErr)
fileId := fileInfo.Id
postWithFiles, resp, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "with files",
FileIds: model.StringArray{fileId},
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
assert.Equal(t, model.StringArray{fileId}, postWithFiles.FileIds)
actualPostWithFiles, resp, err := client.GetPost(context.Background(), postWithFiles.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Equal(t, model.StringArray{fileId}, actualPostWithFiles.FileIds)
})
t.Run("Create posts without the USE_CHANNEL_MENTIONS Permission - returns ephemeral message with mentions and no ephemeral message without mentions", func(t *testing.T) {
wsClient := th.CreateConnectedWebSocketClient(t)
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.RemovePermissionFromRole(t, model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId)
post := basicPost()
post.RootId = rootPost.Id
post.Message = "a post with no channel mentions"
rPost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, rPost)
// Message with no channel mentions should result in no ephemeral message
timeout := time.After(5 * time.Second)
waiting := true
for waiting {
select {
case event := <-wsClient.EventChannel:
require.NotEqual(t, model.WebsocketEventEphemeralMessage, event.EventType(), "should not have ephemeral message event")
case <-timeout:
waiting = false
}
}
post.Message = "a post with @channel"
rPost, resp, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, rPost)
post.Message = "a post with @all"
rPost, resp, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, rPost)
post.Message = "a post with @here"
rPost, resp, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, rPost)
timeout = time.After(5 * time.Second)
expectedEvents := 3 // 3 Posts created with @ mentions should result in 3 websocket events
gotEvents := 0
for gotEvents < expectedEvents {
select {
case event := <-wsClient.EventChannel:
if event.EventType() == model.WebsocketEventEphemeralMessage {
gotEvents++
}
case <-timeout:
require.Fail(t, fmt.Sprintf("Got %d ephemeral messages, expected: %d", gotEvents, expectedEvents))
}
}
})
t.Run("err with integrations-reserved props", func(t *testing.T) {
originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ExperimentalEnableHardenedMode = true
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting
})
rpost, postResp, postErr := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "with props",
Props: model.StringInterface{model.PostPropsFromWebhook: "true"},
})
require.Error(t, postErr)
CheckBadRequestStatus(t, postResp)
assert.Nil(t, rpost)
})
t.Run("invalid post type", func(t *testing.T) {
post := basicPost()
post.Type = model.PostTypeSystemGeneric
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("invalid rootId type", func(t *testing.T) {
post := basicPost()
post.RootId = "junk"
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("RootId points to child post", func(t *testing.T) {
post := basicPost()
post.RootId = childPost.Id
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("invalid ChannelId", func(t *testing.T) {
post := basicPost()
post.ChannelId = "junk"
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("invalid ChannelId", func(t *testing.T) {
post := basicPost()
post.ChannelId = model.NewId()
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("invalid payload", func(t *testing.T) {
r, err := client.DoAPIPost(context.Background(), "/posts", "garbage")
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, r.StatusCode)
})
t.Run("not logged in", func(t *testing.T) {
defer th.LoginBasic(t)
resp, err := client.Logout(context.Background())
require.NoError(t, err)
CheckOKStatus(t, resp)
post := basicPost()
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("should prevent creating post with files when user lacks upload_file permission in target channel (team scheme)", func(t *testing.T) {
th.LoginBasic(t)
ch, scheme := isolatedTeamChannelWithTeamScheme(t, th)
require.NotEmpty(t, scheme.DefaultChannelUserRole)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, scheme.DefaultChannelUserRole)
defer func() {
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, scheme.DefaultChannelUserRole)
}()
post := &model.Post{
ChannelId: ch.Id,
Message: "Test post with file",
FileIds: model.StringArray{fileId},
}
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("should prevent creating post with files when user lacks upload_file permission in target channel (no scheme)", func(t *testing.T) {
th.LoginBasic(t)
ch, cleanup := isolatedNoSchemeChannelWithoutUploadFile(t, th)
defer cleanup()
fileResp, resp, err := th.SystemAdminClient.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
post := &model.Post{
ChannelId: ch.Id,
Message: "Test post with file",
FileIds: model.StringArray{fileId},
}
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("should allow creating post with files when user has upload_file permission", func(t *testing.T) {
th.LoginBasic(t)
ch, _ := isolatedTeamChannelWithTeamScheme(t, th)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
post := &model.Post{
ChannelId: ch.Id,
Message: "Test post with file",
FileIds: model.StringArray{fileId},
}
rpost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, rpost)
assert.Contains(t, rpost.FileIds, fileId)
})
t.Run("CreateAt should match the one provided in the request", func(t *testing.T) {
post := basicPost()
post.CreateAt = 123
rpost, resp, err := th.SystemAdminClient.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
assert.Equal(t, post.CreateAt, rpost.CreateAt, "create at should match")
})
t.Run("Should not be able to define the RemoteId of a post from the API", func(t *testing.T) {
newPost := &model.Post{
RemoteId: new(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)
})
t.Run("message longer than max post size is rejected", func(t *testing.T) {
longPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: strings.Repeat("a", th.App.MaxPostSize()+1),
}
rpost, resp, err := client.CreatePost(context.Background(), longPost)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
assert.Nil(t, rpost)
})
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)
})
}
func TestCreatePostForPriority(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotifications = true
})
t.Run("should return forbidden when post-priority is disabled", func(t *testing.T) {
originalPrioritySetting := *th.App.Config().ServiceSettings.PostPriority
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = false
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = originalPrioritySetting
})
post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "test", Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: new("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: new("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: new("urgent"),
RequestedAck: new(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: new("urgent"),
PersistentNotifications: new(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: new("urgent"),
PersistentNotifications: new(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: new("important"),
PersistentNotifications: new(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: new("urgent"),
PersistentNotifications: new(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: new("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: new(""),
RequestedAck: new(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: new("urgent"),
RequestedAck: new(false),
PersistentNotifications: new(true),
},
}}
_, resp, err := client.CreatePost(context.Background(), p1)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
})
}
func TestCreatePostWithOAuthClient(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
originalOAuthSetting := *th.App.Config().ServiceSettings.EnableOAuthServiceProvider
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableOAuthServiceProvider = true
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableOAuthServiceProvider = originalOAuthSetting
})
oAuthApp, appErr := th.App.CreateOAuthApp(&model.OAuthApp{
CreatorId: th.SystemAdminUser.Id,
Name: "name",
CallbackUrls: []string{"http://test.com"},
Homepage: "http://test.com",
})
require.Nil(t, appErr, "should create an OAuthApp")
session, appErr := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser.Id,
Token: "token",
IsOAuth: true,
Props: model.StringMap{model.SessionPropOAuthAppID: oAuthApp.Id},
})
require.Nil(t, appErr, "should create a session")
post, _, err := th.Client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicPost.ChannelId,
Message: "test message",
})
require.NoError(t, err)
assert.NotContains(t, post.GetProps(), model.PostPropsFromOAuthApp, fmt.Sprintf("contains %s prop when not using OAuth client", model.PostPropsOverrideUsername))
client := th.CreateClient()
client.SetOAuthToken(session.Token)
post, _, err = client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicPost.ChannelId,
Message: "test message",
})
require.NoError(t, err)
assert.Contains(t, post.GetProps(), model.PostPropsFromOAuthApp, fmt.Sprintf("missing %s prop when using OAuth client", model.PostPropsOverrideUsername))
t.Run("allow username and icon overrides", func(t *testing.T) {
originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ExperimentalEnableHardenedMode = true
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting
})
post, _, err = client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test message",
Props: model.StringInterface{model.PostPropsOverrideUsername: "newUsernameValue", model.PostPropsOverrideIconURL: "iconUrlOverrideValue"},
})
require.NoError(t, err)
assert.Contains(t, post.GetProps(), model.PostPropsOverrideUsername, fmt.Sprintf("missing %s prop when using OAuth client", model.PostPropsOverrideUsername))
assert.Contains(t, post.GetProps(), model.PostPropsOverrideIconURL, fmt.Sprintf("missing %s prop when using OAuth client", model.PostPropsOverrideIconURL))
})
}
func TestCreatePostEphemeral(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.SystemAdminClient
ephemeralPost := &model.PostEphemeral{
UserID: th.BasicUser2.Id,
Post: &model.Post{ChannelId: th.BasicChannel.Id, Message: "a" + model.NewId() + "a", Props: model.StringInterface{model.PropsAddChannelMember: "no good"}},
}
rpost, resp, err := client.CreatePostEphemeral(context.Background(), ephemeralPost)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.Equal(t, ephemeralPost.Post.Message, rpost.Message, "message didn't match")
require.Equal(t, 0, int(rpost.EditAt), "newly created ephemeral post shouldn't have EditAt set")
r, err := client.DoAPIPost(context.Background(), "/posts/ephemeral", "garbage")
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, r.StatusCode)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.CreatePostEphemeral(context.Background(), ephemeralPost)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
client = th.Client
_, resp, err = client.CreatePostEphemeral(context.Background(), ephemeralPost)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func testCreatePostWithOutgoingHook(
t *testing.T,
hookContentType, expectedContentType, message, triggerWord string,
fileIds []string,
triggerWhen int,
commentPostType bool,
) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.SystemAdminUser
team := th.BasicTeam
channel := th.BasicChannel
enableOutgoingWebhooks := *th.App.Config().ServiceSettings.EnableOutgoingWebhooks
allowedUntrustedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingWebhooks })
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = allowedUntrustedInternalConnections
})
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true })
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
})
var hook *model.OutgoingWebhook
var post *model.Post
// Create a test server that is the target of the outgoing webhook. It will
// validate the webhook body fields and write to the success channel on
// success/failure.
success := make(chan bool)
wait := make(chan bool, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-wait
requestContentType := r.Header.Get("Content-Type")
if requestContentType != expectedContentType {
t.Logf("Content-Type is %s, should be %s", requestContentType, expectedContentType)
success <- false
return
}
expectedPayload := &model.OutgoingWebhookPayload{
Token: hook.Token,
TeamId: hook.TeamId,
TeamDomain: team.Name,
ChannelId: post.ChannelId,
ChannelName: channel.Name,
Timestamp: post.CreateAt,
UserId: post.UserId,
UserName: user.Username,
PostId: post.Id,
Text: post.Message,
TriggerWord: triggerWord,
FileIds: strings.Join(post.FileIds, ","),
}
// depending on the Content-Type, we expect to find a JSON or form encoded payload
if requestContentType == "application/json" {
decoder := json.NewDecoder(r.Body)
o := &model.OutgoingWebhookPayload{}
err := decoder.Decode(&o)
if err != nil {
th.TestLogger.Warn("Error decoding body", mlog.Err(err))
}
if !reflect.DeepEqual(expectedPayload, o) {
t.Logf("JSON payload is %+v, should be %+v", o, expectedPayload)
success <- false
return
}
} else {
err := r.ParseForm()
if err != nil {
t.Logf("Error parsing form: %q", err)
success <- false
return
}
expectedFormValues, _ := url.ParseQuery(expectedPayload.ToFormValues())
if !reflect.DeepEqual(expectedFormValues, r.Form) {
t.Logf("Form values are: %q\n, should be: %q\n", r.Form, expectedFormValues)
success <- false
return
}
}
respPostType := "" // if is empty or post will do a normal post.
if commentPostType {
respPostType = model.OutgoingHookResponseTypeComment
}
outGoingHookResponse := &model.OutgoingWebhookResponse{
Text: new("some test text"),
Username: "TestCommandServer",
IconURL: "https://hanzo.ai/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) {
th := SetupEnterprise(t).InitBasic(t)
// Enable MoveThreads feature flag
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.MoveThreadsEnabled = true })
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
client := th.Client
ctx := context.Background()
basicUser1 := th.BasicUser
basicUser2 := th.BasicUser2
basicUser3 := th.CreateUser(t)
// Helper function to create a new public channel to move the post to
createPublicChannel := func(teamId, name, displayName string) *model.Channel {
channel, resp, err := client.CreateChannel(ctx, &model.Channel{
TeamId: teamId,
Name: name,
DisplayName: displayName,
Type: model.ChannelTypeOpen,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, channel)
return channel
}
// Create a new private channel to move the post to
privateChannel, resp, err := client.CreateChannel(ctx, &model.Channel{
TeamId: th.BasicTeam.Id,
Name: "test-private-channel",
DisplayName: "Test Private Channel",
Type: model.ChannelTypePrivate,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, privateChannel)
// Create a new direct message channel to move the post to
dmChannel, resp, err := client.CreateDirectChannel(ctx, basicUser1.Id, basicUser2.Id)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, dmChannel)
// Create a new group message channel to move the post to
gmChannel, resp, err := client.CreateGroupChannel(ctx, []string{basicUser1.Id, basicUser2.Id, basicUser3.Id})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, gmChannel)
t.Run("Move to public channel", func(t *testing.T) {
// Create a public channel
publicChannel := createPublicChannel(th.BasicTeam.Id, "test-public-channel", "Test Public Channel")
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Move the post to the public channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: publicChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the post was moved to the public channel
posts, resp, err := client.GetPostsForChannel(ctx, publicChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved
require.Equal(t, 2, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
t.Run("Move to private channel", func(t *testing.T) {
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Move the post to the private channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: privateChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the post was moved to the private channel
posts, resp, err := client.GetPostsForChannel(ctx, privateChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved
require.Equal(t, 2, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
t.Run("Move to direct message channel", func(t *testing.T) {
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Move the post to the direct message channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: dmChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the post was moved to the direct message channel
posts, resp, err := client.GetPostsForChannel(ctx, dmChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 1 post, the post we moved
require.Equal(t, 1, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
t.Run("Move to group message channel", func(t *testing.T) {
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Move the post to the group message channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: gmChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the post was moved to the group message channel
posts, resp, err := client.GetPostsForChannel(ctx, gmChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 1 post, the post we moved
require.Equal(t, 1, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
t.Run("Move thread with more than one post", func(t *testing.T) {
// Create a new public channel to move the post to
pChannel, resp, err := client.CreateChannel(ctx, &model.Channel{
TeamId: th.BasicTeam.Id,
Name: "test-public-channel2",
DisplayName: "Test Public Channel",
Type: model.ChannelTypeOpen,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, pChannel)
// Create a new post to use as the root post
rootPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "root post",
}
rootPost, resp, err = client.CreatePost(ctx, rootPost)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, rootPost)
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
RootId: rootPost.Id,
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Create another post in the thread
post = &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post 2",
RootId: rootPost.Id,
}
newPost2, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost2)
// Move the thread to the public channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: pChannel.Id,
}
resp, err = client.MoveThread(ctx, rootPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the thread was moved to the public channel
posts, resp, err := client.GetPostsForChannel(ctx, pChannel.Id, 0, 100, "", false, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
require.Equal(t, "This thread was moved from another channel", posts.Posts[posts.Order[0]].Message)
require.Equal(t, newPost2.Message, posts.Posts[posts.Order[1]].Message)
require.Equal(t, newPost.Message, posts.Posts[posts.Order[2]].Message)
require.Equal(t, rootPost.Message, posts.Posts[posts.Order[3]].Message)
})
t.Run("Move thread when permitted role is channel admin", func(t *testing.T) {
// Create public channel
publicChannel := createPublicChannel(th.BasicTeam.Id, "test-public-channel-admin", "Test Public Channel Admin")
// Set permitted role as channel admin
enabled := true
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.WranglerSettings = model.WranglerSettings{
MoveThreadToAnotherTeamEnable: &enabled,
PermittedWranglerRoles: []string{model.PermissionsChannelAdmin},
}
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
cfg.WranglerSettings = model.WranglerSettings{}
})
// Login as channel admin and add to channel
th.LoginTeamAdmin(t)
th.AddUserToChannel(t, th.TeamAdminUser, publicChannel)
defer th.LoginBasic(t)
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Move the post to the public channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: publicChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the post was moved to the public channel
posts, resp, err := client.GetPostsForChannel(ctx, publicChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved
require.Equal(t, 2, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
t.Run("check permissions limited by AllowedEmailDomain", func(t *testing.T) {
th.App.UpdateConfig(func(c *model.Config) {
c.WranglerSettings.AllowedEmailDomain = []string{"foo.com", "bar.com"}
})
t.Cleanup(func() {
th.App.UpdateConfig(func(c *model.Config) {
c.WranglerSettings.AllowedEmailDomain = make([]string, 0)
})
})
// Create a public channel
publicChannel := createPublicChannel(th.BasicTeam.Id, "test-public-channel-allowed-email-domain", "Test Public Channel")
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, newPost)
// Move the post to the public channel as a user without the configured domain
moveThreadParams := &model.MoveThreadParams{
ChannelId: publicChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Change the email domain to match the configured setting
th.BasicUser.Email = "[email protected]"
_, resp, err = client.UpdateUser(ctx, th.BasicUser)
require.NoError(t, err)
CheckOKStatus(t, resp)
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
CheckOKStatus(t, resp)
// Check that the post was moved to the public channel
posts, resp, err := client.GetPostsForChannel(ctx, publicChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved
require.Equal(t, 2, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
}
func TestCreatePostPublic(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "#hashtag a" + model.NewId() + "a"}
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Joram Wilander", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemUserRoleId}
ruser, _, err := client.CreateUser(context.Background(), &user)
require.NoError(t, err)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
_, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, appErr := th.App.UpdateUserRoles(th.Context, ruser.Id, model.SystemUserRoleId+" "+model.SystemPostAllPublicRoleId, false)
require.Nil(t, appErr)
appErr = th.App.Srv().InvalidateAllCaches()
require.Nil(t, appErr)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
_, _, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
post.ChannelId = th.BasicPrivateChannel.Id
_, resp, err = client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, appErr = th.App.UpdateUserRoles(th.Context, ruser.Id, model.SystemUserRoleId, false)
require.Nil(t, appErr)
_, appErr = th.App.JoinUserToTeam(th.Context, th.BasicTeam, ruser, "")
require.Nil(t, appErr)
_, appErr = th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, ruser.Id, model.TeamUserRoleId+" "+model.TeamPostAllPublicRoleId)
require.Nil(t, appErr)
appErr = th.App.Srv().InvalidateAllCaches()
require.Nil(t, appErr)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
post.ChannelId = th.BasicPrivateChannel.Id
_, resp, err = client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
post.ChannelId = th.BasicChannel.Id
_, _, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
}
func TestCreatePostAll(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "#hashtag a" + model.NewId() + "a"}
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Joram Wilander", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemUserRoleId}
directChannel, _ := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
ruser, _, err := client.CreateUser(context.Background(), &user)
require.NoError(t, err)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
_, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, appErr := th.App.UpdateUserRoles(th.Context, ruser.Id, model.SystemUserRoleId+" "+model.SystemPostAllRoleId, false)
require.Nil(t, appErr)
appErr = th.App.Srv().InvalidateAllCaches()
require.Nil(t, appErr)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
_, _, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
post.ChannelId = th.BasicPrivateChannel.Id
_, _, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
post.ChannelId = directChannel.Id
_, _, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
_, appErr = th.App.UpdateUserRoles(th.Context, ruser.Id, model.SystemUserRoleId, false)
require.Nil(t, appErr)
_, appErr = th.App.JoinUserToTeam(th.Context, th.BasicTeam, ruser, "")
require.Nil(t, appErr)
_, appErr = th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, ruser.Id, model.TeamUserRoleId+" "+model.TeamPostAllRoleId)
require.Nil(t, appErr)
appErr = th.App.Srv().InvalidateAllCaches()
require.Nil(t, appErr)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
post.ChannelId = th.BasicPrivateChannel.Id
_, _, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
post.ChannelId = th.BasicChannel.Id
_, _, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
post.ChannelId = directChannel.Id
_, resp, err = client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func TestCreatePostSendOutOfChannelMentions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
WebSocketClient := th.CreateConnectedWebSocketClient(t)
inChannelUser := th.CreateUser(t)
th.LinkUserToTeam(t, inChannelUser, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, inChannelUser, th.BasicChannel, false)
require.Nil(t, appErr)
post1 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "@" + inChannelUser.Username}
_, resp, err := client.CreatePost(context.Background(), post1)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
timeout := time.After(5 * time.Second)
waiting := true
for waiting {
select {
case event := <-WebSocketClient.EventChannel:
require.NotEqual(t, model.WebsocketEventEphemeralMessage, event.EventType(), "should not have ephemeral message event")
case <-timeout:
waiting = false
}
}
outOfChannelUser := th.CreateUser(t)
th.LinkUserToTeam(t, outOfChannelUser, th.BasicTeam)
post2 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "@" + outOfChannelUser.Username}
_, resp, err = client.CreatePost(context.Background(), post2)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
timeout = time.After(5 * time.Second)
waiting = true
for waiting {
select {
case event := <-WebSocketClient.EventChannel:
if event.EventType() != model.WebsocketEventEphemeralMessage {
// Ignore any other events
continue
}
var wpost model.Post
err := json.Unmarshal([]byte(event.GetData()["post"].(string)), &wpost)
require.NoError(t, err)
acm, ok := wpost.GetProp(model.PropsAddChannelMember).(map[string]any)
require.True(t, ok, "should have received ephemeral post with 'add_channel_member' in props")
require.True(t, acm["post_id"] != nil, "should not be nil")
require.True(t, acm["user_ids"] != nil, "should not be nil")
require.True(t, acm["usernames"] != nil, "should not be nil")
waiting = false
case <-timeout:
require.FailNow(t, "timed out waiting for ephemeral message event")
}
}
}
func TestCreatePostCheckOnlineStatus(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
api, err := Init(th.Server)
require.NoError(t, err)
session, _ := th.App.GetSession(th.Client.AuthToken)
cli := th.CreateClient()
_, _, err = cli.Login(context.Background(), th.BasicUser2.Username, th.BasicUser2.Password)
require.NoError(t, err)
wsClient := th.CreateConnectedWebSocketClientWithClient(t, cli)
waitForEvent := func(isSetOnline bool) {
timeout := time.After(5 * time.Second)
for {
select {
case ev := <-wsClient.EventChannel:
if ev.EventType() == model.WebsocketEventPosted {
assert.True(t, ev.GetData()["set_online"].(bool) == isSetOnline)
return
}
case <-timeout:
// We just skip the test instead of failing because waiting for more than 5 seconds
// to get a response does not make sense, and it will unnecessarily slow down
// the tests further in an already congested CI environment.
t.Skip("timed out waiting for event")
}
}
}
handler := api.APIHandler(createPost)
resp := httptest.NewRecorder()
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "some message",
}
postJSON, jsonErr := json.Marshal(post)
require.NoError(t, jsonErr)
req := httptest.NewRequest("POST", "/v1/workspace/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", "/v1/workspace/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 TestCreatePostSilentRejectedForHumanUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
api, err := Init(th.Server)
require.NoError(t, err)
session, _ := th.App.GetSession(th.Client.AuthToken)
handler := api.APIHandler(createPost)
resp := httptest.NewRecorder()
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "silent human post",
}
postJSON, jsonErr := json.Marshal(post)
require.NoError(t, jsonErr)
req := httptest.NewRequest("POST", "/v1/workspace/posts?silent=true", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
assert.Equal(t, http.StatusForbidden, resp.Code)
}
func TestCreatePostSilentQueryParam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
api, err := Init(th.Server)
require.NoError(t, err)
handler := api.APIHandler(createPost)
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "silent query param post",
}
postJSON, jsonErr := json.Marshal(post)
require.NoError(t, jsonErr)
t.Run("invalid silent value returns 400", func(t *testing.T) {
session, _ := th.App.GetSession(th.Client.AuthToken)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/v1/workspace/posts?silent=invalid", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
assert.Equal(t, http.StatusBadRequest, resp.Code)
})
t.Run("silent=false does not set prop", func(t *testing.T) {
session, _ := th.App.GetSession(th.Client.AuthToken)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/v1/workspace/posts?silent=false", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
require.Equal(t, http.StatusCreated, resp.Code)
var rp model.Post
require.NoError(t, json.NewDecoder(resp.Body).Decode(&rp))
assert.False(t, rp.HasSilentNotification())
})
t.Run("silent=0 does not set prop", func(t *testing.T) {
session, _ := th.App.GetSession(th.Client.AuthToken)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/v1/workspace/posts?silent=0", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
require.Equal(t, http.StatusCreated, resp.Code)
var rp model.Post
require.NoError(t, json.NewDecoder(resp.Body).Decode(&rp))
assert.False(t, rp.HasSilentNotification())
})
t.Run("empty silent param is ignored", func(t *testing.T) {
session, _ := th.App.GetSession(th.Client.AuthToken)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/v1/workspace/posts?silent=", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
require.Equal(t, http.StatusCreated, resp.Code)
var rp model.Post
require.NoError(t, json.NewDecoder(resp.Body).Decode(&rp))
assert.False(t, rp.HasSilentNotification())
})
t.Run("bot may create silent post via query param", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
bot := th.CreateBotWithSystemAdminClient(t)
botUser, appErr := th.App.GetUser(bot.UserId)
require.Nil(t, appErr)
_, appErr = th.App.UpdateUserRoles(th.Context, bot.UserId, model.TeamUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
th.LinkUserToTeam(t, botUser, th.BasicTeam)
_, appErr = th.App.AddUserToChannel(th.Context, botUser, th.BasicChannel, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableUserAccessTokens = true
})
rtoken, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), bot.UserId, "silent-bot-token", 0)
require.NoError(t, err)
resp := httptest.NewRecorder()
botPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "silent bot post",
UserId: bot.UserId,
}
botPostJSON, err := json.Marshal(botPost)
require.NoError(t, err)
req := httptest.NewRequest("POST", "/v1/workspace/posts?silent=true", bytes.NewReader(botPostJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+rtoken.Token)
handler.ServeHTTP(resp, req)
require.Equal(t, http.StatusCreated, resp.Code)
var rp model.Post
require.NoError(t, json.NewDecoder(resp.Body).Decode(&rp))
assert.True(t, rp.HasSilentNotification())
})
}
func TestUpdatePost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
channel := th.BasicChannel
th.App.Srv().SetLicense(model.NewTestLicense())
fileIds := make([]string, 3)
data, err2 := testutils.ReadTestFile("test.png")
require.NoError(t, err2)
for i := range fileIds {
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileIds[i] = fileResp.FileInfos[0].Id
}
rpost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "zz" + model.NewId() + "a",
FileIds: fileIds,
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
assert.Equal(t, rpost.Message, rpost.Message, "full name didn't match")
assert.EqualValues(t, 0, rpost.EditAt, "Newly created post shouldn't have EditAt set")
assert.Equal(t, model.StringArray(fileIds), rpost.FileIds, "FileIds should have been set")
t.Run("new message, invalid props", func(t *testing.T) {
msg1 := "#hashtag a" + model.NewId() + " update post again"
rpost.Message = msg1
rpost.AddProp(model.PropsAddChannelMember, "no good")
rrupost, _, err := client.UpdatePost(context.Background(), rpost.Id, rpost)
require.NoError(t, err)
assert.Equal(t, msg1, rrupost.Message, "failed to update message")
assert.Equal(t, "#hashtag", rrupost.Hashtags, "failed to update hashtags")
assert.Nil(t, rrupost.GetProp(model.PropsAddChannelMember), "failed to sanitize Props['add_channel_member'], should be nil")
actual, _, err := client.GetPost(context.Background(), rpost.Id, "")
require.NoError(t, err)
assert.Equal(t, msg1, actual.Message, "failed to update message")
assert.Equal(t, "#hashtag", actual.Hashtags, "failed to update hashtags")
assert.Nil(t, actual.GetProp(model.PropsAddChannelMember), "failed to sanitize Props['add_channel_member'], should be nil")
})
t.Run("join/leave post", func(t *testing.T) {
var rpost2 *model.Post
rpost2, _, appErr = th.App.CreatePost(th.Context, &model.Post{
ChannelId: channel.Id,
Message: "zz" + model.NewId() + "a",
Type: model.PostTypeJoinLeave,
UserId: th.BasicUser.Id,
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
up2 := &model.Post{
Id: rpost2.Id,
ChannelId: channel.Id,
Message: "zz" + model.NewId() + " update post 2",
}
_, resp, err := client.UpdatePost(context.Background(), rpost2.Id, up2)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
rpost3, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: channel.Id,
Message: "zz" + model.NewId() + "a",
UserId: th.BasicUser.Id,
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
t.Run("add slack attachments", func(t *testing.T) {
up4 := &model.Post{
Id: rpost3.Id,
ChannelId: channel.Id,
Message: "zz" + model.NewId() + " update post 3",
}
up4.AddProp(model.PostPropsAttachments, []model.MessageAttachment{
{
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("should strip spoofed metadata embeds", func(t *testing.T) {
// MM-67055: Verify that client-supplied metadata.embeds are stripped
post := &model.Post{
ChannelId: channel.Id,
Message: "test message " + model.NewId(),
}
createdPost, _, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
// Try to update with spoofed embed
updatePost := &model.Post{
Id: createdPost.Id,
ChannelId: channel.Id,
Message: "updated message " + model.NewId(),
Metadata: &model.PostMetadata{
Embeds: []*model.PostEmbed{
{
Type: model.PostEmbedPermalink,
Data: &model.PreviewPost{
PostID: "spoofed-post-id",
Post: &model.Post{
Id: "spoofed-post-id",
UserId: th.BasicUser2.Id,
Message: "This is a spoofed message!",
},
},
},
},
},
}
updatedPost, _, err := client.UpdatePost(context.Background(), createdPost.Id, updatePost)
require.NoError(t, err)
// Verify spoofed embed was stripped
if updatedPost.Metadata != nil {
assert.Empty(t, updatedPost.Metadata.Embeds, "spoofed embeds should be stripped")
}
// Double-check by fetching the post
fetchedPost, _, err := client.GetPost(context.Background(), createdPost.Id, "")
require.NoError(t, err)
if fetchedPost.Metadata != nil {
assert.Empty(t, fetchedPost.Metadata.Embeds, "spoofed embeds should not be persisted")
}
})
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("change file ids but not message, 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
})
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
newFileId := fileResp.FileInfos[0].Id
oldPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: channel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
CreateAt: model.GetMillis() - 2000,
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
up := &model.Post{
Id: oldPost.Id,
ChannelId: channel.Id,
Message: oldPost.Message,
FileIds: model.StringArray{newFileId},
}
_, resp, err := client.UpdatePost(context.Background(), oldPost.Id, up)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id)
})
t.Run("change props but not message, 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
})
oldPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: channel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
CreateAt: model.GetMillis() - 2000,
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
up := &model.Post{
Id: oldPost.Id,
ChannelId: channel.Id,
Message: oldPost.Message,
Props: model.StringInterface{"channel_header": "injected"},
}
_, resp, err := client.UpdatePost(context.Background(), oldPost.Id, up)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id)
})
t.Run("change is_pinned but not message, 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
})
oldPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: channel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
CreateAt: model.GetMillis() - 2000,
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
up := &model.Post{
Id: oldPost.Id,
ChannelId: channel.Id,
Message: oldPost.Message,
IsPinned: true,
}
_, resp, err := client.UpdatePost(context.Background(), oldPost.Id, up)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id)
})
t.Run("err with integrations-reserved props", func(t *testing.T) {
originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ExperimentalEnableHardenedMode = true
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting
})
_, resp, err := client.UpdatePost(context.Background(), rpost.Id, &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "with props",
Props: model.StringInterface{model.PostPropsFromWebhook: "true"},
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("should prevent updating post with files when user lacks upload_file permission in target channel (team scheme)", func(t *testing.T) {
ch, scheme := isolatedTeamChannelWithTeamScheme(t, th)
require.NotEmpty(t, scheme.DefaultChannelUserRole)
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: ch.Id,
Message: "Post without files",
}, ch, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, scheme.DefaultChannelUserRole)
defer func() {
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, scheme.DefaultChannelUserRole)
}()
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: ch.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, updatedPost)
})
t.Run("should prevent updating post with files when user lacks upload_file permission in target channel (no scheme)", func(t *testing.T) {
ch, cleanup := isolatedNoSchemeChannelWithoutUploadFile(t, th)
defer cleanup()
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: ch.Id,
Message: "Post without files",
}, ch, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
fileResp, resp, err := th.SystemAdminClient.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: ch.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, updatedPost)
})
t.Run("should allow updating post with files when user has upload_file permission", func(t *testing.T) {
ch, _ := isolatedTeamChannelWithTeamScheme(t, th)
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: ch.Id,
Message: "Post without files",
}, ch, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: ch.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, updatedPost)
assert.Contains(t, updatedPost.FileIds, fileId)
})
t.Run("should prevent editing when create_post permission is revoked", func(t *testing.T) {
th.LoginBasic(t)
postToEdit, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "original message",
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
th.RemovePermissionFromRole(t, model.PermissionCreatePost.Id, model.ChannelUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionCreatePost.Id, model.ChannelUserRoleId)
updatePost := &model.Post{
Id: postToEdit.Id,
ChannelId: channel.Id,
Message: "edited message",
}
_, resp, err := client.UpdatePost(context.Background(), postToEdit.Id, updatePost)
require.Error(t, err)
CheckForbiddenStatus(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(t)
_, resp, err := client.UpdatePost(context.Background(), rpost.Id, rpost)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
})
t.Run("different user, but team admin", func(t *testing.T) {
th.LoginTeamAdmin(t)
_, resp, err := client.UpdatePost(context.Background(), rpost.Id, rpost)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
})
t.Run("different user, but system admin", func(t *testing.T) {
_, _, err := th.SystemAdminClient.UpdatePost(context.Background(), rpost.Id, rpost)
require.NoError(t, err)
})
t.Run("should be able to add new files", func(t *testing.T) {
th.LoginBasic(t)
// create new file
fileResponse, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
require.Equal(t, 1, len(fileResponse.FileInfos))
fileInfo := fileResponse.FileInfos[0]
// create new post
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "zz" + model.NewId() + "a",
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
require.NotNil(t, post)
// update post with new file
post.FileIds = []string{fileInfo.Id}
_, _, err = client.UpdatePost(context.Background(), post.Id, post)
require.NoError(t, err)
updatedPost, _, err := client.GetPost(context.Background(), post.Id, "")
require.NoError(t, err)
require.Equal(t, post.Id, updatedPost.Id)
require.Equal(t, 1, len(updatedPost.FileIds))
require.Equal(t, fileInfo.Id, updatedPost.FileIds[0])
// verify file is attached to the post
fetchedFileInfo, _, err := client.GetFileInfo(context.Background(), fileInfo.Id)
require.NoError(t, err)
require.Equal(t, fileInfo.Id, fetchedFileInfo.Id)
require.Equal(t, post.Id, fetchedFileInfo.PostId)
})
t.Run("should be able to remove files", func(t *testing.T) {
th.LoginBasic(t)
// create new file
fileResponse, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
require.Equal(t, 1, len(fileResponse.FileInfos))
fileInfo := fileResponse.FileInfos[0]
// create new post
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "zz" + model.NewId() + "a",
FileIds: []string{fileInfo.Id},
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
require.NotNil(t, post)
require.Equal(t, 1, len(post.FileIds))
// remove files from post
post.FileIds = []string{}
_, _, err = client.UpdatePost(context.Background(), post.Id, post)
require.NoError(t, err)
updatedPost, _, err := client.GetPost(context.Background(), post.Id, "")
require.NoError(t, err)
require.Equal(t, post.Id, updatedPost.Id)
require.Equal(t, 0, len(updatedPost.FileIds))
// verify file is removed from the post
postFileInfos, err := th.App.Srv().Store().FileInfo().GetForPost(post.Id, true, true, false)
require.NoError(t, err)
require.Equal(t, 1, len(postFileInfos))
require.Equal(t, fileInfo.Id, postFileInfos[0].Id)
require.Greater(t, postFileInfos[0].DeleteAt, int64(0))
})
t.Run("post files remain unchanged when fileIds is nil", func(t *testing.T) {
th.LoginBasic(t)
// create new file
fileResponse, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
require.Equal(t, 1, len(fileResponse.FileInfos))
fileInfo := fileResponse.FileInfos[0]
// create new post
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "zz" + model.NewId() + "a",
FileIds: []string{fileInfo.Id},
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
require.NotNil(t, post)
require.Equal(t, 1, len(post.FileIds))
// update post without specifying fileIds
post.FileIds = nil
post.Message = "updated message"
_, _, err = client.UpdatePost(context.Background(), post.Id, post)
require.NoError(t, err)
updatedPost, _, err := client.GetPost(context.Background(), post.Id, "")
require.NoError(t, err)
require.Equal(t, post.Id, updatedPost.Id)
require.Equal(t, 1, len(updatedPost.FileIds))
require.Equal(t, fileInfo.Id, updatedPost.FileIds[0])
require.Equal(t, "updated message", updatedPost.Message)
// verify file is still part of the post
postFileInfos, err := th.App.Srv().Store().FileInfo().GetForPost(post.Id, true, false, false)
require.NoError(t, err)
require.Equal(t, 1, len(postFileInfos))
require.Equal(t, fileInfo.Id, postFileInfos[0].Id)
require.Equal(t, int64(0), postFileInfos[0].DeleteAt)
})
t.Run("should prevent adding files when edit_file_attachment permission is revoked", func(t *testing.T) {
th.LoginBasic(t)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "Post without files",
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
th.RemovePermissionFromRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: channel.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}
_, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
require.Error(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
require.Equal(t, "You do not have the appropriate permissions.", err.Error())
CheckForbiddenStatus(t, resp)
})
t.Run("should prevent removing files when edit_file_attachment permission is revoked", func(t *testing.T) {
th.LoginBasic(t)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
postWithFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "Post with files",
FileIds: model.StringArray{fileId},
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
th.RemovePermissionFromRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
updatePost := &model.Post{
Id: postWithFiles.Id,
ChannelId: channel.Id,
Message: "Updated post without file",
FileIds: model.StringArray{},
}
_, resp, err := client.UpdatePost(context.Background(), postWithFiles.Id, updatePost)
require.Error(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
require.Equal(t, "You do not have the appropriate permissions.", err.Error())
CheckForbiddenStatus(t, resp)
})
t.Run("should allow updating post with unchanged files when edit_file_attachment permission is revoked", func(t *testing.T) {
th.LoginBasic(t)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
postWithFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "Post with files",
FileIds: model.StringArray{fileId},
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
th.RemovePermissionFromRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
updatePost := &model.Post{
Id: postWithFiles.Id,
ChannelId: channel.Id,
Message: "Updated message only",
FileIds: model.StringArray{fileId},
}
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithFiles.Id, updatePost)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, updatedPost)
assert.Equal(t, "Updated message only", updatedPost.Message)
// Lock in the "unchanged files are preserved" contract — the
// caller revoked PermissionEditFileAttachment and re-submitted
// the same FileIds, so the response must carry them back. A
// silent "files dropped" regression here would still pass the
// message assertion above.
require.NotNil(t, updatedPost.FileIds)
assert.ElementsMatch(t, postWithFiles.FileIds, updatedPost.FileIds)
})
t.Run("should allow changing files when edit_file_attachment permission is present", func(t *testing.T) {
th.LoginBasic(t)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "Post without files",
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: channel.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, updatedPost)
// Lock in the "files CAN be added when the caller has
// PermissionEditFileAttachment" contract — without this
// assertion a regression that silently drops the new
// attachment would still pass the NotNil check above.
require.NotNil(t, updatedPost.FileIds)
assert.ElementsMatch(t, updatePost.FileIds, updatedPost.FileIds)
})
t.Run("should be able to add and remove files simultaneously", func(t *testing.T) {
th.LoginBasic(t)
// create new file
fileResponse1, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
require.Equal(t, 1, len(fileResponse1.FileInfos))
fileInfo1 := fileResponse1.FileInfos[0]
fileResponse2, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
require.Equal(t, 1, len(fileResponse2.FileInfos))
fileInfo2 := fileResponse2.FileInfos[0]
// create new post
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "zz" + model.NewId() + "a",
FileIds: model.StringArray{fileInfo1.Id, fileInfo2.Id},
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
require.NotNil(t, post)
require.Equal(t, 2, len(post.FileIds))
// update post with new file
fileResponse3, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
require.Equal(t, 1, len(fileResponse3.FileInfos))
fileInfo3 := fileResponse3.FileInfos[0]
fileResponse4, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
require.Equal(t, 1, len(fileResponse4.FileInfos))
fileInfo4 := fileResponse4.FileInfos[0]
post.FileIds = []string{fileInfo3.Id, fileInfo4.Id}
_, _, err = client.UpdatePost(context.Background(), post.Id, post)
require.NoError(t, err)
updatedPost, _, err := client.GetPost(context.Background(), post.Id, "")
require.NoError(t, err)
require.Equal(t, post.Id, updatedPost.Id)
require.Equal(t, 2, len(updatedPost.FileIds))
require.Contains(t, updatedPost.FileIds, fileInfo3.Id)
require.Contains(t, updatedPost.FileIds, fileInfo4.Id)
postFiles, err := th.App.Srv().Store().FileInfo().GetForPost(post.Id, true, true, false)
require.NoError(t, err)
require.Equal(t, 4, len(postFiles))
for _, postFile := range postFiles {
if postFile.Id == fileInfo1.Id || postFile.Id == fileInfo2.Id {
require.Greater(t, postFile.DeleteAt, int64(0))
}
if postFile.Id == fileInfo3.Id || postFile.Id == fileInfo4.Id {
require.Equal(t, postFile.PostId, post.Id)
}
}
})
t.Run("message longer than max post size is rejected", func(t *testing.T) {
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)
longPost := &model.Post{
Id: post.Id,
ChannelId: channel.Id,
Message: strings.Repeat("a", th.App.MaxPostSize()+1),
}
updatedPost, resp, err := client.UpdatePost(context.Background(), post.Id, longPost)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
assert.Nil(t, updatedPost)
})
}
func TestUpdateOthersPostInDirectMessageChannel(t *testing.T) {
mainHelper.Parallel(t)
// This test checks that a sysadmin with the "EDIT_OTHERS_POSTS" permission can edit someone else's post in a
// channel without a team (DM/GM). This indirectly checks for the proper cascading all the way to system-wide roles
// on the user object of permissions based on a post in a channel with no team ID.
th := Setup(t).InitBasic(t)
dmChannel := th.CreateDmChannel(t, th.SystemAdminUser)
post := &model.Post{
Message: "asd",
ChannelId: dmChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
CreateAt: 0,
}
post, _, err := th.Client.CreatePost(context.Background(), post)
require.NoError(t, err)
post.Message = "changed"
_, _, err = th.SystemAdminClient.UpdatePost(context.Background(), post.Id, post)
require.NoError(t, err)
}
func TestPatchPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
channel := th.BasicChannel
th.App.Srv().SetLicense(model.NewTestLicense())
fileIDs := make([]string, 3)
data, err2 := testutils.ReadTestFile("test.png")
require.NoError(t, err2)
for i := range fileIDs {
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileIDs[i] = fileResp.FileInfos[0].Id
}
sort.Strings(fileIDs)
post := &model.Post{
ChannelId: channel.Id,
IsPinned: true,
Message: "#hashtag a message",
Props: model.StringInterface{"channel_header": "old_header"},
FileIds: fileIDs[0:2],
HasReactions: true,
}
post, _, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
var rpost *model.Post
t.Run("new message, props, files, HasReactions bit", func(t *testing.T) {
patch := &model.PostPatch{}
patch.IsPinned = new(false)
patch.Message = new("#otherhashtag other message")
patch.Props = &model.StringInterface{"channel_header": "new_header"}
patchFileIds := model.StringArray(fileIDs) // one extra file
patch.FileIds = &patchFileIds
patch.HasReactions = new(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.MessageAttachment{
{
Text: "Hello World",
},
}
patch2.Props = &model.StringInterface{model.PostPropsAttachments: attachments}
var rpost2 *model.Post
rpost2, _, err = client.PatchPost(context.Background(), post.Id, patch2)
require.NoError(t, err)
assert.NotEmpty(t, rpost2.GetProp(model.PostPropsAttachments))
assert.NotEqual(t, rpost.EditAt, rpost2.EditAt)
})
t.Run("invalid requests", func(t *testing.T) {
var origEnableDeveloper bool
th.App.UpdateConfig(func(cfg *model.Config) {
origEnableDeveloper = *cfg.ServiceSettings.EnableDeveloper
*cfg.ServiceSettings.EnableDeveloper = true
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableDeveloper = origEnableDeveloper
})
var r *http.Response
r, err = client.DoAPIPut(context.Background(), "/posts/"+post.Id+"/patch", "garbage")
require.EqualError(t, err, "Invalid or missing post in request body., invalid character 'g' looking for beginning of value")
require.Equal(t, http.StatusBadRequest, r.StatusCode, "wrong status code")
var resp *model.Response
patch := &model.PostPatch{}
_, resp, err = client.PatchPost(context.Background(), "junk", patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("unknown post", func(t *testing.T) {
var resp *model.Response
patch := &model.PostPatch{}
_, resp, err = client.PatchPost(context.Background(), GenerateTestID(), patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("logged out", func(t *testing.T) {
_, err = client.Logout(context.Background())
require.NoError(t, err)
patch := &model.PostPatch{}
_, resp, err := client.PatchPost(context.Background(), post.Id, patch)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("different user", func(t *testing.T) {
th.LoginBasic2(t)
patch := &model.PostPatch{}
_, resp, err := client.PatchPost(context.Background(), post.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("different user, but team admin", func(t *testing.T) {
th.LoginTeamAdmin(t)
patch := &model.PostPatch{}
_, resp, err := client.PatchPost(context.Background(), post.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("different user, but system admin", func(t *testing.T) {
patch := &model.PostPatch{}
_, _, err := th.SystemAdminClient.PatchPost(context.Background(), post.Id, patch)
require.NoError(t, err)
})
t.Run("edit others posts permission can function independently of edit own post", func(t *testing.T) {
th.LoginBasic2(t)
patch := &model.PostPatch{}
_, resp, err := client.PatchPost(context.Background(), post.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Add permission to edit others'
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.RemovePermissionFromRole(t, model.PermissionEditPost.Id, model.ChannelUserRoleId)
th.AddPermissionToRole(t, model.PermissionEditOthersPosts.Id, model.ChannelUserRoleId)
_, _, err = client.PatchPost(context.Background(), post.Id, patch)
require.NoError(t, err)
})
t.Run("should prevent patching when create_post permission is revoked", func(t *testing.T) {
th.LoginBasic(t)
postToEdit, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: channel.Id,
Message: "original message",
})
require.NoError(t, err)
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.RemovePermissionFromRole(t, model.PermissionCreatePost.Id, model.ChannelUserRoleId)
patch := &model.PostPatch{
Message: new("edited message"),
}
_, resp, err := client.PatchPost(context.Background(), postToEdit.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("should prevent patching file ids when edit_file_attachment permission is revoked", func(t *testing.T) {
th.LoginBasic(t)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
postToEdit, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: channel.Id,
Message: "original message",
})
require.NoError(t, err)
th.RemovePermissionFromRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
patch := &model.PostPatch{
FileIds: &model.StringArray{fileId},
}
_, resp, err := client.PatchPost(context.Background(), postToEdit.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("should prevent removing files via patch when edit_file_attachment permission is revoked", func(t *testing.T) {
th.LoginBasic(t)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
postToEdit, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: channel.Id,
Message: "post with file",
FileIds: model.StringArray{fileId},
})
require.NoError(t, err)
th.RemovePermissionFromRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
emptyFileIds := model.StringArray{}
patch := &model.PostPatch{
FileIds: &emptyFileIds,
}
_, resp, err := client.PatchPost(context.Background(), postToEdit.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("should allow patching message without file change when edit_file_attachment permission is revoked", func(t *testing.T) {
th.LoginBasic(t)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
postToEdit, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: channel.Id,
Message: "original message",
FileIds: model.StringArray{fileId},
})
require.NoError(t, err)
th.RemovePermissionFromRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
patch := &model.PostPatch{
Message: model.NewPointer("updated message only"),
}
patchedPost, _, err := client.PatchPost(context.Background(), postToEdit.Id, patch)
require.NoError(t, err)
assert.Equal(t, "updated message only", patchedPost.Message)
})
t.Run("should allow patching with same file ids when edit_file_attachment permission is revoked", func(t *testing.T) {
th.LoginBasic(t)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
postToEdit, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: channel.Id,
Message: "original message",
FileIds: model.StringArray{fileId},
})
require.NoError(t, err)
th.RemovePermissionFromRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionEditFileAttachment.Id, model.ChannelUserRoleId)
sameFileIds := model.StringArray{fileId}
patch := &model.PostPatch{
Message: model.NewPointer("updated message"),
FileIds: &sameFileIds,
}
patchedPost, _, err := client.PatchPost(context.Background(), postToEdit.Id, patch)
require.NoError(t, err)
assert.Equal(t, "updated message", patchedPost.Message)
})
t.Run("should allow patching files when edit_file_attachment permission is present", func(t *testing.T) {
th.LoginBasic(t)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
postToEdit, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: channel.Id,
Message: "original message",
})
require.NoError(t, err)
patch := &model.PostPatch{
FileIds: &model.StringArray{fileId},
}
patchedPost, _, err := client.PatchPost(context.Background(), postToEdit.Id, patch)
require.NoError(t, err)
require.NotNil(t, patchedPost)
})
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: new("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("patch file ids only, 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
})
fileResp, _, err := th.SystemAdminClient.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
newFileId := fileResp.FileInfos[0].Id
oldPost := &model.Post{
ChannelId: channel.Id,
Message: "original message",
CreateAt: model.GetMillis() - 2000,
}
oldPost, _, err = th.SystemAdminClient.CreatePost(context.Background(), oldPost)
require.NoError(t, err)
patch := &model.PostPatch{
FileIds: &model.StringArray{newFileId},
}
_, resp, err := th.SystemAdminClient.PatchPost(context.Background(), oldPost.Id, patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id)
})
t.Run("patch props only, 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
})
oldPost := &model.Post{
ChannelId: channel.Id,
Message: "original message",
CreateAt: model.GetMillis() - 2000,
}
oldPost, _, err := th.SystemAdminClient.CreatePost(context.Background(), oldPost)
require.NoError(t, err)
patch := &model.PostPatch{
Props: &model.StringInterface{"channel_header": "injected"},
}
_, resp, err := th.SystemAdminClient.PatchPost(context.Background(), oldPost.Id, patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id)
})
t.Run("patch is_pinned only, 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
})
oldPost := &model.Post{
ChannelId: channel.Id,
Message: "original message",
CreateAt: model.GetMillis() - 2000,
}
oldPost, _, err := th.SystemAdminClient.CreatePost(context.Background(), oldPost)
require.NoError(t, err)
patch := &model.PostPatch{
IsPinned: new(true),
}
_, resp, err := th.SystemAdminClient.PatchPost(context.Background(), oldPost.Id, patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id)
})
t.Run("patch has_reactions only, 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
})
oldPost := &model.Post{
ChannelId: channel.Id,
Message: "original message",
CreateAt: model.GetMillis() - 2000,
}
oldPost, _, err := th.SystemAdminClient.CreatePost(context.Background(), oldPost)
require.NoError(t, err)
patch := &model.PostPatch{
HasReactions: new(true),
}
_, resp, err := th.SystemAdminClient.PatchPost(context.Background(), oldPost.Id, patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id)
})
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)
})
t.Run("message longer than max post size is rejected", func(t *testing.T) {
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: channel.Id,
Message: "#hashtag a message",
})
require.NoError(t, err)
patch := &model.PostPatch{
Message: new(strings.Repeat("a", th.App.MaxPostSize()+1)),
}
patchedPost, resp, err := client.PatchPost(context.Background(), post.Id, patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
assert.Nil(t, patchedPost)
})
}
func TestPinPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
t.Run("pin post after time limit", 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
})
oldPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
CreateAt: model.GetMillis() - 2000,
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
resp, err := client.PinPost(context.Background(), oldPost.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("idempotent pin/unpin after time limit", 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
})
pinnedPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
IsPinned: true,
CreateAt: model.GetMillis() - 2000,
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
unpinnedPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
IsPinned: false,
CreateAt: model.GetMillis() - 2000,
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
// Both are no-ops and must succeed regardless of age.
_, err := client.PinPost(context.Background(), pinnedPost.Id)
require.NoError(t, err)
_, err = client.UnpinPost(context.Background(), unpinnedPost.Id)
require.NoError(t, err)
})
post := th.BasicPost
_, err := client.PinPost(context.Background(), post.Id)
require.NoError(t, err)
rpost, appErr := th.App.GetSinglePost(th.Context, post.Id, false)
require.Nil(t, appErr)
require.True(t, rpost.IsPinned, "failed to pin post")
resp, err := client.PinPost(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.PinPost(context.Background(), GenerateTestID())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
resp, err = client.PinPost(context.Background(), post.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, err = th.SystemAdminClient.PinPost(context.Background(), post.Id)
require.NoError(t, err)
}
func TestUnpinPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
t.Run("unpin post after time limit", 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
})
oldPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
IsPinned: true,
CreateAt: model.GetMillis() - 2000,
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
resp, err := client.UnpinPost(context.Background(), oldPost.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
pinnedPost := th.CreatePinnedPost(t)
_, err := client.UnpinPost(context.Background(), pinnedPost.Id)
require.NoError(t, err)
rpost, appErr := th.App.GetSinglePost(th.Context, pinnedPost.Id, false)
require.Nil(t, appErr)
require.False(t, rpost.IsPinned)
resp, err := client.UnpinPost(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.UnpinPost(context.Background(), GenerateTestID())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
resp, err = client.UnpinPost(context.Background(), pinnedPost.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, err = th.SystemAdminClient.UnpinPost(context.Background(), pinnedPost.Id)
require.NoError(t, err)
}
func TestGetPostsForChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
post3 := &model.Post{ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a", RootId: post1.Id}
post3, _, _ = client.CreatePost(context.Background(), post3)
time.Sleep(300 * time.Millisecond)
since := model.GetMillis()
time.Sleep(300 * time.Millisecond)
post4 := th.CreatePost(t)
th.TestForAllClients(t, func(t *testing.T, c *model.Client4) {
posts, resp, err := c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 60, "", false, false)
require.NoError(t, err)
require.Equal(t, post4.Id, posts.Order[0], "wrong order")
require.Equal(t, post3.Id, posts.Order[1], "wrong order")
require.Equal(t, post2.Id, posts.Order[2], "wrong order")
require.Equal(t, post1.Id, posts.Order[3], "wrong order")
require.Nil(t, posts.HasNext, "HasNext should not be returned")
posts, resp, _ = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 3, resp.Etag, false, false)
CheckEtag(t, posts, resp)
posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "wrong number returned")
_, ok := posts.Posts[post3.Id]
require.True(t, ok, "missing comment")
_, ok = posts.Posts[post1.Id]
require.True(t, ok, "missing root post")
posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 1, 1, "", false, false)
require.NoError(t, err)
require.Equal(t, post3.Id, posts.Order[0], "wrong order")
posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 10000, 10000, "", false, false)
require.NoError(t, err)
require.Empty(t, posts.Order, "should be no posts")
})
post5 := th.CreatePost(t)
th.TestForAllClients(t, func(t *testing.T, c *model.Client4) {
posts, _, err := c.GetPostsSince(context.Background(), th.BasicChannel.Id, since, false)
require.NoError(t, err)
require.Len(t, posts.Posts, 2, "should return 2 posts")
// "since" query to return empty NextPostId and PrevPostId
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
found := make([]bool, 2)
for _, p := range posts.Posts {
require.LessOrEqual(t, since, p.CreateAt, "bad create at for post returned")
if p.Id == post4.Id {
found[0] = true
} else if p.Id == post5.Id {
found[1] = true
}
}
for _, f := range found {
require.True(t, f, "missing post")
}
_, resp, err := c.GetPostsForChannel(context.Background(), "", 0, 60, "", false, false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = c.GetPostsForChannel(context.Background(), "junk", 0, 60, "", false, false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
_, resp, err := client.GetPostsForChannel(context.Background(), model.NewId(), 0, 60, "", false, false)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.GetPostsForChannel(context.Background(), model.NewId(), 0, 60, "", false, false)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
// more tests for next_post_id, prev_post_id, and order
// There are 12 posts composed of first 2 system messages and 10 created posts
_, _, err = client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
th.CreatePost(t) // post6
post7 := th.CreatePost(t)
post8 := th.CreatePost(t)
th.CreatePost(t) // post9
post10 := th.CreatePost(t)
var posts *model.PostList
th.TestForAllClients(t, func(t *testing.T, c *model.Client4) {
// get the system post IDs posted before the created posts above
posts, _, err = c.GetPostsBefore(context.Background(), th.BasicChannel.Id, post1.Id, 0, 2, "", false, false)
require.NoError(t, err)
systemPostId1 := posts.Order[1]
// similar to '/posts'
posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 60, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 12, "expected 12 posts")
require.Equal(t, post10.Id, posts.Order[0], "posts not in order")
require.Equal(t, systemPostId1, posts.Order[11], "posts not in order")
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
// similar to '/posts?per_page=3'
posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "expected 3 posts")
require.Equal(t, post10.Id, posts.Order[0], "posts not in order")
require.Equal(t, post8.Id, posts.Order[2], "should return 3 posts and match order")
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, post7.Id, posts.PrevPostId, "should return post7.Id as PrevPostId")
// similar to '/posts?per_page=3&page=1'
posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 1, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "expected 3 posts")
require.Equal(t, post7.Id, posts.Order[0], "posts not in order")
require.Equal(t, post5.Id, posts.Order[2], "posts not in order")
require.Equal(t, post8.Id, posts.NextPostId, "should return post8.Id as NextPostId")
require.Equal(t, post4.Id, posts.PrevPostId, "should return post4.Id as PrevPostId")
// similar to '/posts?per_page=3&page=2'
posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 2, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "expected 3 posts")
require.Equal(t, post4.Id, posts.Order[0], "posts not in order")
require.Equal(t, post2.Id, posts.Order[2], "should return 3 posts and match order")
require.Equal(t, post5.Id, posts.NextPostId, "should return post5.Id as NextPostId")
require.Equal(t, post1.Id, posts.PrevPostId, "should return post1.Id as PrevPostId")
// similar to '/posts?per_page=3&page=3'
posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 3, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "expected 3 posts")
require.Equal(t, post1.Id, posts.Order[0], "posts not in order")
require.Equal(t, systemPostId1, posts.Order[2], "should return 3 posts and match order")
require.Equal(t, post2.Id, posts.NextPostId, "should return post2.Id as NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
// similar to '/posts?per_page=3&page=4'
posts, _, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 4, 3, "", false, false)
require.NoError(t, err)
require.Empty(t, posts.Order, "should return 0 post")
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
})
th.TestForAllClients(t, func(t *testing.T, c *model.Client4) {
channel := th.CreatePublicChannel(t)
th.CreatePostWithClient(t, th.SystemAdminClient, channel)
_, err = th.SystemAdminClient.DeleteChannel(context.Background(), channel.Id)
require.NoError(t, err)
_, _, err = c.GetPostsForChannel(context.Background(), channel.Id, 0, 10, "", false, false)
require.NoError(t, err)
}, "Should allow retrieving posts if the channel is archived")
_, err = client.DeletePost(context.Background(), post10.Id)
require.NoError(t, err)
_, err = client.DeletePost(context.Background(), post8.Id)
require.NoError(t, err)
// include deleted posts for non-admin users.
_, resp, err = client.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", false, true)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
// include deleted posts for admin users.
posts, resp, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", false, true)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Len(t, posts.Order, 12, "expected 12 posts")
// not include deleted posts for admin users.
posts, resp, err = c.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", false, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Len(t, posts.Order, 10, "expected 10 posts")
// System admin can access public channel without being member
adminPublicChannel := th.CreatePublicChannel(t)
th.CreateMessagePostNoClient(t, adminPublicChannel, "admin channel post", model.GetMillis())
posts, resp, err = c.GetPostsForChannel(context.Background(), adminPublicChannel.Id, 0, 100, "", false, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotEmpty(t, posts.Order)
// System admin can access private channel without being member
privateChannel := th.CreatePrivateChannel(t)
th.CreateMessagePostNoClient(t, privateChannel, "private channel post", model.GetMillis())
posts, resp, err = c.GetPostsForChannel(context.Background(), privateChannel.Id, 0, 100, "", false, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotEmpty(t, posts.Order)
// System admin can access direct messages without being member
dmChannel := th.CreateDmChannel(t, th.BasicUser2)
th.CreateMessagePostNoClient(t, dmChannel, "test1", model.GetMillis())
posts, resp, err = c.GetPostsForChannel(context.Background(), dmChannel.Id, 0, 100, "", false, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotEmpty(t, posts.Order)
// System admin can access group messages without being member
user3 := th.CreateUser(t)
gmChannel, _, err := th.Client.CreateGroupChannel(context.Background(), []string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id})
require.NoError(t, err)
th.CreateMessagePostNoClient(t, gmChannel, "test2", model.GetMillis())
posts, resp, err = c.GetPostsForChannel(context.Background(), gmChannel.Id, 0, 100, "", false, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotEmpty(t, posts.Order)
})
}
func TestGetFlaggedPostsForUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
user := th.BasicUser
team1 := th.BasicTeam
channel1 := th.BasicChannel
post1 := th.CreatePost(t)
channel2 := th.CreatePublicChannel(t)
post2 := th.CreatePostWithClient(t, client, channel2)
preference := model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: post1.Id,
Value: "true",
}
_, err := client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference})
require.NoError(t, err)
preference.Name = post2.Id
_, err = client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference})
require.NoError(t, err)
opl := model.NewPostList()
opl.AddPost(post1)
opl.AddOrder(post1.Id)
rpl, _, err := client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 0, 10)
require.NoError(t, err)
require.Len(t, rpl.Posts, 1, "should have returned 1 post")
require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched")
rpl, _, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 0, 1)
require.NoError(t, err)
require.Len(t, rpl.Posts, 1, "should have returned 1 post")
rpl, _, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 1, 1)
require.NoError(t, err)
require.Empty(t, rpl.Posts)
rpl, _, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, GenerateTestID(), 0, 10)
require.NoError(t, err)
require.Empty(t, rpl.Posts)
rpl, _, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, "junk", 0, 10)
require.Error(t, err)
require.Nil(t, rpl)
opl.AddPost(post2)
opl.AddOrder(post2.Id)
rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 0, 10)
require.NoError(t, err)
require.Len(t, rpl.Posts, 2, "should have returned 2 posts")
require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched")
rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 0, 1)
require.NoError(t, err)
require.Len(t, rpl.Posts, 1, "should have returned 1 post")
rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 1, 1)
require.NoError(t, err)
require.Len(t, rpl.Posts, 1, "should have returned 1 post")
rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 1000, 10)
require.NoError(t, err)
require.Empty(t, rpl.Posts)
rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, GenerateTestID(), 0, 10)
require.NoError(t, err)
require.Empty(t, rpl.Posts)
rpl, _, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, "junk", 0, 10)
require.Error(t, err)
require.Nil(t, rpl)
channel3 := th.CreatePrivateChannel(t)
post4 := th.CreatePostWithClient(t, client, channel3)
preference.Name = post4.Id
_, err = client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference})
require.NoError(t, err)
opl.AddPost(post4)
opl.AddOrder(post4.Id)
rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10)
require.NoError(t, err)
require.Len(t, rpl.Posts, 3, "should have returned 3 posts")
require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched")
rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 2)
require.NoError(t, err)
require.Len(t, rpl.Posts, 2, "should have returned 2 posts")
rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 2, 2)
require.NoError(t, err)
require.Len(t, rpl.Posts, 1, "should have returned 1 post")
rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 1000, 10)
require.NoError(t, err)
require.Empty(t, rpl.Posts)
channel4 := th.CreateChannelWithClient(t, th.SystemAdminClient, model.ChannelTypePrivate)
post5 := th.CreatePostWithClient(t, th.SystemAdminClient, channel4)
preference.Name = post5.Id
resp, err := client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10)
require.NoError(t, err)
require.Len(t, rpl.Posts, 3, "should have returned 3 posts")
require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched")
th.AddUserToChannel(t, user, channel4)
_, err = client.UpdatePreferences(context.Background(), user.Id, model.Preferences{preference})
require.NoError(t, err)
rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10)
require.NoError(t, err)
opl.AddPost(post5)
opl.AddOrder(post5.Id)
require.Len(t, rpl.Posts, 4, "should have returned 4 posts")
require.Equal(t, opl.Posts, rpl.Posts, "posts should have matched")
appErr := th.App.RemoveUserFromChannel(th.Context, user.Id, "", channel4)
assert.Nil(t, appErr, "unable to remove user from channel")
rpl, _, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10)
require.NoError(t, err)
opl2 := model.NewPostList()
opl2.AddPost(post1)
opl2.AddOrder(post1.Id)
opl2.AddPost(post2)
opl2.AddOrder(post2.Id)
opl2.AddPost(post4)
opl2.AddOrder(post4.Id)
require.Len(t, rpl.Posts, 3, "should have returned 3 posts")
require.Equal(t, opl2.Posts, rpl.Posts, "posts should have matched")
_, resp, err = client.GetFlaggedPostsForUser(context.Background(), "junk", 0, 10)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetFlaggedPostsForUser(context.Background(), GenerateTestID(), 0, 10)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 0, 10)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, resp, err = client.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 0, 10)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, resp, err = client.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.SystemAdminClient.GetFlaggedPostsForUserInChannel(context.Background(), user.Id, channel1.Id, 0, 10)
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFlaggedPostsForUserInTeam(context.Background(), user.Id, team1.Id, 0, 10)
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10)
require.NoError(t, err)
mockStore := mocks.Store{}
mockPostStore := mocks.PostStore{}
mockPostStore.On("GetFlaggedPosts", mock.AnythingOfType("string"), mock.AnythingOfType("int"), mock.AnythingOfType("int")).Return(nil, errors.New("some-error"))
mockPostStore.On("ClearCaches").Return()
mockStore.On("Team").Return(th.App.Srv().Store().Team())
mockStore.On("Channel").Return(th.App.Srv().Store().Channel())
mockStore.On("User").Return(th.App.Srv().Store().User())
mockStore.On("Scheme").Return(th.App.Srv().Store().Scheme())
mockStore.On("Post").Return(&mockPostStore)
mockStore.On("FileInfo").Return(th.App.Srv().Store().FileInfo())
mockStore.On("Webhook").Return(th.App.Srv().Store().Webhook())
mockStore.On("System").Return(th.App.Srv().Store().System())
mockStore.On("License").Return(th.App.Srv().Store().License())
mockStore.On("Role").Return(th.App.Srv().Store().Role())
mockStore.On("Close").Return(nil)
// Playbooks DB job requires a plugin mock
pluginStore := mocks.PluginStore{}
pluginStore.On("List", mock.Anything, mock.Anything, mock.Anything).Return([]string{}, nil)
mockStore.On("Plugin").Return(&pluginStore)
th.App.Srv().SetStore(&mockStore)
_, resp, err = th.SystemAdminClient.GetFlaggedPostsForUser(context.Background(), user.Id, 0, 10)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
}
func TestGetPostsBefore(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
post3 := th.CreatePost(t)
post4 := th.CreatePost(t)
post5 := th.CreatePost(t)
posts, _, err := client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post3.Id, 0, 100, "", false, false)
require.NoError(t, err)
found := make([]bool, 2)
for _, p := range posts.Posts {
if p.Id == post1.Id {
found[0] = true
} else if p.Id == post2.Id {
found[1] = true
}
require.NotEqual(t, post4.Id, p.Id, "returned posts after")
require.NotEqual(t, post5.Id, p.Id, "returned posts after")
}
for _, f := range found {
require.True(t, f, "missing post")
}
require.Equal(t, post3.Id, posts.NextPostId, "should match NextPostId")
require.Equal(t, "", posts.PrevPostId, "should match empty PrevPostId")
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post4.Id, 1, 1, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Posts, 1, "too many posts returned")
require.Equal(t, post2.Id, posts.Order[0], "should match returned post")
require.Equal(t, post3.Id, posts.NextPostId, "should match NextPostId")
require.Equal(t, post1.Id, posts.PrevPostId, "should match PrevPostId")
_, resp, err := client.GetPostsBefore(context.Background(), th.BasicChannel.Id, "junk", 1, 1, "", false, false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post5.Id, 0, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Posts, 3, "should match length of posts returned")
require.Equal(t, post4.Id, posts.Order[0], "should match returned post")
require.Equal(t, post2.Id, posts.Order[2], "should match returned post")
require.Equal(t, post5.Id, posts.NextPostId, "should match NextPostId")
require.Equal(t, post1.Id, posts.PrevPostId, "should match PrevPostId")
// get the system post IDs posted before the created posts above
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post1.Id, 0, 2, "", false, false)
require.NoError(t, err)
systemPostId2 := posts.Order[0]
systemPostId1 := posts.Order[1]
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post5.Id, 1, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Posts, 3, "should match length of posts returned")
require.Equal(t, post1.Id, posts.Order[0], "should match returned post")
require.Equal(t, systemPostId2, posts.Order[1], "should match returned post")
require.Equal(t, systemPostId1, posts.Order[2], "should match returned post")
require.Equal(t, post2.Id, posts.NextPostId, "should match NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return empty PrevPostId")
// more tests for next_post_id, prev_post_id, and order
// There are 12 posts composed of first 2 system messages and 10 created posts
post6 := th.CreatePost(t)
th.CreatePost(t) // post7
post8 := th.CreatePost(t)
post9 := th.CreatePost(t)
post10 := th.CreatePost(t) // post10
// similar to '/posts?before=post9'
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 0, 60, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 10, "expected 10 posts")
require.Equal(t, post8.Id, posts.Order[0], "posts not in order")
require.Equal(t, systemPostId1, posts.Order[9], "posts not in order")
require.Equal(t, post9.Id, posts.NextPostId, "should return post9.Id as NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
// similar to '/posts?before=post9&per_page=3'
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 0, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "expected 3 posts")
require.Equal(t, post8.Id, posts.Order[0], "posts not in order")
require.Equal(t, post6.Id, posts.Order[2], "should return 3 posts and match order")
require.Equal(t, post9.Id, posts.NextPostId, "should return post9.Id as NextPostId")
require.Equal(t, post5.Id, posts.PrevPostId, "should return post5.Id as PrevPostId")
// similar to '/posts?before=post9&per_page=3&page=1'
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 1, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "expected 3 posts")
require.Equal(t, post5.Id, posts.Order[0], "posts not in order")
require.Equal(t, post3.Id, posts.Order[2], "posts not in order")
require.Equal(t, post6.Id, posts.NextPostId, "should return post6.Id as NextPostId")
require.Equal(t, post2.Id, posts.PrevPostId, "should return post2.Id as PrevPostId")
// similar to '/posts?before=post9&per_page=3&page=2'
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 2, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "expected 3 posts")
require.Equal(t, post2.Id, posts.Order[0], "posts not in order")
require.Equal(t, systemPostId2, posts.Order[2], "posts not in order")
require.Equal(t, post3.Id, posts.NextPostId, "should return post3.Id as NextPostId")
require.Equal(t, systemPostId1, posts.PrevPostId, "should return systemPostId1 as PrevPostId")
// similar to '/posts?before=post1&per_page=3'
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post1.Id, 0, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 2, "expected 2 posts")
require.Equal(t, systemPostId2, posts.Order[0], "posts not in order")
require.Equal(t, systemPostId1, posts.Order[1], "posts not in order")
require.Equal(t, post1.Id, posts.NextPostId, "should return post1.Id as NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
// similar to '/posts?before=systemPostId1'
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, systemPostId1, 0, 60, "", false, false)
require.NoError(t, err)
require.Empty(t, posts.Order, "should return 0 post")
require.Equal(t, systemPostId1, posts.NextPostId, "should return systemPostId1 as NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
// similar to '/posts?before=systemPostId1&per_page=60&page=1'
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, systemPostId1, 1, 60, "", false, false)
require.NoError(t, err)
require.Empty(t, posts.Order, "should return 0 posts")
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
// similar to '/posts?before=non-existent-post'
nonExistentPostId := model.NewId()
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, nonExistentPostId, 0, 60, "", false, false)
require.NoError(t, err)
require.Empty(t, posts.Order, "should return 0 post")
require.Equal(t, nonExistentPostId, posts.NextPostId, "should return nonExistentPostId as NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
_, err = client.DeletePost(context.Background(), post9.Id)
require.NoError(t, err)
_, err = client.DeletePost(context.Background(), post8.Id)
require.NoError(t, err)
// include deleted posts for non-admin users.
_, resp, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post9.Id, 0, 60, "", false, true)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
// include deleted posts for admin users.
posts, resp, err = c.GetPostsBefore(context.Background(), th.BasicChannel.Id, post10.Id, 0, 60, "", false, true)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Len(t, posts.Order, 11, "expected 11 posts")
// not include deleted posts for admin users.
posts, resp, err = c.GetPostsBefore(context.Background(), th.BasicChannel.Id, post10.Id, 0, 60, "", false, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Len(t, posts.Order, 9, "expected 9 posts")
})
}
func TestGetPostsAfter(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
post3 := th.CreatePost(t)
post4 := th.CreatePost(t)
post5 := th.CreatePost(t)
posts, _, err := client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post3.Id, 0, 100, "", false, false)
require.NoError(t, err)
found := make([]bool, 2)
for _, p := range posts.Posts {
if p.Id == post4.Id {
found[0] = true
} else if p.Id == post5.Id {
found[1] = true
}
require.NotEqual(t, post1.Id, p.Id, "returned posts before")
require.NotEqual(t, post2.Id, p.Id, "returned posts before")
}
for _, f := range found {
require.True(t, f, "missing post")
}
require.Equal(t, "", posts.NextPostId, "should match empty NextPostId")
require.Equal(t, post3.Id, posts.PrevPostId, "should match PrevPostId")
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 1, 1, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Posts, 1, "too many posts returned")
require.Equal(t, post4.Id, posts.Order[0], "should match returned post")
require.Equal(t, post5.Id, posts.NextPostId, "should match NextPostId")
require.Equal(t, post3.Id, posts.PrevPostId, "should match PrevPostId")
_, resp, err := client.GetPostsAfter(context.Background(), th.BasicChannel.Id, "junk", 1, 1, "", false, false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 0, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Posts, 3, "should match length of posts returned")
require.Equal(t, post4.Id, posts.Order[0], "should match returned post")
require.Equal(t, post2.Id, posts.Order[2], "should match returned post")
require.Equal(t, post5.Id, posts.NextPostId, "should match NextPostId")
require.Equal(t, post1.Id, posts.PrevPostId, "should match PrevPostId")
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 1, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Posts, 1, "should match length of posts returned")
require.Equal(t, post5.Id, posts.Order[0], "should match returned post")
require.Equal(t, "", posts.NextPostId, "should match NextPostId")
require.Equal(t, post4.Id, posts.PrevPostId, "should match PrevPostId")
// more tests for next_post_id, prev_post_id, and order
// There are 12 posts composed of first 2 system messages and 10 created posts
post6 := th.CreatePost(t)
th.CreatePost(t) // post7
post8 := th.CreatePost(t)
post9 := th.CreatePost(t)
post10 := th.CreatePost(t)
// similar to '/posts?after=post2'
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 0, 60, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 8, "expected 8 posts")
require.Equal(t, post10.Id, posts.Order[0], "should match order")
require.Equal(t, post3.Id, posts.Order[7], "should match order")
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, post2.Id, posts.PrevPostId, "should return post2.Id as PrevPostId")
// similar to '/posts?after=post2&per_page=3'
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 0, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "expected 3 posts")
require.Equal(t, post5.Id, posts.Order[0], "should match order")
require.Equal(t, post3.Id, posts.Order[2], "should return 3 posts and match order")
require.Equal(t, post6.Id, posts.NextPostId, "should return post6.Id as NextPostId")
require.Equal(t, post2.Id, posts.PrevPostId, "should return post2.Id as PrevPostId")
// similar to '/posts?after=post2&per_page=3&page=1'
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 1, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "expected 3 posts")
require.Equal(t, post8.Id, posts.Order[0], "should match order")
require.Equal(t, post6.Id, posts.Order[2], "should match order")
require.Equal(t, post9.Id, posts.NextPostId, "should return post9.Id as NextPostId")
require.Equal(t, post5.Id, posts.PrevPostId, "should return post5.Id as PrevPostId")
// similar to '/posts?after=post2&per_page=3&page=2'
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post2.Id, 2, 3, "", false, false)
require.NoError(t, err)
require.Len(t, posts.Order, 2, "expected 2 posts")
require.Equal(t, post10.Id, posts.Order[0], "should match order")
require.Equal(t, post9.Id, posts.Order[1], "should match order")
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, post8.Id, posts.PrevPostId, "should return post8.Id as PrevPostId")
// similar to '/posts?after=post10'
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post10.Id, 0, 60, "", false, false)
require.NoError(t, err)
require.Empty(t, posts.Order, "should return 0 post")
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, post10.Id, posts.PrevPostId, "should return post10.Id as PrevPostId")
// similar to '/posts?after=post10&page=1'
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post10.Id, 1, 60, "", false, false)
require.NoError(t, err)
require.Empty(t, posts.Order, "should return 0 post")
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, "", posts.PrevPostId, "should return an empty PrevPostId")
// similar to '/posts?after=non-existent-post'
nonExistentPostId := model.NewId()
posts, _, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, nonExistentPostId, 0, 60, "", false, false)
require.NoError(t, err)
require.Empty(t, posts.Order, "should return 0 post")
require.Equal(t, "", posts.NextPostId, "should return an empty NextPostId")
require.Equal(t, nonExistentPostId, posts.PrevPostId, "should return nonExistentPostId as PrevPostId")
_, err = client.DeletePost(context.Background(), post10.Id)
require.NoError(t, err)
_, err = client.DeletePost(context.Background(), post9.Id)
require.NoError(t, err)
// include deleted posts for non-admin users.
_, resp, err = client.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 0, 60, "", false, true)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
// include deleted posts for admin users.
posts, resp, err = c.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 0, 60, "", false, true)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Len(t, posts.Order, 9, "expected 9 posts")
// not include deleted posts for admin users.
posts, resp, err = c.GetPostsAfter(context.Background(), th.BasicChannel.Id, post1.Id, 0, 60, "", false, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Len(t, posts.Order, 7, "expected 7 posts")
})
}
func TestGetPostsForChannelAroundLastUnread(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
userId := th.BasicUser.Id
channelId := th.BasicChannel.Id
// 12 posts = 2 systems posts + 10 created posts below
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
post3 := th.CreatePost(t)
post4 := th.CreatePost(t)
post5 := th.CreatePost(t)
replyPost := &model.Post{ChannelId: channelId, Message: model.NewId(), RootId: post4.Id}
post6, _, err := client.CreatePost(context.Background(), replyPost)
require.NoError(t, err)
post7, _, err := client.CreatePost(context.Background(), replyPost)
require.NoError(t, err)
post8, _, err := client.CreatePost(context.Background(), replyPost)
require.NoError(t, err)
post9, _, err := client.CreatePost(context.Background(), replyPost)
require.NoError(t, err)
post10, _, err := client.CreatePost(context.Background(), replyPost)
require.NoError(t, err)
postIdNames := map[string]string{
post1.Id: "post1",
post2.Id: "post2",
post3.Id: "post3",
post4.Id: "post4",
post5.Id: "post5",
post6.Id: "post6 (reply to post4)",
post7.Id: "post7 (reply to post4)",
post8.Id: "post8 (reply to post4)",
post9.Id: "post9 (reply to post4)",
post10.Id: "post10 (reply to post4)",
}
namePost := func(postId string) string {
name, ok := postIdNames[postId]
if ok {
return name
}
return fmt.Sprintf("unknown (%s)", postId)
}
namePosts := func(postIds []string) []string {
namedPostIds := make([]string, 0, len(postIds))
for _, postId := range postIds {
namedPostIds = append(namedPostIds, namePost(postId))
}
return namedPostIds
}
namePostsMap := func(posts map[string]*model.Post) []string {
namedPostIds := make([]string, 0, len(posts))
for postId := range posts {
namedPostIds = append(namedPostIds, namePost(postId))
}
sort.Strings(namedPostIds)
return namedPostIds
}
assertPostList := func(t *testing.T, expected, actual *model.PostList) {
t.Helper()
require.Equal(t, namePosts(expected.Order), namePosts(actual.Order), "unexpected post order")
require.Equal(t, namePostsMap(expected.Posts), namePostsMap(actual.Posts), "unexpected posts")
require.Equal(t, namePost(expected.NextPostId), namePost(actual.NextPostId), "unexpected next post id")
require.Equal(t, namePost(expected.PrevPostId), namePost(actual.PrevPostId), "unexpected prev post id")
}
// Setting limit_after to zero should fail with a 400 BadRequest.
posts, resp, err := client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 20, 0, false)
require.Error(t, err)
CheckErrorID(t, err, "api.context.invalid_url_param.app_error")
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
require.Nil(t, posts)
// All returned posts are all read by the user, since it's created by the user itself.
posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 20, 20, false)
require.NoError(t, err)
require.Len(t, posts.Order, 12, "Should return 12 posts only since there's no unread post")
// Set channel member's last viewed to 0.
// All returned posts are latest posts as if all previous posts were already read by the user.
channelMember, err := th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId)
require.NoError(t, err)
channelMember.LastViewedAt = 0
_, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember)
require.NoError(t, err)
th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId)
posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 20, 20, false)
require.NoError(t, err)
require.Len(t, posts.Order, 12, "Should return 12 posts only since there's no unread post")
// get the first system post generated before the created posts above
posts, _, err = client.GetPostsBefore(context.Background(), th.BasicChannel.Id, post1.Id, 0, 2, "", false, false)
require.NoError(t, err)
systemPost0 := posts.Posts[posts.Order[0]]
postIdNames[systemPost0.Id] = "system post 0"
systemPost1 := posts.Posts[posts.Order[1]]
postIdNames[systemPost1.Id] = "system post 1"
// Set channel member's last viewed before post1.
channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId)
require.NoError(t, err)
channelMember.LastViewedAt = post1.CreateAt - 1
_, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember)
require.NoError(t, err)
th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId)
posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 3, 3, false)
require.NoError(t, err)
assertPostList(t, &model.PostList{
Order: []string{post3.Id, post2.Id, post1.Id, systemPost0.Id, systemPost1.Id},
Posts: map[string]*model.Post{
systemPost0.Id: systemPost0,
systemPost1.Id: systemPost1,
post1.Id: post1,
post2.Id: post2,
post3.Id: post3,
},
NextPostId: post4.Id,
PrevPostId: "",
}, posts)
// Set channel member's last viewed before post6.
channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId)
require.NoError(t, err)
channelMember.LastViewedAt = post6.CreateAt - 1
_, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember)
require.NoError(t, err)
th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId)
posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 3, 3, false)
require.NoError(t, err)
assertPostList(t, &model.PostList{
Order: []string{post8.Id, post7.Id, post6.Id, post5.Id, post4.Id, post3.Id},
Posts: map[string]*model.Post{
post3.Id: post3,
post4.Id: post4,
post5.Id: post5,
post6.Id: post6,
post7.Id: post7,
post8.Id: post8,
post9.Id: post9,
post10.Id: post10,
},
NextPostId: post9.Id,
PrevPostId: post2.Id,
}, posts)
// Set channel member's last viewed before post10.
channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId)
require.NoError(t, err)
channelMember.LastViewedAt = post10.CreateAt - 1
_, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember)
require.NoError(t, err)
th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId)
posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 3, 3, false)
require.NoError(t, err)
assertPostList(t, &model.PostList{
Order: []string{post10.Id, post9.Id, post8.Id, post7.Id},
Posts: map[string]*model.Post{
post4.Id: post4,
post6.Id: post6,
post7.Id: post7,
post8.Id: post8,
post9.Id: post9,
post10.Id: post10,
},
NextPostId: "",
PrevPostId: post6.Id,
}, posts)
// Set channel member's last viewed equal to post10.
channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId)
require.NoError(t, err)
channelMember.LastViewedAt = post10.CreateAt
_, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember)
require.NoError(t, err)
th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId)
posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 3, 3, false)
require.NoError(t, err)
assertPostList(t, &model.PostList{
Order: []string{post10.Id, post9.Id, post8.Id},
Posts: map[string]*model.Post{
post4.Id: post4,
post6.Id: post6,
post7.Id: post7,
post8.Id: post8,
post9.Id: post9,
post10.Id: post10,
},
NextPostId: "",
PrevPostId: post7.Id,
}, posts)
// Set channel member's last viewed to just before a new reply to a previous thread, not
// otherwise in the requested window.
post11 := th.CreatePost(t)
post12, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: channelId,
Message: model.NewId(),
RootId: post4.Id,
})
require.NoError(t, err)
post13 := th.CreatePost(t)
postIdNames[post11.Id] = "post11"
postIdNames[post12.Id] = "post12 (reply to post4)"
postIdNames[post13.Id] = "post13"
channelMember, err = th.App.Srv().Store().Channel().GetMember(th.Context, channelId, userId)
require.NoError(t, err)
channelMember.LastViewedAt = post12.CreateAt - 1
_, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, channelMember)
require.NoError(t, err)
th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelId)
posts, _, err = client.GetPostsAroundLastUnread(context.Background(), userId, channelId, 1, 2, false)
require.NoError(t, err)
assertPostList(t, &model.PostList{
Order: []string{post13.Id, post12.Id, post11.Id},
Posts: map[string]*model.Post{
post4.Id: post4,
post6.Id: post6,
post7.Id: post7,
post8.Id: post8,
post9.Id: post9,
post10.Id: post10,
post11.Id: post11,
post12.Id: post12,
post13.Id: post13,
},
NextPostId: "",
PrevPostId: post10.Id,
}, posts)
}
// TestGetPostsForChannelAroundLastUnreadBurnOnRead verifies that burn-on-read
// posts which have expired for the requesting user are excluded from the
// response and, crucially, that the NextPostId/PrevPostId pagination cursors
// step over them instead of pointing at a post the client can no longer see.
// A cursor that referenced an expired post made the webapp believe more posts
// were available, leaving the channel stuck loading (MM-67500).
func TestGetPostsForChannelAroundLastUnreadBurnOnRead(t *testing.T) {
mainHelper.Parallel(t)
th := SetupEnterprise(t).InitBasic(t)
enableBurnOnReadFeature(th)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.FeatureFlags.BurnOnRead = true
})
// user1 is the viewer, user2 creates burn-on-read posts
user1 := th.BasicUser
client1 := th.Client
user2 := th.CreateUser(t)
th.LinkUserToTeam(t, user2, th.BasicTeam)
client2 := th.CreateClient()
_, _, err := client2.Login(context.Background(), user2.Email, user2.Password)
require.NoError(t, err)
createChannel := func(t *testing.T) *model.Channel {
t.Helper()
ch := th.CreatePublicChannel(t)
// since the channel is created with th.Client,
// user1 will be in the channel.
th.AddUserToChannel(t, user2, ch)
t.Cleanup(func() {
_, err := th.Client.DeleteChannel(context.Background(), ch.Id)
require.NoError(t, err)
})
return ch
}
createRegularPost := func(t *testing.T, client *model.Client4, channel *model.Channel, message string) *model.Post {
t.Helper()
post := &model.Post{
ChannelId: channel.Id,
Message: message,
}
created, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
return created
}
createBurnOnReadPost := func(t *testing.T, client *model.Client4, channel *model.Channel, message string) *model.Post {
t.Helper()
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypeBurnOnRead,
}
created, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
return created
}
setLastViewedAt := func(t *testing.T, channelID, userID string, at int64) {
t.Helper()
member, err := th.App.Srv().Store().Channel().GetMember(th.Context, channelID, userID)
require.NoError(t, err)
member.LastViewedAt = at
_, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, member)
require.NoError(t, err)
th.App.Srv().Store().Post().InvalidateLastPostTimeCache(channelID)
}
burnPost := func(t *testing.T, client *model.Client4, postID string) {
t.Helper()
resp, err := client.BurnPost(context.Background(), postID)
require.NoError(t, err)
CheckOKStatus(t, resp)
}
t.Run("expired BoR post is excluded from results and NextPostId/PrevPostId skip it", func(t *testing.T) {
ch := createChannel(t)
// Create posts: regular1, bor (by user2), regular2, regular3
regular1 := createRegularPost(t, client1, ch, "regular 1")
borPost := createBurnOnReadPost(t, client2, ch, "secret message")
regular2 := createRegularPost(t, client1, ch, "regular 2")
regular3 := createRegularPost(t, client1, ch, "regular 3")
// Set last viewed before the BoR post so it becomes the first unread
setLastViewedAt(t, ch.Id, user1.Id, borPost.CreateAt-1)
// User1 reveals then burns the BoR post (expires the read receipt)
_, resp, err := client1.RevealPost(context.Background(), borPost.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
burnPost(t, client1, borPost.Id)
// Call GetPostsAroundLastUnread
posts, _, err := client1.GetPostsAroundLastUnread(context.Background(), user1.Id, ch.Id, 5, 5, false)
require.NoError(t, err)
// BoR post should NOT be in the Order or Posts
assert.NotContains(t, posts.Order, borPost.Id, "expired BoR post should not be in Order")
assert.NotContains(t, posts.Posts, borPost.Id, "expired BoR post should not be in Posts")
// regular2 and regular3 should be present
assert.Contains(t, posts.Posts, regular2.Id, "regular2 should be in Posts")
assert.Contains(t, posts.Posts, regular3.Id, "regular3 should be in Posts")
// PrevPostId should skip the expired BoR post and point to regular1
assert.Equal(t, regular1.Id, posts.PrevPostId, "PrevPostId should skip expired BoR post and point to regular1")
})
t.Run("non-expired BoR post is visible and NextPostId/PrevPostId reference correctly", func(t *testing.T) {
ch := createChannel(t)
// Create posts: regular1, regular2, borPost (by user2), regular3
// borPost is positioned after regular2 so it falls just outside
// the page window when small limits are used.
regular1 := createRegularPost(t, client1, ch, "regular 1")
regular2 := createRegularPost(t, client1, ch, "regular 2")
borPost := createBurnOnReadPost(t, client2, ch, "secret message")
_ = createRegularPost(t, client1, ch, "regular 3")
// Set last viewed before regular1 so it becomes the first unread
setLastViewedAt(t, ch.Id, user1.Id, regular1.CreateAt-1)
// Reveal the BoR post (receipt is still valid, NOT expired)
_, _, err := client1.RevealPost(context.Background(), borPost.Id)
require.NoError(t, err)
// Use limit_after=2 so the window contains regular1 + regular2.
// borPost sits just outside the window and should appear as NextPostId.
posts, _, err := client1.GetPostsAroundLastUnread(context.Background(), user1.Id, ch.Id, 1, 2, false)
require.NoError(t, err)
// regular2 must be in the window; borPost must be outside it
assert.Contains(t, posts.Order, regular2.Id, "regular2 should be in the page window")
assert.NotContains(t, posts.Order, borPost.Id, "borPost should be outside the page window")
// Non-expired revealed BoR post should appear as NextPostId
assert.Equal(t, borPost.Id, posts.NextPostId, "non-expired revealed BoR post should appear as NextPostId")
// Also verify with full limits that the BoR post is included in results
postsFull, _, err := client1.GetPostsAroundLastUnread(context.Background(), user1.Id, ch.Id, 5, 5, false)
require.NoError(t, err)
assert.Contains(t, postsFull.Order, borPost.Id, "non-expired BoR post should be in Order with full page")
assert.Contains(t, postsFull.Posts, borPost.Id, "non-expired BoR post should be in Posts with full page")
})
t.Run("unrevealed BoR post appears with empty message", func(t *testing.T) {
ch := createChannel(t)
_ = createRegularPost(t, client1, ch, "regular 1")
borPost := createBurnOnReadPost(t, client2, ch, "secret message")
_ = createRegularPost(t, client1, ch, "regular 2")
// Set last viewed before borPost
setLastViewedAt(t, ch.Id, user1.Id, borPost.CreateAt-1)
// Do NOT reveal the BoR post
posts, _, err := client1.GetPostsAroundLastUnread(context.Background(), user1.Id, ch.Id, 5, 5, false)
require.NoError(t, err)
// BoR post should be in the list but with empty message (unrevealed)
assert.Contains(t, posts.Order, borPost.Id, "unrevealed BoR post should still be in Order")
assert.Contains(t, posts.Posts, borPost.Id, "unrevealed BoR post should still be in Posts")
assert.Empty(t, posts.Posts[borPost.Id].Message, "unrevealed BoR post should have empty message")
})
t.Run("multiple expired BoR posts are all excluded from results", func(t *testing.T) {
ch := createChannel(t)
regular1 := createRegularPost(t, client1, ch, "regular 1")
bor1 := createBurnOnReadPost(t, client2, ch, "secret 1")
bor2 := createBurnOnReadPost(t, client2, ch, "secret 2")
regular2 := createRegularPost(t, client1, ch, "regular 2")
// Set last viewed before bor1
setLastViewedAt(t, ch.Id, user1.Id, bor1.CreateAt-1)
// Reveal then burn both BoR posts (expires the read receipts)
_, _, err := client1.RevealPost(context.Background(), bor1.Id)
require.NoError(t, err)
_, _, err = client1.RevealPost(context.Background(), bor2.Id)
require.NoError(t, err)
burnPost(t, client1, bor1.Id)
burnPost(t, client1, bor2.Id)
posts, _, err := client1.GetPostsAroundLastUnread(context.Background(), user1.Id, ch.Id, 5, 5, false)
require.NoError(t, err)
// Both BoR posts should be excluded from Order and Posts
assert.NotContains(t, posts.Order, bor1.Id, "expired bor1 should not be in Order")
assert.NotContains(t, posts.Order, bor2.Id, "expired bor2 should not be in Order")
assert.NotContains(t, posts.Posts, bor1.Id, "expired bor1 should not be in Posts")
assert.NotContains(t, posts.Posts, bor2.Id, "expired bor2 should not be in Posts")
// Regular posts should be present
assert.Contains(t, posts.Posts, regular2.Id, "regular2 should be in Posts")
assert.Equal(t, regular1.Id, posts.PrevPostId, "PrevPostId should skip both expired BoR posts and point to regular1")
})
t.Run("BoR feature disabled - expired receipt posts still appear normally", func(t *testing.T) {
ch := createChannel(t)
_ = createRegularPost(t, client1, ch, "regular 1")
borPost := createBurnOnReadPost(t, client2, ch, "secret message")
_ = createRegularPost(t, client1, ch, "regular 2")
setLastViewedAt(t, ch.Id, user1.Id, borPost.CreateAt-1)
// Reveal then burn (expires the read receipt)
_, _, err := client1.RevealPost(context.Background(), borPost.Id)
require.NoError(t, err)
burnPost(t, client1, borPost.Id)
// Disable the feature
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.EnableBurnOnRead = model.NewPointer(false)
})
t.Cleanup(func() {
enableBurnOnReadFeature(th)
})
posts, _, err := client1.GetPostsAroundLastUnread(context.Background(), user1.Id, ch.Id, 5, 5, false)
require.NoError(t, err)
// With feature disabled, the BoR post should still be in the list
// when the feature is disabled, only the creation of the post is limited.
assert.Contains(t, posts.Order, borPost.Id, "BoR post should appear when feature is disabled")
assert.Contains(t, posts.Posts, borPost.Id, "BoR post should appear when feature is disabled")
})
t.Run("author sees own BoR post without needing to reveal", func(t *testing.T) {
ch := createChannel(t)
_ = createRegularPost(t, client2, ch, "regular 1")
borPost := createBurnOnReadPost(t, client2, ch, "my secret message")
_ = createRegularPost(t, client2, ch, "regular 2")
setLastViewedAt(t, ch.Id, user2.Id, borPost.CreateAt-1)
// Author (user2) calls the endpoint - should see their own BoR post
posts, _, err := client2.GetPostsAroundLastUnread(context.Background(), user2.Id, ch.Id, 5, 5, false)
require.NoError(t, err)
assert.Contains(t, posts.Order, borPost.Id, "author should see own BoR post in Order")
assert.Contains(t, posts.Posts, borPost.Id, "author should see own BoR post in Posts")
assert.Equal(t, "my secret message", posts.Posts[borPost.Id].Message, "author should see full BoR post message")
})
// createExpiredBoRRun creates n burn-on-read posts (authored by user2) and has
// user1 reveal then burn each one, leaving an expired read receipt for user1.
createExpiredBoRRun := func(t *testing.T, ch *model.Channel, n int) []string {
t.Helper()
ids := make([]string, 0, n)
for i := range n {
bor := createBurnOnReadPost(t, client2, ch, fmt.Sprintf("secret %d", i))
_, _, err := client1.RevealPost(context.Background(), bor.Id)
require.NoError(t, err)
burnPost(t, client1, bor.Id)
ids = append(ids, bor.Id)
}
return ids
}
// These two cases cover the reviewer's MM-67500 concern: when MORE than one
// page (30) worth of expired burn-on-read posts sits between the first-unread
// post and the post at the far end, the server must page past all of them so
// the far post stays reachable and no cursor points at a hidden expired post.
t.Run("newest post stays reachable past a full page of expired BoR posts", func(t *testing.T) {
ch := createChannel(t)
firstUnread := createRegularPost(t, client1, ch, "first unread")
expired := createExpiredBoRRun(t, ch, 35) // more than one 30-post page
newest := createRegularPost(t, client1, ch, "newest")
setLastViewedAt(t, ch.Id, user1.Id, firstUnread.CreateAt-1)
posts, _, err := client1.GetPostsAroundLastUnread(context.Background(), user1.Id, ch.Id, 30, 30, false)
require.NoError(t, err)
assert.Contains(t, posts.Order, firstUnread.Id, "first unread post should be returned")
assert.Contains(t, posts.Order, newest.Id, "newest post must be reachable past the expired BoR posts")
for _, id := range expired {
assert.NotContains(t, posts.Order, id, "expired BoR post must not be returned")
assert.NotEqual(t, id, posts.NextPostId, "NextPostId must never point at an expired BoR post")
}
})
t.Run("oldest post stays reachable past a full page of expired BoR posts", func(t *testing.T) {
ch := createChannel(t)
oldest := createRegularPost(t, client1, ch, "oldest")
expired := createExpiredBoRRun(t, ch, 35) // more than one 30-post page
firstUnread := createRegularPost(t, client1, ch, "first unread")
newest := createRegularPost(t, client1, ch, "newest")
setLastViewedAt(t, ch.Id, user1.Id, firstUnread.CreateAt-1)
posts, _, err := client1.GetPostsAroundLastUnread(context.Background(), user1.Id, ch.Id, 30, 30, false)
require.NoError(t, err)
assert.Contains(t, posts.Order, firstUnread.Id, "first unread post should be returned")
assert.Contains(t, posts.Order, newest.Id, "newest post should be returned")
assert.Contains(t, posts.Order, oldest.Id, "oldest post must be reachable past the expired BoR posts")
for _, id := range expired {
assert.NotContains(t, posts.Order, id, "expired BoR post must not be returned")
assert.NotEqual(t, id, posts.PrevPostId, "PrevPostId must never point at an expired BoR post")
}
})
}
func TestGetPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// TODO: migrate this entirely to the subtest's client
// once the other methods are migrated too.
client := th.Client
var privatePost *model.Post
th.TestForAllClients(t, func(t *testing.T, c *model.Client4) {
t.Helper()
post, resp, err := c.GetPost(context.Background(), th.BasicPost.Id, "")
require.NoError(t, err)
require.Equal(t, th.BasicPost.Id, post.Id, "post ids don't match")
post, resp, err = c.GetPost(context.Background(), th.BasicPost.Id, resp.Etag)
require.NoError(t, err)
CheckEtag(t, post, resp)
_, resp, err = c.GetPost(context.Background(), "", "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, resp, err = c.GetPost(context.Background(), "junk", "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = c.GetPost(context.Background(), model.NewId(), "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, err = client.RemoveUserFromChannel(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
t.Cleanup(func() {
// Add the user back to the channel
_, _, err = client.AddChannelMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
})
// Channel is public, should be able to read post
_, _, err = c.GetPost(context.Background(), th.BasicPost.Id, "")
require.NoError(t, err)
privatePost = th.CreatePostWithClient(t, client, th.BasicPrivateChannel)
_, _, err = c.GetPost(context.Background(), privatePost.Id, "")
require.NoError(t, err)
})
_, err := client.RemoveUserFromChannel(context.Background(), th.BasicPrivateChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
// Channel is private, should not be able to read post
_, resp, err := client.GetPost(context.Background(), privatePost.Id, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// But local client should.
_, _, err = th.LocalClient.GetPost(context.Background(), privatePost.Id, "")
require.NoError(t, err)
// Delete post
_, err = th.SystemAdminClient.DeletePost(context.Background(), th.BasicPost.Id)
require.NoError(t, err)
// Normal client should get 404 when trying to access deleted post normally
_, resp, err = client.GetPost(context.Background(), th.BasicPost.Id, "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// Normal client should get unauthorized when trying to access deleted post
_, resp, err = client.GetPostIncludeDeleted(context.Background(), th.BasicPost.Id, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// System client should get 404 when trying to access deleted post normally
_, resp, err = th.SystemAdminClient.GetPost(context.Background(), th.BasicPost.Id, "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// System client should be able to access deleted post with include_deleted param
post, _, err := th.SystemAdminClient.GetPostIncludeDeleted(context.Background(), th.BasicPost.Id, "")
require.NoError(t, err)
require.Equal(t, th.BasicPost.Id, post.Id)
_, err = client.Logout(context.Background())
require.NoError(t, err)
// Normal client should get unauthorized, but local client should get 404.
_, resp, err = client.GetPost(context.Background(), model.NewId(), "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, resp, err = th.LocalClient.GetPost(context.Background(), model.NewId(), "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}
func TestDeletePost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
t.Run("Post not found", func(t *testing.T) {
resp, err := client.DeletePost(context.Background(), "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("Post doesn't exist", func(t *testing.T) {
resp, err := client.DeletePost(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("No permissions to delete a post", func(t *testing.T) {
resp, err := client.DeletePost(context.Background(), th.BasicPost.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("Try to delete a post across different user roles", func(t *testing.T) {
_, _, err := client.Login(context.Background(), th.TeamAdminUser.Email, th.TeamAdminUser.Password)
require.NoError(t, err)
_, cErr := client.DeletePost(context.Background(), th.BasicPost.Id)
require.NoError(t, cErr)
post := th.CreatePost(t)
post2 := th.CreatePost(t)
user := th.CreateUser(t)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
resp, err := client.DeletePost(context.Background(), post.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
resp, err = client.DeletePost(context.Background(), model.NewId())
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, err = th.SystemAdminClient.DeletePost(context.Background(), post.Id)
require.NoError(t, err)
_, err = th.LocalClient.DeletePost(context.Background(), post2.Id)
require.NoError(t, err)
})
}
func TestPermanentDeletePost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
enableAPIPostDeletion := *th.App.Config().ServiceSettings.EnableAPIPostDeletion
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableAPIPostDeletion = &enableAPIPostDeletion })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIPostDeletion = false })
t.Run("Post not found", func(t *testing.T) {
resp, err := client.PermanentDeletePost(context.Background(), "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("Post doesn't exist", func(t *testing.T) {
resp, err := client.PermanentDeletePost(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("Permanent deletion not available through API if EnableAPIPostDeletion is not set", func(t *testing.T) {
resp, err := th.SystemAdminClient.PermanentDeletePost(context.Background(), th.BasicPost.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("Permanent deletion available through local mode even if EnableAPIPostDeletion is not set", func(t *testing.T) {
post := th.CreatePost(t)
_, err := th.LocalClient.PermanentDeletePost(context.Background(), post.Id)
require.NoError(t, err)
})
t.Run("No permissions to permanently delete a post", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIPostDeletion = true })
resp, err := client.PermanentDeletePost(context.Background(), th.BasicPost.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("Try to permanently delete a post across different user roles", func(t *testing.T) {
_, _, err := client.Login(context.Background(), th.TeamAdminUser.Email, th.TeamAdminUser.Password)
require.NoError(t, err)
resp, err := client.PermanentDeletePost(context.Background(), th.BasicPost.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
post := th.CreatePost(t)
post2 := th.CreatePost(t)
user := th.CreateUser(t)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
resp, err = client.PermanentDeletePost(context.Background(), post.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
resp, err = client.PermanentDeletePost(context.Background(), post.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, err = th.SystemAdminClient.PermanentDeletePost(context.Background(), post.Id)
require.NoError(t, err)
_, err = th.LocalClient.PermanentDeletePost(context.Background(), post2.Id)
require.NoError(t, err)
})
}
func TestWebHubMembership(t *testing.T) {
mainHelper.Parallel(t)
t.Run("WithChannelIteration", func(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
*cfg.ServiceSettings.EnableWebHubChannelIteration = true
}).InitBasic(t)
_testWebHubMembership(th, t)
})
t.Run("WithoutChannelIteration", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_testWebHubMembership(th, t)
})
}
func _testWebHubMembership(th *TestHelper, t *testing.T) {
t.Helper()
u1 := th.CreateUser(t)
th.LinkUserToTeam(t, u1, th.BasicTeam)
th.AddUserToChannel(t, u1, th.BasicChannel)
ch2 := th.CreatePrivateChannel(t)
u2 := th.CreateUser(t)
th.LinkUserToTeam(t, u2, th.BasicTeam)
th.AddUserToChannel(t, u2, ch2)
quitChan := make(chan struct{})
var wg sync.WaitGroup
wg.Add(3)
for _, obj := range []struct {
testName string
user *model.User
}{
{
testName: "basicUser",
user: th.BasicUser,
},
{
testName: "u1",
user: u1,
},
{
testName: "u2",
user: u2,
},
} {
cli := th.CreateClient()
_, _, err := cli.Login(context.Background(), obj.user.Username, obj.user.Password)
require.NoError(t, err)
wsClient := th.CreateConnectedWebSocketClientWithClient(t, cli)
go func(testName string) {
defer wg.Done()
var cnt int
for {
select {
case event := <-wsClient.EventChannel:
if event.EventType() == model.WebsocketEventPosted {
var post model.Post
err := json.Unmarshal([]byte(event.GetData()["post"].(string)), &post)
require.NoError(t, err)
cnt++
// Cases:
// Post to basicChannel should go to u1 and basicUser.
// Add u1 to ch2.
// Post to ch2 should go to u1, u2 and basicUser.
// Remove u1 from ch2.
// Post to ch2 should go to u2 and basicUser.
switch testName {
case "basicUser":
if cnt == 1 {
assert.Equal(t, th.BasicChannel.Id, post.ChannelId)
} else if cnt == 2 {
assert.Equal(t, ch2.Id, post.ChannelId)
} else if cnt == 3 {
// After removing, there will be a "removed from channel post"
assert.Equal(t, ch2.Id, post.ChannelId)
} else if cnt == 4 {
assert.Equal(t, ch2.Id, post.ChannelId)
} else {
assert.Fail(t, "more than 4 messages arrived for basicUser")
}
case "u1":
// First msg should be from basicChannel
if cnt == 1 {
assert.Equal(t, th.BasicChannel.Id, post.ChannelId)
} else if cnt == 2 {
// second should be from ch2
assert.Equal(t, ch2.Id, post.ChannelId)
} else {
assert.Fail(t, "more than 2 messages arrived for u1")
}
case "u2":
if cnt == 1 {
assert.Equal(t, ch2.Id, post.ChannelId)
} else if cnt == 2 {
// After removing, there will be a "removed from channel post"
assert.Equal(t, ch2.Id, post.ChannelId)
} else if cnt == 3 {
assert.Equal(t, ch2.Id, post.ChannelId)
} else {
assert.Fail(t, "more than 3 messages arrived for u2")
}
}
}
case <-quitChan:
return
}
}
}(obj.testName)
}
// Will send to basic channel
th.CreatePost(t)
// Add u1 to ch2
th.AddUserToChannel(t, u1, ch2)
// Send post to ch2
th.CreatePostWithClient(t, th.Client, ch2)
// Remove u1 from ch2
th.RemoveUserFromChannel(t, u1, ch2)
// Send post to ch2
th.CreatePostWithClient(t, th.Client, ch2)
// It is possible to create a signalling mechanism from the goroutines
// after all events are received, but we also want to verify that no additional
// events are being sent.
time.Sleep(2 * time.Second)
close(quitChan)
wg.Wait()
}
func TestWebHubCloseConnOnDBFail(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
*cfg.ServiceSettings.EnableWebHubChannelIteration = true
}).InitBasic(t)
defer func() {
_, err := th.Server.Store().GetInternalMasterDB().Exec(`ALTER TABLE dummy RENAME to ChannelMembers`)
require.NoError(t, err)
// Asserting that the error message is present in the log
testlib.AssertLog(t, th.LogBuffer, mlog.LvlError.Name, "Error while registering to hub")
}()
cli := th.CreateClient()
_, _, err := cli.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
require.NoError(t, err)
_, err = th.Server.Store().GetInternalMasterDB().Exec(`ALTER TABLE ChannelMembers RENAME to dummy`)
require.NoError(t, err)
wsClient, err := th.CreateWebSocketClientWithClient(cli)
require.NoError(t, err)
wsClient.Listen()
select {
case <-wsClient.EventChannel: // event channel should be closed on failure
case <-time.After(5 * time.Second):
require.FailNow(t, "timed out waiting for event")
}
wsClient.Close()
require.NoError(t, th.TestLogger.Flush())
}
func TestDeletePostEvent(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
WebSocketClient := th.CreateConnectedWebSocketClient(t)
_, err := th.SystemAdminClient.DeletePost(context.Background(), th.BasicPost.Id)
require.NoError(t, err)
var received, exit bool
for !received && !exit {
select {
case event := <-WebSocketClient.EventChannel:
if event.EventType() == model.WebsocketEventPostDeleted {
var post model.Post
err := json.Unmarshal([]byte(event.GetData()["post"].(string)), &post)
require.NoError(t, err)
received = true
}
case <-time.After(5 * time.Second):
exit = true
}
}
require.True(t, received)
}
func TestDeletePostMessage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, th.SystemAdminUser, th.BasicChannel, false)
require.Nil(t, appErr)
testCases := []struct {
description string
client *model.Client4
delete_by any
}{
{"Do not send delete_by to regular user", th.Client, nil},
{"Send delete_by to system admin user", th.SystemAdminClient, th.SystemAdminUser.Id},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
wsClient := th.CreateConnectedWebSocketClientWithClient(t, tc.client)
post := th.CreatePost(t)
_, err := th.SystemAdminClient.DeletePost(context.Background(), post.Id)
require.NoError(t, err)
timeout := time.After(5 * time.Second)
for {
select {
case ev := <-wsClient.EventChannel:
if ev.EventType() == model.WebsocketEventPostDeleted {
assert.Equal(t, tc.delete_by, ev.GetData()["delete_by"])
return
}
case <-timeout:
// We just skip the test instead of failing because waiting for more than 5 seconds
// to get a response does not make sense, and it will unnecessarily slow down
// the tests further in an already congested CI environment.
t.Skip("timed out waiting for event")
}
}
})
}
}
func TestGetPostThread(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a", RootId: th.BasicPost.Id}
post, _, _ = client.CreatePost(context.Background(), post)
list, resp, err := client.GetPostThread(context.Background(), th.BasicPost.Id, "", false)
require.NoError(t, err)
var list2 *model.PostList
list2, resp, _ = client.GetPostThread(context.Background(), th.BasicPost.Id, resp.Etag, false)
CheckEtag(t, list2, resp)
require.Equal(t, th.BasicPost.Id, list.Order[0], "wrong order")
_, ok := list.Posts[th.BasicPost.Id]
require.True(t, ok, "should have had post")
_, ok = list.Posts[post.Id]
require.True(t, ok, "should have had post")
_, resp, err = client.GetPostThread(context.Background(), "junk", "", false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetPostThread(context.Background(), model.NewId(), "", false)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, err = client.RemoveUserFromChannel(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
// Channel is public, should be able to read post
_, _, err = client.GetPostThread(context.Background(), th.BasicPost.Id, "", false)
require.NoError(t, err)
privatePost := th.CreatePostWithClient(t, client, th.BasicPrivateChannel)
_, _, err = client.GetPostThread(context.Background(), privatePost.Id, "", false)
require.NoError(t, err)
_, err = client.RemoveUserFromChannel(context.Background(), th.BasicPrivateChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
// Channel is private, should not be able to read post
_, resp, err = client.GetPostThread(context.Background(), privatePost.Id, "", false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Test the new query parameters - updatesOnly, fromUpdateAt
// Sending some bad params
_, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{
UpdatesOnly: true, // updatesOnly is true but fromUpdateAt is not set
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// Test error when both fromUpdateAt and fromCreateAt are set
_, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{
FromUpdateAt: 12345,
FromCreateAt: 12345,
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// Test error when updatesOnly is used with direction="up"
_, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{
UpdatesOnly: true,
FromUpdateAt: 12345,
Direction: "up",
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// Test valid parameters
// This should work with proper parameters
_, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{
UpdatesOnly: true,
FromUpdateAt: 12345,
Direction: "down",
})
require.NoError(t, err)
CheckOKStatus(t, resp)
list, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{
UpdatesOnly: true,
Direction: "down",
FromUpdateAt: post.UpdateAt,
})
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Len(t, list.Order, 1)
assert.Len(t, list.Posts, 1)
require.Equal(t, th.BasicPost.Id, list.Order[0], "wrong order")
// Test with just fromUpdateAt parameter
_, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{
FromUpdateAt: 12345,
})
require.NoError(t, err)
CheckOKStatus(t, resp)
// Sending other bad params unrelated to the new changes
_, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{
CollapsedThreads: true,
FromPost: "something",
PerPage: 10,
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetPostThreadWithOpts(context.Background(), th.BasicPost.Id, "", model.GetPostsOptions{
CollapsedThreads: true,
Direction: "sideways",
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.GetPostThread(context.Background(), model.NewId(), "", false)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.SystemAdminClient.GetPostThread(context.Background(), th.BasicPost.Id, "", false)
require.NoError(t, err)
}
func TestSearchPosts(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.LoginBasic(t)
client := th.Client
message := "search for post1"
_ = th.CreateMessagePost(t, message)
message = "search for post2"
post2 := th.CreateMessagePost(t, message)
message = "#hashtag search for post3"
post3 := th.CreateMessagePost(t, message)
message = "hashtag for post4"
_ = th.CreateMessagePost(t, message)
archivedChannel := th.CreatePublicChannel(t)
_ = th.CreateMessagePostWithClient(t, th.Client, archivedChannel, "#hashtag for post3")
_, err := th.Client.DeleteChannel(context.Background(), archivedChannel.Id)
require.NoError(t, err)
otherTeam := th.CreateTeam(t)
channelInOtherTeam := th.CreateChannelWithClientAndTeam(t, th.Client, model.ChannelTypeOpen, otherTeam.Id)
_ = th.AddUserToChannel(t, th.BasicUser, channelInOtherTeam)
_ = th.CreateMessagePostWithClient(t, th.Client, channelInOtherTeam, "search for post 5")
terms := "search"
isOrSearch := false
timezoneOffset := 5
searchParams := model.SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
TimeZoneOffset: &timezoneOffset,
}
allTeamsPosts, _, err := client.SearchPostsWithParams(context.Background(), "", &searchParams)
require.NoError(t, err)
require.Len(t, allTeamsPosts.Order, 4, "wrong search along multiple teams")
terms = "search"
isOrSearch = false
timezoneOffset = 5
searchParams = model.SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
TimeZoneOffset: &timezoneOffset,
}
posts, _, err := client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "wrong search")
terms = "search"
page := 0
perPage := 2
searchParams = model.SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
TimeZoneOffset: &timezoneOffset,
Page: &page,
PerPage: &perPage,
}
posts2, _, err := client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
// We don't support paging for DB search yet, modify this when we do.
require.Len(t, posts2.Order, 3, "Wrong number of posts")
assert.Equal(t, posts.Order[0], posts2.Order[0])
assert.Equal(t, posts.Order[1], posts2.Order[1])
page = 1
searchParams = model.SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
TimeZoneOffset: &timezoneOffset,
Page: &page,
PerPage: &perPage,
}
posts2, _, err = client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
// We don't support paging for DB search yet, modify this when we do.
require.Empty(t, posts2.Order, "Wrong number of posts")
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "search", false)
require.NoError(t, err)
require.Len(t, posts.Order, 3, "wrong search")
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "post2", false)
require.NoError(t, err)
require.Len(t, posts.Order, 1, "wrong number of posts")
require.Equal(t, post2.Id, posts.Order[0], "wrong search")
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "#hashtag", false)
require.NoError(t, err)
require.Len(t, posts.Order, 1, "wrong number of posts")
require.Equal(t, post3.Id, posts.Order[0], "wrong search")
terms = "#hashtag"
includeDeletedChannels := true
searchParams = model.SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
TimeZoneOffset: &timezoneOffset,
IncludeDeletedChannels: &includeDeletedChannels,
}
posts, _, err = client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
require.Len(t, posts.Order, 2, "wrong search")
// Archived channels are always included now, so this should return the same result
posts, _, err = client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
require.Len(t, posts.Order, 2, "wrong search")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "*", false)
require.Empty(t, posts.Order, "searching for just * shouldn't return any results")
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "post1 post2", true)
require.NoError(t, err)
require.Len(t, posts.Order, 2, "wrong search results")
_, resp, err := client.SearchPosts(context.Background(), "junk", "#sgtitlereview", false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.SearchPosts(context.Background(), model.NewId(), "#sgtitlereview", false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "", false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "#sgtitlereview", false)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestSearchHashtagPosts(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.LoginBasic(t)
client := th.Client
message := "#sgtitlereview with space"
assert.NotNil(t, th.CreateMessagePost(t, message))
message = "#sgtitlereview\n with return"
assert.NotNil(t, th.CreateMessagePost(t, message))
message = "no hashtag"
assert.NotNil(t, th.CreateMessagePost(t, message))
posts, _, err := client.SearchPosts(context.Background(), th.BasicTeam.Id, "#sgtitlereview", false)
require.NoError(t, err)
require.Len(t, posts.Order, 2, "wrong search results")
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := client.SearchPosts(context.Background(), th.BasicTeam.Id, "#sgtitlereview", false)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestSearchPostsInChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.LoginBasic(t)
client := th.Client
channel := th.CreatePublicChannel(t)
message := "sgtitlereview with space"
_ = th.CreateMessagePost(t, message)
message = "sgtitlereview\n with return"
_ = th.CreateMessagePostWithClient(t, client, th.BasicChannel2, message)
message = "other message with no return"
_ = th.CreateMessagePostWithClient(t, client, th.BasicChannel2, message)
message = "other message with no return"
_ = th.CreateMessagePostWithClient(t, client, channel, message)
posts, _, _ := client.SearchPosts(context.Background(), th.BasicTeam.Id, "channel:", false)
require.Empty(t, posts.Order, "wrong number of posts for search 'channel:'")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "in:", false)
require.Empty(t, posts.Order, "wrong number of posts for search 'in:'")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "channel:"+th.BasicChannel.Name, false)
require.Lenf(t, posts.Order, 2, "wrong number of posts returned for search 'channel:%v'", th.BasicChannel.Name)
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "in:"+th.BasicChannel2.Name, false)
require.Lenf(t, posts.Order, 2, "wrong number of posts returned for search 'in:%v'", th.BasicChannel2.Name)
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "channel:"+th.BasicChannel2.Name, false)
require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'channel:%v'", th.BasicChannel2.Name)
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "ChAnNeL:"+th.BasicChannel2.Name, false)
require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'ChAnNeL:%v'", th.BasicChannel2.Name)
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "sgtitlereview", false)
require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'sgtitlereview'")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "sgtitlereview channel:"+th.BasicChannel.Name, false)
require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'sgtitlereview channel:%v'", th.BasicChannel.Name)
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "sgtitlereview in: "+th.BasicChannel2.Name, false)
require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'sgtitlereview in: %v'", th.BasicChannel2.Name)
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "sgtitlereview channel: "+th.BasicChannel2.Name, false)
require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'sgtitlereview channel: %v'", th.BasicChannel2.Name)
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "channel: "+th.BasicChannel2.Name+" channel: "+channel.Name, false)
require.Lenf(t, posts.Order, 3, "wrong number of posts for 'channel: %v channel: %v'", th.BasicChannel2.Name, channel.Name)
}
func TestSearchPostsFromUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
th.LoginTeamAdmin(t)
user := th.CreateUser(t)
th.LinkUserToTeam(t, user, th.BasicTeam)
_, appErr := th.App.AddUserToChannel(th.Context, user, th.BasicChannel, false)
require.Nil(t, appErr)
_, appErr = th.App.AddUserToChannel(th.Context, user, th.BasicChannel2, false)
require.Nil(t, appErr)
message := "sgtitlereview with space"
_ = th.CreateMessagePost(t, message)
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic2(t)
message = "sgtitlereview\n with return"
_ = th.CreateMessagePostWithClient(t, client, th.BasicChannel2, message)
posts, _, err := client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.TeamAdminUser.Username, false)
require.NoError(t, err)
require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v'", th.TeamAdminUser.Username)
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false)
require.NoError(t, err)
require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'from: %v", th.BasicUser2.Username)
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" sgtitlereview", false)
require.NoError(t, err)
require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'from: %v'", th.BasicUser2.Username)
message = "hullo"
_ = th.CreateMessagePost(t, message)
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" in:"+th.BasicChannel.Name, false)
require.NoError(t, err)
require.Len(t, posts.Order, 1, "wrong number of posts for search 'from: %v in:", th.BasicUser2.Username, th.BasicChannel.Name)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
// wait for the join/leave messages to be created for user3 since they're done asynchronously
time.Sleep(100 * time.Millisecond)
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false)
require.NoError(t, err)
require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v'", th.BasicUser2.Username)
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username, false)
require.NoError(t, err)
require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v from: %v'", th.BasicUser2.Username, user.Username)
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name, false)
require.NoError(t, err)
require.Len(t, posts.Order, 1, "wrong number of posts")
message = "coconut"
_ = th.CreateMessagePostWithClient(t, client, th.BasicChannel2, message)
posts, _, err = client.SearchPosts(context.Background(), th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name+" coconut", false)
require.NoError(t, err)
require.Len(t, posts.Order, 1, "wrong number of posts")
}
func TestSearchPostsWithDateFlags(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.LoginBasic(t)
client := th.Client
message := "sgtitlereview\n with return"
createDate := time.Date(2018, 8, 1, 5, 0, 0, 0, time.UTC)
_ = th.CreateMessagePostNoClient(t, th.BasicChannel, message, utils.MillisFromTime(createDate))
message = "other message with no return"
createDate = time.Date(2018, 8, 2, 5, 0, 0, 0, time.UTC)
_ = th.CreateMessagePostNoClient(t, th.BasicChannel, message, utils.MillisFromTime(createDate))
message = "other message with no return"
createDate = time.Date(2018, 8, 3, 5, 0, 0, 0, time.UTC)
_ = th.CreateMessagePostNoClient(t, th.BasicChannel, message, utils.MillisFromTime(createDate))
posts, _, _ := client.SearchPosts(context.Background(), th.BasicTeam.Id, "return", false)
require.Len(t, posts.Order, 3, "wrong number of posts")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "on:", false)
require.Empty(t, posts.Order, "wrong number of posts")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "after:", false)
require.Empty(t, posts.Order, "wrong number of posts")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "before:", false)
require.Empty(t, posts.Order, "wrong number of posts")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "on:2018-08-01", false)
require.Len(t, posts.Order, 1, "wrong number of posts")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "after:2018-08-01", false)
resultCount := 0
for _, post := range posts.Posts {
if post.UserId == th.BasicUser.Id {
resultCount = resultCount + 1
}
}
require.Equal(t, 2, resultCount, "wrong number of posts")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "before:2018-08-02", false)
require.Len(t, posts.Order, 1, "wrong number of posts")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "before:2018-08-03 after:2018-08-02", false)
require.Empty(t, posts.Order, "wrong number of posts")
posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "before:2018-08-03 after:2018-08-01", false)
require.Len(t, posts.Order, 1, "wrong number of posts")
}
func TestGetFileInfosForPost(t *testing.T) {
th := Setup(t).InitBasic(t)
client := th.Client
fileIds := make([]string, 3)
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
for i := range 3 {
fileResp, _, uploadErr := client.UploadFile(context.Background(), data, th.BasicChannel.Id, "test.png")
require.NoError(t, uploadErr, "failed to upload file %d", i)
fileIds[i] = fileResp.FileInfos[0].Id
}
post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a", FileIds: fileIds}
post, _, err = client.CreatePost(context.Background(), post)
require.NoError(t, err)
// File info association can be async; poll until all 3 are visible (MM-46902).
var infos []*model.FileInfo
var resp *model.Response
require.Eventually(t, func() bool {
infos, resp, err = client.GetFileInfosForPost(context.Background(), post.Id, "")
return err == nil && len(infos) == 3
}, 10*time.Second, 200*time.Millisecond, "expected 3 file infos")
found := false
for _, info := range infos {
if info.Id == fileIds[0] {
found = true
}
}
require.True(t, found, "missing file info")
infos, resp, _ = client.GetFileInfosForPost(context.Background(), post.Id, resp.Etag)
CheckEtag(t, infos, resp)
infos, _, err = client.GetFileInfosForPost(context.Background(), th.BasicPost.Id, "")
require.NoError(t, err)
require.Empty(t, infos, "should have no file infos")
_, resp, err = client.GetFileInfosForPost(context.Background(), "junk", "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetFileInfosForPost(context.Background(), model.NewId(), "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Delete post
_, err = th.SystemAdminClient.DeletePost(context.Background(), post.Id)
require.NoError(t, err)
// Normal client should get 404 when trying to access deleted post normally
_, resp, err = client.GetFileInfosForPost(context.Background(), post.Id, "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// Normal client should get unauthorized when trying to access deleted post
_, resp, err = client.GetFileInfosForPostIncludeDeleted(context.Background(), post.Id, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// System client should get 404 when trying to access deleted post normally
_, resp, err = th.SystemAdminClient.GetFileInfosForPost(context.Background(), post.Id, "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// System client should be able to access deleted post with include_deleted param
infos, _, err = th.SystemAdminClient.GetFileInfosForPostIncludeDeleted(context.Background(), post.Id, "")
require.NoError(t, err)
require.Len(t, infos, 3, "missing file infos")
found = false
for _, info := range infos {
if info.Id == fileIds[0] {
found = true
}
}
require.True(t, found, "missing file info")
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.GetFileInfosForPost(context.Background(), model.NewId(), "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.SystemAdminClient.GetFileInfosForPost(context.Background(), th.BasicPost.Id, "")
require.NoError(t, err)
}
func TestSetChannelUnread(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
u1 := th.BasicUser
u2 := th.BasicUser2
s2, _ := th.App.GetSession(th.Client.AuthToken)
_, _, err := th.Client.Login(context.Background(), u1.Email, u1.Password)
require.NoError(t, err)
c1 := th.BasicChannel
c1toc2 := &model.ChannelView{ChannelId: th.BasicChannel2.Id, PrevChannelId: c1.Id}
now := utils.MillisFromTime(time.Now())
th.CreateMessagePostNoClient(t, c1, "AAA", now)
p2 := th.CreateMessagePostNoClient(t, c1, "BBB", now+10)
th.CreateMessagePostNoClient(t, c1, "CCC", now+20)
pp1 := th.CreateMessagePostNoClient(t, th.BasicPrivateChannel, "Sssh!", now)
pp2 := th.CreateMessagePostNoClient(t, th.BasicPrivateChannel, "You Sssh!", now+10)
require.NotNil(t, pp1)
require.NotNil(t, pp2)
// Ensure that post have been read
unread, appErr := th.App.GetChannelUnread(th.Context, c1.Id, u1.Id)
require.Nil(t, appErr)
require.Equal(t, int64(4), unread.MsgCount)
unread, appErr = th.App.GetChannelUnread(th.Context, c1.Id, u2.Id)
require.Nil(t, appErr)
require.Equal(t, int64(4), unread.MsgCount)
_, appErr = th.App.ViewChannel(th.Context, c1toc2, u2.Id, s2.Id, false)
require.Nil(t, appErr)
unread, appErr = th.App.GetChannelUnread(th.Context, c1.Id, u2.Id)
require.Nil(t, appErr)
require.Equal(t, int64(0), unread.MsgCount)
t.Run("Unread last one", func(t *testing.T) {
var r *model.Response
r, err = th.Client.SetPostUnread(context.Background(), u1.Id, p2.Id, true)
require.NoError(t, err)
CheckOKStatus(t, r)
unread, appErr := th.App.GetChannelUnread(th.Context, c1.Id, u1.Id)
require.Nil(t, appErr)
assert.Equal(t, int64(2), unread.MsgCount)
})
t.Run("Unread on a direct channel", func(t *testing.T) {
dc := th.CreateDmChannel(t, u2)
th.CreateMessagePostNoClient(t, dc, "test1", now)
p := th.CreateMessagePostNoClient(t, dc, "test2", now+10)
require.NotNil(t, p)
th.CreateMessagePostNoClient(t, dc, "test3", now+20)
p1 := th.CreateMessagePostNoClient(t, dc, "test4", now+30)
require.NotNil(t, p1)
// Ensure that post have been read
unread, err := th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(4), unread.MsgCount)
cv := &model.ChannelView{ChannelId: dc.Id}
_, appErr := th.App.ViewChannel(th.Context, cv, u1.Id, s2.Id, false)
require.Nil(t, appErr)
unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(0), unread.MsgCount)
r, _ := th.Client.SetPostUnread(context.Background(), u1.Id, p.Id, false)
assert.Equal(t, 200, r.StatusCode)
unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(3), unread.MsgCount)
// Ensure that post have been read
_, appErr = th.App.ViewChannel(th.Context, cv, u1.Id, s2.Id, false)
require.Nil(t, appErr)
unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(0), unread.MsgCount)
r, _ = th.Client.SetPostUnread(context.Background(), u1.Id, p1.Id, false)
assert.Equal(t, 200, r.StatusCode)
unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(1), unread.MsgCount)
})
t.Run("Unread on a direct channel in a thread", func(t *testing.T) {
dc := th.CreateDmChannel(t, th.CreateUser(t))
rootPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: u1.Id, CreateAt: now, ChannelId: dc.Id, Message: "root"}, dc, model.CreatePostFlags{})
require.Nil(t, appErr)
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 10, ChannelId: dc.Id, Message: "reply 1"}, dc, model.CreatePostFlags{})
require.Nil(t, appErr)
reply2, _, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 20, ChannelId: dc.Id, Message: "reply 2"}, dc, model.CreatePostFlags{})
require.Nil(t, appErr)
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 30, ChannelId: dc.Id, Message: "reply 3"}, dc, model.CreatePostFlags{})
require.Nil(t, appErr)
// Ensure that post have been read
unread, err := th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(4), unread.MsgCount)
require.Equal(t, int64(1), unread.MsgCountRoot)
cv := &model.ChannelView{ChannelId: dc.Id}
_, appErr = th.App.ViewChannel(th.Context, cv, u1.Id, s2.Id, false)
require.Nil(t, appErr)
unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(0), unread.MsgCount)
require.Equal(t, int64(0), unread.MsgCountRoot)
r, _ := th.Client.SetPostUnread(context.Background(), u1.Id, rootPost.Id, false)
assert.Equal(t, 200, r.StatusCode)
unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(4), unread.MsgCount)
require.Equal(t, int64(1), unread.MsgCountRoot)
// Ensure that post have been read
_, appErr = th.App.ViewChannel(th.Context, cv, u1.Id, s2.Id, false)
require.Nil(t, appErr)
unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(0), unread.MsgCount)
require.Equal(t, int64(0), unread.MsgCountRoot)
r, _ = th.Client.SetPostUnread(context.Background(), u1.Id, reply2.Id, false)
assert.Equal(t, 200, r.StatusCode)
unread, err = th.App.GetChannelUnread(th.Context, dc.Id, u1.Id)
require.Nil(t, err)
require.Equal(t, int64(2), unread.MsgCount)
require.Equal(t, int64(0), unread.MsgCountRoot)
})
t.Run("Unread on a private channel", func(t *testing.T) {
r, _ := th.Client.SetPostUnread(context.Background(), u1.Id, pp2.Id, true)
assert.Equal(t, 200, r.StatusCode)
unread, appErr := th.App.GetChannelUnread(th.Context, th.BasicPrivateChannel.Id, u1.Id)
require.Nil(t, appErr)
assert.Equal(t, int64(1), unread.MsgCount)
r, _ = th.Client.SetPostUnread(context.Background(), u1.Id, pp1.Id, true)
assert.Equal(t, 200, r.StatusCode)
unread, appErr = th.App.GetChannelUnread(th.Context, th.BasicPrivateChannel.Id, u1.Id)
require.Nil(t, appErr)
assert.Equal(t, int64(2), unread.MsgCount)
})
t.Run("Can't unread an imaginary post", func(t *testing.T) {
r, _ := th.Client.SetPostUnread(context.Background(), u1.Id, "invalid4ofngungryquinj976y", true)
assert.Equal(t, http.StatusForbidden, r.StatusCode)
})
// let's create another user to test permissions
u3 := th.CreateUser(t)
c3 := th.CreateClient()
_, _, err = c3.Login(context.Background(), u3.Email, u3.Password)
require.NoError(t, err)
t.Run("Can't unread channels you don't belong to", func(t *testing.T) {
r, _ := c3.SetPostUnread(context.Background(), u3.Id, pp1.Id, true)
assert.Equal(t, http.StatusForbidden, r.StatusCode)
})
t.Run("Can't unread users you don't have permission to edit", func(t *testing.T) {
r, _ := c3.SetPostUnread(context.Background(), u1.Id, pp1.Id, true)
assert.Equal(t, http.StatusForbidden, r.StatusCode)
})
t.Run("Can't unread if user is not logged in", func(t *testing.T) {
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
response, err := th.Client.SetPostUnread(context.Background(), u1.Id, p2.Id, true)
require.Error(t, err)
CheckUnauthorizedStatus(t, response)
})
}
func TestSetPostUnreadWithoutCollapsedThreads(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
// user2: first root mention @user1
// - user1: hello
// - user2: mention @u1
// - user1: another reply
// - user2: another mention @u1
// user1: a root post
// user2: Another root mention @u1
user1Mention := " @" + th.BasicUser.Username
rootPost1, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "first root mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, appErr)
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hello"}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, appErr)
replyPost1, _, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, appErr)
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another reply"}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, appErr)
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, appErr)
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "a root post"}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, appErr)
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another root mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, appErr)
t.Run("Mark reply post as unread", func(t *testing.T) {
userWSClient := th.CreateConnectedWebSocketClient(t)
_, err := th.Client.SetPostUnread(context.Background(), th.BasicUser.Id, replyPost1.Id, false)
require.NoError(t, err)
channelUnread, appErr := th.App.GetChannelUnread(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
require.Nil(t, appErr)
require.Equal(t, int64(3), channelUnread.MentionCount)
// MentionCountRoot should be zero so that supported clients don't show a mention badge for the channel
require.Equal(t, int64(0), channelUnread.MentionCountRoot)
require.Equal(t, int64(5), channelUnread.MsgCount)
// MentionCountRoot should be zero so that supported clients don't show the channel as unread
require.Equal(t, channelUnread.MsgCountRoot, int64(0))
// test websocket event for marking post as unread
var caught bool
var exit bool
var data map[string]any
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventPostUnread {
caught = true
data = ev.GetData()
}
case <-time.After(5 * time.Second):
exit = true
}
if exit {
break
}
}
require.Truef(t, caught, "User should have received %s event", model.WebsocketEventPostUnread)
msgCount, ok := data["msg_count"]
require.True(t, ok)
require.EqualValues(t, 3, msgCount)
mentionCount, ok := data["mention_count"]
require.True(t, ok)
require.EqualValues(t, 3, mentionCount)
threadMembership, appErr := th.App.GetThreadMembershipForUser(th.BasicUser.Id, rootPost1.Id)
require.Nil(t, appErr)
thread, appErr := th.App.GetThreadForUser(th.Context, threadMembership, false)
require.Nil(t, appErr)
require.Equal(t, int64(2), thread.UnreadMentions)
require.Equal(t, int64(3), thread.UnreadReplies)
})
t.Run("Mark root post as unread", func(t *testing.T) {
_, err := th.Client.SetPostUnread(context.Background(), th.BasicUser.Id, rootPost1.Id, false)
require.NoError(t, err)
channelUnread, appErr := th.App.GetChannelUnread(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
require.Nil(t, appErr)
require.Equal(t, int64(4), channelUnread.MentionCount)
require.Equal(t, int64(2), channelUnread.MentionCountRoot)
require.Equal(t, int64(7), channelUnread.MsgCount)
require.Equal(t, int64(3), channelUnread.MsgCountRoot)
})
}
func TestGetPostsByIds(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
post1 := th.CreatePost(t)
post2 := th.CreatePost(t)
posts, response, err := client.GetPostsByIds(context.Background(), []string{post1.Id, post2.Id})
require.NoError(t, err)
CheckOKStatus(t, response)
require.Len(t, posts, 2, "wrong number returned")
require.Equal(t, posts[0].Id, post2.Id)
require.Equal(t, posts[1].Id, post1.Id)
_, response, err = client.GetPostsByIds(context.Background(), []string{})
require.Error(t, err)
CheckBadRequestStatus(t, response)
_, response, err = client.GetPostsByIds(context.Background(), []string{"abc123"})
require.Error(t, err)
CheckNotFoundStatus(t, response)
}
func TestGetEditHistoryForPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "new message",
UserId: th.BasicUser.Id,
}
rpost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
time.Sleep(1 * time.Millisecond)
t.Run("unedited post", func(t *testing.T) {
history, resp, err := client.GetEditHistoryForPost(context.Background(), rpost.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
require.Len(t, history, 0)
})
// update the post message
patch := &model.PostPatch{
Message: new("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: new("new message edited again"),
}
_, response2, err2 := client.PatchPost(context.Background(), rpost.Id, patch)
require.NoError(t, err2)
CheckOKStatus(t, response2)
t.Run("update history correctly", func(t *testing.T) {
history, response3, err3 := client.GetEditHistoryForPost(context.Background(), rpost.Id)
require.NoError(t, err3)
CheckOKStatus(t, response3)
require.Len(t, history, 2)
require.Equal(t, "new message edited", history[0].Message)
require.Equal(t, "new message", history[1].Message)
})
t.Run("logged out", func(t *testing.T) {
_, err := client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := client.GetEditHistoryForPost(context.Background(), rpost.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("different user", func(t *testing.T) {
th.LoginBasic2(t)
_, resp, err := client.GetEditHistoryForPost(context.Background(), rpost.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("edit history includes file metadata", func(t *testing.T) {
th.LoginBasic(t)
fileInfo1, appErr := th.App.UploadFile(th.Context, []byte("data"), th.BasicChannel.Id, "test")
require.Nil(t, appErr)
fileInfo2, appErr := th.App.UploadFile(th.Context, []byte("data"), th.BasicChannel.Id, "test")
require.Nil(t, appErr)
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "new message",
UserId: th.BasicUser.Id,
FileIds: []string{fileInfo1.Id, fileInfo2.Id},
}
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
require.Contains(t, createdPost.FileIds, fileInfo1.Id)
require.Contains(t, createdPost.FileIds, fileInfo2.Id)
patch = &model.PostPatch{
Message: new("new message 1"),
}
_, response, err := client.PatchPost(context.Background(), createdPost.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, response)
patch = &model.PostPatch{
Message: new("new message 2"),
}
_, response, err = client.PatchPost(context.Background(), createdPost.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, response)
patch = &model.PostPatch{
Message: new("new message 3"),
}
_, response, err = client.PatchPost(context.Background(), createdPost.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, response)
editHistory, resp, err := client.GetEditHistoryForPost(context.Background(), createdPost.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
for _, editHistoryItem := range editHistory {
require.Len(t, editHistoryItem.FileIds, 2)
require.Contains(t, editHistoryItem.FileIds, fileInfo1.Id)
require.Contains(t, editHistoryItem.FileIds, fileInfo2.Id)
}
})
}
func TestCreatePostNotificationsWithCRT(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
rpost := th.CreatePost(t)
testCases := []struct {
name string
post *model.Post
notifyProps model.StringMap
mentions bool
followers bool
}{
{
name: "When default is NONE, comments is NEVER, desktop threads is ALL, and has no mentions",
post: &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "reply",
UserId: th.BasicUser2.Id,
RootId: rpost.Id,
},
notifyProps: model.StringMap{
model.DesktopNotifyProp: model.UserNotifyNone,
model.CommentsNotifyProp: model.CommentsNotifyNever,
model.DesktopThreadsNotifyProp: model.UserNotifyAll,
},
mentions: false,
followers: false,
},
{
name: "When default is NONE, comments is NEVER, desktop threads is ALL, and has mentions",
post: &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "mention @" + th.BasicUser.Username,
UserId: th.BasicUser2.Id,
RootId: rpost.Id,
},
notifyProps: model.StringMap{
model.DesktopNotifyProp: model.UserNotifyNone,
model.CommentsNotifyProp: model.CommentsNotifyNever,
model.DesktopThreadsNotifyProp: model.UserNotifyAll,
},
mentions: true,
followers: false,
},
{
name: "When default is MENTION, comments is NEVER, desktop threads is ALL, and has no mentions",
post: &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "reply",
UserId: th.BasicUser2.Id,
RootId: rpost.Id,
},
notifyProps: model.StringMap{
model.DesktopNotifyProp: model.UserNotifyMention,
model.CommentsNotifyProp: model.CommentsNotifyNever,
model.DesktopThreadsNotifyProp: model.UserNotifyAll,
},
mentions: false,
followers: true,
},
{
name: "When default is MENTION, comments is ANY, desktop threads is MENTION, and has no mentions",
post: &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "reply",
UserId: th.BasicUser2.Id,
RootId: rpost.Id,
},
notifyProps: model.StringMap{
model.DesktopNotifyProp: model.UserNotifyMention,
model.CommentsNotifyProp: model.CommentsNotifyAny,
model.DesktopThreadsNotifyProp: model.UserNotifyMention,
},
mentions: false,
followers: false,
},
{
name: "When default is MENTION, comments is NEVER, desktop threads is MENTION, and has mentions",
post: &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "reply @" + th.BasicUser.Username,
UserId: th.BasicUser2.Id,
RootId: rpost.Id,
},
notifyProps: model.StringMap{
model.DesktopNotifyProp: model.UserNotifyMention,
model.CommentsNotifyProp: model.CommentsNotifyNever,
model.DesktopThreadsNotifyProp: model.UserNotifyMention,
},
mentions: true,
followers: true,
},
}
// reset the cache so that channel member notify props includes all users
th.App.Srv().Store().Channel().ClearCaches()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
userWSClient := th.CreateConnectedWebSocketClient(t)
patch := &model.UserPatch{}
patch.NotifyProps = model.CopyStringMap(th.BasicUser.NotifyProps)
maps.Copy(patch.NotifyProps, tc.notifyProps)
// update user's notify props
_, _, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, patch)
require.NoError(t, err)
// post a reply on the thread
reply, _, 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 {
continue
}
data := ev.GetData()
post, ok := data["post"]
if !ok {
continue
}
var evPost model.Post
require.NoError(t, json.Unmarshal([]byte(post.(string)), &evPost))
if evPost.Id != reply.Id {
continue
}
caught = true
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)
}
return
case <-time.After(5 * time.Second):
return
}
}
}()
require.Truef(t, caught, "User should have received %s event", model.WebsocketEventPosted)
})
}
}
func TestGetPostStripActionIntegrations(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "with slack attachment action",
}
post.AddProp(model.PostPropsAttachments, []*model.MessageAttachment{
{
Text: "Slack Attachment Text",
Fields: []*model.MessageAttachmentField{
{
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 waitForPostReminderEphemeral(t *testing.T, userWSClient *model.WebSocketClient, timeout time.Duration) *model.Post {
t.Helper()
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() != model.WebsocketEventEphemeralMessage {
continue
}
data := ev.GetData()
postJSON, ok := data["post"].(string)
require.True(t, ok)
var parsedPost model.Post
err := json.Unmarshal([]byte(postJSON), &parsedPost)
require.NoError(t, err)
if parsedPost.GetProp("type") != model.PostTypeReminder {
continue
}
return &parsedPost
case <-timer.C:
require.Fail(t, "timed out waiting for post reminder ephemeral websocket event")
return nil
}
}
}
func TestPostReminder(t *testing.T) {
th := Setup(t).InitBasic(t)
client := th.Client
userWSClient := th.CreateConnectedWebSocketClient(t)
// Set target time 1s in the future so the reminder processor picks it up
// on its next tick rather than relying on it already being in the past (MM-60329).
targetTime := time.Now().UTC().Unix() + 1
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)
parsedPost := waitForPostReminderEphemeral(t, userWSClient, 15*time.Second)
require.NotNil(t, parsedPost)
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))
}
func TestPostReminderReplyEphemeralUsesThreadRoot(t *testing.T) {
th := Setup(t).InitBasic(t)
client := th.Client
userWSClient := th.CreateConnectedWebSocketClient(t)
replyPost := &model.Post{
ChannelId: th.BasicChannel.Id,
RootId: th.BasicPost.Id,
Message: "reply for reminder " + model.NewId(),
}
replyCreated, _, err := client.CreatePost(context.Background(), replyPost)
require.NoError(t, err)
targetTime := time.Now().UTC().Unix() + 1
resp, err := client.SetPostReminder(context.Background(), &model.PostReminder{
TargetTime: targetTime,
PostId: replyCreated.Id,
UserId: th.BasicUser.Id,
})
require.NoError(t, err)
CheckOKStatus(t, resp)
user, _, err := client.GetUser(context.Background(), replyCreated.UserId, "")
require.NoError(t, err)
parsedPost := waitForPostReminderEphemeral(t, userWSClient, 15*time.Second)
require.NotNil(t, parsedPost)
assert.Equal(t, model.PostTypeEphemeral, parsedPost.Type)
assert.Equal(t, th.BasicPost.Id, parsedPost.RootId, "ephemeral should be threaded under the thread root so RHS/center thread views show the ack")
assert.Equal(t, replyCreated.Id, parsedPost.GetProp("post_id").(string))
require.Equal(t, float64(targetTime), parsedPost.GetProp("target_time").(float64))
require.Equal(t, user.Username, parsedPost.GetProp("username").(string))
require.Equal(t, th.BasicTeam.Name, parsedPost.GetProp("team_name").(string))
}
func TestPostGetInfo(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.RemovePermissionFromRole(t, model.PermissionManagePrivateChannelMembers.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionManagePrivateChannelMembers.Id, model.ChannelUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionManagePrivateChannelMembers.Id, model.TeamUserRoleId)
client := th.Client
guestUser, guestClient := th.CreateGuestAndClient(t)
otherTeamMemberClient := th.CreateClient()
_, _, err := otherTeamMemberClient.Login(context.Background(), th.BasicUser2.Username, th.BasicUser2.Password)
require.NoError(t, err)
outsiderUser := th.CreateUser(t)
outsiderClient := th.CreateClient()
_, _, err = outsiderClient.Login(context.Background(), outsiderUser.Username, outsiderUser.Password)
require.NoError(t, err)
sysadminClient := th.SystemAdminClient
_, _, err = sysadminClient.AddTeamMember(context.Background(), th.BasicTeam.Id, th.SystemAdminUser.Id)
require.NoError(t, err)
openChannel, _, err := client.CreateChannel(context.Background(), &model.Channel{TeamId: th.BasicTeam.Id, Type: model.ChannelTypeOpen, Name: "open-channel", DisplayName: "Open Channel"})
require.NoError(t, err)
_, _, err = sysadminClient.AddChannelMember(context.Background(), openChannel.Id, th.SystemAdminUser.Id)
require.NoError(t, err)
openPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: openChannel.Id})
require.NoError(t, err)
privateChannel, _, err := sysadminClient.CreateChannel(context.Background(), &model.Channel{TeamId: th.BasicTeam.Id, Type: model.ChannelTypePrivate, Name: "private-channel", DisplayName: "Private Channel"})
require.NoError(t, err)
privatePost, _, err := sysadminClient.CreatePost(context.Background(), &model.Post{ChannelId: privateChannel.Id})
require.NoError(t, err)
privateChannelBasicUser, _, err := client.CreateChannel(context.Background(), &model.Channel{TeamId: th.BasicTeam.Id, Type: model.ChannelTypePrivate, Name: "private-channel-basic-user", DisplayName: "Private Channel - Basic User"})
require.NoError(t, err)
privatePostBasicUser, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: privateChannelBasicUser.Id})
require.NoError(t, err)
user3 := th.CreateUser(t)
gmChannel, _, err := client.CreateGroupChannel(context.Background(), []string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id})
require.NoError(t, err)
gmPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: gmChannel.Id})
require.NoError(t, err)
dmChannel, _, err := client.CreateDirectChannel(context.Background(), th.BasicUser.Id, th.BasicUser2.Id)
require.NoError(t, err)
dmPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: dmChannel.Id})
require.NoError(t, err)
openTeam, _, err := sysadminClient.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "open-team", DisplayName: "Open Team", AllowOpenInvite: true})
require.NoError(t, err)
openTeamOpenChannel, _, err := sysadminClient.CreateChannel(context.Background(), &model.Channel{TeamId: openTeam.Id, Type: model.ChannelTypeOpen, Name: "open-team-open-channel", DisplayName: "Open Team - Open Channel"})
require.NoError(t, err)
openTeamOpenPost, _, err := sysadminClient.CreatePost(context.Background(), &model.Post{ChannelId: openTeamOpenChannel.Id})
require.NoError(t, err)
// Alt team is a team without the sysadmin in it.
altOpenTeam, _, err := client.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "alt-open-team", DisplayName: "Alt Open Team", AllowOpenInvite: true})
require.NoError(t, err)
altOpenTeamOpenChannel, _, err := client.CreateChannel(context.Background(), &model.Channel{TeamId: altOpenTeam.Id, Type: model.ChannelTypeOpen, Name: "alt-open-team-open-channel", DisplayName: "Open Team - Open Channel"})
require.NoError(t, err)
altOpenTeamOpenPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: altOpenTeamOpenChannel.Id})
require.NoError(t, err)
inviteTeam, _, err := sysadminClient.CreateTeam(context.Background(), &model.Team{Type: model.TeamInvite, Name: "invite-team", DisplayName: "Invite Team"})
require.NoError(t, err)
inviteTeamOpenChannel, _, err := sysadminClient.CreateChannel(context.Background(), &model.Channel{TeamId: inviteTeam.Id, Type: model.ChannelTypeOpen, Name: "invite-team-open-channel", DisplayName: "Invite Team - Open Channel"})
require.NoError(t, err)
inviteTeamOpenPost, _, err := sysadminClient.CreatePost(context.Background(), &model.Post{ChannelId: inviteTeamOpenChannel.Id})
require.NoError(t, err)
testCases := []struct {
name string
team *model.Team
hasJoinedTeam bool
channel *model.Channel
hasJoinedChannel bool
post *model.Post
client *model.Client4
assertSetup func(t *testing.T)
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,
},
{
name: "Open post - Current team - Guest user outside channel",
team: th.BasicTeam,
hasJoinedTeam: true,
channel: openChannel,
hasJoinedChannel: false,
post: openPost,
client: guestClient,
assertSetup: func(t *testing.T) {
t.Helper()
_, appErr := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, guestUser.Id)
require.Nil(t, appErr)
_, appErr = th.App.GetChannelMember(th.Context, openChannel.Id, guestUser.Id)
require.NotNil(t, appErr)
},
hasAccess: false,
},
// 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 - Basic user with join private teams permission",
team: inviteTeam,
hasJoinedTeam: false,
channel: inviteTeamOpenChannel,
hasJoinedChannel: false,
post: inviteTeamOpenPost,
client: client,
assertSetup: func(t *testing.T) {
t.Helper()
_, appErr := th.App.GetTeamMember(th.Context, inviteTeam.Id, th.BasicUser.Id)
require.NotNil(t, appErr)
_, appErr = th.App.GetChannelMember(th.Context, inviteTeamOpenChannel.Id, th.BasicUser.Id)
require.NotNil(t, appErr)
require.False(t, th.App.HasPermissionToTeam(th.Context, th.BasicUser.Id, inviteTeam.Id, model.PermissionJoinPrivateTeams))
require.False(t, th.App.HasPermissionToTeam(th.Context, th.BasicUser.Id, inviteTeam.Id, model.PermissionJoinPublicChannels))
th.AddPermissionToRole(t, model.PermissionJoinPrivateTeams.Id, model.SystemUserRoleId)
th.AddPermissionToRole(t, model.PermissionJoinPublicChannels.Id, model.SystemUserRoleId)
t.Cleanup(func() {
th.RemovePermissionFromRole(t, model.PermissionJoinPublicChannels.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionJoinPrivateTeams.Id, model.SystemUserRoleId)
})
require.True(t, th.App.HasPermissionToTeam(th.Context, th.BasicUser.Id, inviteTeam.Id, model.PermissionJoinPrivateTeams))
require.True(t, th.App.HasPermissionToTeam(th.Context, th.BasicUser.Id, inviteTeam.Id, model.PermissionJoinPublicChannels))
},
hasAccess: true,
},
{
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) {
if tc.assertSetup != nil {
tc.assertSetup(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)
}
})
}
t.Run("Open post - Current team - Non-member denied when compliance is enabled", func(t *testing.T) {
info, resp, err := otherTeamMemberClient.GetPostInfo(context.Background(), openPost.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, openChannel.Id, info.ChannelId)
require.True(t, info.HasJoinedTeam)
require.False(t, info.HasJoinedChannel)
originalComplianceEnabled := *th.App.Config().ComplianceSettings.Enable
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ComplianceSettings.Enable = true
})
t.Cleanup(func() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ComplianceSettings.Enable = originalComplianceEnabled
})
})
_, resp, err = otherTeamMemberClient.GetPostInfo(context.Background(), openPost.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("Open post - Open team - Non-member denied when compliance is enabled", func(t *testing.T) {
_, appErr := th.App.GetTeamMember(th.Context, openTeam.Id, th.BasicUser.Id)
require.NotNil(t, appErr)
_, appErr = th.App.GetChannelMember(th.Context, openTeamOpenChannel.Id, th.BasicUser.Id)
require.NotNil(t, appErr)
info, resp, err := client.GetPostInfo(context.Background(), openTeamOpenPost.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, openTeamOpenChannel.Id, info.ChannelId)
require.False(t, info.HasJoinedTeam)
require.False(t, info.HasJoinedChannel)
originalComplianceEnabled := *th.App.Config().ComplianceSettings.Enable
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ComplianceSettings.Enable = true
})
t.Cleanup(func() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ComplianceSettings.Enable = originalComplianceEnabled
})
})
_, resp, err = client.GetPostInfo(context.Background(), openTeamOpenPost.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("Private post - Same-team non-member with manage members permission is denied", func(t *testing.T) {
_, appErr := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, th.BasicUser2.Id)
require.Nil(t, appErr)
_, appErr = th.App.GetChannelMember(th.Context, privateChannelBasicUser.Id, th.BasicUser2.Id)
require.NotNil(t, appErr)
th.AddPermissionToRole(t, model.PermissionManagePrivateChannelMembers.Id, model.TeamUserRoleId)
t.Cleanup(func() {
th.RemovePermissionFromRole(t, model.PermissionManagePrivateChannelMembers.Id, model.TeamUserRoleId)
})
hasPermission, isMember := th.App.HasPermissionToChannel(th.Context, th.BasicUser2.Id, privateChannelBasicUser.Id, model.PermissionManagePrivateChannelMembers)
require.True(t, hasPermission)
require.False(t, isMember)
_, resp, err := otherTeamMemberClient.GetPostInfo(context.Background(), privatePostBasicUser.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("DM post - Outsider is denied", func(t *testing.T) {
_, appErr := th.App.GetChannelMember(th.Context, dmChannel.Id, outsiderUser.Id)
require.NotNil(t, appErr)
_, resp, err := outsiderClient.GetPostInfo(context.Background(), dmPost.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("GM post - Outsider is denied", func(t *testing.T) {
_, appErr := th.App.GetChannelMember(th.Context, gmChannel.Id, outsiderUser.Id)
require.NotNil(t, appErr)
_, resp, err := outsiderClient.GetPostInfo(context.Background(), gmPost.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
}
func TestAcknowledgePost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
client := th.Client
post := th.BasicPost
ack, _, err := client.AcknowledgePost(context.Background(), post.Id, th.BasicUser.Id)
require.NoError(t, err)
acks, appErr := th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, appErr)
require.Len(t, acks, 1)
require.Equal(t, acks[0], ack)
_, resp, err := client.AcknowledgePost(context.Background(), "junk", th.BasicUser.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.AcknowledgePost(context.Background(), GenerateTestID(), th.BasicUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = client.AcknowledgePost(context.Background(), post.Id, "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.AcknowledgePost(context.Background(), post.Id, th.BasicUser2.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.AcknowledgePost(context.Background(), post.Id, th.BasicUser.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.SystemAdminClient.AcknowledgePost(context.Background(), post.Id, th.SystemAdminUser.Id)
require.NoError(t, err)
}
func TestUnacknowledgePost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
client := th.Client
post := th.BasicPost
ack, _, err := client.AcknowledgePost(context.Background(), post.Id, th.BasicUser.Id)
require.NoError(t, err)
acks, appErr := th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, appErr)
require.Len(t, acks, 1)
require.Equal(t, acks[0], ack)
resp, err := client.UnacknowledgePost(context.Background(), "junk", th.BasicUser.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.UnacknowledgePost(context.Background(), GenerateTestID(), th.BasicUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = client.UnacknowledgePost(context.Background(), post.Id, "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.UnacknowledgePost(context.Background(), post.Id, th.BasicUser2.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.UnacknowledgePost(context.Background(), post.Id, th.BasicUser.Id)
require.NoError(t, err)
acks, appErr = th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, appErr)
require.Len(t, acks, 0)
_, err = client.Logout(context.Background())
require.NoError(t, err)
resp, err = client.UnacknowledgePost(context.Background(), post.Id, th.BasicUser.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestRestorePostVersion(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
t.Run("should restore post version successfully", func(t *testing.T) {
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
}
createdPost, response, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, response)
patch, response, err := client.PatchPost(context.Background(), createdPost.Id, &model.PostPatch{
Message: new("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: new("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: new("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: new("edited message 1"),
})
require.NoError(t, err)
CheckOKStatus(t, response)
require.Equal(t, "edited message 1", patch.Message)
// verify edit history
editHistory, response, err := client.GetEditHistoryForPost(context.Background(), createdPost.Id)
require.NoError(t, err)
CheckOKStatus(t, response)
require.Equal(t, 1, len(editHistory))
require.Equal(t, "original message", editHistory[0].Message)
// now we'll restore to the original version
th.LoginBasic2(t)
restoredPost, response, err := th.Client.RestorePostVersion(context.Background(), createdPost.Id, editHistory[0].Id)
require.Error(t, err)
CheckForbiddenStatus(t, response)
require.Nil(t, restoredPost)
})
t.Run("system admin should not be able to restore someone else's post", func(t *testing.T) {
th.LoginBasic(t)
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
}
createdPost, response, err := th.Client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, response)
patch, response, err := th.Client.PatchPost(context.Background(), createdPost.Id, &model.PostPatch{
Message: new("edited message 1"),
})
require.NoError(t, err)
CheckOKStatus(t, response)
require.Equal(t, "edited message 1", patch.Message)
// verify edit history
editHistory, response, err := th.Client.GetEditHistoryForPost(context.Background(), createdPost.Id)
require.NoError(t, err)
CheckOKStatus(t, response)
require.Equal(t, 1, len(editHistory))
require.Equal(t, "original message", editHistory[0].Message)
// now we'll restore to the original version
th.LoginSystemAdmin(t)
restoredPost, response, err := th.SystemAdminClient.RestorePostVersion(context.Background(), createdPost.Id, editHistory[0].Id)
require.Error(t, err)
CheckForbiddenStatus(t, response)
require.Nil(t, restoredPost)
})
t.Run("restore post version blocked when 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
})
// Create post in the past via app layer (bypasses API time limit)
oldPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
CreateAt: model.GetMillis() - 2000,
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
// Patch via app layer to create edit history (bypasses API time limit check)
_, _, appErr = th.App.PatchPost(th.Context, oldPost.Id, &model.PostPatch{
Message: new("edited message"),
}, &model.UpdatePostOptions{})
require.Nil(t, appErr)
// Get edit history
editHistory, response, err := client.GetEditHistoryForPost(context.Background(), oldPost.Id)
require.NoError(t, err)
CheckOKStatus(t, response)
require.Equal(t, 1, len(editHistory))
// Restore should be blocked by time limit
_, resp, err := client.RestorePostVersion(context.Background(), oldPost.Id, editHistory[0].Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id)
})
t.Run("restore post version with file change blocked when 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
})
// Upload a file for the original post
fileResp, _, err := client.UploadFile(context.Background(), []byte("data"), th.BasicChannel.Id, "test")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
// Create post in the past with file attached
oldPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "original message",
UserId: th.BasicUser.Id,
FileIds: model.StringArray{fileId},
CreateAt: model.GetMillis() - 2000,
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
// Remove file via app layer to create history entry that has the file
emptyFiles := model.StringArray{}
_, _, appErr = th.App.PatchPost(th.Context, oldPost.Id, &model.PostPatch{
Message: new("edited message"),
FileIds: &emptyFiles,
}, &model.UpdatePostOptions{})
require.Nil(t, appErr)
// Get edit history (should have the entry with the original file)
editHistory, response, err := client.GetEditHistoryForPost(context.Background(), oldPost.Id)
require.NoError(t, err)
CheckOKStatus(t, response)
require.Equal(t, 1, len(editHistory))
require.Equal(t, 1, len(editHistory[0].FileIds))
// Restore (which would re-attach the file) should be blocked by time limit
_, resp, err := client.RestorePostVersion(context.Background(), oldPost.Id, editHistory[0].Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id)
})
}
func TestRevealPost(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
// Enable BurnOnRead feature flag
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, th.BasicChannel)
// Helper to create burn-on-read post
createBurnOnReadPost := func(client *model.Client4, channel *model.Channel) *model.Post {
post := &model.Post{
ChannelId: channel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, createdPost)
return createdPost
}
// Helper to create and login second user
createSecondUser := func(channel *model.Channel) (*model.User, *model.Client4) {
user2 := th.CreateUser(t)
th.LinkUserToTeam(t, user2, th.BasicTeam)
if channel != nil {
th.AddUserToChannel(t, user2, channel)
}
client2 := th.CreateClient()
_, _, err := client2.Login(context.Background(), user2.Email, user2.Password)
require.NoError(t, err)
t.Cleanup(func() {
_, err = client2.Logout(context.Background())
require.NoError(t, err)
})
return user2, client2
}
t.Run("feature not enabled, reveal returns 501", func(t *testing.T) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(th.SystemAdminClient, th.BasicChannel)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.FeatureFlags.BurnOnRead = false
})
t.Cleanup(func() {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.FeatureFlags.BurnOnRead = true
})
})
_, resp, err := th.Client.RevealPost(context.Background(), post.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
regularPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "regular message",
}
createdPost, resp, err := th.Client.CreatePost(context.Background(), regularPost)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, client2 := createSecondUser(th.BasicChannel)
revealedPost, resp, err := client2.RevealPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "app.reveal_post.not_burn_on_read.app_error")
require.Nil(t, revealedPost)
}, "reveal regular post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
revealedPost, resp, err := th.Client.RevealPost(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
require.Nil(t, revealedPost)
}, "reveal non-existing post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(client, th.BasicChannel)
revealedPost, resp, err := client.RevealPost(context.Background(), post.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Nil(t, revealedPost)
CheckErrorID(t, err, "api.post.reveal_post.cannot_reveal_own_post.app_error")
}, "try reveal own post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
revealedPost, resp, err := client.RevealPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, revealedPost)
require.Equal(t, post.Id, revealedPost.Id)
require.Equal(t, "burn on read message", revealedPost.Message)
require.NotNil(t, revealedPost.Metadata)
require.NotZero(t, revealedPost.Metadata.ExpireAt)
}, "reveal someone elses post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
_, client2 := createSecondUser(th.BasicChannel)
createdPost, resp, err := client2.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
// Manually expire the post
storePost, err := th.App.Srv().Store().Post().Get(th.Context, createdPost.Id, model.GetPostsOptions{}, "", th.App.Config().GetSanitizeOptions())
require.NoError(t, err)
require.Len(t, storePost.Posts, 1)
postToUpdate := storePost.Posts[createdPost.Id]
postToUpdate.AddProp(model.PostPropsExpireAt, model.GetMillis()-1000)
_, err = th.App.Srv().Store().Post().Overwrite(th.Context, postToUpdate)
require.NoError(t, err)
revealedPost, resp, err := client.RevealPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
require.Nil(t, revealedPost)
CheckErrorID(t, err, "app.reveal_post.post_expired.app_error")
}, "reveal expired post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
user := th.BasicUser
if client == th.SystemAdminClient {
user = th.SystemAdminUser
}
// Create expired read receipt
readReceipt, err := th.App.Srv().Store().ReadReceipt().Save(th.Context, &model.ReadReceipt{
PostID: post.Id,
UserID: user.Id,
ExpireAt: model.GetMillis() - 1000,
})
require.NoError(t, err)
require.NotNil(t, readReceipt)
revealedPost, resp, err := client.RevealPost(context.Background(), post.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
require.Nil(t, revealedPost)
CheckErrorID(t, err, "app.post.get.app_error")
}, "reveal post with expired read receipt")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(nil)
privateChannel, resp, err := client2.CreateChannel(context.Background(), &model.Channel{
TeamId: th.BasicTeam.Id,
Type: model.ChannelTypePrivate,
Name: GenerateTestChannelName(),
DisplayName: "Private Channel",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
post := &model.Post{
ChannelId: privateChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := client2.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
revealedPost, resp, err := client.RevealPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
require.Nil(t, revealedPost)
}, "user without channel access")
t.Run("cookie auth without X-Requested-With header should be rejected", func(t *testing.T) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
// Build a raw HTTP request using cookie-based auth (no Authorization header)
// and without the X-Requested-With header — simulating a passive resource load
revealURL := th.Client.APIURL + "/posts/" + post.Id + "/reveal"
req, err := http.NewRequest("GET", revealURL, nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: model.SessionCookieToken,
Value: th.Client.AuthToken,
})
resp, err := th.Client.HTTPClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("cookie auth with X-Requested-With header should be accepted", func(t *testing.T) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
// Build a raw HTTP request using cookie-based auth with the X-Requested-With header
revealURL := th.Client.APIURL + "/posts/" + post.Id + "/reveal"
req, err := http.NewRequest("GET", revealURL, nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: model.SessionCookieToken,
Value: th.Client.AuthToken,
})
req.Header.Set(model.HeaderRequestedWith, model.HeaderRequestedWithXML)
resp, err := th.Client.HTTPClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
})
}
func TestCreateBurnOnReadPost(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
// Enable BurnOnRead feature flag
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, th.BasicChannel)
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
createdPost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, createdPost)
require.Equal(t, model.PostTypeBurnOnRead, createdPost.Type)
}, "create burn on read post")
t.Run("reveal burn on read post and verify in channel posts", func(t *testing.T) {
enableBurnOnReadFeature(th)
// Create burn-on-read post with basic user
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := th.Client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, createdPost)
// Create websocket client for system admin to receive reveal event
wsClient := th.CreateConnectedWebSocketClientWithClient(t, th.SystemAdminClient)
// Get posts for channel with system admin client - verify post is not revealed by default
posts, resp, err := th.SystemAdminClient.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, posts)
require.NotNil(t, posts.Posts[createdPost.Id])
unrevealedPost := posts.Posts[createdPost.Id]
require.Equal(t, "", unrevealedPost.Message)
// Check if the metadata is empty
require.Equal(t, model.PostMetadata{}, *unrevealedPost.Metadata)
// Reveal the post with system admin client
revealedPost, resp, err := th.SystemAdminClient.RevealPost(context.Background(), createdPost.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, revealedPost)
require.Equal(t, "burn on read message", revealedPost.Message)
require.NotNil(t, revealedPost.Metadata)
require.NotZero(t, revealedPost.Metadata.ExpireAt)
// Verify websocket client receives the reveal event
var eventPost model.Post
require.Eventually(t, func() bool {
select {
case event := <-wsClient.EventChannel:
if event.EventType() == model.WebsocketEventPostRevealed {
eventPostJSON, ok := event.GetData()["post"].(string)
if !ok {
return false
}
err = json.Unmarshal([]byte(eventPostJSON), &eventPost)
if err != nil {
return false
}
return eventPost.Id == createdPost.Id
}
default:
return false
}
return false
}, 5*time.Second, 100*time.Millisecond, "should have received post_revealed websocket event")
require.Equal(t, createdPost.Id, eventPost.Id)
require.Equal(t, "burn on read message", eventPost.Message)
require.NotNil(t, eventPost.Metadata)
require.NotZero(t, eventPost.Metadata.ExpireAt)
// Get the single post - verify it's revealed
singlePost, resp, err := th.SystemAdminClient.GetPost(context.Background(), createdPost.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, singlePost)
require.Equal(t, "burn on read message", singlePost.Message)
require.NotNil(t, singlePost.Metadata)
require.NotZero(t, singlePost.Metadata.ExpireAt)
// Query for posts in channel again - verify this time it's revealed
postsAfterReveal, resp, err := th.SystemAdminClient.GetPostsForChannel(context.Background(), th.BasicChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, postsAfterReveal)
require.NotNil(t, postsAfterReveal.Posts[createdPost.Id])
revealedPostInChannel := postsAfterReveal.Posts[createdPost.Id]
require.Equal(t, "burn on read message", revealedPostInChannel.Message)
require.NotNil(t, revealedPostInChannel.Metadata)
require.NotZero(t, revealedPostInChannel.Metadata.ExpireAt)
})
t.Run("Create post send back pending post ID for post creator", func(t *testing.T) {
post := &model.Post{
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
PendingPostId: model.NewId(),
}
createdPost, response, err := th.Client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, response)
require.NotNil(t, createdPost)
require.Equal(t, post.PendingPostId, createdPost.PendingPostId)
})
}
func TestBurnPost(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
// Enable BurnOnRead feature flag
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, th.BasicChannel)
// Helper to create burn-on-read post
createBurnOnReadPost := func(client *model.Client4, channel *model.Channel) *model.Post {
post := &model.Post{
ChannelId: channel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, createdPost)
return createdPost
}
// Helper to create and login second user
createSecondUser := func(channel *model.Channel) (*model.User, *model.Client4) {
user2 := th.CreateUser(t)
th.LinkUserToTeam(t, user2, th.BasicTeam)
if channel != nil {
th.AddUserToChannel(t, user2, channel)
}
client2 := th.CreateClient()
_, _, err := client2.Login(context.Background(), user2.Email, user2.Password)
require.NoError(t, err)
t.Cleanup(func() {
_, err = client2.Logout(context.Background())
require.NoError(t, err)
})
return user2, client2
}
t.Run("feature not enabled, burn post allowed", func(t *testing.T) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(th.SystemAdminClient, th.BasicChannel)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.EnableBurnOnRead = new(false)
})
_, resp, err := th.Client.RevealPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
resp, err = th.Client.BurnPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
})
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
regularPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "regular message",
}
createdPost, resp, err := th.Client.CreatePost(context.Background(), regularPost)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
resp, err = client.BurnPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "app.burn_post.not_burn_on_read.app_error")
}, "burn regular post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
resp, err := client.BurnPost(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}, "burn non-existing post")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(client, th.BasicChannel)
resp, err := client.BurnPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
// Verify post is permanently deleted
_, resp, err = client.GetPost(context.Background(), post.Id, "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}, "author burns own post - permanently deleted")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
resp, err := client.BurnPost(context.Background(), post.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "app.burn_post.not_revealed.app_error")
}, "non-author burns post without read receipt")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(th.BasicChannel)
post := createBurnOnReadPost(client2, th.BasicChannel)
// Create websocket client to receive burn event
wsClient := th.CreateConnectedWebSocketClientWithClient(t, client)
// Create expired read receipt
revealedPost, resp, err := client.RevealPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, revealedPost)
resp, err = client.BurnPost(context.Background(), post.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
userID := th.BasicUser.Id
if client == th.SystemAdminClient {
userID = th.SystemAdminUser.Id
}
// Verify receipt ExpireAt is unchanged (no-op)
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, post.Id, userID)
require.NoError(t, err)
require.LessOrEqual(t, receipt.ExpireAt, revealedPost.Metadata.ExpireAt)
// Verify websocket client receives the burn event
var eventPostID string
require.Eventually(t, func() bool {
select {
case event := <-wsClient.EventChannel:
if event.EventType() == model.WebsocketEventPostBurned {
var ok bool
eventPostID, ok = event.GetData()["post_id"].(string)
if !ok {
return false
}
return eventPostID == post.Id
}
default:
return false
}
return false
}, 5*time.Second, 100*time.Millisecond, "should have received post_burned websocket event")
require.Equal(t, post.Id, eventPostID)
}, "non-author burns post with expired read receipt")
th.TestForRegularAndSystemAdminClients(t, func(t *testing.T, client *model.Client4) {
enableBurnOnReadFeature(th)
_, client2 := createSecondUser(nil)
privateChannel, resp, err := client2.CreateChannel(context.Background(), &model.Channel{
TeamId: th.BasicTeam.Id,
Type: model.ChannelTypePrivate,
Name: GenerateTestChannelName(),
DisplayName: "Private Channel",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
post := &model.Post{
ChannelId: privateChannel.Id,
Message: "burn on read message",
Type: model.PostTypeBurnOnRead,
}
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
createdPost, resp, err := client2.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
resp, err = client.BurnPost(context.Background(), createdPost.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}, "user without channel access")
t.Run("unauthorized access", func(t *testing.T) {
enableBurnOnReadFeature(th)
post := createBurnOnReadPost(th.Client, th.BasicChannel)
// Create unauthenticated client
unauthClient := th.CreateClient()
resp, err := unauthClient.BurnPost(context.Background(), post.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}
func TestUpdateCardPostByNonOwner(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.IntegratedBoards = true
}).InitBasic(t)
client := th.Client
channel := th.BasicChannel
// User 1 creates a card post
cardPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "original card message",
Type: model.PostTypeCard,
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
// User 2 (non-owner, but channel member) updates the card
th.LoginBasic2(t)
updatedPost := cardPost.Clone()
updatedPost.Message = "updated by user2"
rpost, _, err := client.UpdatePost(context.Background(), cardPost.Id, updatedPost)
require.NoError(t, err)
assert.Equal(t, "updated by user2", rpost.Message)
}
func TestDeleteCardPostByNonOwner(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.IntegratedBoards = true
}).InitBasic(t)
client := th.Client
channel := th.BasicChannel
// User 1 creates a card post
cardPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "card to delete",
Type: model.PostTypeCard,
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
// User 2 (non-owner, but channel member) deletes the card
th.LoginBasic2(t)
_, err := client.DeletePost(context.Background(), cardPost.Id)
require.NoError(t, err)
// Verify the post is deleted
_, resp, err := client.GetPost(context.Background(), cardPost.Id, "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}
func TestPatchCardPostByNonOwner(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.IntegratedBoards = true
}).InitBasic(t)
client := th.Client
channel := th.BasicChannel
// User 1 creates a card post
cardPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "original card for patching",
Type: model.PostTypeCard,
}, channel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
// User 2 (non-owner, but channel member) patches the card
th.LoginBasic2(t)
patch := &model.PostPatch{
Message: new("patched by user2"),
}
rpost, _, err := client.PatchPost(context.Background(), cardPost.Id, patch)
require.NoError(t, err)
assert.Equal(t, "patched by user2", rpost.Message)
t.Run("user not in channel cannot patch card", func(t *testing.T) {
// Create a user not in the channel
user := th.CreateUser(t)
cli := th.CreateClient()
_, _, err := cli.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
patch := &model.PostPatch{
Message: new("should fail"),
}
_, resp, err := cli.PatchPost(context.Background(), cardPost.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestCreateCardPostWithFeatureFlagDisabled(t *testing.T) {
mainHelper.Parallel(t)
t.Run("card post rejected when IntegratedBoards flag is disabled", func(t *testing.T) {
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.IntegratedBoards = false
}).InitBasic(t)
client := th.Client
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "this is a card",
Type: model.PostTypeCard,
}
_, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("card post allowed when IntegratedBoards flag is enabled", func(t *testing.T) {
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.IntegratedBoards = true
}).InitBasic(t)
client := th.Client
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "card post",
Type: model.PostTypeCard,
}
rpost, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, rpost)
assert.Equal(t, model.PostTypeCard, rpost.Type)
assert.Equal(t, "card post", rpost.Message)
})
t.Run("non-card post allowed when IntegratedBoards flag is disabled", func(t *testing.T) {
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.IntegratedBoards = false
}).InitBasic(t)
client := th.Client
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "this is a regular post",
}
rpost, _, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
assert.Equal(t, "", rpost.Type)
})
}
// MM-68140: POST /posts/rewrite must reject root_id for threads in channels the user cannot read
// before any thread content is used (e.g. sent to the AI bridge).
func TestRewritePostRequiresReadAccessToRootThread(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
dm := th.CreateDmChannel(t, th.BasicUser2)
secretToken := "MM68140_SECRET_REWRITE_HTTP_" + model.NewId()
root := th.CreateMessagePostWithClient(t, th.Client, dm, secretToken)
attacker := th.CreateUserWithClient(t, th.SystemAdminClient)
th.LinkUserToTeam(t, attacker, th.BasicTeam)
attackerClient := th.CreateClient()
_, _, err := attackerClient.Login(context.Background(), attacker.Email, attacker.Password)
require.NoError(t, err)
req := model.RewriteRequest{
AgentID: model.NewId(),
Message: "text to shorten",
Action: model.RewriteActionShorten,
RootID: root.Id,
}
reqBody, err := json.Marshal(req)
require.NoError(t, err)
// Use raw HTTP so we can read the body on non-2xx (DoAPIPostJSON returns an error and closes the body for status >= 300).
httpReq, err := http.NewRequestWithContext(context.Background(), http.MethodPost, attackerClient.APIURL+"/posts/rewrite", bytes.NewReader(reqBody))
require.NoError(t, err)
httpReq.Header.Set(model.HeaderAuth, attackerClient.AuthType+" "+attackerClient.AuthToken)
httpReq.Header.Set("Content-Type", "application/json")
respHTTP, err := attackerClient.HTTPClient.Do(httpReq)
require.NoError(t, err)
defer respHTTP.Body.Close()
bodyBytes, err := io.ReadAll(respHTTP.Body)
require.NoError(t, err)
resp := model.BuildResponse(respHTTP)
require.Equalf(t, http.StatusForbidden, resp.StatusCode,
"rewrite with root_id in an unreadable channel must return forbidden before using thread content; status=%d body=%s", resp.StatusCode, string(bodyBytes))
assert.NotContains(t, string(bodyBytes), secretToken, "response must not leak private thread content")
}