mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-19 02:06:37 +08:00
* [MM-69885] Allow integrations to update mm_blocks_actions on their own posts * prune mm_blocks_actions on post updates to match remaining buttons
372 lines
12 KiB
Go
372 lines
12 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package sharedchannel
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
|
|
)
|
|
|
|
func TestProcessSyncMessage(t *testing.T) {
|
|
remoteID := model.NewId()
|
|
channelID := model.NewId()
|
|
rc := &model.RemoteCluster{
|
|
RemoteId: remoteID,
|
|
Name: "test-remote",
|
|
}
|
|
|
|
makeConfig := func() *model.Config {
|
|
cfg := &model.Config{}
|
|
cfg.SetDefaults()
|
|
return cfg
|
|
}
|
|
|
|
t.Run("returns SyncResponse directly for empty sync message", func(t *testing.T) {
|
|
channel := &model.Channel{Id: channelID, Type: model.ChannelTypeOpen}
|
|
|
|
mockChannelStore := &mocks.ChannelStore{}
|
|
mockChannelStore.On("Get", channelID, true).Return(channel, nil)
|
|
|
|
mockSCStore := &mocks.SharedChannelStore{}
|
|
mockSCStore.On("HasRemote", channelID, remoteID).Return(true, nil)
|
|
|
|
mockStore := &mocks.Store{}
|
|
mockStore.On("Channel").Return(mockChannelStore)
|
|
mockStore.On("SharedChannel").Return(mockSCStore)
|
|
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
mockServer := &MockServerIface{}
|
|
mockServer.On("GetStore").Return(mockStore)
|
|
mockServer.On("Log").Return(logger)
|
|
mockServer.On("Config").Return(makeConfig())
|
|
|
|
scs := &Service{server: mockServer}
|
|
|
|
syncMsg := &model.SyncMsg{
|
|
ChannelId: channelID,
|
|
}
|
|
|
|
resp, err := scs.ProcessSyncMessage(request.TestContext(t), syncMsg, rc)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, resp.PostErrors)
|
|
assert.Empty(t, resp.UserErrors)
|
|
})
|
|
|
|
t.Run("returns error when channel not found", func(t *testing.T) {
|
|
mockChannelStore := &mocks.ChannelStore{}
|
|
mockChannelStore.On("Get", channelID, true).Return(nil, store.NewErrNotFound("Channel", channelID))
|
|
|
|
mockStore := &mocks.Store{}
|
|
mockStore.On("Channel").Return(mockChannelStore)
|
|
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
mockServer := &MockServerIface{}
|
|
mockServer.On("GetStore").Return(mockStore)
|
|
mockServer.On("Log").Return(logger)
|
|
mockServer.On("Config").Return(makeConfig())
|
|
|
|
scs := &Service{server: mockServer}
|
|
|
|
syncMsg := &model.SyncMsg{
|
|
ChannelId: channelID,
|
|
}
|
|
|
|
_, err := scs.ProcessSyncMessage(request.TestContext(t), syncMsg, rc)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "channel not found")
|
|
})
|
|
|
|
t.Run("returns error when channel not shared with remote", func(t *testing.T) {
|
|
channel := &model.Channel{Id: channelID, Type: model.ChannelTypeOpen}
|
|
|
|
mockChannelStore := &mocks.ChannelStore{}
|
|
mockChannelStore.On("Get", channelID, true).Return(channel, nil)
|
|
|
|
mockSCStore := &mocks.SharedChannelStore{}
|
|
mockSCStore.On("HasRemote", channelID, remoteID).Return(false, nil)
|
|
|
|
mockStore := &mocks.Store{}
|
|
mockStore.On("Channel").Return(mockChannelStore)
|
|
mockStore.On("SharedChannel").Return(mockSCStore)
|
|
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
mockServer := &MockServerIface{}
|
|
mockServer.On("GetStore").Return(mockStore)
|
|
mockServer.On("Log").Return(logger)
|
|
mockServer.On("Config").Return(makeConfig())
|
|
|
|
scs := &Service{server: mockServer}
|
|
|
|
syncMsg := &model.SyncMsg{
|
|
ChannelId: channelID,
|
|
}
|
|
|
|
_, err := scs.ProcessSyncMessage(request.TestContext(t), syncMsg, rc)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrChannelNotShared)
|
|
})
|
|
|
|
t.Run("returns error for global user sync with posts", func(t *testing.T) {
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
mockServer := &MockServerIface{}
|
|
mockServer.On("Log").Return(logger)
|
|
mockServer.On("Config").Return(makeConfig())
|
|
|
|
scs := &Service{server: mockServer}
|
|
|
|
syncMsg := &model.SyncMsg{
|
|
ChannelId: "", // global user sync
|
|
Posts: []*model.Post{{Id: model.NewId()}},
|
|
}
|
|
|
|
_, err := scs.ProcessSyncMessage(request.TestContext(t), syncMsg, rc)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "should not contain posts")
|
|
})
|
|
|
|
t.Run("returns empty response for global user sync with no users", func(t *testing.T) {
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
mockServer := &MockServerIface{}
|
|
mockServer.On("Log").Return(logger)
|
|
mockServer.On("Config").Return(makeConfig())
|
|
|
|
scs := &Service{server: mockServer}
|
|
|
|
syncMsg := &model.SyncMsg{
|
|
ChannelId: "", // global user sync
|
|
}
|
|
|
|
resp, err := scs.ProcessSyncMessage(request.TestContext(t), syncMsg, rc)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, resp.UsersSyncd)
|
|
})
|
|
|
|
t.Run("post with mismatched channelID is reported in PostErrors", func(t *testing.T) {
|
|
channel := &model.Channel{Id: channelID, Type: model.ChannelTypeOpen}
|
|
wrongChannelID := model.NewId()
|
|
postID := model.NewId()
|
|
|
|
mockChannelStore := &mocks.ChannelStore{}
|
|
mockChannelStore.On("Get", channelID, true).Return(channel, nil)
|
|
|
|
mockSCStore := &mocks.SharedChannelStore{}
|
|
mockSCStore.On("HasRemote", channelID, remoteID).Return(true, nil)
|
|
|
|
mockStore := &mocks.Store{}
|
|
mockStore.On("Channel").Return(mockChannelStore)
|
|
mockStore.On("SharedChannel").Return(mockSCStore)
|
|
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
mockServer := &MockServerIface{}
|
|
mockServer.On("GetStore").Return(mockStore)
|
|
mockServer.On("Log").Return(logger)
|
|
mockServer.On("Config").Return(makeConfig())
|
|
|
|
scs := &Service{server: mockServer}
|
|
|
|
syncMsg := &model.SyncMsg{
|
|
ChannelId: channelID,
|
|
Posts: []*model.Post{
|
|
{Id: postID, ChannelId: wrongChannelID},
|
|
},
|
|
}
|
|
|
|
resp, err := scs.ProcessSyncMessage(request.TestContext(t), syncMsg, rc)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, resp.PostErrors, postID)
|
|
})
|
|
}
|
|
|
|
func TestUpsertSyncUserStatus(t *testing.T) {
|
|
setup := func(remoteID string, user *model.User) (*Service, *MockAppIface, *model.Status, *model.RemoteCluster) {
|
|
var userID string
|
|
if user == nil {
|
|
userID = model.NewId()
|
|
} else {
|
|
userID = user.Id
|
|
}
|
|
|
|
status := &model.Status{
|
|
UserId: userID,
|
|
Status: model.StatusDnd,
|
|
}
|
|
remoteCluster := &model.RemoteCluster{
|
|
RemoteId: remoteID,
|
|
Name: "test-remote",
|
|
}
|
|
|
|
mockUserStore := &mocks.UserStore{}
|
|
if user == nil {
|
|
mockUserStore.On("Get", mockTypeContext, mock.Anything).Return(nil, store.NewErrNotFound("User", userID))
|
|
} else {
|
|
mockUserStore.On("Get", mockTypeContext, user.Id).Return(user, nil)
|
|
}
|
|
|
|
mockStore := &mocks.Store{}
|
|
mockStore.On("User").Return(mockUserStore)
|
|
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
|
|
mockServer := &MockServerIface{}
|
|
mockServer.On("GetStore").Return(mockStore)
|
|
mockServer.On("Log").Return(logger)
|
|
|
|
mockApp := &MockAppIface{}
|
|
mockApp.On("SaveAndBroadcastStatus", status).Return()
|
|
|
|
scs := &Service{
|
|
server: mockServer,
|
|
app: mockApp,
|
|
}
|
|
|
|
return scs, mockApp, status, remoteCluster
|
|
}
|
|
|
|
t.Run("should broadcast changes to a remote user's status", func(t *testing.T) {
|
|
remoteID := model.NewId()
|
|
user := &model.User{
|
|
Id: model.NewId(),
|
|
RemoteId: new(remoteID),
|
|
}
|
|
|
|
scs, mockApp, status, remoteCluster := setup(remoteID, user)
|
|
|
|
err := scs.upsertSyncUserStatus(request.TestContext(t), status, remoteCluster)
|
|
|
|
require.NoError(t, err)
|
|
mockApp.AssertCalled(t, "SaveAndBroadcastStatus", status)
|
|
})
|
|
|
|
t.Run("should return an error when the user doesn't exist locally", func(t *testing.T) {
|
|
remoteID := model.NewId()
|
|
var user *model.User
|
|
|
|
scs, mockApp, status, remoteCluster := setup(remoteID, user)
|
|
|
|
rctx := request.TestContext(t)
|
|
err := scs.upsertSyncUserStatus(rctx, status, remoteCluster)
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "error getting user when syncing status")
|
|
mockApp.AssertNotCalled(t, "SaveAndBroadcastStatus")
|
|
})
|
|
|
|
t.Run("should return an error when attempting to sync a local user", func(t *testing.T) {
|
|
remoteID := model.NewId()
|
|
user := &model.User{
|
|
Id: model.NewId(),
|
|
RemoteId: nil,
|
|
}
|
|
|
|
scs, mockApp, status, remoteCluster := setup(remoteID, user)
|
|
|
|
rctx := request.TestContext(t)
|
|
err := scs.upsertSyncUserStatus(rctx, status, remoteCluster)
|
|
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrRemoteIDMismatch)
|
|
assert.Contains(t, err.Error(), "error updating user status")
|
|
mockApp.AssertNotCalled(t, "SaveAndBroadcastStatus")
|
|
})
|
|
|
|
t.Run("should return an error when attempting to sync a user from a different remote", func(t *testing.T) {
|
|
remoteID := model.NewId()
|
|
anotherRemoteID := model.NewId()
|
|
user := &model.User{
|
|
Id: model.NewId(),
|
|
RemoteId: new(anotherRemoteID),
|
|
}
|
|
|
|
scs, mockApp, status, remoteCluster := setup(remoteID, user)
|
|
|
|
rctx := request.TestContext(t)
|
|
err := scs.upsertSyncUserStatus(rctx, status, remoteCluster)
|
|
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrRemoteIDMismatch)
|
|
assert.Contains(t, err.Error(), "error updating user status")
|
|
mockApp.AssertNotCalled(t, "SaveAndBroadcastStatus")
|
|
})
|
|
}
|
|
|
|
func TestUpsertSyncPost(t *testing.T) {
|
|
remoteID := model.NewId()
|
|
channelID := model.NewId()
|
|
channel := &model.Channel{Id: channelID, Type: model.ChannelTypeOpen}
|
|
rc := &model.RemoteCluster{RemoteId: remoteID, Name: "test-remote"}
|
|
|
|
setup := func(t *testing.T, existing *model.Post) (*Service, *MockAppIface) {
|
|
mockPostStore := &mocks.PostStore{}
|
|
mockPostStore.On("GetSingle", mock.Anything, existing.Id, true).Return(existing, nil)
|
|
|
|
mockStore := &mocks.Store{}
|
|
mockStore.On("Post").Return(mockPostStore)
|
|
|
|
logger := mlog.CreateConsoleTestLogger(t)
|
|
mockServer := &MockServerIface{}
|
|
mockServer.On("GetStore").Return(mockStore)
|
|
mockServer.On("Log").Return(logger)
|
|
|
|
mockApp := &MockAppIface{}
|
|
|
|
return &Service{server: mockServer, app: mockApp}, mockApp
|
|
}
|
|
|
|
t.Run("rejects edit of a post owned by a different remote", func(t *testing.T) {
|
|
otherRemoteID := model.NewId()
|
|
postID := model.NewId()
|
|
existing := &model.Post{Id: postID, ChannelId: channelID, Message: "original", RemoteId: new(otherRemoteID)}
|
|
|
|
scs, mockApp := setup(t, existing)
|
|
|
|
_, err := scs.upsertSyncPost(&model.Post{Id: postID, ChannelId: channelID, Message: "tampered"}, channel, rc, nil)
|
|
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrRemoteIDMismatch)
|
|
mockApp.AssertNotCalled(t, "UpdatePost", mock.Anything, mock.Anything, mock.Anything)
|
|
})
|
|
|
|
t.Run("rejects delete of a post owned by a local user", func(t *testing.T) {
|
|
postID := model.NewId()
|
|
existing := &model.Post{Id: postID, ChannelId: channelID, Message: "original", RemoteId: nil}
|
|
|
|
scs, mockApp := setup(t, existing)
|
|
|
|
_, err := scs.upsertSyncPost(&model.Post{Id: postID, ChannelId: channelID, DeleteAt: model.GetMillis()}, channel, rc, nil)
|
|
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrRemoteIDMismatch)
|
|
mockApp.AssertNotCalled(t, "DeletePost", mock.Anything, mock.Anything, mock.Anything)
|
|
})
|
|
|
|
t.Run("allows edit of a post owned by the sending remote", func(t *testing.T) {
|
|
postID := model.NewId()
|
|
existing := &model.Post{Id: postID, ChannelId: channelID, Message: "original", RemoteId: new(remoteID)}
|
|
|
|
scs, mockApp := setup(t, existing)
|
|
updated := &model.Post{Id: postID, ChannelId: channelID, Message: "updated"}
|
|
// A federated, remote-owned edit must be allowed to carry mm_blocks_actions
|
|
// changes through the UpdatePost freeze — the origin cluster is the trust
|
|
// boundary. Assert the option is set so a revert to nil options is caught.
|
|
allowsMmBlocks := mock.MatchedBy(func(opts *model.UpdatePostOptions) bool {
|
|
return opts != nil && opts.AllowMmBlocksActionsUpdate
|
|
})
|
|
mockApp.On("UpdatePost", mock.Anything, mock.Anything, allowsMmBlocks).Return(updated, false, (*model.AppError)(nil))
|
|
|
|
_, err := scs.upsertSyncPost(&model.Post{Id: postID, ChannelId: channelID, Message: "updated"}, channel, rc, nil)
|
|
|
|
require.NoError(t, err)
|
|
mockApp.AssertCalled(t, "UpdatePost", mock.Anything, mock.Anything, allowsMmBlocks)
|
|
})
|
|
}
|