mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-13 13:08:56 -04:00
* ci: enable fullyparallel mode for server tests Replace os.Setenv, os.Chdir, and global state mutations with parallel-safe alternatives (t.Setenv, t.Chdir, test hooks) across 37 files. Refactor GetLogRootPath and MM_INSTALL_TYPE to use package-level test hooks instead of environment variables. This enables gotestsum --fullparallel, allowing all test packages to run with maximum parallelism within each shard. Co-authored-by: Claude <claude@anthropic.com> * ci: split fullyparallel from continue-on-error in workflow template - Add new boolean input 'allow-failure' separate from 'fullyparallel' - Change continue-on-error to use allow-failure instead of fullyparallel - Update server-ci.yml to pass allow-failure: true for test coverage job - Allows independent control of parallel execution and failure tolerance Co-authored-by: Claude <claude@anthropic.com> * fix: protect TestOverrideLogRootPath with sync.Mutex for parallel tests - Replace global var TestOverrideLogRootPath with mutex-protected functions - Add SetTestOverrideLogRootPath() and getTestOverrideLogRootPath() functions - Update GetLogRootPath() to use thread-safe getter - Update all test files to use SetTestOverrideLogRootPath() with t.Cleanup() - Fixes race condition when running tests with t.Parallel() Co-authored-by: Claude <claude@anthropic.com> * fix: configure audit settings before server setup in tests - Move ExperimentalAuditSettings from UpdateConfig() to config defaults - Pass audit config via app.Config() option in SetupWithServerOptions() - Fixes audit test setup ordering to configure BEFORE server initialization - Resolves CodeRabbit's audit config timing issue in api4 tests Co-authored-by: Claude <claude@anthropic.com> * fix: implement SetTestOverrideLogRootPath mutex in logger.go The previous commit updated test callers to use SetTestOverrideLogRootPath() but didn't actually create the function in config/logger.go, causing build failures across all CI shards. This commit: - Replaces the exported var TestOverrideLogRootPath with mutex-protected unexported state (testOverrideLogRootPath + testOverrideLogRootMu) - Adds exported SetTestOverrideLogRootPath() setter - Adds unexported getTestOverrideLogRootPath() getter - Updates GetLogRootPath() to use the thread-safe getter - Fixes log_test.go callers that were missed in the previous commit Co-authored-by: Claude <claude@anthropic.com> * fix(test): use SetupConfig for access_control feature flag registration InitAccessControlPolicy() checks FeatureFlags.AttributeBasedAccessControl at route registration time during server startup. Setting the flag via UpdateConfig after Setup() is too late — routes are never registered and API calls return 404. Use SetupConfig() to pass the feature flag in the initial config before server startup, ensuring routes are properly registered. Co-authored-by: Claude <claude@anthropic.com> * fix(test): restore BurnOnRead flag state in TestRevealPost subtest The 'feature not enabled' subtest disables BurnOnRead without restoring it via t.Cleanup. Subsequent subtests inherit the disabled state, which can cause 501 errors when they expect the feature to be available. Add t.Cleanup to restore FeatureFlags.BurnOnRead = true after the subtest completes. Co-authored-by: Claude <claude@anthropic.com> * fix(test): restore EnableSharedChannelsMemberSync flag via t.Cleanup The test disables EnableSharedChannelsMemberSync without restoring it. If the subtest exits early (e.g., require failure), later sibling subtests inherit a disabled flag and become flaky. Add t.Cleanup to restore the flag after the subtest completes. Co-authored-by: Claude <claude@anthropic.com> * Fix test parallelism: use instance-scoped overrides and init-time audit config Replace package-level test globals (TestOverrideInstallType, SetTestOverrideLogRootPath) with fields on PlatformService so each test gets its own instance without process-wide mutation. Fix three audit tests (TestUserLoginAudit, TestLogoutAuditAuthStatus, TestUpdatePasswordAudit) that configured the audit logger after server init — the audit logger only reads config at startup, so pass audit settings via app.Config() at init time instead. Also revert the Go 1.24.13 downgrade and bump mattermost-govet to v2.0.2 for Go 1.25.8 compatibility. * Fix audit unit tests * Fix MMCLOUDURL unit tests * Fixed unit tests using MM_NOTIFY_ADMIN_COOL_OFF_DAYS * Make app migrations idempotent for parallel test safety Change System().Save() to System().SaveOrUpdate() in all migration completion markers. When two parallel tests share a database pool entry, both may race through the check-then-insert migration pattern. Save() causes a duplicate key fatal crash; SaveOrUpdate() makes the second write a harmless no-op. * test: address review feedback on fullyparallel PR - Use SetLogRootPathOverride() setter instead of direct field access in platform/support_packet_test.go and platform/log_test.go (pvev) - Restore TestGetLogRootPath in config/logger_test.go to keep MM_LOG_PATH env var coverage; test uses t.Setenv so it runs serially which is fine (pvev) - Fix misleading comment in config_test.go: code uses t.Setenv, not os.Setenv (jgheithcock) Co-authored-by: Claude <claude@anthropic.com> * fix: add missing os import in post_test.go The os import was dropped during a merge conflict resolution while burn-on-read shared channel tests from master still use os.Setenv. Co-authored-by: Claude <claude@anthropic.com> --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: wiggin77 <wiggin77@warpmail.net> Co-authored-by: Mattermost Build <build@mattermost.com>
6076 lines
203 KiB
Go
6076 lines
203 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
storemocks "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
|
|
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
|
"github.com/mattermost/mattermost/server/v8/platform/services/imageproxy"
|
|
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine/mocks"
|
|
)
|
|
|
|
func enableBoRFeature(th *TestHelper) {
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.EnableBurnOnRead = model.NewPointer(true)
|
|
})
|
|
}
|
|
|
|
func makePendingPostId(user *model.User) string {
|
|
return fmt.Sprintf("%s:%s", user.Id, strconv.FormatInt(model.GetMillis(), 10))
|
|
}
|
|
|
|
func TestCreatePostDeduplicate(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("duplicate create post is idempotent", func(t *testing.T) {
|
|
session := &model.Session{
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
session, err := th.App.CreateSession(th.Context, session)
|
|
require.Nil(t, err)
|
|
|
|
pendingPostId := makePendingPostId(th.BasicUser)
|
|
|
|
post, _, err := th.App.CreatePostAsUser(th.Context.WithSession(session), &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
PendingPostId: pendingPostId,
|
|
}, session.Id, true)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "message", post.Message)
|
|
|
|
duplicatePost, _, err := th.App.CreatePostAsUser(th.Context.WithSession(session), &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
PendingPostId: pendingPostId,
|
|
}, session.Id, true)
|
|
require.Nil(t, err)
|
|
require.Equal(t, post.Id, duplicatePost.Id, "should have returned previously created post id")
|
|
require.Equal(t, "message", duplicatePost.Message)
|
|
})
|
|
|
|
t.Run("post rejected by plugin leaves cache ready for non-deduplicated try", func(t *testing.T) {
|
|
setupPluginAPITest(t, `
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
allow bool
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
if !p.allow {
|
|
p.allow = true
|
|
return nil, "rejected"
|
|
}
|
|
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`, `{"id": "testrejectfirstpost", "server": {"executable": "backend.exe"}}`, "testrejectfirstpost", th.App, th.Context)
|
|
|
|
session := &model.Session{
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
session, err := th.App.CreateSession(th.Context, session)
|
|
require.Nil(t, err)
|
|
|
|
pendingPostId := makePendingPostId(th.BasicUser)
|
|
|
|
post, _, err := th.App.CreatePostAsUser(th.Context.WithSession(session), &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
PendingPostId: pendingPostId,
|
|
}, session.Id, true)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "Post rejected by plugin. rejected", err.Id)
|
|
require.Nil(t, post)
|
|
|
|
duplicatePost, _, err := th.App.CreatePostAsUser(th.Context.WithSession(session), &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
PendingPostId: pendingPostId,
|
|
}, session.Id, true)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "message", duplicatePost.Message)
|
|
})
|
|
|
|
t.Run("slow posting after cache entry blocks duplicate request", func(t *testing.T) {
|
|
setupPluginAPITest(t, `
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"time"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
instant bool
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
if !p.instant {
|
|
p.instant = true
|
|
time.Sleep(3 * time.Second)
|
|
}
|
|
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`, `{"id": "testdelayfirstpost", "server": {"executable": "backend.exe"}}`, "testdelayfirstpost", th.App, th.Context)
|
|
|
|
session := &model.Session{
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
session, err := th.App.CreateSession(th.Context, session)
|
|
require.Nil(t, err)
|
|
|
|
var post *model.Post
|
|
pendingPostId := makePendingPostId(th.BasicUser)
|
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
// Launch a goroutine to make the first CreatePost call that will get delayed
|
|
// by the plugin above.
|
|
wg.Go(func() {
|
|
var appErr *model.AppError
|
|
post, _, appErr = th.App.CreatePostAsUser(th.Context.WithSession(session), &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "plugin delayed",
|
|
PendingPostId: pendingPostId,
|
|
}, session.Id, true)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, post.Message, "plugin delayed")
|
|
})
|
|
|
|
// Give the goroutine above a chance to start and get delayed by the plugin.
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Try creating a duplicate post
|
|
duplicatePost, _, err := th.App.CreatePostAsUser(th.Context.WithSession(session), &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "plugin delayed",
|
|
PendingPostId: pendingPostId,
|
|
}, session.Id, true)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "api.post.deduplicate_create_post.pending", err.Id)
|
|
require.Nil(t, duplicatePost)
|
|
|
|
// Wait for the first CreatePost to finish to ensure assertions are made.
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("duplicate create post after cache expires is not idempotent", func(t *testing.T) {
|
|
originalCacheTTL := pendingPostIDsCacheTTL
|
|
pendingPostIDsCacheTTL = time.Second
|
|
t.Cleanup(func() {
|
|
pendingPostIDsCacheTTL = originalCacheTTL
|
|
})
|
|
|
|
session := &model.Session{
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
session, err := th.App.CreateSession(th.Context, session)
|
|
require.Nil(t, err)
|
|
|
|
pendingPostId := makePendingPostId(th.BasicUser)
|
|
|
|
post, _, err := th.App.CreatePostAsUser(th.Context.WithSession(session), &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
PendingPostId: pendingPostId,
|
|
}, session.Id, true)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "message", post.Message)
|
|
|
|
time.Sleep(pendingPostIDsCacheTTL)
|
|
|
|
duplicatePost, _, err := th.App.CreatePostAsUser(th.Context.WithSession(session), &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
PendingPostId: pendingPostId,
|
|
}, session.Id, true)
|
|
require.Nil(t, err)
|
|
require.NotEqual(t, post.Id, duplicatePost.Id, "should have created new post id")
|
|
require.Equal(t, "message", duplicatePost.Message)
|
|
})
|
|
|
|
t.Run("Permissison to post required to resolve from pending post cache", func(t *testing.T) {
|
|
sessionBasicUser := &model.Session{
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
sessionBasicUser, err := th.App.CreateSession(th.Context, sessionBasicUser)
|
|
require.Nil(t, err)
|
|
|
|
sessionBasicUser2 := &model.Session{
|
|
UserId: th.BasicUser2.Id,
|
|
}
|
|
sessionBasicUser2, err = th.App.CreateSession(th.Context, sessionBasicUser2)
|
|
require.Nil(t, err)
|
|
|
|
pendingPostId := makePendingPostId(th.BasicUser)
|
|
|
|
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, th.BasicUser, privateChannel)
|
|
|
|
post, _, err := th.App.CreatePostAsUser(th.Context.WithSession(sessionBasicUser), &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: privateChannel.Id,
|
|
Message: "message",
|
|
PendingPostId: pendingPostId,
|
|
}, sessionBasicUser.Id, true)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "message", post.Message)
|
|
|
|
postAsDifferentUser, _, err := th.App.CreatePostAsUser(th.Context.WithSession(sessionBasicUser2), &model.Post{
|
|
UserId: th.BasicUser2.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message2",
|
|
PendingPostId: pendingPostId,
|
|
}, sessionBasicUser2.Id, true)
|
|
require.Nil(t, err)
|
|
require.NotEqual(t, post.Id, postAsDifferentUser.Id, "should have created new post id")
|
|
require.Equal(t, "message2", postAsDifferentUser.Message)
|
|
|
|
// Both posts should exist unchanged
|
|
actualPost, err := th.App.GetSinglePost(th.Context, post.Id, false)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "message", actualPost.Message)
|
|
assert.Equal(t, privateChannel.Id, actualPost.ChannelId)
|
|
|
|
actualPostAsDifferentUser, err := th.App.GetSinglePost(th.Context, postAsDifferentUser.Id, false)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "message2", actualPostAsDifferentUser.Message)
|
|
assert.Equal(t, th.BasicChannel.Id, actualPostAsDifferentUser.ChannelId)
|
|
})
|
|
}
|
|
|
|
func TestAttachFilesToPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should attach files", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
info1, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
info2, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
post := th.BasicPost
|
|
post.FileIds = []string{info1.Id, info2.Id}
|
|
|
|
attachedFiles, appErr := th.App.attachFilesToPost(th.Context, post, post.FileIds)
|
|
assert.Nil(t, appErr)
|
|
assert.Len(t, attachedFiles, 2)
|
|
assert.Contains(t, attachedFiles, info1.Id)
|
|
assert.Contains(t, attachedFiles, info2.Id)
|
|
|
|
infos, _, appErr := th.App.GetFileInfosForPost(th.Context, post.Id, false, false)
|
|
assert.Nil(t, appErr)
|
|
assert.Len(t, infos, 2)
|
|
})
|
|
|
|
t.Run("should return only successfully attached files after failing to add files", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
info1, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
PostId: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
info2, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
post := th.BasicPost
|
|
post.FileIds = []string{info1.Id, info2.Id}
|
|
|
|
attachedFiles, appErr := th.App.attachFilesToPost(th.Context, post, post.FileIds)
|
|
assert.Nil(t, appErr)
|
|
assert.Len(t, attachedFiles, 1)
|
|
assert.Contains(t, attachedFiles, info2.Id)
|
|
|
|
infos, _, appErr := th.App.GetFileInfosForPost(th.Context, post.Id, false, false)
|
|
assert.Nil(t, appErr)
|
|
assert.Len(t, infos, 1)
|
|
assert.Equal(t, info2.Id, infos[0].Id)
|
|
|
|
updated, appErr := th.App.GetSinglePost(th.Context, post.Id, false)
|
|
require.Nil(t, appErr)
|
|
assert.Len(t, updated.FileIds, 1)
|
|
assert.Contains(t, updated.FileIds, info2.Id)
|
|
})
|
|
}
|
|
|
|
func TestUpdatePostEditAt(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
post := th.BasicPost.Clone()
|
|
|
|
post.IsPinned = true
|
|
saved, isMemberForPreviews, err := th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, saved.EditAt, post.EditAt, "shouldn't have updated post.EditAt when pinning post")
|
|
assert.True(t, isMemberForPreviews)
|
|
post = saved.Clone()
|
|
|
|
time.Sleep(time.Millisecond * 100)
|
|
|
|
post.Message = model.NewId()
|
|
saved, isMemberForPreviews, err = th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
|
|
require.Nil(t, err)
|
|
assert.NotEqual(t, saved.EditAt, post.EditAt, "should have updated post.EditAt when updating post message")
|
|
assert.True(t, isMemberForPreviews)
|
|
time.Sleep(time.Millisecond * 200)
|
|
}
|
|
|
|
func TestUpdatePostTimeLimit(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
post := th.BasicPost.Clone()
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicense())
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.PostEditTimeLimit = -1
|
|
})
|
|
_, _, err := th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
|
|
require.Nil(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.PostEditTimeLimit = 1000000000
|
|
})
|
|
post.Message = model.NewId()
|
|
|
|
_, _, err = th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
|
|
require.Nil(t, err, "should allow you to edit the post")
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.PostEditTimeLimit = 1
|
|
})
|
|
post.Message = model.NewId()
|
|
_, _, err = th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
|
|
require.Nil(t, err, "should allow you to edit an old post because the time check is applied above in the call hierarchy")
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.PostEditTimeLimit = -1
|
|
})
|
|
}
|
|
|
|
func TestUpdatePostInArchivedChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
archivedChannel := th.CreateChannel(t, th.BasicTeam)
|
|
post := th.CreatePost(t, archivedChannel)
|
|
appErr := th.App.DeleteChannel(th.Context, archivedChannel, "")
|
|
require.Nil(t, appErr)
|
|
|
|
_, _, err := th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "api.post.update_post.can_not_update_post_in_deleted.error", err.Id)
|
|
}
|
|
|
|
func TestPostReplyToPostWhereRootPosterLeftChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
// This test ensures that when replying to a root post made by a user who has since left the channel, the reply
|
|
// post completes successfully. This is a regression test for PLT-6523.
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channel := th.BasicChannel
|
|
userInChannel := th.BasicUser2
|
|
userNotInChannel := th.BasicUser
|
|
rootPost := th.BasicPost
|
|
|
|
_, err := th.App.AddUserToChannel(th.Context, userInChannel, channel, false)
|
|
require.Nil(t, err)
|
|
|
|
err = th.App.RemoveUserFromChannel(th.Context, userNotInChannel.Id, "", channel)
|
|
require.Nil(t, err)
|
|
replyPost := model.Post{
|
|
Message: "asd",
|
|
ChannelId: channel.Id,
|
|
RootId: rootPost.Id,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: userInChannel.Id,
|
|
CreateAt: 0,
|
|
}
|
|
|
|
_, _, err = th.App.CreatePostAsUser(th.Context, &replyPost, "", true)
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
func TestPostAttachPostToChildPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channel := th.BasicChannel
|
|
user := th.BasicUser
|
|
rootPost := th.BasicPost
|
|
|
|
replyPost1 := model.Post{
|
|
Message: "reply one",
|
|
ChannelId: channel.Id,
|
|
RootId: rootPost.Id,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: user.Id,
|
|
CreateAt: 0,
|
|
}
|
|
|
|
res1, _, err := th.App.CreatePostAsUser(th.Context, &replyPost1, "", true)
|
|
require.Nil(t, err)
|
|
|
|
replyPost2 := model.Post{
|
|
Message: "reply two",
|
|
ChannelId: channel.Id,
|
|
RootId: res1.Id,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: user.Id,
|
|
CreateAt: 0,
|
|
}
|
|
|
|
_, _, err = th.App.CreatePostAsUser(th.Context, &replyPost2, "", true)
|
|
assert.Equalf(t, err.StatusCode, http.StatusBadRequest, "Expected BadRequest error, got %v", err)
|
|
|
|
replyPost3 := model.Post{
|
|
Message: "reply three",
|
|
ChannelId: channel.Id,
|
|
RootId: rootPost.Id,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: user.Id,
|
|
CreateAt: 0,
|
|
}
|
|
|
|
_, _, err = th.App.CreatePostAsUser(th.Context, &replyPost3, "", true)
|
|
assert.Nil(t, err)
|
|
}
|
|
|
|
func TestUpdatePostPluginHooks(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("Should stop processing at first reject", func(t *testing.T) {
|
|
setupMultiPluginAPITest(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBeUpdated(c *plugin.Context, newPost, oldPost *model.Post) (*model.Post, string) {
|
|
return nil, "rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBeUpdated(c *plugin.Context, newPost, oldPost *model.Post) (*model.Post, string) {
|
|
if (newPost == nil) {
|
|
return nil, "nil post"
|
|
}
|
|
newPost.Message = newPost.Message + "fromplugin"
|
|
return newPost, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, []string{
|
|
`{"id": "testrejectfirstpost", "server": {"executable": "backend.exe"}}`,
|
|
`{"id": "testupdatepost", "server": {"executable": "backend.exe"}}`,
|
|
}, []string{
|
|
"testrejectfirstpost", "testupdatepost",
|
|
}, true, th.App, th.Context)
|
|
|
|
pendingPostId := makePendingPostId(th.BasicUser)
|
|
post, _, err := th.App.CreatePostAsUser(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
PendingPostId: pendingPostId,
|
|
}, "", true)
|
|
require.Nil(t, err)
|
|
|
|
post.Message = "new message"
|
|
updatedPost, _, err := th.App.UpdatePost(th.Context, post, nil)
|
|
require.Nil(t, updatedPost)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "Post rejected by plugin. rejected", err.Id)
|
|
})
|
|
|
|
t.Run("Should update", func(t *testing.T) {
|
|
setupMultiPluginAPITest(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBeUpdated(c *plugin.Context, newPost, oldPost *model.Post) (*model.Post, string) {
|
|
newPost.Message = newPost.Message + " 1"
|
|
return newPost, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBeUpdated(c *plugin.Context, newPost, oldPost *model.Post) (*model.Post, string) {
|
|
newPost.Message = "2 " + newPost.Message
|
|
return newPost, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, []string{
|
|
`{"id": "testaddone", "server": {"executable": "backend.exe"}}`,
|
|
`{"id": "testaddtwo", "server": {"executable": "backend.exe"}}`,
|
|
}, []string{
|
|
"testaddone", "testaddtwo",
|
|
}, true, th.App, th.Context)
|
|
|
|
pendingPostId := makePendingPostId(th.BasicUser)
|
|
post, _, err := th.App.CreatePostAsUser(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
PendingPostId: pendingPostId,
|
|
}, "", true)
|
|
require.Nil(t, err)
|
|
|
|
post.Message = "new message"
|
|
updatedPost, isMemberForPreviews, err := th.App.UpdatePost(th.Context, post, nil)
|
|
require.True(t, isMemberForPreviews)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, updatedPost)
|
|
require.Equal(t, "2 new message 1", updatedPost.Message)
|
|
})
|
|
}
|
|
|
|
func TestPostChannelMentions(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channel := th.BasicChannel
|
|
user := th.BasicUser
|
|
|
|
// Create context with session for the user to properly test sanitization
|
|
ctx := th.Context.WithSession(&model.Session{UserId: user.Id})
|
|
|
|
channelToMention, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
DisplayName: "Mention Test",
|
|
Name: "mention-test",
|
|
Type: model.ChannelTypeOpen,
|
|
TeamId: th.BasicTeam.Id,
|
|
}, false)
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteChannel(th.Context, channelToMention)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
channelToMention2, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
DisplayName: "Mention Test2",
|
|
Name: "mention-test2",
|
|
Type: model.ChannelTypeOpen,
|
|
TeamId: th.BasicTeam.Id,
|
|
}, false)
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteChannel(th.Context, channelToMention2)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
_, err = th.App.AddUserToChannel(th.Context, user, channel, false)
|
|
require.Nil(t, err)
|
|
|
|
post := &model.Post{
|
|
Message: fmt.Sprintf("hello, ~%v!", channelToMention.Name),
|
|
ChannelId: channel.Id,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: user.Id,
|
|
CreateAt: 0,
|
|
}
|
|
|
|
post, _, err = th.App.CreatePostAsUser(ctx, post, "", true)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, map[string]any{
|
|
"mention-test": map[string]any{
|
|
"display_name": "Mention Test",
|
|
"team_name": th.BasicTeam.Name,
|
|
},
|
|
}, post.GetProp(model.PostPropsChannelMentions))
|
|
|
|
post.Message = fmt.Sprintf("goodbye, ~%v!", channelToMention2.Name)
|
|
result, isMemberForPreviews, err := th.App.UpdatePost(ctx, post, nil)
|
|
require.True(t, isMemberForPreviews)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, map[string]any{
|
|
"mention-test2": map[string]any{
|
|
"display_name": "Mention Test2",
|
|
"team_name": th.BasicTeam.Name,
|
|
},
|
|
}, result.GetProp(model.PostPropsChannelMentions))
|
|
|
|
result.Message = "no more mentions!"
|
|
result, isMemberForPreviews, err = th.App.UpdatePost(ctx, result, nil)
|
|
require.True(t, isMemberForPreviews)
|
|
require.Nil(t, err)
|
|
assert.Nil(t, result.GetProp(model.PostPropsChannelMentions))
|
|
}
|
|
|
|
func TestImageProxy(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupWithStoreMock(t)
|
|
|
|
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
|
mockUserStore := storemocks.UserStore{}
|
|
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
|
|
mockPostStore := storemocks.PostStore{}
|
|
mockPostStore.On("GetMaxPostSize").Return(65535, nil)
|
|
mockSystemStore := storemocks.SystemStore{}
|
|
mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil)
|
|
mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil)
|
|
mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil)
|
|
|
|
mockStore.On("User").Return(&mockUserStore)
|
|
mockStore.On("Post").Return(&mockPostStore)
|
|
mockStore.On("System").Return(&mockSystemStore)
|
|
mockStore.On("GetDBSchemaVersion").Return(1, nil)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
|
|
|
testHMACKey := model.NewTestPassword()
|
|
|
|
for name, tc := range map[string]struct {
|
|
ProxyType string
|
|
ProxyURL string
|
|
ProxyOptions string
|
|
ImageURL string
|
|
ProxiedImageURL string
|
|
ProxiedRemovedImageURL string
|
|
}{
|
|
"atmos/camo": {
|
|
ProxyType: model.ImageProxyTypeAtmosCamo,
|
|
ProxyURL: "https://127.0.0.1",
|
|
ProxyOptions: testHMACKey,
|
|
ImageURL: "http://mydomain.com/myimage",
|
|
ProxiedRemovedImageURL: "http://mydomain.com/myimage",
|
|
ProxiedImageURL: "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage",
|
|
},
|
|
"atmos/camo_SameSite": {
|
|
ProxyType: model.ImageProxyTypeAtmosCamo,
|
|
ProxyURL: "https://127.0.0.1",
|
|
ProxyOptions: testHMACKey,
|
|
ImageURL: "http://mymattermost.com/myimage",
|
|
ProxiedRemovedImageURL: "http://mymattermost.com/myimage",
|
|
ProxiedImageURL: "http://mymattermost.com/myimage",
|
|
},
|
|
"atmos/camo_PathOnly": {
|
|
ProxyType: model.ImageProxyTypeAtmosCamo,
|
|
ProxyURL: "https://127.0.0.1",
|
|
ProxyOptions: testHMACKey,
|
|
ImageURL: "/myimage",
|
|
ProxiedRemovedImageURL: "http://mymattermost.com/myimage",
|
|
ProxiedImageURL: "http://mymattermost.com/myimage",
|
|
},
|
|
"atmos/camo_EmptyImageURL": {
|
|
ProxyType: model.ImageProxyTypeAtmosCamo,
|
|
ProxyURL: "https://127.0.0.1",
|
|
ProxyOptions: testHMACKey,
|
|
ImageURL: "",
|
|
ProxiedRemovedImageURL: "",
|
|
ProxiedImageURL: "",
|
|
},
|
|
"local": {
|
|
ProxyType: model.ImageProxyTypeLocal,
|
|
ImageURL: "http://mydomain.com/myimage",
|
|
ProxiedRemovedImageURL: "http://mydomain.com/myimage",
|
|
ProxiedImageURL: "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage",
|
|
},
|
|
"local_SameSite": {
|
|
ProxyType: model.ImageProxyTypeLocal,
|
|
ImageURL: "http://mymattermost.com/myimage",
|
|
ProxiedRemovedImageURL: "http://mymattermost.com/myimage",
|
|
ProxiedImageURL: "http://mymattermost.com/myimage",
|
|
},
|
|
"local_PathOnly": {
|
|
ProxyType: model.ImageProxyTypeLocal,
|
|
ImageURL: "/myimage",
|
|
ProxiedRemovedImageURL: "http://mymattermost.com/myimage",
|
|
ProxiedImageURL: "http://mymattermost.com/myimage",
|
|
},
|
|
"local_EmptyImageURL": {
|
|
ProxyType: model.ImageProxyTypeLocal,
|
|
ImageURL: "",
|
|
ProxiedRemovedImageURL: "",
|
|
ProxiedImageURL: "",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ImageProxySettings.Enable = model.NewPointer(true)
|
|
cfg.ImageProxySettings.ImageProxyType = model.NewPointer(tc.ProxyType)
|
|
cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewPointer(tc.ProxyOptions)
|
|
cfg.ImageProxySettings.RemoteImageProxyURL = model.NewPointer(tc.ProxyURL)
|
|
})
|
|
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
Message: "",
|
|
}
|
|
|
|
list := model.NewPostList()
|
|
list.Posts[post.Id] = post
|
|
|
|
assert.Equal(t, "", th.App.PostWithProxyAddedToImageURLs(post).Message)
|
|
|
|
assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message)
|
|
post.Message = ""
|
|
assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message)
|
|
|
|
if tc.ImageURL != "" {
|
|
post.Message = ""
|
|
assert.Equal(t, "", th.App.PostWithProxyAddedToImageURLs(post).Message)
|
|
assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message)
|
|
post.Message = ""
|
|
assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeletePostWithFileAttachments(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Create a post with a file attachment.
|
|
teamID := th.BasicTeam.Id
|
|
channelID := th.BasicChannel.Id
|
|
userID := th.BasicUser.Id
|
|
filename := "test"
|
|
data := []byte("abcd")
|
|
|
|
info1, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data, true)
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info1.Id)
|
|
require.NoError(t, err)
|
|
appErr := th.App.RemoveFile(info1.Path)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
post := &model.Post{
|
|
Message: "asd",
|
|
ChannelId: channelID,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: userID,
|
|
CreateAt: 0,
|
|
FileIds: []string{info1.Id},
|
|
}
|
|
|
|
post, _, err = th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
assert.Nil(t, err)
|
|
|
|
// Delete the post.
|
|
_, err = th.App.DeletePost(th.Context, post.Id, userID)
|
|
assert.Nil(t, err)
|
|
|
|
// Wait for the cleanup routine to finish.
|
|
time.Sleep(time.Millisecond * 100)
|
|
|
|
// Check that the file can no longer be reached.
|
|
_, err = th.App.GetFileInfo(th.Context, info1.Id)
|
|
assert.NotNil(t, err)
|
|
}
|
|
|
|
func TestDeletePostWithRestrictedDM(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("cannot delete post in restricted DM", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.TeamSettings.RestrictDirectMessage = model.DirectMessageTeam
|
|
})
|
|
|
|
// Create a DM channel between two users who don't share a team
|
|
dmChannel := th.CreateDmChannel(t, th.BasicUser2)
|
|
|
|
// Ensure the two users do not share a team
|
|
teams, err := th.App.GetTeamsForUser(th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
for _, team := range teams {
|
|
teamErr := th.App.RemoveUserFromTeam(th.Context, team.Id, th.BasicUser.Id, th.SystemAdminUser.Id)
|
|
require.Nil(t, teamErr)
|
|
}
|
|
teams, err = th.App.GetTeamsForUser(th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
for _, team := range teams {
|
|
teamErr := th.App.RemoveUserFromTeam(th.Context, team.Id, th.BasicUser2.Id, th.SystemAdminUser.Id)
|
|
require.Nil(t, teamErr)
|
|
}
|
|
|
|
// Create separate teams for each user
|
|
team1 := th.CreateTeam(t)
|
|
team2 := th.CreateTeam(t)
|
|
th.LinkUserToTeam(t, th.BasicUser, team1)
|
|
th.LinkUserToTeam(t, th.BasicUser2, team2)
|
|
|
|
// Create a post in the DM channel
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: dmChannel.Id,
|
|
Message: "test post",
|
|
}
|
|
post, _, err = th.App.CreatePost(th.Context, post, dmChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
// Try to delete the post
|
|
_, appErr := th.App.DeletePost(th.Context, post.Id, th.BasicUser.Id)
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.post.delete_post.can_not_delete_from_restricted_dm.error", appErr.Id)
|
|
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
|
|
|
// Reset config
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.TeamSettings.RestrictDirectMessage = model.DirectMessageAny
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestDeletePostInArchivedChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
archivedChannel := th.CreateChannel(t, th.BasicTeam)
|
|
post := th.CreatePost(t, archivedChannel)
|
|
appErr := th.App.DeleteChannel(th.Context, archivedChannel, "")
|
|
require.Nil(t, appErr)
|
|
|
|
_, err := th.App.DeletePost(th.Context, post.Id, "")
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "api.post.delete_post.can_not_delete_post_in_deleted.error", err.Id)
|
|
}
|
|
|
|
func TestCreatePost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("call PreparePostForClient before returning", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
*cfg.ImageProxySettings.Enable = true
|
|
*cfg.ImageProxySettings.ImageProxyType = "atmos/camo"
|
|
*cfg.ImageProxySettings.RemoteImageProxyURL = "https://127.0.0.1"
|
|
*cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewTestPassword()
|
|
})
|
|
|
|
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
|
|
|
imageURL := "http://mydomain.com/myimage"
|
|
proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
rpost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "", rpost.Message)
|
|
})
|
|
|
|
t.Run("Sets prop MENTION_HIGHLIGHT_DISABLED when it should", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
|
|
t.Run("Does not set prop when user has USE_CHANNEL_MENTIONS", func(t *testing.T) {
|
|
postWithNoMention := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "This post does not have mentions",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
rpost, _, err := th.App.CreatePost(th.Context, postWithNoMention, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, rpost.GetProps(), model.StringInterface{})
|
|
|
|
postWithMention := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "This post has @here mention @all",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
rpost, _, err = th.App.CreatePost(th.Context, postWithMention, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, rpost.GetProps(), model.StringInterface{})
|
|
})
|
|
|
|
t.Run("Sets prop when post has mentions and user does not have USE_CHANNEL_MENTIONS", func(t *testing.T) {
|
|
th.RemovePermissionFromRole(t, model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId)
|
|
th.RemovePermissionFromRole(t, model.PermissionUseChannelMentions.Id, model.ChannelAdminRoleId)
|
|
|
|
postWithNoMention := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "This post does not have mentions",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
rpost, _, err := th.App.CreatePost(th.Context, postWithNoMention, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, rpost.GetProps(), model.StringInterface{})
|
|
|
|
postWithMention := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "This post has @here mention @all",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
rpost, _, err = th.App.CreatePost(th.Context, postWithMention, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, rpost.GetProp(model.PostPropsMentionHighlightDisabled), true)
|
|
|
|
th.AddPermissionToRole(t, model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId)
|
|
th.AddPermissionToRole(t, model.PermissionUseChannelMentions.Id, model.ChannelAdminRoleId)
|
|
})
|
|
})
|
|
|
|
t.Run("Sets PostPropsPreviewedPost when a permalink is the first link", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
|
|
referencedPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
channelForPreview := th.CreateChannel(t, th.BasicTeam)
|
|
previewPost := &model.Post{
|
|
ChannelId: channelForPreview.Id,
|
|
Message: permalink,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
previewPost, _, err = th.App.CreatePost(th.Context, previewPost, channelForPreview, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
assert.Equal(t, previewPost.GetProps(), model.StringInterface{"previewed_post": referencedPost.Id})
|
|
})
|
|
|
|
t.Run("creates a single record for a permalink preview post", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channelForPreview := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
referencedPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://foobar.com"
|
|
*cfg.ServiceSettings.EnablePermalinkPreviews = true
|
|
})
|
|
|
|
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
previewPost := &model.Post{
|
|
ChannelId: channelForPreview.Id,
|
|
Message: permalink,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
previewPost, _, err = th.App.CreatePost(th.Context, previewPost, channelForPreview, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
sqlStore := th.GetSqlStore()
|
|
sql := fmt.Sprintf("select count(*) from Posts where Id = '%[1]s' or OriginalId = '%[1]s';", previewPost.Id)
|
|
var val int64
|
|
err2 := sqlStore.GetMaster().Get(&val, sql)
|
|
require.NoError(t, err2)
|
|
|
|
require.EqualValues(t, int64(1), val)
|
|
})
|
|
|
|
t.Run("sanitizes post metadata appropriately", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
|
|
user1 := th.CreateUser(t)
|
|
user2 := th.CreateUser(t)
|
|
directChannel, err := th.App.createDirectChannel(th.Context, user1.Id, user2.Id)
|
|
require.Nil(t, err)
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
testCases := []struct {
|
|
Description string
|
|
Channel *model.Channel
|
|
Author string
|
|
Length int
|
|
}{
|
|
{
|
|
Description: "removes metadata from post for members who cannot read channel",
|
|
Channel: directChannel,
|
|
Author: user1.Id,
|
|
Length: 0,
|
|
},
|
|
{
|
|
Description: "does not remove metadata from post for members who can read channel",
|
|
Channel: th.BasicChannel,
|
|
Author: th.BasicUser.Id,
|
|
Length: 1,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
referencedPost := &model.Post{
|
|
ChannelId: testCase.Channel.Id,
|
|
Message: "hello world",
|
|
UserId: testCase.Author,
|
|
}
|
|
referencedPost, _, err = th.App.CreatePost(th.Context, referencedPost, testCase.Channel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
previewPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: permalink,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
previewPost, _, err = th.App.CreatePost(th.Context, previewPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
require.Len(t, previewPost.Metadata.Embeds, testCase.Length)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Should not allow to create posts on shared DMs", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := setupSharedChannels(t).InitBasic(t)
|
|
|
|
user1 := th.CreateUser(t)
|
|
user2 := th.CreateUser(t)
|
|
dm, appErr := th.App.createDirectChannel(th.Context, user1.Id, user2.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, dm)
|
|
|
|
// we can't create direct channels with remote users, so we
|
|
// have to force the channel to be shared through the store to
|
|
// simulate preexisting shared DMs
|
|
sc := &model.SharedChannel{
|
|
ChannelId: dm.Id,
|
|
Type: dm.Type,
|
|
Home: true,
|
|
ShareName: "shareddm",
|
|
CreatorId: user1.Id,
|
|
RemoteId: model.NewId(),
|
|
}
|
|
_, scErr := th.Server.Store().SharedChannel().Save(sc)
|
|
require.NoError(t, scErr)
|
|
|
|
// and we update the channel to mark it as shared
|
|
dm.Shared = model.NewPointer(true)
|
|
_, err := th.Server.Store().Channel().Update(th.Context, dm)
|
|
require.NoError(t, err)
|
|
|
|
newPost := &model.Post{
|
|
ChannelId: dm.Id,
|
|
Message: "hello world",
|
|
UserId: user1.Id,
|
|
}
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, newPost, dm, model.CreatePostFlags{})
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, createdPost)
|
|
})
|
|
|
|
t.Run("Should not allow to create posts on shared GMs", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := setupSharedChannels(t).InitBasic(t)
|
|
|
|
user1 := th.CreateUser(t)
|
|
user2 := th.CreateUser(t)
|
|
user3 := th.CreateUser(t)
|
|
gm, appErr := th.App.createGroupChannel(th.Context, []string{user1.Id, user2.Id, user3.Id}, user1.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, gm)
|
|
|
|
// we can't create group channels with remote users, so we
|
|
// have to force the channel to be shared through the store to
|
|
// simulate preexisting shared GMs
|
|
sc := &model.SharedChannel{
|
|
ChannelId: gm.Id,
|
|
Type: gm.Type,
|
|
Home: true,
|
|
ShareName: "sharedgm",
|
|
CreatorId: user1.Id,
|
|
RemoteId: model.NewId(),
|
|
}
|
|
_, err := th.Server.Store().SharedChannel().Save(sc)
|
|
require.NoError(t, err)
|
|
|
|
// and we update the channel to mark it as shared
|
|
gm.Shared = model.NewPointer(true)
|
|
_, err = th.Server.Store().Channel().Update(th.Context, gm)
|
|
require.NoError(t, err)
|
|
|
|
newPost := &model.Post{
|
|
ChannelId: gm.Id,
|
|
Message: "hello world",
|
|
UserId: user1.Id,
|
|
}
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, newPost, gm, model.CreatePostFlags{})
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, createdPost)
|
|
})
|
|
|
|
t.Run("MM-40016 should not panic with `concurrent map read and map write`", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channelForPreview := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
for range 20 {
|
|
user := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, user, th.BasicTeam)
|
|
th.AddUserToChannel(t, user, channelForPreview)
|
|
}
|
|
|
|
referencedPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://example.com"
|
|
*cfg.ServiceSettings.EnablePermalinkPreviews = true
|
|
})
|
|
|
|
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
previewPost := &model.Post{
|
|
ChannelId: channelForPreview.Id,
|
|
Message: permalink,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
previewPost, _, err = th.App.CreatePost(th.Context, previewPost, channelForPreview, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
n := 1000
|
|
var wg sync.WaitGroup
|
|
wg.Add(n)
|
|
for range n {
|
|
go func() {
|
|
defer wg.Done()
|
|
post := previewPost.Clone()
|
|
_, _, appErr := th.App.UpdatePost(th.Context, post, nil)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should sanitize the force notifications prop if the flag is not set", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
|
|
postToCreate := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
postToCreate.AddProp(model.PostPropsForceNotification, model.NewId())
|
|
createdPost, _, err := th.App.CreatePost(th.Context, postToCreate, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
require.Empty(t, createdPost.GetProp(model.PostPropsForceNotification))
|
|
})
|
|
|
|
t.Run("should add the force notifications prop if the flag is set", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
|
|
postToCreate := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
createdPost, _, err := th.App.CreatePost(th.Context, postToCreate, th.BasicChannel, model.CreatePostFlags{ForceNotification: true})
|
|
require.Nil(t, err)
|
|
require.NotEmpty(t, createdPost.GetProp(model.PostPropsForceNotification))
|
|
})
|
|
|
|
t.Run("creates post with type card", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "card post",
|
|
Type: model.PostTypeCard,
|
|
}
|
|
|
|
rpost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, rpost)
|
|
assert.Equal(t, model.PostTypeCard, rpost.Type)
|
|
assert.Equal(t, "card post", rpost.Message)
|
|
})
|
|
|
|
t.Run("Should remove post file IDs for burn on read posts", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable BurnOnRead feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
|
|
enableBoRFeature(th)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "hello world",
|
|
Type: model.PostTypeBurnOnRead,
|
|
FileIds: []string{model.NewId()},
|
|
}
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
require.Empty(t, createdPost.FileIds)
|
|
})
|
|
|
|
t.Run("should reject burn-on-read posts in shared channels", func(t *testing.T) {
|
|
os.Setenv("MM_FEATUREFLAGS_BURNONREAD", "true")
|
|
t.Cleanup(func() {
|
|
os.Unsetenv("MM_FEATUREFLAGS_BURNONREAD")
|
|
})
|
|
th := setupSharedChannels(t).InitBasic(t)
|
|
enableBoRFeature(th)
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
sc := &model.SharedChannel{
|
|
ChannelId: channel.Id,
|
|
TeamId: th.BasicTeam.Id,
|
|
Type: channel.Type,
|
|
Home: true,
|
|
ShareName: "shared-bor-test",
|
|
CreatorId: th.BasicUser.Id,
|
|
RemoteId: model.NewId(),
|
|
}
|
|
_, scErr := th.Server.Store().SharedChannel().Save(sc)
|
|
require.NoError(t, scErr)
|
|
|
|
channel.Shared = model.NewPointer(true)
|
|
_, err := th.Server.Store().Channel().Update(th.Context, channel)
|
|
require.NoError(t, err)
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn-on-read in shared channel",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, createdPost)
|
|
require.Equal(t, "api.post.fill_in_post_props.burn_on_read.shared_channel.app_error", appErr.Id)
|
|
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
|
})
|
|
|
|
t.Run("should allow burn-on-read posts in non-shared channels", func(t *testing.T) {
|
|
os.Setenv("MM_FEATUREFLAGS_BURNONREAD", "true")
|
|
t.Cleanup(func() {
|
|
os.Unsetenv("MM_FEATUREFLAGS_BURNONREAD")
|
|
})
|
|
th := Setup(t).InitBasic(t)
|
|
enableBoRFeature(th)
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
require.False(t, channel.IsShared())
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn-on-read in non-shared channel",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
require.Equal(t, model.PostTypeBurnOnRead, createdPost.Type)
|
|
})
|
|
}
|
|
|
|
func TestPatchPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("call PreparePostForClient before returning", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
*cfg.ImageProxySettings.Enable = true
|
|
*cfg.ImageProxySettings.ImageProxyType = "atmos/camo"
|
|
*cfg.ImageProxySettings.RemoteImageProxyURL = "https://127.0.0.1"
|
|
*cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewTestPassword()
|
|
})
|
|
|
|
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
|
|
|
imageURL := "http://mydomain.com/myimage"
|
|
proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
rpost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.NotEqual(t, "", rpost.Message)
|
|
|
|
patch := &model.PostPatch{
|
|
Message: model.NewPointer(""),
|
|
}
|
|
|
|
rpost, _, err = th.App.PatchPost(th.Context, rpost.Id, patch, nil)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "", rpost.Message)
|
|
})
|
|
|
|
t.Run("Sets Prop MENTION_HIGHLIGHT_DISABLED when it should", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "This post does not have mentions",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
rpost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("Does not set prop when user has USE_CHANNEL_MENTIONS", func(t *testing.T) {
|
|
patchWithNoMention := &model.PostPatch{Message: model.NewPointer("This patch has no channel mention")}
|
|
|
|
rpost, _, err = th.App.PatchPost(th.Context, rpost.Id, patchWithNoMention, nil)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, rpost.GetProps(), model.StringInterface{})
|
|
|
|
patchWithMention := &model.PostPatch{Message: model.NewPointer("This patch has a mention now @here")}
|
|
|
|
rpost, _, err = th.App.PatchPost(th.Context, rpost.Id, patchWithMention, nil)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, rpost.GetProps(), model.StringInterface{})
|
|
})
|
|
|
|
t.Run("Sets prop when user does not have USE_CHANNEL_MENTIONS", func(t *testing.T) {
|
|
th.RemovePermissionFromRole(t, model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId)
|
|
th.RemovePermissionFromRole(t, model.PermissionUseChannelMentions.Id, model.ChannelAdminRoleId)
|
|
|
|
patchWithNoMention := &model.PostPatch{Message: model.NewPointer("This patch still does not have a mention")}
|
|
rpost, _, err = th.App.PatchPost(th.Context, rpost.Id, patchWithNoMention, nil)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, rpost.GetProps(), model.StringInterface{})
|
|
|
|
patchWithMention := &model.PostPatch{Message: model.NewPointer("This patch has a mention now @here")}
|
|
|
|
rpost, _, err = th.App.PatchPost(th.Context, rpost.Id, patchWithMention, nil)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, rpost.GetProp(model.PostPropsMentionHighlightDisabled), true)
|
|
|
|
th.AddPermissionToRole(t, model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId)
|
|
th.AddPermissionToRole(t, model.PermissionUseChannelMentions.Id, model.ChannelAdminRoleId)
|
|
})
|
|
})
|
|
|
|
t.Run("cannot patch post in restricted DM", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.TeamSettings.RestrictDirectMessage = model.DirectMessageTeam
|
|
})
|
|
|
|
// Create a DM channel between two users who don't share a team
|
|
dmChannel := th.CreateDmChannel(t, th.BasicUser2)
|
|
|
|
// Ensure the two users do not share a team
|
|
teams, err := th.App.GetTeamsForUser(th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
for _, team := range teams {
|
|
teamErr := th.App.RemoveUserFromTeam(th.Context, team.Id, th.BasicUser.Id, th.SystemAdminUser.Id)
|
|
require.Nil(t, teamErr)
|
|
}
|
|
teams, err = th.App.GetTeamsForUser(th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
for _, team := range teams {
|
|
teamErr := th.App.RemoveUserFromTeam(th.Context, team.Id, th.BasicUser2.Id, th.SystemAdminUser.Id)
|
|
require.Nil(t, teamErr)
|
|
}
|
|
|
|
// Create separate teams for each user
|
|
team1 := th.CreateTeam(t)
|
|
team2 := th.CreateTeam(t)
|
|
th.LinkUserToTeam(t, th.BasicUser, team1)
|
|
th.LinkUserToTeam(t, th.BasicUser2, team2)
|
|
|
|
// Create a post in the DM channel
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: dmChannel.Id,
|
|
Message: "test post",
|
|
}
|
|
post, _, err = th.App.CreatePost(th.Context, post, dmChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
// Try to patch the post
|
|
patch := &model.PostPatch{
|
|
Message: model.NewPointer("updated message"),
|
|
}
|
|
_, _, appErr := th.App.PatchPost(th.Context, post.Id, patch, model.DefaultUpdatePostOptions())
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.post.patch_post.can_not_update_post_in_restricted_dm.error", appErr.Id)
|
|
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
|
|
|
// Reset config
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.TeamSettings.RestrictDirectMessage = model.DirectMessageAny
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestCreatePostAsUser(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("marks channel as viewed for regular user", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
channelMemberBefore, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(1 * time.Millisecond)
|
|
_, _, appErr := th.App.CreatePostAsUser(th.Context, post, "", true)
|
|
require.Nil(t, appErr)
|
|
|
|
channelMemberAfter, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
require.Greater(t, channelMemberAfter.LastViewedAt, channelMemberBefore.LastViewedAt)
|
|
})
|
|
|
|
t.Run("does not mark channel as viewed for webhook from user", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
post.AddProp(model.PostPropsFromWebhook, "true")
|
|
|
|
channelMemberBefore, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(1 * time.Millisecond)
|
|
_, _, appErr := th.App.CreatePostAsUser(th.Context, post, "", true)
|
|
require.Nil(t, appErr)
|
|
|
|
channelMemberAfter, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, channelMemberAfter.LastViewedAt, channelMemberBefore.LastViewedAt)
|
|
})
|
|
|
|
t.Run("does not mark channel as viewed for bot user in channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
bot := th.CreateBot(t)
|
|
|
|
botUser, appErr := th.App.GetUser(bot.UserId)
|
|
require.Nil(t, appErr)
|
|
|
|
th.LinkUserToTeam(t, botUser, th.BasicTeam)
|
|
th.AddUserToChannel(t, botUser, th.BasicChannel)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test",
|
|
UserId: bot.UserId,
|
|
}
|
|
|
|
channelMemberBefore, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(1 * time.Millisecond)
|
|
_, _, appErr = th.App.CreatePostAsUser(th.Context, post, "", true)
|
|
require.Nil(t, appErr)
|
|
|
|
channelMemberAfter, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, channelMemberAfter.LastViewedAt, channelMemberBefore.LastViewedAt)
|
|
})
|
|
|
|
t.Run("does not log warning for bot user not in channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
bot := th.CreateBot(t)
|
|
|
|
botUser, appErr := th.App.GetUser(bot.UserId)
|
|
require.Nil(t, appErr)
|
|
|
|
th.LinkUserToTeam(t, botUser, th.BasicTeam)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test",
|
|
UserId: bot.UserId,
|
|
}
|
|
|
|
_, _, appErr = th.App.CreatePostAsUser(th.Context, post, "", true)
|
|
require.Nil(t, appErr)
|
|
|
|
require.NoError(t, th.TestLogger.Flush())
|
|
|
|
testlib.AssertNoLog(t, th.LogBuffer, mlog.LvlWarn.Name, "Failed to get membership")
|
|
})
|
|
|
|
t.Run("marks channel as viewed for reply post when CRT is off", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOff
|
|
})
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test",
|
|
UserId: th.BasicUser2.Id,
|
|
}
|
|
rootPost, _, appErr := th.App.CreatePostAsUser(th.Context, post, "", true)
|
|
require.Nil(t, appErr)
|
|
|
|
channelMemberBefore, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(1 * time.Millisecond)
|
|
replyPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test reply",
|
|
UserId: th.BasicUser.Id,
|
|
RootId: rootPost.Id,
|
|
}
|
|
_, _, appErr = th.App.CreatePostAsUser(th.Context, replyPost, "", true)
|
|
require.Nil(t, appErr)
|
|
|
|
channelMemberAfter, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
require.NotEqual(t, channelMemberAfter.LastViewedAt, channelMemberBefore.LastViewedAt)
|
|
})
|
|
|
|
t.Run("does not mark channel as viewed for reply post when CRT is on", func(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
|
|
})
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test",
|
|
UserId: th.BasicUser2.Id,
|
|
}
|
|
rootPost, _, appErr := th.App.CreatePostAsUser(th.Context, post, "", true)
|
|
require.Nil(t, appErr)
|
|
|
|
channelMemberBefore, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(1 * time.Millisecond)
|
|
replyPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test reply",
|
|
UserId: th.BasicUser.Id,
|
|
RootId: rootPost.Id,
|
|
}
|
|
_, _, appErr = th.App.CreatePostAsUser(th.Context, replyPost, "", true)
|
|
require.Nil(t, appErr)
|
|
|
|
channelMemberAfter, err := th.App.Srv().Store().Channel().GetMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, channelMemberAfter.LastViewedAt, channelMemberBefore.LastViewedAt)
|
|
})
|
|
|
|
t.Run("cannot create post as user in restricted DM", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.TeamSettings.RestrictDirectMessage = model.DirectMessageTeam
|
|
})
|
|
|
|
// Create a DM channel between two users who don't share a team
|
|
dmChannel := th.CreateDmChannel(t, th.BasicUser2)
|
|
|
|
// Ensure the two users do not share a team
|
|
teams, err := th.App.GetTeamsForUser(th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
for _, team := range teams {
|
|
teamErr := th.App.RemoveUserFromTeam(th.Context, team.Id, th.BasicUser.Id, th.SystemAdminUser.Id)
|
|
require.Nil(t, teamErr)
|
|
}
|
|
teams, err = th.App.GetTeamsForUser(th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
for _, team := range teams {
|
|
teamErr := th.App.RemoveUserFromTeam(th.Context, team.Id, th.BasicUser2.Id, th.SystemAdminUser.Id)
|
|
require.Nil(t, teamErr)
|
|
}
|
|
|
|
// Create separate teams for each user
|
|
team1 := th.CreateTeam(t)
|
|
team2 := th.CreateTeam(t)
|
|
th.LinkUserToTeam(t, th.BasicUser, team1)
|
|
th.LinkUserToTeam(t, th.BasicUser2, team2)
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: dmChannel.Id,
|
|
Message: "test post",
|
|
}
|
|
|
|
_, _, appErr := th.App.CreatePostAsUser(th.Context, post, "", true)
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.post.create_post.can_not_post_in_restricted_dm.error", appErr.Id)
|
|
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
|
|
|
// Reset config
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.TeamSettings.RestrictDirectMessage = model.DirectMessageAny
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestPatchPostInArchivedChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
archivedChannel := th.CreateChannel(t, th.BasicTeam)
|
|
post := th.CreatePost(t, archivedChannel)
|
|
appErr := th.App.DeleteChannel(th.Context, archivedChannel, "")
|
|
require.Nil(t, appErr)
|
|
|
|
_, _, err := th.App.PatchPost(th.Context, post.Id, &model.PostPatch{IsPinned: model.NewPointer(true)}, nil)
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "api.post.patch_post.can_not_update_post_in_deleted.error", err.Id)
|
|
}
|
|
|
|
func TestUpdateEphemeralPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("Post contains preview if the user has permissions", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
|
|
referencedPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
testPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: permalink,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
testPost, _ = th.App.UpdateEphemeralPost(th.Context, th.BasicUser.Id, testPost)
|
|
require.NotNil(t, testPost.Metadata)
|
|
require.Len(t, testPost.Metadata.Embeds, 1)
|
|
require.Equal(t, model.PostEmbedPermalink, testPost.Metadata.Embeds[0].Type)
|
|
})
|
|
|
|
t.Run("Post does not contain preview if the user has no permissions", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, th.BasicUser, privateChannel)
|
|
th.AddUserToChannel(t, th.BasicUser2, th.BasicChannel)
|
|
|
|
referencedPost := &model.Post{
|
|
ChannelId: privateChannel.Id,
|
|
Message: "hello world",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
testPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: permalink,
|
|
UserId: th.BasicUser2.Id,
|
|
}
|
|
|
|
testPost, _ = th.App.UpdateEphemeralPost(th.Context, th.BasicUser2.Id, testPost)
|
|
require.Nil(t, testPost.Metadata.Embeds)
|
|
})
|
|
}
|
|
|
|
func TestUpdatePost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("call PreparePostForClient before returning", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
*cfg.ImageProxySettings.Enable = true
|
|
*cfg.ImageProxySettings.ImageProxyType = "atmos/camo"
|
|
*cfg.ImageProxySettings.RemoteImageProxyURL = "https://127.0.0.1"
|
|
*cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewTestPassword()
|
|
})
|
|
|
|
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
|
|
|
imageURL := "http://mydomain.com/myimage"
|
|
proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
rpost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.NotEqual(t, "", rpost.Message)
|
|
|
|
post.Id = rpost.Id
|
|
post.Message = ""
|
|
|
|
rpost, isMemberForPreviews, err := th.App.UpdatePost(th.Context, post, nil)
|
|
require.True(t, isMemberForPreviews)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "", rpost.Message)
|
|
})
|
|
|
|
t.Run("Sets PostPropsPreviewedPost when a post is updated to have a permalink as the first link", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
|
|
referencedPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
channelForTestPost := th.CreateChannel(t, th.BasicTeam)
|
|
testPost := &model.Post{
|
|
ChannelId: channelForTestPost.Id,
|
|
Message: "hello world",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
testPost, _, err = th.App.CreatePost(th.Context, testPost, channelForTestPost, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, model.StringInterface{}, testPost.GetProps())
|
|
|
|
testPost.Message = permalink
|
|
testPost, isMemberForPreviews, err := th.App.UpdatePost(th.Context, testPost, nil)
|
|
require.True(t, isMemberForPreviews)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, model.StringInterface{model.PostPropsPreviewedPost: referencedPost.Id}, testPost.GetProps())
|
|
})
|
|
|
|
t.Run("sanitizes post metadata appropriately", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
|
|
user1 := th.CreateUser(t)
|
|
user2 := th.CreateUser(t)
|
|
directChannel, err := th.App.createDirectChannel(th.Context, user1.Id, user2.Id)
|
|
require.Nil(t, err)
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
testCases := []struct {
|
|
Description string
|
|
Channel *model.Channel
|
|
Author string
|
|
Length int
|
|
}{
|
|
{
|
|
Description: "removes metadata from post for members who cannot read channel",
|
|
Channel: directChannel,
|
|
Author: user1.Id,
|
|
Length: 0,
|
|
},
|
|
{
|
|
Description: "does not remove metadata from post for members who can read channel",
|
|
Channel: th.BasicChannel,
|
|
Author: th.BasicUser.Id,
|
|
Length: 1,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
referencedPost := &model.Post{
|
|
ChannelId: testCase.Channel.Id,
|
|
Message: "hello world",
|
|
UserId: testCase.Author,
|
|
}
|
|
_, _, err = th.App.CreatePost(th.Context, referencedPost, testCase.Channel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
previewPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
previewPost, _, err = th.App.CreatePost(th.Context, previewPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
previewPost.Message = permalink
|
|
previewPost, isMemberForPreviews, err := th.App.UpdatePost(th.Context, previewPost, nil)
|
|
require.True(t, isMemberForPreviews)
|
|
require.Nil(t, err)
|
|
|
|
require.Len(t, previewPost.Metadata.Embeds, testCase.Length)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("should strip client-supplied embeds", func(t *testing.T) {
|
|
// MM-67055: Verify that client-supplied metadata.embeds are stripped.
|
|
// This prevents WebSocket message spoofing via permalink embeds.
|
|
//
|
|
// Note: Priority and Acknowledgements are stored in separate database tables,
|
|
// not in post metadata. Shared Channels handles them separately via
|
|
// syncRemotePriorityMetadata and syncRemoteAcknowledgementsMetadata after
|
|
// calling UpdatePost. See sync_recv.go::upsertSyncPost
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
// Create a basic post
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original message",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
createdPost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
// Try to update with spoofed embeds (the attack vector)
|
|
updatePost := &model.Post{
|
|
Id: createdPost.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "updated message",
|
|
UserId: th.BasicUser.Id,
|
|
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: "Spoofed message from another user!",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
updatedPost, _, err := th.App.UpdatePost(th.Context, updatePost, nil)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, updatedPost.Metadata)
|
|
|
|
// Verify embeds were stripped
|
|
assert.Empty(t, updatedPost.Metadata.Embeds, "spoofed embeds should be stripped")
|
|
})
|
|
|
|
t.Run("cannot update post in restricted DM", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.TeamSettings.RestrictDirectMessage = model.DirectMessageTeam
|
|
})
|
|
|
|
// Create a DM channel between two users who don't share a team
|
|
dmChannel := th.CreateDmChannel(t, th.BasicUser2)
|
|
|
|
// Ensure the two users do not share a team
|
|
teams, err := th.App.GetTeamsForUser(th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
for _, team := range teams {
|
|
teamErr := th.App.RemoveUserFromTeam(th.Context, team.Id, th.BasicUser.Id, th.SystemAdminUser.Id)
|
|
require.Nil(t, teamErr)
|
|
}
|
|
teams, err = th.App.GetTeamsForUser(th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
for _, team := range teams {
|
|
teamErr := th.App.RemoveUserFromTeam(th.Context, team.Id, th.BasicUser2.Id, th.SystemAdminUser.Id)
|
|
require.Nil(t, teamErr)
|
|
}
|
|
|
|
// Create separate teams for each user
|
|
team1 := th.CreateTeam(t)
|
|
team2 := th.CreateTeam(t)
|
|
th.LinkUserToTeam(t, th.BasicUser, team1)
|
|
th.LinkUserToTeam(t, th.BasicUser2, team2)
|
|
|
|
// Create a post in the DM channel
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: dmChannel.Id,
|
|
Message: "test post",
|
|
}
|
|
post, _, err = th.App.CreatePost(th.Context, post, dmChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
// Try to update the post
|
|
post.Message = "updated message"
|
|
_, _, appErr := th.App.UpdatePost(th.Context, post, model.DefaultUpdatePostOptions())
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "api.post.update_post.can_not_update_post_in_restricted_dm.error", appErr.Id)
|
|
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
|
|
|
// Reset config
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.TeamSettings.RestrictDirectMessage = model.DirectMessageAny
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestSearchPostsForUser(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
perPage := 5
|
|
searchTerm := "searchTerm"
|
|
|
|
setup := func(t *testing.T, enableElasticsearch bool) (*TestHelper, []*model.Post) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
posts := make([]*model.Post, 7)
|
|
for i := 0; i < cap(posts); i++ {
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: searchTerm,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
|
|
require.Nil(t, err)
|
|
|
|
posts[i] = post
|
|
}
|
|
|
|
if enableElasticsearch {
|
|
th.App.Srv().SetLicense(model.NewTestLicense("elastic_search"))
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ElasticsearchSettings.EnableIndexing = true
|
|
*cfg.ElasticsearchSettings.EnableSearching = true
|
|
})
|
|
} else {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ElasticsearchSettings.EnableSearching = false
|
|
})
|
|
}
|
|
|
|
return th, posts
|
|
}
|
|
|
|
t.Run("should return everything as first page of posts from database", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th, posts := setup(t, false)
|
|
|
|
page := 0
|
|
|
|
results, allPostHaveMembership, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{
|
|
posts[6].Id,
|
|
posts[5].Id,
|
|
posts[4].Id,
|
|
posts[3].Id,
|
|
posts[2].Id,
|
|
posts[1].Id,
|
|
posts[0].Id,
|
|
}, results.Order)
|
|
assert.True(t, allPostHaveMembership)
|
|
})
|
|
|
|
t.Run("should not return later pages of posts from database", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th, _ := setup(t, false)
|
|
|
|
page := 1
|
|
|
|
results, allPostHaveMembership, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{}, results.Order)
|
|
assert.True(t, allPostHaveMembership)
|
|
})
|
|
|
|
t.Run("should return first page of posts from ElasticSearch", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th, posts := setup(t, true)
|
|
|
|
page := 0
|
|
resultsPage := []string{
|
|
posts[6].Id,
|
|
posts[5].Id,
|
|
posts[4].Id,
|
|
posts[3].Id,
|
|
posts[2].Id,
|
|
}
|
|
|
|
es := &mocks.SearchEngineInterface{}
|
|
es.On("SearchPosts", mock.Anything, mock.Anything, page, perPage).Return(resultsPage, nil, nil)
|
|
es.On("Start").Return(nil).Maybe()
|
|
es.On("IsActive").Return(true)
|
|
es.On("IsSearchEnabled").Return(true)
|
|
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
|
|
defer func() {
|
|
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
|
|
}()
|
|
|
|
results, allPostHaveMembership, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, resultsPage, results.Order)
|
|
assert.True(t, allPostHaveMembership)
|
|
es.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should return later pages of posts from ElasticSearch", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th, posts := setup(t, true)
|
|
|
|
page := 1
|
|
resultsPage := []string{
|
|
posts[1].Id,
|
|
posts[0].Id,
|
|
}
|
|
|
|
es := &mocks.SearchEngineInterface{}
|
|
es.On("SearchPosts", mock.Anything, mock.Anything, page, perPage).Return(resultsPage, nil, nil)
|
|
es.On("Start").Return(nil).Maybe()
|
|
es.On("IsActive").Return(true)
|
|
es.On("IsSearchEnabled").Return(true)
|
|
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
|
|
defer func() {
|
|
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
|
|
}()
|
|
|
|
results, allPostHaveMembership, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, resultsPage, results.Order)
|
|
assert.True(t, allPostHaveMembership)
|
|
es.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should fall back to database if ElasticSearch fails on first page", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th, posts := setup(t, true)
|
|
|
|
page := 0
|
|
|
|
es := &mocks.SearchEngineInterface{}
|
|
es.On("SearchPosts", mock.Anything, mock.Anything, page, perPage).Return(nil, nil, &model.AppError{})
|
|
es.On("GetName").Return("mock")
|
|
es.On("Start").Return(nil).Maybe()
|
|
es.On("IsActive").Return(true)
|
|
es.On("IsSearchEnabled").Return(true)
|
|
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
|
|
defer func() {
|
|
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
|
|
}()
|
|
|
|
results, allPostHaveMembership, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{
|
|
posts[6].Id,
|
|
posts[5].Id,
|
|
posts[4].Id,
|
|
posts[3].Id,
|
|
posts[2].Id,
|
|
posts[1].Id,
|
|
posts[0].Id,
|
|
}, results.Order)
|
|
assert.True(t, allPostHaveMembership)
|
|
es.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should return nothing if ElasticSearch fails on later pages", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th, _ := setup(t, true)
|
|
|
|
page := 1
|
|
|
|
es := &mocks.SearchEngineInterface{}
|
|
es.On("SearchPosts", mock.Anything, mock.Anything, page, perPage).Return(nil, nil, &model.AppError{})
|
|
es.On("GetName").Return("mock")
|
|
es.On("Start").Return(nil).Maybe()
|
|
es.On("IsActive").Return(true)
|
|
es.On("IsSearchEnabled").Return(true)
|
|
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
|
|
defer func() {
|
|
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
|
|
}()
|
|
|
|
results, allPostHaveMembership, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{}, results.Order)
|
|
assert.True(t, allPostHaveMembership)
|
|
es.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should return the same results if there is a tilde in the channel name", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th, _ := setup(t, false)
|
|
|
|
page := 0
|
|
|
|
searchQueryWithPrefix := fmt.Sprintf("in:~%s %s", th.BasicChannel.Name, searchTerm)
|
|
|
|
resultsWithPrefix, _, err := th.App.SearchPostsForUser(th.Context, searchQueryWithPrefix, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
assert.Nil(t, err)
|
|
assert.Greater(t, len(resultsWithPrefix.PostList.Posts), 0, "searching using a tilde in front of a channel should return results")
|
|
searchQueryWithoutPrefix := fmt.Sprintf("in:%s %s", th.BasicChannel.Name, searchTerm)
|
|
|
|
resultsWithoutPrefix, _, err := th.App.SearchPostsForUser(th.Context, searchQueryWithoutPrefix, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, len(resultsWithPrefix.Posts), len(resultsWithoutPrefix.Posts), "searching using a tilde in front of a channel should return the same number of results")
|
|
for k, v := range resultsWithPrefix.Posts {
|
|
assert.Equal(t, v, resultsWithoutPrefix.Posts[k], "post at %s was different", k)
|
|
}
|
|
})
|
|
|
|
t.Run("should return the same results if there is an 'at' in the user", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th, _ := setup(t, false)
|
|
|
|
page := 0
|
|
|
|
searchQueryWithPrefix := fmt.Sprintf("from:@%s %s", th.BasicUser.Username, searchTerm)
|
|
|
|
resultsWithPrefix, _, err := th.App.SearchPostsForUser(th.Context, searchQueryWithPrefix, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
assert.Nil(t, err)
|
|
assert.Greater(t, len(resultsWithPrefix.PostList.Posts), 0, "searching using a 'at' symbol in front of a channel should return results")
|
|
searchQueryWithoutPrefix := fmt.Sprintf("from:@%s %s", th.BasicUser.Username, searchTerm)
|
|
|
|
resultsWithoutPrefix, _, err := th.App.SearchPostsForUser(th.Context, searchQueryWithoutPrefix, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, len(resultsWithPrefix.Posts), len(resultsWithoutPrefix.Posts), "searching using an 'at' symbol in front of a channel should return the same number of results")
|
|
for k, v := range resultsWithPrefix.Posts {
|
|
assert.Equal(t, v, resultsWithoutPrefix.Posts[k], "post at %s was different", k)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCountMentionsFromPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should not count posts without mentions", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test2",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test3",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 0, count)
|
|
})
|
|
|
|
t.Run("should count keyword mentions", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
user2.NotifyProps[model.MentionKeysNotifyProp] = "apple"
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test2",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "apple",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// post1 and post3 should mention the user
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 2, count)
|
|
})
|
|
|
|
t.Run("should count channel-wide mentions when enabled", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
user2.NotifyProps[model.ChannelMentionsNotifyProp] = "true"
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "@channel",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "@all",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// post2 and post3 should mention the user
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 2, count)
|
|
})
|
|
|
|
t.Run("should not count channel-wide mentions when disabled for user", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
user2.NotifyProps[model.ChannelMentionsNotifyProp] = "false"
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "@channel",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "@all",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 0, count)
|
|
})
|
|
|
|
t.Run("should not count channel-wide mentions when disabled for channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
user2.NotifyProps[model.ChannelMentionsNotifyProp] = "true"
|
|
|
|
_, err := th.App.UpdateChannelMemberNotifyProps(th.Context, map[string]string{
|
|
model.IgnoreChannelMentionsNotifyProp: model.IgnoreChannelMentionsOn,
|
|
}, channel.Id, user2.Id)
|
|
require.Nil(t, err)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "@channel",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "@all",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 0, count)
|
|
})
|
|
|
|
t.Run("should count comment mentions when using COMMENTS_NOTIFY_ROOT", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
user2.NotifyProps[model.CommentsNotifyProp] = model.CommentsNotifyRoot
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post1.Id,
|
|
Message: "test2",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
post3, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test3",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post3.Id,
|
|
Message: "test4",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post3.Id,
|
|
Message: "test5",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// post2 should mention the user
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 1, count)
|
|
})
|
|
|
|
t.Run("should count comment mentions when using COMMENTS_NOTIFY_ANY", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
user2.NotifyProps[model.CommentsNotifyProp] = model.CommentsNotifyAny
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post1.Id,
|
|
Message: "test2",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
post3, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test3",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post3.Id,
|
|
Message: "test4",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post3.Id,
|
|
Message: "test5",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// post2 and post5 should mention the user
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 2, count)
|
|
})
|
|
|
|
t.Run("should count mentions caused by being added to the channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test",
|
|
Type: model.PostTypeAddToChannel,
|
|
Props: map[string]any{
|
|
model.PostPropsAddedUserId: model.NewId(),
|
|
},
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test2",
|
|
Type: model.PostTypeAddToChannel,
|
|
Props: map[string]any{
|
|
model.PostPropsAddedUserId: user2.Id,
|
|
},
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test3",
|
|
Type: model.PostTypeAddToChannel,
|
|
Props: map[string]any{
|
|
model.PostPropsAddedUserId: user2.Id,
|
|
},
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// should be mentioned by post2 and post3
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 2, count)
|
|
})
|
|
|
|
t.Run("should return the number of posts made by the other user for a direct channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel, err := th.App.createDirectChannel(th.Context, user1.Id, user2.Id)
|
|
require.Nil(t, err)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test2",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 2, count)
|
|
|
|
count, _, _, err = th.App.countMentionsFromPost(th.Context, user1, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 0, count)
|
|
})
|
|
|
|
t.Run("should return the number of posts made by the other user for a group message", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
user3 := th.SystemAdminUser
|
|
|
|
channel, err := th.App.createGroupChannel(th.Context, []string{user1.Id, user2.Id, user3.Id}, user1.Id)
|
|
require.Nil(t, err)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test2",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user3.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test3",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 3, count)
|
|
|
|
count, _, _, err = th.App.countMentionsFromPost(th.Context, user1, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 1, count)
|
|
})
|
|
|
|
t.Run("should not count mentions from the before the given post", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
_, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
post2, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test2",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// post1 and post3 should mention the user, but we only count post3
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post2)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 1, count)
|
|
})
|
|
|
|
t.Run("should not count mentions from the user's own posts", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// post2 should mention the user
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 1, count)
|
|
})
|
|
|
|
t.Run("should include comments made before the given post when counting comment mentions", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
user2.NotifyProps[model.CommentsNotifyProp] = model.CommentsNotifyAny
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test1",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post1.Id,
|
|
Message: "test2",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
post3, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test3",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post1.Id,
|
|
Message: "test4",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// post4 should mention the user
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post3)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 1, count)
|
|
})
|
|
|
|
t.Run("should not include comments made before the given post when rootPost is inaccessible", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Create an Entry license with post history limits
|
|
license := model.NewTestLicenseSKU(model.LicenseShortSkuMattermostEntry)
|
|
license.Limits = &model.LicenseLimits{
|
|
PostHistory: 10000, // Set some post history limit to enable filtering
|
|
}
|
|
th.App.Srv().SetLicense(license)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
user2.NotifyProps[model.CommentsNotifyProp] = model.CommentsNotifyAny
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test1",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post1.Id,
|
|
Message: "test2",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
time.Sleep(time.Millisecond * 2)
|
|
|
|
post3, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test3",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post1.Id,
|
|
Message: "test4",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// Make posts created before post3 inaccessible
|
|
e := th.App.Srv().Store().System().SaveOrUpdate(&model.System{
|
|
Name: model.SystemLastAccessiblePostTime,
|
|
Value: strconv.FormatInt(post3.CreateAt, 10),
|
|
})
|
|
require.NoError(t, e)
|
|
|
|
// post4 should mention the user, but since post2 is inaccessible due to the cloud plan's limit,
|
|
// post4 does not notify the user.
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post3)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Zero(t, count)
|
|
})
|
|
|
|
t.Run("should count mentions from the user's webhook posts", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test1",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
Props: map[string]any{
|
|
model.PostPropsFromWebhook: "true",
|
|
},
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// post3 should mention the user
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 1, count)
|
|
})
|
|
|
|
t.Run("should count multiple pages of mentions", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
numPosts := 215
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
for i := 0; i < numPosts-1; i++ {
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
// Every post should mention the user
|
|
|
|
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, numPosts, count)
|
|
})
|
|
|
|
t.Run("should count urgent mentions", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.PostPriority = true
|
|
})
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
user2.NotifyProps[model.MentionKeysNotifyProp] = "apple"
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
Metadata: &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
},
|
|
},
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "apple",
|
|
Metadata: &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
},
|
|
},
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// all posts mention the user but only post1, post3 are urgent
|
|
|
|
_, _, count, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 2, count)
|
|
})
|
|
}
|
|
|
|
func TestFillInPostProps(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should not add disable group highlight to post props for user with group mention permissions", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
|
|
|
|
user1 := th.BasicUser
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test123123 @group1 @group2 blah blah blah",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
err = th.App.FillInPostProps(th.Context, post1, channel)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, post1.Props, model.StringInterface{})
|
|
})
|
|
|
|
t.Run("should not add disable group highlight to post props for app without license", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
id := model.NewId()
|
|
guest := &model.User{
|
|
Email: "success+" + id + "@simulator.amazonses.com",
|
|
Username: "un_" + id,
|
|
Nickname: "nn_" + id,
|
|
Password: model.NewTestPassword(),
|
|
EmailVerified: true,
|
|
}
|
|
guest, err := th.App.CreateGuest(th.Context, guest)
|
|
require.Nil(t, err)
|
|
th.LinkUserToTeam(t, guest, th.BasicTeam)
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, guest, channel)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: guest.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test123123 @group1 @group2 blah blah blah",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
err = th.App.FillInPostProps(th.Context, post1, channel)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, post1.Props, model.StringInterface{})
|
|
})
|
|
|
|
t.Run("should add disable group highlight to post props for guest user", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
|
|
|
|
id := model.NewId()
|
|
guest := &model.User{
|
|
Email: "success+" + id + "@simulator.amazonses.com",
|
|
Username: "un_" + id,
|
|
Nickname: "nn_" + id,
|
|
Password: model.NewTestPassword(),
|
|
EmailVerified: true,
|
|
}
|
|
guest, err := th.App.CreateGuest(th.Context, guest)
|
|
require.Nil(t, err)
|
|
th.LinkUserToTeam(t, guest, th.BasicTeam)
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, guest, channel)
|
|
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: guest.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test123123 @group1 @group2 blah blah blah",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
err = th.App.FillInPostProps(th.Context, post1, channel)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, post1.Props, model.StringInterface{"disable_group_highlight": true})
|
|
})
|
|
|
|
t.Run("should set AI-generated username when user ID is post creator", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
post1 := &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test post",
|
|
}
|
|
post1.AddProp(model.PostPropsAIGeneratedByUserID, user1.Id)
|
|
|
|
err := th.App.FillInPostProps(th.Context, post1, channel)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, user1.Id, post1.GetProp(model.PostPropsAIGeneratedByUserID))
|
|
assert.Equal(t, user1.Username, post1.GetProp(model.PostPropsAIGeneratedByUsername))
|
|
})
|
|
|
|
t.Run("should set AI-generated username when user ID is a bot", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
// Create a bot
|
|
bot, appErr := th.App.CreateBot(th.Context, &model.Bot{
|
|
Username: "testbot",
|
|
Description: "test bot",
|
|
OwnerId: user1.Id,
|
|
})
|
|
require.Nil(t, appErr)
|
|
|
|
post1 := &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test post generated by bot",
|
|
}
|
|
post1.AddProp(model.PostPropsAIGeneratedByUserID, bot.UserId)
|
|
|
|
err := th.App.FillInPostProps(th.Context, post1, channel)
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, bot.UserId, post1.GetProp(model.PostPropsAIGeneratedByUserID))
|
|
assert.Equal(t, bot.Username, post1.GetProp(model.PostPropsAIGeneratedByUsername))
|
|
})
|
|
|
|
t.Run("should return error when user ID is a different non-bot user", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
post1 := &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test post",
|
|
}
|
|
// Try to set AI-generated user ID to a different user (not post creator, not bot)
|
|
post1.AddProp(model.PostPropsAIGeneratedByUserID, user2.Id)
|
|
|
|
err := th.App.FillInPostProps(th.Context, post1, channel)
|
|
|
|
// Should return an error since user2 is neither the post creator nor a bot
|
|
assert.NotNil(t, err)
|
|
assert.Equal(t, "FillInPostProps", err.Where)
|
|
assert.Equal(t, http.StatusBadRequest, err.StatusCode)
|
|
})
|
|
|
|
t.Run("should remove AI-generated prop when user ID does not exist", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
post1 := &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "test post",
|
|
}
|
|
// Set AI-generated user ID to a non-existent user
|
|
post1.AddProp(model.PostPropsAIGeneratedByUserID, model.NewId())
|
|
|
|
err := th.App.FillInPostProps(th.Context, post1, channel)
|
|
|
|
assert.Nil(t, err)
|
|
// The property should be removed since the user doesn't exist
|
|
assert.Nil(t, post1.GetProp(model.PostPropsAIGeneratedByUserID))
|
|
assert.Nil(t, post1.GetProp(model.PostPropsAIGeneratedByUsername))
|
|
})
|
|
|
|
t.Run("should not populate channel mentions for channels in teams where the user is not a member", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
team2 := th.CreateTeam(t)
|
|
th.LinkUserToTeam(t, user2, team2)
|
|
|
|
// Create a channel in team2 which user1 is not a member of
|
|
channel2, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
DisplayName: "Channel in Team 2",
|
|
Name: "channel-in-team-2",
|
|
Type: model.ChannelTypeOpen,
|
|
TeamId: team2.Id,
|
|
CreatorId: user2.Id,
|
|
}, false)
|
|
require.Nil(t, err)
|
|
|
|
dmChannelBetweenUser1AndUser2 := th.CreateDmChannel(t, user2)
|
|
|
|
th.Context.Session().UserId = user1.Id
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: dmChannelBetweenUser1AndUser2.Id,
|
|
Message: "Testing out i should not be able to mention channel2 from team2? ~" + channel2.Name,
|
|
}, dmChannelBetweenUser1AndUser2, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
err = th.App.FillInPostProps(th.Context, post, dmChannelBetweenUser1AndUser2)
|
|
require.Nil(t, err)
|
|
|
|
mentions := post.GetProp(model.PostPropsChannelMentions)
|
|
require.Nil(t, mentions)
|
|
})
|
|
|
|
t.Run("should populate channel mentions for channels in teams where the user is a member", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
dmChannel := th.CreateDmChannel(t, user2)
|
|
|
|
th.Context.Session().UserId = user1.Id
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: dmChannel.Id,
|
|
Message: "Check out ~" + channel.Name,
|
|
}, dmChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
mentions := post.GetProp(model.PostPropsChannelMentions)
|
|
require.NotNil(t, mentions)
|
|
|
|
mentionsMap, ok := mentions.(map[string]any)
|
|
require.True(t, ok)
|
|
require.Contains(t, mentionsMap, channel.Name)
|
|
})
|
|
}
|
|
|
|
func TestThreadMembership(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should update memberships for conversation participants", func(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
|
|
})
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
|
|
postRoot, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "root post",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: postRoot.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// first user should now be part of the thread since they replied to a post
|
|
memberships, err2 := th.App.GetThreadMembershipsForUser(user1.Id, th.BasicTeam.Id)
|
|
require.NoError(t, err2)
|
|
require.Len(t, memberships, 1)
|
|
// second user should also be part of a thread since they were mentioned
|
|
memberships, err2 = th.App.GetThreadMembershipsForUser(user2.Id, th.BasicTeam.Id)
|
|
require.NoError(t, err2)
|
|
require.Len(t, memberships, 1)
|
|
|
|
post2, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "second post",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user2.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: post2.Id,
|
|
Message: fmt.Sprintf("@%s", user1.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// first user should now be part of two threads
|
|
memberships, err2 = th.App.GetThreadMembershipsForUser(user1.Id, th.BasicTeam.Id)
|
|
require.NoError(t, err2)
|
|
require.Len(t, memberships, 2)
|
|
})
|
|
}
|
|
|
|
func TestFollowThreadSkipsParticipants(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
|
|
})
|
|
|
|
channel := th.BasicChannel
|
|
user := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
sysadmin := th.SystemAdminUser
|
|
|
|
appErr := th.App.JoinChannel(th.Context, channel, user.Id)
|
|
require.Nil(t, appErr)
|
|
appErr = th.App.JoinChannel(th.Context, channel, user2.Id)
|
|
require.Nil(t, appErr)
|
|
_, appErr = th.App.JoinUserToTeam(th.Context, th.BasicTeam, sysadmin, sysadmin.Id)
|
|
require.Nil(t, appErr)
|
|
appErr = th.App.JoinChannel(th.Context, channel, sysadmin.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
p1, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: user.Id, ChannelId: channel.Id, Message: "Hi @" + sysadmin.Username}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: user.Id, ChannelId: channel.Id, Message: "Hola"}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
threadMembership, appErr := th.App.GetThreadMembershipForUser(user.Id, p1.Id)
|
|
require.Nil(t, appErr)
|
|
thread, appErr := th.App.GetThreadForUser(th.Context, threadMembership, false)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, thread.Participants, 1) // length should be 1, the original poster, since sysadmin was just mentioned but didn't post
|
|
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: sysadmin.Id, ChannelId: channel.Id, Message: "sysadmin reply"}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
threadMembership, appErr = th.App.GetThreadMembershipForUser(user.Id, p1.Id)
|
|
require.Nil(t, appErr)
|
|
thread, appErr = th.App.GetThreadForUser(th.Context, threadMembership, false)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, thread.Participants, 2) // length should be 2, the original poster and sysadmin, since sysadmin participated now
|
|
|
|
// another user follows the thread
|
|
appErr = th.App.UpdateThreadFollowForUser(user2.Id, th.BasicTeam.Id, p1.Id, true)
|
|
require.Nil(t, appErr)
|
|
|
|
threadMembership, appErr = th.App.GetThreadMembershipForUser(user2.Id, p1.Id)
|
|
require.Nil(t, appErr)
|
|
thread, appErr = th.App.GetThreadForUser(th.Context, threadMembership, false)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, thread.Participants, 2) // length should be 2, since follow shouldn't update participant list, only user1 and sysadmin are participants
|
|
for _, p := range thread.Participants {
|
|
require.True(t, p.Id == sysadmin.Id || p.Id == user.Id)
|
|
}
|
|
|
|
oldID := threadMembership.PostId
|
|
threadMembership.PostId = "notfound"
|
|
_, appErr = th.App.GetThreadForUser(th.Context, threadMembership, false)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
|
|
|
threadMembership.Following = false
|
|
threadMembership.PostId = oldID
|
|
_, appErr = th.App.GetThreadForUser(th.Context, threadMembership, false)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
|
}
|
|
|
|
func TestAutofollowBasedOnRootPost(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
|
|
})
|
|
|
|
channel := th.BasicChannel
|
|
user := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
appErr := th.App.JoinChannel(th.Context, channel, user.Id)
|
|
require.Nil(t, appErr)
|
|
appErr = th.App.JoinChannel(th.Context, channel, user2.Id)
|
|
require.Nil(t, appErr)
|
|
p1, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: user.Id, ChannelId: channel.Id, Message: "Hi @" + user2.Username}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
m, err := th.App.GetThreadMembershipsForUser(user2.Id, th.BasicTeam.Id)
|
|
require.NoError(t, err)
|
|
require.Len(t, m, 0)
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: user.Id, ChannelId: channel.Id, Message: "Hola"}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
m, err = th.App.GetThreadMembershipsForUser(user2.Id, th.BasicTeam.Id)
|
|
require.NoError(t, err)
|
|
require.Len(t, m, 1)
|
|
}
|
|
|
|
func TestViewChannelShouldNotUpdateThreads(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
|
|
})
|
|
|
|
channel := th.BasicChannel
|
|
user := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
appErr := th.App.JoinChannel(th.Context, channel, user.Id)
|
|
require.Nil(t, appErr)
|
|
appErr = th.App.JoinChannel(th.Context, channel, user2.Id)
|
|
require.Nil(t, appErr)
|
|
p1, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: user.Id, ChannelId: channel.Id, Message: "Hi @" + user2.Username}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: user.Id, ChannelId: channel.Id, Message: "Hola"}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
m, err := th.App.GetThreadMembershipsForUser(user2.Id, th.BasicTeam.Id)
|
|
require.NoError(t, err)
|
|
|
|
_, appErr = th.App.ViewChannel(th.Context, &model.ChannelView{
|
|
ChannelId: channel.Id,
|
|
PrevChannelId: "",
|
|
}, user2.Id, "", true)
|
|
require.Nil(t, appErr)
|
|
|
|
m1, err := th.App.GetThreadMembershipsForUser(user2.Id, th.BasicTeam.Id)
|
|
require.NoError(t, err)
|
|
require.Equal(t, m[0].LastViewed, m1[0].LastViewed) // opening the channel shouldn't update threads
|
|
}
|
|
|
|
func TestCollapsedThreadFetch(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
|
|
})
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
t.Run("should only return root posts, enriched", func(t *testing.T) {
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
defer func() {
|
|
appErr := th.App.DeleteChannel(th.Context, channel, user1.Id)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
postRoot, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "root post",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: postRoot.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
thread, err := th.App.Srv().Store().Thread().Get(postRoot.Id)
|
|
require.NoError(t, err)
|
|
require.Len(t, thread.Participants, 1)
|
|
_, appErr = th.App.MarkChannelAsUnreadFromPost(th.Context, postRoot.Id, user1.Id, true)
|
|
require.Nil(t, appErr)
|
|
l, appErr := th.App.GetPostsForChannelAroundLastUnread(th.Context, channel.Id, user1.Id, 10, 10, true, true, false)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, l.Order, 1)
|
|
require.EqualValues(t, 1, l.Posts[postRoot.Id].ReplyCount)
|
|
require.EqualValues(t, []string{user1.Id}, []string{l.Posts[postRoot.Id].Participants[0].Id})
|
|
require.Empty(t, l.Posts[postRoot.Id].Participants[0].Email)
|
|
require.NotZero(t, l.Posts[postRoot.Id].LastReplyAt)
|
|
require.True(t, *l.Posts[postRoot.Id].IsFollowing)
|
|
|
|
// try extended fetch
|
|
l, appErr = th.App.GetPostsForChannelAroundLastUnread(th.Context, channel.Id, user1.Id, 10, 10, true, true, true)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, l.Order, 1)
|
|
require.NotEmpty(t, l.Posts[postRoot.Id].Participants[0].Email)
|
|
})
|
|
|
|
t.Run("Should not panic on unexpected db error", func(t *testing.T) {
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, channel)
|
|
defer func() {
|
|
appErr := th.App.DeleteChannel(th.Context, channel, user1.Id)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
postRoot, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "root post",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// we introduce a race to trigger an unexpected error from the db side.
|
|
var wg sync.WaitGroup
|
|
wg.Go(func() {
|
|
err := th.Server.Store().Post().PermanentDeleteByUser(th.Context, user1.Id)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
require.NotPanics(t, func() {
|
|
// We're only testing that this doesn't panic, not checking the error
|
|
// #nosec G104 - purposely not checking error as we're in a NotPanics block
|
|
_, _, _ = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: postRoot.Id,
|
|
Message: fmt.Sprintf("@%s", user2.Username),
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
})
|
|
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("should sanitize participant data", func(t *testing.T) {
|
|
id := model.NewId()
|
|
user3, appErr := th.App.CreateUser(th.Context, &model.User{
|
|
Email: "success+" + id + "@simulator.amazonses.com",
|
|
Username: "un_" + id,
|
|
Nickname: "nn_" + id,
|
|
AuthData: model.NewPointer("bobbytables"),
|
|
AuthService: "saml",
|
|
EmailVerified: true,
|
|
})
|
|
require.Nil(t, appErr)
|
|
defer func() {
|
|
appErr = th.App.PermanentDeleteUser(th.Context, user3)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
defer func() {
|
|
appErr = th.App.DeleteChannel(th.Context, channel, user1.Id)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
th.LinkUserToTeam(t, user3, th.BasicTeam)
|
|
th.AddUserToChannel(t, user3, channel)
|
|
|
|
postRoot, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "root post",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user3.Id,
|
|
ChannelId: channel.Id,
|
|
RootId: postRoot.Id,
|
|
Message: "reply",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
thread, err := th.App.Srv().Store().Thread().Get(postRoot.Id)
|
|
require.NoError(t, err)
|
|
require.Len(t, thread.Participants, 1)
|
|
|
|
// extended fetch posts page
|
|
l, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
|
|
UserId: user1.Id,
|
|
ChannelId: channel.Id,
|
|
PerPage: int(10),
|
|
SkipFetchThreads: false,
|
|
CollapsedThreads: true,
|
|
CollapsedThreadsExtended: true,
|
|
})
|
|
require.Nil(t, appErr)
|
|
require.Len(t, l.Order, 1)
|
|
require.NotEmpty(t, l.Posts[postRoot.Id].Participants[0].Email)
|
|
require.Empty(t, l.Posts[postRoot.Id].Participants[0].AuthData)
|
|
|
|
_, appErr = th.App.MarkChannelAsUnreadFromPost(th.Context, postRoot.Id, user1.Id, true)
|
|
require.Nil(t, appErr)
|
|
|
|
// extended fetch posts around
|
|
l, appErr = th.App.GetPostsForChannelAroundLastUnread(th.Context, channel.Id, user1.Id, 10, 10, true, true, true)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, l.Order, 1)
|
|
require.NotEmpty(t, l.Posts[postRoot.Id].Participants[0].Email)
|
|
require.Empty(t, l.Posts[postRoot.Id].Participants[0].AuthData)
|
|
|
|
// extended fetch post thread
|
|
opts := model.GetPostsOptions{
|
|
SkipFetchThreads: false,
|
|
CollapsedThreads: true,
|
|
CollapsedThreadsExtended: true,
|
|
}
|
|
|
|
l, appErr = th.App.GetPostThread(th.Context, postRoot.Id, opts, user1.Id)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, l.Order, 2)
|
|
require.NotEmpty(t, l.Posts[postRoot.Id].Participants[0].Email)
|
|
require.Empty(t, l.Posts[postRoot.Id].Participants[0].AuthData)
|
|
})
|
|
}
|
|
|
|
func TestSharedChannelSyncForPostActions(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("creating a post in a shared channel performs a content sync when sync service is running on that node", func(t *testing.T) {
|
|
th := setupSharedChannels(t).InitBasic(t)
|
|
|
|
sharedChannelService := NewMockSharedChannelService(th.Server.GetSharedChannelSyncService())
|
|
th.Server.SetSharedChannelSyncService(sharedChannelService)
|
|
testCluster := &testlib.FakeClusterInterface{}
|
|
th.Server.Platform().SetCluster(testCluster)
|
|
|
|
user := th.BasicUser
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam, WithShared(true))
|
|
|
|
_, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "Hello folks",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err, "Creating a post should not error")
|
|
|
|
require.Len(t, sharedChannelService.channelNotifications, 1)
|
|
assert.Equal(t, channel.Id, sharedChannelService.channelNotifications[0])
|
|
})
|
|
|
|
t.Run("updating a post in a shared channel performs a content sync when sync service is running on that node", func(t *testing.T) {
|
|
th := setupSharedChannels(t).InitBasic(t)
|
|
|
|
sharedChannelService := NewMockSharedChannelService(th.Server.GetSharedChannelSyncService())
|
|
th.Server.SetSharedChannelSyncService(sharedChannelService)
|
|
testCluster := &testlib.FakeClusterInterface{}
|
|
th.Server.Platform().SetCluster(testCluster)
|
|
|
|
user := th.BasicUser
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam, WithShared(true))
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "Hello folks",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err, "Creating a post should not error")
|
|
|
|
_, _, err = th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
|
|
require.Nil(t, err, "Updating a post should not error")
|
|
|
|
require.Len(t, sharedChannelService.channelNotifications, 2)
|
|
assert.Equal(t, channel.Id, sharedChannelService.channelNotifications[0])
|
|
assert.Equal(t, channel.Id, sharedChannelService.channelNotifications[1])
|
|
})
|
|
|
|
t.Run("deleting a post in a shared channel performs a content sync when sync service is running on that node", func(t *testing.T) {
|
|
th := setupSharedChannels(t).InitBasic(t)
|
|
|
|
sharedChannelService := NewMockSharedChannelService(th.Server.GetSharedChannelSyncService())
|
|
th.Server.SetSharedChannelSyncService(sharedChannelService)
|
|
testCluster := &testlib.FakeClusterInterface{}
|
|
th.Server.Platform().SetCluster(testCluster)
|
|
|
|
user := th.BasicUser
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam, WithShared(true))
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: user.Id,
|
|
ChannelId: channel.Id,
|
|
Message: "Hello folks",
|
|
}, channel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err, "Creating a post should not error")
|
|
|
|
_, err = th.App.DeletePost(th.Context, post.Id, user.Id)
|
|
require.Nil(t, err, "Deleting a post should not error")
|
|
|
|
// one creation and two deletes
|
|
require.Len(t, sharedChannelService.channelNotifications, 3)
|
|
assert.Equal(t, channel.Id, sharedChannelService.channelNotifications[0])
|
|
assert.Equal(t, channel.Id, sharedChannelService.channelNotifications[1])
|
|
assert.Equal(t, channel.Id, sharedChannelService.channelNotifications[2])
|
|
})
|
|
}
|
|
|
|
func TestAutofollowOnPostingAfterUnfollow(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
|
|
})
|
|
|
|
channel := th.BasicChannel
|
|
user := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
appErr := th.App.JoinChannel(th.Context, channel, user.Id)
|
|
require.Nil(t, appErr)
|
|
appErr = th.App.JoinChannel(th.Context, channel, user2.Id)
|
|
require.Nil(t, appErr)
|
|
p1, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: user.Id, ChannelId: channel.Id, Message: "Hi @" + user2.Username}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: user2.Id, ChannelId: channel.Id, Message: "Hola"}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: user.Id, ChannelId: channel.Id, Message: "reply"}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// unfollow thread
|
|
m, err := th.App.Srv().Store().Thread().MaintainMembership(user.Id, p1.Id, store.ThreadMembershipOpts{
|
|
Following: false,
|
|
UpdateFollowing: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, m.Following)
|
|
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: user.Id, ChannelId: channel.Id, Message: "another reply"}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// User should be following thread after posting in it, even after previously
|
|
// unfollowing it, if ThreadAutoFollow is true
|
|
m, appErr = th.App.GetThreadMembershipForUser(user.Id, p1.Id)
|
|
require.Nil(t, appErr)
|
|
require.True(t, m.Following)
|
|
}
|
|
|
|
func TestGetPostIfAuthorized(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("Private channel", func(t *testing.T) {
|
|
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: privateChannel.Id, Message: "Hello"}, privateChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, post)
|
|
|
|
session1, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, session1)
|
|
|
|
session2, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, session2)
|
|
|
|
// User is not authorized to get post
|
|
_, err, _ = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
|
|
require.NotNil(t, err)
|
|
|
|
// User is authorized to get post
|
|
_, err, _ = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
|
|
require.Nil(t, err)
|
|
})
|
|
|
|
t.Run("Public channel", func(t *testing.T) {
|
|
publicChannel := th.CreateChannel(t, th.BasicTeam)
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: publicChannel.Id, Message: "Hello"}, publicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, post)
|
|
|
|
session1, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, session1)
|
|
|
|
session2, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, session2)
|
|
|
|
// User is authorized to get post
|
|
_, err, _ = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
|
|
require.Nil(t, err)
|
|
|
|
// User is authorized to get post
|
|
_, err, _ = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
|
|
require.Nil(t, err)
|
|
|
|
th.App.UpdateConfig(func(c *model.Config) {
|
|
b := true
|
|
c.ComplianceSettings.Enable = &b
|
|
})
|
|
|
|
// User is not authorized to get post
|
|
_, err, _ = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
|
|
require.NotNil(t, err)
|
|
|
|
// User is authorized to get post
|
|
_, err, _ = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
|
|
require.Nil(t, err)
|
|
})
|
|
}
|
|
|
|
// MM-68140: thread context for rewrite must not be built from posts in channels the session cannot read.
|
|
func TestBuildThreadContextForRewriteRequiresChannelReadAccess(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("direct message between other users", func(t *testing.T) {
|
|
secretToken := "MM68140_SECRET_DM_THREAD_" + model.NewId()
|
|
dm := th.CreateDmChannel(t, th.BasicUser2)
|
|
root, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: dm.Id,
|
|
Message: secretToken,
|
|
}, dm, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
RootId: root.Id,
|
|
UserId: th.BasicUser2.Id,
|
|
ChannelId: dm.Id,
|
|
Message: "reply only visible to DM participants",
|
|
}, dm, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
attacker := th.CreateUser(t)
|
|
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: attacker.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
ctx := th.Context.WithSession(session)
|
|
|
|
contextStr, appErr := th.App.buildThreadContextForRewrite(ctx, root.Id)
|
|
|
|
require.NotNil(t, appErr, "expected permission error when root_id is in a channel the user cannot read, got nil")
|
|
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
|
|
assert.NotContains(t, contextStr, secretToken)
|
|
})
|
|
|
|
t.Run("private channel the user is not a member of", func(t *testing.T) {
|
|
secretToken := "MM68140_SECRET_PRIVATE_THREAD_" + model.NewId()
|
|
privateCh := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
root, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: privateCh.Id,
|
|
Message: secretToken,
|
|
}, privateCh, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
ctx := th.Context.WithSession(session)
|
|
|
|
contextStr, appErr := th.App.buildThreadContextForRewrite(ctx, root.Id)
|
|
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
|
|
assert.NotContains(t, contextStr, secretToken)
|
|
})
|
|
}
|
|
|
|
// MM-68140: additional edge cases for thread context authorization and anchor resolution.
|
|
func TestBuildThreadContextForRewriteEdgeCasesMM68140(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("reply post id as root_id resolves thread and includes root message", func(t *testing.T) {
|
|
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
|
|
require.Nil(t, appErr)
|
|
|
|
rootSecret := "MM68140_ROOT_VIA_REPLY_ANCHOR_" + model.NewId()
|
|
root, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: rootSecret,
|
|
}, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
reply, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
RootId: root.Id,
|
|
UserId: th.BasicUser2.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "reply anchor",
|
|
}, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
ctx := th.Context.WithSession(session)
|
|
|
|
contextStr, appErr := th.App.buildThreadContextForRewrite(ctx, reply.Id)
|
|
require.Nil(t, appErr)
|
|
assert.Contains(t, contextStr, rootSecret)
|
|
assert.Contains(t, contextStr, "reply anchor")
|
|
})
|
|
|
|
t.Run("nonexistent post id returns not found", func(t *testing.T) {
|
|
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
ctx := th.Context.WithSession(session)
|
|
|
|
_, appErr := th.App.buildThreadContextForRewrite(ctx, model.NewId())
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
|
})
|
|
|
|
t.Run("soft-deleted anchor post returns not found", func(t *testing.T) {
|
|
root, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "to be deleted",
|
|
}, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
_, err = th.App.DeletePost(th.Context, root.Id, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
|
|
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
ctx := th.Context.WithSession(session)
|
|
|
|
_, appErr := th.App.buildThreadContextForRewrite(ctx, root.Id)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
|
})
|
|
|
|
t.Run("guest on team cannot use root_id for private channel they are not in", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.GuestAccountsSettings.Enable = true
|
|
})
|
|
|
|
guest := th.CreateGuest(t)
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guest.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
privateCh := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
secretToken := "MM68140_GUEST_PRIVATE_" + model.NewId()
|
|
root, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: privateCh.Id,
|
|
Message: secretToken,
|
|
}, privateCh, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: guest.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
ctx := th.Context.WithSession(session)
|
|
|
|
contextStr, appErr := th.App.buildThreadContextForRewrite(ctx, root.Id)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
|
|
assert.NotContains(t, contextStr, secretToken)
|
|
})
|
|
|
|
t.Run("system admin may read thread context for DM they do not participate in", func(t *testing.T) {
|
|
dm := th.CreateDmChannel(t, th.BasicUser2)
|
|
secretToken := "MM68140_ADMIN_DM_THREAD_" + model.NewId()
|
|
root, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: dm.Id,
|
|
Message: secretToken,
|
|
}, dm, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
RootId: root.Id,
|
|
UserId: th.BasicUser2.Id,
|
|
ChannelId: dm.Id,
|
|
Message: "dm reply",
|
|
}, dm, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.SystemAdminUser.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
ctx := th.Context.WithSession(session)
|
|
|
|
contextStr, appErr := th.App.buildThreadContextForRewrite(ctx, root.Id)
|
|
require.Nil(t, appErr)
|
|
assert.Contains(t, contextStr, secretToken)
|
|
})
|
|
|
|
t.Run("member can build context after channel is archived", func(t *testing.T) {
|
|
ch := th.CreateChannel(t, th.BasicTeam)
|
|
root, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: ch.Id,
|
|
Message: "MM68140_ARCHIVED_ROOT",
|
|
}, ch, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, &model.Post{
|
|
RootId: root.Id,
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: ch.Id,
|
|
Message: "reply in archived",
|
|
}, ch, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
appErr := th.App.DeleteChannel(th.Context, ch, th.SystemAdminUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
session, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
|
require.Nil(t, err)
|
|
ctx := th.Context.WithSession(session)
|
|
|
|
contextStr, appErr := th.App.buildThreadContextForRewrite(ctx, root.Id)
|
|
require.Nil(t, appErr)
|
|
assert.Contains(t, contextStr, "MM68140_ARCHIVED_ROOT")
|
|
})
|
|
}
|
|
|
|
func TestShouldNotRefollowOnOthersReply(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
|
|
})
|
|
|
|
channel := th.BasicChannel
|
|
user := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
appErr := th.App.JoinChannel(th.Context, channel, user.Id)
|
|
require.Nil(t, appErr)
|
|
appErr = th.App.JoinChannel(th.Context, channel, user2.Id)
|
|
require.Nil(t, appErr)
|
|
p1, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: user.Id, ChannelId: channel.Id, Message: "Hi @" + user2.Username}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: user2.Id, ChannelId: channel.Id, Message: "Hola"}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// User2 unfollows thread
|
|
m, err := th.App.Srv().Store().Thread().MaintainMembership(user2.Id, p1.Id, store.ThreadMembershipOpts{
|
|
Following: false,
|
|
UpdateFollowing: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, m.Following)
|
|
|
|
// user posts in the thread
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: user.Id, ChannelId: channel.Id, Message: "another reply"}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// User2 should still not be following the thread because they manually
|
|
// unfollowed the thread
|
|
m, appErr = th.App.GetThreadMembershipForUser(user2.Id, p1.Id)
|
|
require.Nil(t, appErr)
|
|
require.False(t, m.Following)
|
|
|
|
// user posts in the thread mentioning user2
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: p1.Id, UserId: user.Id, ChannelId: channel.Id, Message: "reply with mention @" + user2.Username}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// User2 should now be following the thread because they were explicitly mentioned
|
|
m, appErr = th.App.GetThreadMembershipForUser(user2.Id, p1.Id)
|
|
require.Nil(t, appErr)
|
|
require.True(t, m.Following)
|
|
}
|
|
|
|
func TestGetLastAccessiblePostTime(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupWithStoreMock(t)
|
|
|
|
// Setup store mocks needed for GetServerLimits
|
|
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
|
mockUserStore := storemocks.UserStore{}
|
|
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
|
|
mockStore.On("User").Return(&mockUserStore)
|
|
|
|
// Test with no license - should return 0
|
|
r, err := th.App.GetLastAccessiblePostTime()
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, int64(0), r)
|
|
|
|
// Test with Entry license but no limits configured - should return 0
|
|
entryLicenseNoLimits := model.NewTestLicenseSKU(model.LicenseShortSkuMattermostEntry)
|
|
entryLicenseNoLimits.Limits = nil // No limits configured
|
|
th.App.Srv().SetLicense(entryLicenseNoLimits)
|
|
r, err = th.App.GetLastAccessiblePostTime()
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, int64(0), r, "Entry license with no limits should return 0")
|
|
|
|
// Test with Entry license with zero post history limit - should return 0
|
|
entryLicenseZeroLimit := model.NewTestLicenseSKU(model.LicenseShortSkuMattermostEntry)
|
|
entryLicenseZeroLimit.Limits = &model.LicenseLimits{PostHistory: 0} // Zero limit
|
|
th.App.Srv().SetLicense(entryLicenseZeroLimit)
|
|
r, err = th.App.GetLastAccessiblePostTime()
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, int64(0), r, "Entry license with zero post history limit should return 0")
|
|
|
|
// Test with Entry license that has post history limits
|
|
entryLicenseWithLimits := model.NewTestLicenseSKU(model.LicenseShortSkuMattermostEntry)
|
|
entryLicenseWithLimits.Limits = &model.LicenseLimits{PostHistory: 1000} // Actual limit
|
|
th.App.Srv().SetLicense(entryLicenseWithLimits)
|
|
|
|
// Test case 1: No system value found (ErrNotFound) - should return 0
|
|
mockSystemStore := storemocks.SystemStore{}
|
|
mockStore.On("System").Return(&mockSystemStore)
|
|
mockSystemStore.On("GetByName", mock.Anything).Return(nil, store.NewErrNotFound("", ""))
|
|
r, err = th.App.GetLastAccessiblePostTime()
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, int64(0), r)
|
|
|
|
// Test case 2: Database error - should return error
|
|
mockSystemStore = storemocks.SystemStore{}
|
|
mockStore.On("System").Return(&mockSystemStore)
|
|
mockSystemStore.On("GetByName", mock.Anything).Return(nil, errors.New("database error"))
|
|
_, err = th.App.GetLastAccessiblePostTime()
|
|
assert.NotNil(t, err)
|
|
|
|
// Test case 3: Valid system value found - should return parsed timestamp
|
|
mockSystemStore = storemocks.SystemStore{}
|
|
mockStore.On("System").Return(&mockSystemStore)
|
|
mockSystemStore.On("GetByName", mock.Anything).Return(&model.System{Name: model.SystemLastAccessiblePostTime, Value: "1234567890"}, nil)
|
|
r, err = th.App.GetLastAccessiblePostTime()
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, int64(1234567890), r)
|
|
}
|
|
|
|
func TestComputeLastAccessiblePostTime(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("Updates the time, if Entry license limit is applicable", func(t *testing.T) {
|
|
th := SetupWithStoreMock(t)
|
|
|
|
// Set Entry license with post history limit of 100 messages
|
|
entryLicensePostsLimit := model.NewTestLicenseSKU(model.LicenseShortSkuMattermostEntry)
|
|
entryLicensePostsLimit.Limits = &model.LicenseLimits{PostHistory: 100}
|
|
th.App.Srv().SetLicense(entryLicensePostsLimit)
|
|
|
|
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
|
mockPostStore := storemocks.PostStore{}
|
|
mockPostStore.On("GetNthRecentPostTime", int64(100)).Return(int64(1234567890), nil)
|
|
mockSystemStore := storemocks.SystemStore{}
|
|
mockSystemStore.On("SaveOrUpdate", mock.Anything).Return(nil)
|
|
mockStore.On("Post").Return(&mockPostStore)
|
|
mockStore.On("System").Return(&mockSystemStore)
|
|
|
|
err := th.App.ComputeLastAccessiblePostTime()
|
|
assert.NoError(t, err)
|
|
|
|
// Verify that the system value was saved with the calculated timestamp
|
|
mockSystemStore.AssertCalled(t, "SaveOrUpdate", &model.System{
|
|
Name: model.SystemLastAccessiblePostTime,
|
|
Value: "1234567890",
|
|
})
|
|
})
|
|
|
|
t.Run("Remove the time if license limit is NOT applicable", func(t *testing.T) {
|
|
th := SetupWithStoreMock(t)
|
|
|
|
// Set license without post history limits (using test license without limits)
|
|
license := model.NewTestLicense()
|
|
license.Limits = nil // No limits
|
|
th.App.Srv().SetLicense(license)
|
|
|
|
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
|
mockSystemStore := storemocks.SystemStore{}
|
|
mockSystemStore.On("GetByName", model.SystemLastAccessiblePostTime).Return(&model.System{Name: model.SystemLastAccessiblePostTime, Value: "1234567890"}, nil)
|
|
mockSystemStore.On("PermanentDeleteByName", model.SystemLastAccessiblePostTime).Return(nil, nil)
|
|
mockStore.On("System").Return(&mockSystemStore)
|
|
|
|
err := th.App.ComputeLastAccessiblePostTime()
|
|
assert.NoError(t, err)
|
|
|
|
// Verify that SaveOrUpdate was not called (no new timestamp calculated)
|
|
mockSystemStore.AssertNotCalled(t, "SaveOrUpdate", mock.Anything)
|
|
// Verify that the previous value was deleted
|
|
mockSystemStore.AssertCalled(t, "PermanentDeleteByName", model.SystemLastAccessiblePostTime)
|
|
})
|
|
}
|
|
|
|
func TestGetEditHistoryForPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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)
|
|
|
|
// update the post message
|
|
patch := &model.PostPatch{
|
|
Message: model.NewPointer("new message edited"),
|
|
}
|
|
_, _, err1 := th.App.PatchPost(th.Context, rpost.Id, patch, nil)
|
|
require.Nil(t, err1)
|
|
|
|
// update the post message again
|
|
patch = &model.PostPatch{
|
|
Message: model.NewPointer("new message edited again"),
|
|
}
|
|
|
|
_, _, err2 := th.App.PatchPost(th.Context, rpost.Id, patch, nil)
|
|
require.Nil(t, err2)
|
|
|
|
t.Run("should return the edit history", func(t *testing.T) {
|
|
edits, err := th.App.GetEditHistoryForPost(post.Id)
|
|
require.Nil(t, err)
|
|
|
|
require.Len(t, edits, 2)
|
|
require.Equal(t, "new message edited", edits[0].Message)
|
|
require.Equal(t, "new message", edits[1].Message)
|
|
})
|
|
|
|
t.Run("should return an error if the post is not found", func(t *testing.T) {
|
|
edits, err := th.App.GetEditHistoryForPost("invalid-post-id")
|
|
require.NotNil(t, err)
|
|
require.Empty(t, edits)
|
|
})
|
|
|
|
t.Run("edit history should contain file metadata", func(t *testing.T) {
|
|
fileBytes := []byte("file contents")
|
|
fileInfo, err := th.App.UploadFile(th.Context, fileBytes, th.BasicChannel.Id, "file.txt")
|
|
require.Nil(t, err)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "new message",
|
|
UserId: th.BasicUser.Id,
|
|
FileIds: model.StringArray{fileInfo.Id},
|
|
}
|
|
|
|
_, _, err = th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
patch := &model.PostPatch{
|
|
Message: model.NewPointer("new message edited"),
|
|
}
|
|
_, _, appErr := th.App.PatchPost(th.Context, post.Id, patch, nil)
|
|
require.Nil(t, appErr)
|
|
|
|
patch = &model.PostPatch{
|
|
Message: model.NewPointer("new message edited 2"),
|
|
}
|
|
_, _, appErr = th.App.PatchPost(th.Context, post.Id, patch, nil)
|
|
require.Nil(t, appErr)
|
|
|
|
patch = &model.PostPatch{
|
|
Message: model.NewPointer("new message edited 3"),
|
|
}
|
|
_, _, appErr = th.App.PatchPost(th.Context, post.Id, patch, nil)
|
|
require.Nil(t, appErr)
|
|
|
|
edits, err := th.App.GetEditHistoryForPost(post.Id)
|
|
require.Nil(t, err)
|
|
|
|
require.Len(t, edits, 3)
|
|
|
|
for _, edit := range edits {
|
|
require.Len(t, edit.FileIds, 1)
|
|
require.Equal(t, fileInfo.Id, edit.FileIds[0])
|
|
require.Len(t, edit.Metadata.Files, 1)
|
|
require.Equal(t, fileInfo.Id, edit.Metadata.Files[0].Id)
|
|
}
|
|
})
|
|
|
|
t.Run("edit history should contain file metadata even if the file info is deleted", func(t *testing.T) {
|
|
fileBytes := []byte("file contents")
|
|
fileInfo, appErr := th.App.UploadFile(th.Context, fileBytes, th.BasicChannel.Id, "file.txt")
|
|
require.Nil(t, appErr)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "new message",
|
|
UserId: th.BasicUser.Id,
|
|
FileIds: model.StringArray{fileInfo.Id},
|
|
}
|
|
|
|
_, _, appErr = th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
|
|
patch := &model.PostPatch{
|
|
Message: model.NewPointer("new message edited"),
|
|
}
|
|
_, _, appErr = th.App.PatchPost(th.Context, post.Id, patch, nil)
|
|
require.Nil(t, appErr)
|
|
|
|
patch = &model.PostPatch{
|
|
Message: model.NewPointer("new message edited 2"),
|
|
}
|
|
_, _, appErr = th.App.PatchPost(th.Context, post.Id, patch, nil)
|
|
require.Nil(t, appErr)
|
|
|
|
patch = &model.PostPatch{
|
|
Message: model.NewPointer("new message edited 3"),
|
|
}
|
|
_, _, appErr = th.App.PatchPost(th.Context, post.Id, patch, nil)
|
|
require.Nil(t, appErr)
|
|
|
|
// now delete the file info, and it should still be include in edit history metadata
|
|
_, err := th.App.Srv().Store().FileInfo().DeleteForPost(th.Context, post.Id)
|
|
require.NoError(t, err)
|
|
|
|
edits, appErr := th.App.GetEditHistoryForPost(post.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
require.Len(t, edits, 3)
|
|
|
|
for _, edit := range edits {
|
|
require.Len(t, edit.FileIds, 1)
|
|
require.Equal(t, fileInfo.Id, edit.FileIds[0])
|
|
require.Len(t, edit.Metadata.Files, 1)
|
|
require.Equal(t, fileInfo.Id, edit.Metadata.Files[0].Id)
|
|
require.Greater(t, edit.Metadata.Files[0].DeleteAt, int64(0))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCopyWranglerPostlist(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Create a post with a file attachment
|
|
fileBytes := []byte("file contents")
|
|
fileInfo, err := th.App.UploadFile(th.Context, fileBytes, th.BasicChannel.Id, "file.txt")
|
|
require.Nil(t, err)
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test message",
|
|
UserId: th.BasicUser.Id,
|
|
FileIds: []string{fileInfo.Id},
|
|
}
|
|
rootPost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// Add a reaction to the post
|
|
reaction := &model.Reaction{
|
|
UserId: th.BasicUser.Id,
|
|
PostId: rootPost.Id,
|
|
EmojiName: "smile",
|
|
}
|
|
_, err = th.App.SaveReactionForPost(th.Context, reaction)
|
|
require.Nil(t, err)
|
|
|
|
// Copy the post to a new channel
|
|
targetChannel := &model.Channel{
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "test-channel",
|
|
Type: model.ChannelTypeOpen,
|
|
}
|
|
targetChannel, err = th.App.CreateChannel(th.Context, targetChannel, false)
|
|
require.Nil(t, err)
|
|
wpl := &model.WranglerPostList{
|
|
Posts: []*model.Post{rootPost},
|
|
FileAttachmentCount: 1,
|
|
}
|
|
newRootPost, _, err := th.App.CopyWranglerPostlist(th.Context, wpl, targetChannel)
|
|
require.Nil(t, err)
|
|
|
|
// Check that the new post has the same message and file attachment
|
|
require.Equal(t, rootPost.Message, newRootPost.Message)
|
|
require.Len(t, newRootPost.FileIds, 1)
|
|
|
|
// Check that the new post has the same reaction
|
|
reactions, err := th.App.GetReactionsForPost(newRootPost.Id)
|
|
require.Nil(t, err)
|
|
require.Len(t, reactions, 1)
|
|
require.Equal(t, reaction.EmojiName, reactions[0].EmojiName)
|
|
}
|
|
|
|
func TestValidateMoveOrCopy(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.WranglerSettings.MoveThreadFromPrivateChannelEnable = model.NewPointer(true)
|
|
cfg.WranglerSettings.MoveThreadFromDirectMessageChannelEnable = model.NewPointer(true)
|
|
cfg.WranglerSettings.MoveThreadFromGroupMessageChannelEnable = model.NewPointer(true)
|
|
cfg.WranglerSettings.MoveThreadToAnotherTeamEnable = model.NewPointer(true)
|
|
cfg.WranglerSettings.MoveThreadMaxCount = model.NewPointer(int64(100))
|
|
})
|
|
|
|
t.Run("empty post list", func(t *testing.T) {
|
|
err := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{}, th.BasicChannel, th.BasicChannel, th.BasicUser)
|
|
require.Error(t, err)
|
|
require.Equal(t, "The wrangler post list contains no posts", err.Error())
|
|
})
|
|
|
|
t.Run("moving from private channel with MoveThreadFromPrivateChannelEnable disabled", func(t *testing.T) {
|
|
privateChannel := &model.Channel{
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "private-channel",
|
|
Type: model.ChannelTypePrivate,
|
|
}
|
|
privateChannel, err := th.App.CreateChannel(th.Context, privateChannel, false)
|
|
require.Nil(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.WranglerSettings.MoveThreadFromPrivateChannelEnable = model.NewPointer(false)
|
|
})
|
|
|
|
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: privateChannel.Id}}}, privateChannel, th.BasicChannel, th.BasicUser)
|
|
require.Error(t, e)
|
|
require.Equal(t, "Wrangler is currently configured to not allow moving posts from private channels", e.Error())
|
|
})
|
|
|
|
t.Run("moving from direct channel with MoveThreadFromDirectMessageChannelEnable disabled", func(t *testing.T) {
|
|
directChannel, err := th.App.createDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, directChannel)
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.WranglerSettings.MoveThreadFromDirectMessageChannelEnable = model.NewPointer(false)
|
|
})
|
|
|
|
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: directChannel.Id}}}, directChannel, th.BasicChannel, th.BasicUser)
|
|
require.Error(t, e)
|
|
require.Equal(t, "Wrangler is currently configured to not allow moving posts from direct message channels", e.Error())
|
|
})
|
|
|
|
t.Run("moving from group channel with MoveThreadFromGroupMessageChannelEnable disabled", func(t *testing.T) {
|
|
groupChannel := &model.Channel{
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "group-channel",
|
|
Type: model.ChannelTypeGroup,
|
|
}
|
|
groupChannel, err := th.App.CreateChannel(th.Context, groupChannel, false)
|
|
require.Nil(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.WranglerSettings.MoveThreadFromGroupMessageChannelEnable = model.NewPointer(false)
|
|
})
|
|
|
|
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: groupChannel.Id}}}, groupChannel, th.BasicChannel, th.BasicUser)
|
|
require.Error(t, e)
|
|
require.Equal(t, "Wrangler is currently configured to not allow moving posts from group message channels", e.Error())
|
|
})
|
|
|
|
t.Run("moving to different team with MoveThreadToAnotherTeamEnable disabled", func(t *testing.T) {
|
|
team := &model.Team{
|
|
Name: "testteam",
|
|
DisplayName: "testteam",
|
|
Type: model.TeamOpen,
|
|
}
|
|
|
|
targetTeam, err := th.App.CreateTeam(th.Context, team)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, targetTeam)
|
|
|
|
targetChannel := &model.Channel{
|
|
TeamId: targetTeam.Id,
|
|
Name: "test-channel",
|
|
Type: model.ChannelTypeOpen,
|
|
}
|
|
|
|
targetChannel, err = th.App.CreateChannel(th.Context, targetChannel, false)
|
|
require.Nil(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.WranglerSettings.MoveThreadToAnotherTeamEnable = model.NewPointer(false)
|
|
})
|
|
|
|
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: th.BasicChannel.Id}}}, th.BasicChannel, targetChannel, th.BasicUser)
|
|
require.Error(t, e)
|
|
require.Equal(t, "Wrangler is currently configured to not allow moving messages to different teams", e.Error())
|
|
})
|
|
|
|
t.Run("moving to channel user is not a member of", func(t *testing.T) {
|
|
targetChannel := &model.Channel{
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "test-channel",
|
|
Type: model.ChannelTypePrivate,
|
|
}
|
|
targetChannel, err := th.App.CreateChannel(th.Context, targetChannel, false)
|
|
require.Nil(t, err)
|
|
|
|
err = th.App.RemoveUserFromChannel(th.Context, th.BasicUser.Id, th.SystemAdminUser.Id, th.BasicChannel)
|
|
require.Nil(t, err)
|
|
|
|
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: th.BasicChannel.Id}}}, th.BasicChannel, targetChannel, th.BasicUser)
|
|
require.Error(t, e)
|
|
require.Equal(t, fmt.Sprintf("channel with ID %s doesn't exist or you are not a member", targetChannel.Id), e.Error())
|
|
})
|
|
|
|
t.Run("moving thread longer than MoveThreadMaxCount", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.WranglerSettings.MoveThreadMaxCount = 1
|
|
})
|
|
|
|
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: th.BasicChannel.Id}, {ChannelId: th.BasicChannel.Id}}}, th.BasicChannel, th.BasicChannel, th.BasicUser)
|
|
require.Error(t, e)
|
|
require.Equal(t, "the thread is 2 posts long, but this command is configured to only move threads of up to 1 posts", e.Error())
|
|
})
|
|
}
|
|
|
|
func TestPermanentDeletePost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable BurnOnRead feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
|
|
|
|
t.Run("should permanently delete a post and its file attachment", func(t *testing.T) {
|
|
// Create a post with a file attachment.
|
|
teamID := th.BasicTeam.Id
|
|
channelID := th.BasicChannel.Id
|
|
userID := th.BasicUser.Id
|
|
filename := "test"
|
|
data := []byte("abcd")
|
|
|
|
info1, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data, true)
|
|
assert.Nil(t, err)
|
|
|
|
post := &model.Post{
|
|
Message: "asd",
|
|
ChannelId: channelID,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: userID,
|
|
CreateAt: 0,
|
|
FileIds: []string{info1.Id},
|
|
}
|
|
|
|
post, _, err = th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
assert.Nil(t, err)
|
|
|
|
// Delete the post.
|
|
err = th.App.PermanentDeletePost(th.Context, post.Id, userID)
|
|
assert.Nil(t, err)
|
|
|
|
// Wait for the cleanup routine to finish.
|
|
time.Sleep(time.Millisecond * 100)
|
|
|
|
// Check that the post can no longer be reached.
|
|
_, err = th.App.GetSinglePost(th.Context, post.Id, true)
|
|
assert.NotNil(t, err)
|
|
|
|
// Check that the file can no longer be reached.
|
|
_, err = th.App.GetFileInfo(th.Context, info1.Id)
|
|
assert.NotNil(t, err)
|
|
})
|
|
|
|
t.Run("should permanently delete a post that is soft deleted", func(t *testing.T) {
|
|
// Create a post with a file attachment.
|
|
teamID := th.BasicTeam.Id
|
|
channelID := th.BasicChannel.Id
|
|
userID := th.BasicUser.Id
|
|
filename := "test"
|
|
data := []byte("abcd")
|
|
|
|
info1, appErr := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data, true)
|
|
require.Nil(t, appErr)
|
|
|
|
post := &model.Post{
|
|
Message: "asd",
|
|
ChannelId: channelID,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: userID,
|
|
CreateAt: 0,
|
|
FileIds: []string{info1.Id},
|
|
}
|
|
|
|
post, _, appErr = th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
assert.Nil(t, appErr)
|
|
|
|
infos, err := th.App.Srv().Store().FileInfo().GetForPost(post.Id, true, true, false)
|
|
require.NoError(t, err)
|
|
assert.Len(t, infos, 1)
|
|
|
|
// Soft delete the post.
|
|
_, appErr = th.App.DeletePost(th.Context, post.Id, userID)
|
|
assert.Nil(t, appErr)
|
|
|
|
// Wait for the cleanup routine to finish.
|
|
time.Sleep(time.Millisecond * 100)
|
|
|
|
// Delete the post.
|
|
appErr = th.App.PermanentDeletePost(th.Context, post.Id, userID)
|
|
assert.Nil(t, appErr)
|
|
|
|
// Check that the post can no longer be reached.
|
|
_, appErr = th.App.GetSinglePost(th.Context, post.Id, true)
|
|
assert.NotNil(t, appErr)
|
|
|
|
infos, err = th.App.Srv().Store().FileInfo().GetForPost(post.Id, true, true, false)
|
|
require.NoError(t, err)
|
|
assert.Len(t, infos, 0)
|
|
})
|
|
|
|
t.Run("should permanently delete a burn-on-read post and its file attachments", func(t *testing.T) {
|
|
// Enable feature with license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.EnableBurnOnRead = model.NewPointer(true)
|
|
})
|
|
|
|
// Create a burn-on-read post with a file attachment
|
|
teamID := th.BasicTeam.Id
|
|
channelID := th.BasicChannel.Id
|
|
userID := th.BasicUser.Id
|
|
filename := "burn_on_read_file"
|
|
data := []byte("burn on read file content")
|
|
|
|
info1, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data, true)
|
|
require.Nil(t, err)
|
|
|
|
post := &model.Post{
|
|
Message: "burn on read message with file",
|
|
ChannelId: channelID,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: userID,
|
|
CreateAt: 0,
|
|
FileIds: []string{info1.Id},
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
|
|
|
|
post, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, model.PostTypeBurnOnRead, post.Type)
|
|
|
|
// Verify that the post has empty message and file IDs (stored in TemporaryPosts)
|
|
assert.Empty(t, post.Message)
|
|
assert.Empty(t, post.FileIds)
|
|
|
|
// Verify that TemporaryPost exists with original content
|
|
tmpPost, tmpErr := th.App.Srv().Store().TemporaryPost().Get(th.Context, post.Id, true)
|
|
require.NoError(t, tmpErr)
|
|
require.NotNil(t, tmpPost)
|
|
assert.Equal(t, "burn on read message with file", tmpPost.Message)
|
|
assert.Equal(t, model.StringArray{info1.Id}, tmpPost.FileIDs)
|
|
|
|
// Verify file info exists before deletion
|
|
_, err = th.App.GetFileInfo(th.Context, info1.Id)
|
|
require.Nil(t, err)
|
|
|
|
// Permanently delete the post
|
|
appErr = th.App.PermanentDeletePost(th.Context, post.Id, userID)
|
|
require.Nil(t, appErr)
|
|
|
|
// Check that the post can no longer be reached
|
|
_, err = th.App.GetSinglePost(th.Context, post.Id, true)
|
|
assert.NotNil(t, err)
|
|
|
|
// Check that the file also deleted
|
|
_, err = th.App.GetFileInfo(th.Context, info1.Id)
|
|
assert.NotNil(t, err)
|
|
|
|
// Verify TemporaryPost is also deleted
|
|
_, tmpErr = th.App.Srv().Store().TemporaryPost().Get(th.Context, post.Id, true)
|
|
assert.Error(t, tmpErr)
|
|
assert.True(t, store.IsErrNotFound(tmpErr))
|
|
})
|
|
|
|
t.Run("should send unrevealed post in websocket broadcast", func(t *testing.T) {
|
|
// Enable feature with license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.EnableBurnOnRead = model.NewPointer(true)
|
|
})
|
|
|
|
// Create a burn-on-read post
|
|
//teamID := th.BasicTeam.Id
|
|
channelID := th.BasicChannel.Id
|
|
userID := th.BasicUser.Id
|
|
|
|
wsMessages, closeWS := connectFakeWebSocket(t, th, userID, "", []model.WebsocketEventType{model.WebsocketEventPostDeleted})
|
|
defer closeWS()
|
|
|
|
post := &model.Post{
|
|
Message: "burn on read message with file",
|
|
ChannelId: channelID,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: userID,
|
|
CreateAt: 0,
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
|
|
|
|
post, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, model.PostTypeBurnOnRead, post.Type)
|
|
|
|
appErr = th.App.PermanentDeletePost(th.Context, post.Id, userID)
|
|
require.Nil(t, appErr)
|
|
|
|
var received *model.WebSocketEvent
|
|
select {
|
|
case received = <-wsMessages:
|
|
// the post sent in websocket payload shouldn't contain message or file IDs
|
|
data := received.GetData()
|
|
postJSON, ok := data["post"].(string)
|
|
require.True(t, ok)
|
|
var receivedPost model.Post
|
|
err := json.Unmarshal([]byte(postJSON), &receivedPost)
|
|
require.NoError(t, err)
|
|
require.Equal(t, post.Id, receivedPost.Id)
|
|
require.Equal(t, "", receivedPost.Message)
|
|
require.Equal(t, 0, len(receivedPost.FileIds))
|
|
case <-time.After(10 * time.Second):
|
|
require.Fail(t, "Did not receive websocket message in time")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSendTestMessage(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("Should create the post with the correct prop", func(t *testing.T) {
|
|
post, result := th.App.SendTestMessage(th.Context, th.BasicUser.Id)
|
|
assert.Nil(t, result)
|
|
assert.NotEmpty(t, post.GetProp(model.PostPropsForceNotification))
|
|
})
|
|
}
|
|
|
|
func TestPopulateEditHistoryFileMetadata(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("should populate file metadata for all posts", func(t *testing.T) {
|
|
fileInfo1, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
fileInfo2, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
post1 := th.CreatePost(t, th.BasicChannel, func(post *model.Post) {
|
|
post.FileIds = model.StringArray{fileInfo1.Id}
|
|
})
|
|
|
|
post2 := th.CreatePost(t, th.BasicChannel, func(post *model.Post) {
|
|
post.FileIds = model.StringArray{fileInfo2.Id}
|
|
})
|
|
|
|
appErr := th.App.populateEditHistoryFileMetadata([]*model.Post{post1, post2})
|
|
require.Nil(t, appErr)
|
|
|
|
require.Len(t, post1.Metadata.Files, 1)
|
|
require.Equal(t, fileInfo1.Id, post1.Metadata.Files[0].Id)
|
|
|
|
require.Len(t, post2.Metadata.Files, 1)
|
|
require.Equal(t, fileInfo2.Id, post2.Metadata.Files[0].Id)
|
|
})
|
|
|
|
t.Run("should populate file metadata even for deleted posts", func(t *testing.T) {
|
|
fileInfo1, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
fileInfo2, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
post1 := th.CreatePost(t, th.BasicChannel, func(post *model.Post) {
|
|
post.FileIds = model.StringArray{fileInfo1.Id}
|
|
})
|
|
|
|
post2 := th.CreatePost(t, th.BasicChannel, func(post *model.Post) {
|
|
post.FileIds = model.StringArray{fileInfo2.Id}
|
|
})
|
|
|
|
_, appErr := th.App.DeletePost(th.Context, post1.Id, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
_, appErr = th.App.DeletePost(th.Context, post2.Id, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
appErr = th.App.populateEditHistoryFileMetadata([]*model.Post{post1, post2})
|
|
require.Nil(t, appErr)
|
|
|
|
require.Len(t, post1.Metadata.Files, 1)
|
|
require.Equal(t, fileInfo1.Id, post1.Metadata.Files[0].Id)
|
|
|
|
require.Len(t, post2.Metadata.Files, 1)
|
|
require.Equal(t, fileInfo2.Id, post2.Metadata.Files[0].Id)
|
|
})
|
|
|
|
t.Run("should populate file metadata even for deleted fileInfos", func(t *testing.T) {
|
|
fileInfo1, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
fileInfo2, err := th.App.Srv().Store().FileInfo().Save(th.Context,
|
|
&model.FileInfo{
|
|
CreatorId: th.BasicUser.Id,
|
|
Path: "path.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
post1 := th.CreatePost(t, th.BasicChannel, func(post *model.Post) {
|
|
post.FileIds = model.StringArray{fileInfo1.Id}
|
|
})
|
|
|
|
post2 := th.CreatePost(t, th.BasicChannel, func(post *model.Post) {
|
|
post.FileIds = model.StringArray{fileInfo2.Id}
|
|
})
|
|
|
|
_, err = th.App.Srv().Store().FileInfo().DeleteForPost(th.Context, post1.Id)
|
|
require.NoError(t, err)
|
|
|
|
_, err = th.App.Srv().Store().FileInfo().DeleteForPost(th.Context, post2.Id)
|
|
require.NoError(t, err)
|
|
|
|
appErr := th.App.populateEditHistoryFileMetadata([]*model.Post{post1, post2})
|
|
require.Nil(t, appErr)
|
|
|
|
require.Len(t, post1.Metadata.Files, 1)
|
|
require.Equal(t, fileInfo1.Id, post1.Metadata.Files[0].Id)
|
|
require.Greater(t, post1.Metadata.Files[0].DeleteAt, int64(0))
|
|
|
|
require.Len(t, post2.Metadata.Files, 1)
|
|
require.Equal(t, fileInfo2.Id, post2.Metadata.Files[0].Id)
|
|
require.Greater(t, post2.Metadata.Files[0].DeleteAt, int64(0))
|
|
})
|
|
}
|
|
|
|
func TestFilterPostsByChannelPermissions(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.GuestAccountsSettings.Enable = true
|
|
})
|
|
|
|
guestUser := th.CreateGuest(t)
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guestUser.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
|
|
_, appErr = th.App.AddUserToChannel(th.Context, guestUser, privateChannel, false)
|
|
require.Nil(t, appErr)
|
|
_, appErr = th.App.AddUserToChannel(th.Context, guestUser, th.BasicChannel, false)
|
|
require.Nil(t, appErr)
|
|
|
|
post1 := th.CreatePost(t, th.BasicChannel)
|
|
post2 := th.CreatePost(t, privateChannel)
|
|
post3 := th.CreatePost(t, th.BasicChannel)
|
|
|
|
t.Run("should filter posts when user has read_channel_content permission", func(t *testing.T) {
|
|
postList := model.NewPostList()
|
|
postList.Posts[post1.Id] = post1
|
|
postList.Posts[post2.Id] = post2
|
|
postList.Posts[post3.Id] = post3
|
|
postList.Order = []string{post1.Id, post2.Id, post3.Id}
|
|
|
|
allPostHaveMembership, appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, postList.Posts, 3)
|
|
require.Len(t, postList.Order, 3)
|
|
require.True(t, allPostHaveMembership)
|
|
})
|
|
|
|
t.Run("should filter posts when guest has read_channel_content permission", func(t *testing.T) {
|
|
postList := model.NewPostList()
|
|
postList.Posts[post1.Id] = post1
|
|
postList.Posts[post2.Id] = post2
|
|
postList.Posts[post3.Id] = post3
|
|
postList.Order = []string{post1.Id, post2.Id, post3.Id}
|
|
|
|
allPostHaveMembership, appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, guestUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, postList.Posts, 3)
|
|
require.Len(t, postList.Order, 3)
|
|
require.True(t, allPostHaveMembership)
|
|
})
|
|
|
|
t.Run("should filter posts when guest does not have read_channel_content permission", func(t *testing.T) {
|
|
channelGuestRole, appErr := th.App.GetRoleByName(th.Context, model.ChannelGuestRoleId)
|
|
require.Nil(t, appErr)
|
|
|
|
originalPermissions := make([]string, len(channelGuestRole.Permissions))
|
|
copy(originalPermissions, channelGuestRole.Permissions)
|
|
|
|
newPermissions := []string{}
|
|
for _, perm := range channelGuestRole.Permissions {
|
|
if perm != model.PermissionReadChannelContent.Id && perm != model.PermissionReadChannel.Id {
|
|
newPermissions = append(newPermissions, perm)
|
|
}
|
|
}
|
|
|
|
_, appErr = th.App.PatchRole(channelGuestRole, &model.RolePatch{
|
|
Permissions: &newPermissions,
|
|
})
|
|
require.Nil(t, appErr)
|
|
|
|
defer func() {
|
|
_, err := th.App.PatchRole(channelGuestRole, &model.RolePatch{
|
|
Permissions: &originalPermissions,
|
|
})
|
|
require.Nil(t, err)
|
|
}()
|
|
|
|
postList := model.NewPostList()
|
|
postList.Posts[post1.Id] = post1
|
|
postList.Posts[post2.Id] = post2
|
|
postList.Posts[post3.Id] = post3
|
|
postList.Order = []string{post1.Id, post2.Id, post3.Id}
|
|
|
|
allPostHaveMembership, appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, guestUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, postList.Posts, 0)
|
|
require.Len(t, postList.Order, 0)
|
|
require.True(t, allPostHaveMembership)
|
|
})
|
|
|
|
t.Run("should handle empty post list", func(t *testing.T) {
|
|
postList := model.NewPostList()
|
|
allPostHaveMembership, appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, postList.Posts, 0)
|
|
require.Len(t, postList.Order, 0)
|
|
require.True(t, allPostHaveMembership)
|
|
})
|
|
|
|
t.Run("should handle nil post list", func(t *testing.T) {
|
|
_, appErr := th.App.FilterPostsByChannelPermissions(th.Context, nil, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
})
|
|
|
|
t.Run("should handle posts with empty channel IDs", func(t *testing.T) {
|
|
postList := model.NewPostList()
|
|
postWithoutChannel := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: "",
|
|
Message: "test",
|
|
}
|
|
postList.Posts[postWithoutChannel.Id] = postWithoutChannel
|
|
postList.Order = []string{postWithoutChannel.Id}
|
|
|
|
allPostHaveMembership, appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, postList.Posts, 0)
|
|
require.Len(t, postList.Order, 0)
|
|
require.True(t, allPostHaveMembership)
|
|
})
|
|
|
|
t.Run("should handle posts from non-existent channels", func(t *testing.T) {
|
|
postList := model.NewPostList()
|
|
postWithInvalidChannel := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: model.NewId(),
|
|
Message: "test",
|
|
}
|
|
postList.Posts[postWithInvalidChannel.Id] = postWithInvalidChannel
|
|
postList.Order = []string{postWithInvalidChannel.Id}
|
|
|
|
allPostHaveMembership, appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.Len(t, postList.Posts, 0)
|
|
require.Len(t, postList.Order, 0)
|
|
require.True(t, allPostHaveMembership)
|
|
})
|
|
}
|
|
|
|
func TestRevealPost(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable BurnOnRead feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
|
|
|
|
// Helper to create a burn-on-read post
|
|
createBurnOnReadPost := func() *model.Post {
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn on read message",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
return createdPost
|
|
}
|
|
|
|
// Helper to create a regular post
|
|
createRegularPost := func() *model.Post {
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "regular message",
|
|
}
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
return createdPost
|
|
}
|
|
|
|
// Create a second user for testing
|
|
user2 := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, user2, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, th.BasicChannel)
|
|
|
|
t.Run("post type non burn on read", func(t *testing.T) {
|
|
regularPost := createRegularPost()
|
|
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, regularPost, user2.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, revealedPost)
|
|
require.Equal(t, "app.reveal_post.not_burn_on_read.app_error", appErr.Id)
|
|
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
|
})
|
|
|
|
t.Run("post doesn't have required prop", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
// Create a burn-on-read post without expire_at prop
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn on read message",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
// First save the post normally (which will add expire_at automatically)
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// Now manually remove the expire_at prop to test missing prop scenario
|
|
createdPost.SetProps(make(model.StringInterface))
|
|
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, createdPost, user2.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, revealedPost)
|
|
require.Equal(t, "app.reveal_post.missing_expire_at.app_error", appErr.Id)
|
|
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
|
})
|
|
|
|
t.Run("post with invalid expire_at prop type", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
post := createBurnOnReadPost()
|
|
|
|
// Manually set invalid expire_at type
|
|
post.SetProps(make(model.StringInterface))
|
|
post.AddProp(model.PostPropsExpireAt, "invalid_string")
|
|
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, post, user2.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, revealedPost)
|
|
require.Equal(t, "app.reveal_post.missing_expire_at.app_error", appErr.Id)
|
|
})
|
|
|
|
t.Run("post with zero expire_at", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
post := createBurnOnReadPost()
|
|
|
|
// Manually set zero expire_at
|
|
post.SetProps(make(model.StringInterface))
|
|
post.AddProp(model.PostPropsExpireAt, float64(0))
|
|
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, post, user2.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, revealedPost)
|
|
require.Equal(t, "app.reveal_post.missing_expire_at.app_error", appErr.Id)
|
|
})
|
|
|
|
t.Run("read receipt does not exist", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
post := createBurnOnReadPost()
|
|
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, post, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
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 read receipt was created
|
|
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, post.Id, user2.Id)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, receipt)
|
|
require.Equal(t, post.Id, receipt.PostID)
|
|
require.Equal(t, user2.Id, receipt.UserID)
|
|
require.Equal(t, revealedPost.Metadata.ExpireAt, receipt.ExpireAt)
|
|
})
|
|
|
|
t.Run("read receipt exists and not expired", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
post := createBurnOnReadPost()
|
|
|
|
// First reveal to create receipt
|
|
revealedPost1, appErr := th.App.RevealPost(th.Context, post, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, revealedPost1)
|
|
require.NotZero(t, revealedPost1.Metadata.ExpireAt)
|
|
|
|
// Reveal again - should succeed and return the same post
|
|
revealedPost2, appErr := th.App.RevealPost(th.Context, post, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, revealedPost2)
|
|
require.Equal(t, "burn on read message", revealedPost2.Message)
|
|
require.NotNil(t, revealedPost2.Metadata)
|
|
require.Equal(t, revealedPost1.Metadata.ExpireAt, revealedPost2.Metadata.ExpireAt)
|
|
})
|
|
|
|
t.Run("read receipt exists but expired", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
post := createBurnOnReadPost()
|
|
|
|
// Create an expired read receipt
|
|
expiredReceipt := &model.ReadReceipt{
|
|
UserID: user2.Id,
|
|
PostID: post.Id,
|
|
ExpireAt: model.GetMillis() - 1000, // Expired 1 second ago
|
|
}
|
|
_, err := th.App.Srv().Store().ReadReceipt().Save(th.Context, expiredReceipt)
|
|
require.NoError(t, err)
|
|
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, post, user2.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, revealedPost)
|
|
require.Equal(t, "app.reveal_post.read_receipt_expired.error", appErr.Id)
|
|
require.Equal(t, http.StatusForbidden, appErr.StatusCode)
|
|
})
|
|
|
|
t.Run("post expired", func(t *testing.T) {
|
|
post := createBurnOnReadPost()
|
|
|
|
// Manually set expired expire_at
|
|
post.SetProps(make(model.StringInterface))
|
|
post.AddProp(model.PostPropsExpireAt, model.GetMillis()-1000)
|
|
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, post, user2.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, revealedPost)
|
|
require.Equal(t, "app.reveal_post.post_expired.app_error", appErr.Id)
|
|
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
|
})
|
|
|
|
t.Run("revealed post preserves existing metadata", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
fileBytes := []byte("test")
|
|
fileInfo, err := th.App.UploadFile(th.Context, fileBytes, th.BasicChannel.Id, "file.txt")
|
|
require.Nil(t, err)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn on read message",
|
|
Type: model.PostTypeBurnOnRead,
|
|
FileIds: model.StringArray{fileInfo.Id},
|
|
}
|
|
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, createdPost, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, revealedPost)
|
|
require.NotNil(t, revealedPost.Metadata)
|
|
require.NotZero(t, revealedPost.Metadata.ExpireAt)
|
|
require.Len(t, revealedPost.Metadata.Files, 1)
|
|
})
|
|
|
|
t.Run("updateTemporaryPostIfAllRead bypasses cache to get fresh data", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
// Create a third user to have multiple recipients
|
|
user3 := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, user3, th.BasicTeam)
|
|
th.AddUserToChannel(t, user3, th.BasicChannel)
|
|
|
|
post := createBurnOnReadPost()
|
|
|
|
// Get initial TemporaryPost ExpireAt (should be max TTL - 24 hours)
|
|
tmpPostInitial, err := th.App.Srv().Store().TemporaryPost().Get(th.Context, post.Id, true)
|
|
require.NoError(t, err)
|
|
initialExpireAt := tmpPostInitial.ExpireAt
|
|
|
|
// First reveal by user2 creates a read receipt
|
|
_, appErr := th.App.RevealPost(th.Context, post, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
// Get the read receipt's ExpireAt (should be 5 minutes)
|
|
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, post.Id, user2.Id)
|
|
require.NoError(t, err)
|
|
|
|
// Verify receipt ExpireAt is less than initial tmpPost ExpireAt (5 minutes vs 24 hours)
|
|
require.Less(t, receipt.ExpireAt, initialExpireAt)
|
|
|
|
// At this point, user3 hasn't revealed yet, so TemporaryPost shouldn't be updated
|
|
tmpPostAfterFirstReveal, err := th.App.Srv().Store().TemporaryPost().Get(th.Context, post.Id, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, initialExpireAt, tmpPostAfterFirstReveal.ExpireAt)
|
|
|
|
// Now user3 reveals - this should trigger updateTemporaryPostIfAllRead
|
|
_, appErr = th.App.RevealPost(th.Context, post, user3.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
// Fetch the TemporaryPost with cache bypass to verify it was updated
|
|
tmpPostAfterAllReveal, err := th.App.Srv().Store().TemporaryPost().Get(th.Context, post.Id, false)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the ExpireAt was updated to match the shortest receipt ExpireAt
|
|
require.Less(t, tmpPostAfterAllReveal.ExpireAt, initialExpireAt)
|
|
// Should be close to the first recipient's ExpireAt (within a few ms due to timing)
|
|
require.InDelta(t, receipt.ExpireAt, tmpPostAfterAllReveal.ExpireAt, 10000) // 10 second tolerance
|
|
})
|
|
}
|
|
|
|
func TestBurnPost(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable BurnOnRead feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
|
|
|
|
// feature flag, configuration and license is not checked for this feature
|
|
// so we set these to enable the feature to create a burn on read post
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.EnableBurnOnRead = model.NewPointer(true)
|
|
})
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, th.BasicChannel) // author of the post
|
|
th.AddUserToChannel(t, th.BasicUser2, th.BasicChannel) // recipient of the post
|
|
|
|
// Helper to create a burn-on-read post
|
|
createBurnOnReadPost := func() *model.Post {
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn on read message",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
post.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(model.DefaultExpirySeconds*1000))
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
return createdPost
|
|
}
|
|
|
|
// Helper to create a regular post
|
|
createRegularPost := func() *model.Post {
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "regular message",
|
|
}
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
return createdPost
|
|
}
|
|
|
|
t.Run("burn on read post", func(t *testing.T) {
|
|
post := createBurnOnReadPost()
|
|
|
|
appErr := th.App.BurnPost(th.Context, post, th.BasicUser.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
// Verify post is deleted
|
|
post, err := th.App.Srv().Store().Post().GetSingle(th.Context, post.Id, false)
|
|
require.Error(t, err)
|
|
require.Nil(t, post)
|
|
require.True(t, store.IsErrNotFound(err))
|
|
})
|
|
|
|
t.Run("regular post", func(t *testing.T) {
|
|
post := createRegularPost()
|
|
|
|
appErr := th.App.BurnPost(th.Context, post, th.BasicUser.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "app.burn_post.not_burn_on_read.app_error", appErr.Id)
|
|
})
|
|
|
|
t.Run("read receipt does not exist", func(t *testing.T) {
|
|
post := createBurnOnReadPost()
|
|
|
|
appErr := th.App.BurnPost(th.Context, post, th.BasicUser2.Id, "")
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "app.burn_post.not_revealed.app_error", appErr.Id)
|
|
})
|
|
|
|
t.Run("read receipt exists but expired", func(t *testing.T) {
|
|
post := createBurnOnReadPost()
|
|
|
|
// Create an expired read receipt
|
|
expiredTime := model.GetMillis() - 1000 // Expired 1 second ago
|
|
expiredReceipt := &model.ReadReceipt{
|
|
UserID: th.BasicUser2.Id,
|
|
PostID: post.Id,
|
|
ExpireAt: expiredTime,
|
|
}
|
|
_, err := th.App.Srv().Store().ReadReceipt().Save(th.Context, expiredReceipt)
|
|
require.NoError(t, err)
|
|
|
|
appErr := th.App.BurnPost(th.Context, post, th.BasicUser2.Id, "")
|
|
require.Nil(t, appErr) // this is a no-op
|
|
|
|
// Verify receipt ExpireAt is unchanged (no-op)
|
|
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, post.Id, th.BasicUser2.Id)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expiredTime, receipt.ExpireAt)
|
|
})
|
|
|
|
t.Run("read receipt exists and not expired", func(t *testing.T) {
|
|
post := createBurnOnReadPost()
|
|
|
|
// Create a read receipt that is not expired
|
|
notExpiredReceipt := &model.ReadReceipt{
|
|
UserID: th.BasicUser2.Id,
|
|
PostID: post.Id,
|
|
ExpireAt: model.GetMillis() + 10000, // Not expired 10 seconds from now
|
|
}
|
|
_, err := th.App.Srv().Store().ReadReceipt().Save(th.Context, notExpiredReceipt)
|
|
require.NoError(t, err)
|
|
|
|
appErr := th.App.BurnPost(th.Context, post, th.BasicUser2.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
// Verify receipt ExpireAt is updated to current time
|
|
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, post.Id, th.BasicUser2.Id)
|
|
require.NoError(t, err)
|
|
require.LessOrEqual(t, receipt.ExpireAt, model.GetMillis())
|
|
})
|
|
}
|
|
|
|
func TestGetFlaggedPostsWithExpiredBurnOnRead(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable BurnOnRead feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
|
|
|
|
// Create a second user for testing
|
|
user2 := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, user2, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, th.BasicChannel)
|
|
|
|
t.Run("expired burn-on-read post should not be returned in flagged posts", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
// Create a burn-on-read post
|
|
borPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn on read message",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
borPost.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(10*1000)) // 10 seconds
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, borPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
|
|
// User2 reveals the post
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, createdPost, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, revealedPost)
|
|
|
|
// User2 saves/flags the post
|
|
preference := model.Preference{
|
|
UserId: user2.Id,
|
|
Category: model.PreferenceCategoryFlaggedPost,
|
|
Name: createdPost.Id,
|
|
Value: "true",
|
|
}
|
|
err := th.App.Srv().Store().Preference().Save(model.Preferences{preference})
|
|
require.NoError(t, err)
|
|
|
|
// Verify post appears in flagged posts before expiration
|
|
flaggedPosts, appErr := th.App.GetFlaggedPosts(th.Context, user2.Id, 0, 10)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, flaggedPosts)
|
|
require.Contains(t, flaggedPosts.Order, createdPost.Id)
|
|
require.NotNil(t, flaggedPosts.Posts[createdPost.Id])
|
|
|
|
// Simulate expiration by updating the receipt's ExpireAt to the past
|
|
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, createdPost.Id, user2.Id)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, receipt)
|
|
|
|
receipt.ExpireAt = model.GetMillis() - 1000 // 1 second in the past
|
|
_, err = th.App.Srv().Store().ReadReceipt().Update(th.Context, receipt)
|
|
require.NoError(t, err)
|
|
|
|
// Get flagged posts again - expired post should be filtered out
|
|
flaggedPosts, appErr = th.App.GetFlaggedPosts(th.Context, user2.Id, 0, 10)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, flaggedPosts)
|
|
require.NotContains(t, flaggedPosts.Order, createdPost.Id, "Expired burn-on-read post should not be in flagged posts")
|
|
require.Nil(t, flaggedPosts.Posts[createdPost.Id], "Expired burn-on-read post should not be in posts map")
|
|
})
|
|
|
|
t.Run("expired burn-on-read post should not be returned in flagged posts for team", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
// Create a burn-on-read post
|
|
borPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn on read team message",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
borPost.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(10*1000))
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, borPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// User2 reveals and flags the post
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, createdPost, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, revealedPost)
|
|
|
|
preference := model.Preference{
|
|
UserId: user2.Id,
|
|
Category: model.PreferenceCategoryFlaggedPost,
|
|
Name: createdPost.Id,
|
|
Value: "true",
|
|
}
|
|
err := th.App.Srv().Store().Preference().Save(model.Preferences{preference})
|
|
require.NoError(t, err)
|
|
|
|
// Expire the receipt
|
|
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, createdPost.Id, user2.Id)
|
|
require.NoError(t, err)
|
|
receipt.ExpireAt = model.GetMillis() - 1000
|
|
_, err = th.App.Srv().Store().ReadReceipt().Update(th.Context, receipt)
|
|
require.NoError(t, err)
|
|
|
|
// Get flagged posts for team
|
|
flaggedPosts, appErr := th.App.GetFlaggedPostsForTeam(th.Context, user2.Id, th.BasicTeam.Id, 0, 10)
|
|
require.Nil(t, appErr)
|
|
require.NotContains(t, flaggedPosts.Order, createdPost.Id)
|
|
})
|
|
|
|
t.Run("expired burn-on-read post should not be returned in flagged posts for channel", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
// Create a burn-on-read post
|
|
borPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn on read channel message",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
borPost.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(10*1000))
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, borPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// User2 reveals and flags the post
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, createdPost, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, revealedPost)
|
|
|
|
preference := model.Preference{
|
|
UserId: user2.Id,
|
|
Category: model.PreferenceCategoryFlaggedPost,
|
|
Name: createdPost.Id,
|
|
Value: "true",
|
|
}
|
|
err := th.App.Srv().Store().Preference().Save(model.Preferences{preference})
|
|
require.NoError(t, err)
|
|
|
|
// Expire the receipt
|
|
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, createdPost.Id, user2.Id)
|
|
require.NoError(t, err)
|
|
receipt.ExpireAt = model.GetMillis() - 1000
|
|
_, err = th.App.Srv().Store().ReadReceipt().Update(th.Context, receipt)
|
|
require.NoError(t, err)
|
|
|
|
// Get flagged posts for channel
|
|
flaggedPosts, appErr := th.App.GetFlaggedPostsForChannel(th.Context, user2.Id, th.BasicChannel.Id, 0, 10)
|
|
require.Nil(t, appErr)
|
|
require.NotContains(t, flaggedPosts.Order, createdPost.Id)
|
|
})
|
|
|
|
t.Run("non-expired burn-on-read post should appear in flagged posts", func(t *testing.T) {
|
|
enableBoRFeature(th)
|
|
|
|
// Create a burn-on-read post with long expiration
|
|
borPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "burn on read message still valid",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
borPost.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(3600*1000)) // 1 hour
|
|
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, borPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// User2 reveals and flags the post
|
|
revealedPost, appErr := th.App.RevealPost(th.Context, createdPost, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, revealedPost)
|
|
|
|
preference := model.Preference{
|
|
UserId: user2.Id,
|
|
Category: model.PreferenceCategoryFlaggedPost,
|
|
Name: createdPost.Id,
|
|
Value: "true",
|
|
}
|
|
err := th.App.Srv().Store().Preference().Save(model.Preferences{preference})
|
|
require.NoError(t, err)
|
|
|
|
// Get flagged posts - post should be present
|
|
flaggedPosts, appErr := th.App.GetFlaggedPosts(th.Context, user2.Id, 0, 10)
|
|
require.Nil(t, appErr)
|
|
require.Contains(t, flaggedPosts.Order, createdPost.Id, "Non-expired burn-on-read post should be in flagged posts")
|
|
require.NotNil(t, flaggedPosts.Posts[createdPost.Id])
|
|
|
|
// Verify metadata is populated correctly
|
|
post := flaggedPosts.Posts[createdPost.Id]
|
|
require.NotNil(t, post.Metadata)
|
|
require.NotZero(t, post.Metadata.ExpireAt)
|
|
require.Greater(t, post.Metadata.ExpireAt, model.GetMillis())
|
|
})
|
|
}
|
|
|
|
func TestBurnOnReadRestrictionsForDMsAndBots(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable BurnOnRead feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.EnableBurnOnRead = model.NewPointer(true)
|
|
cfg.ServiceSettings.BurnOnReadMaximumTimeToLiveSeconds = model.NewPointer(600)
|
|
cfg.ServiceSettings.BurnOnReadDurationSeconds = model.NewPointer(600)
|
|
})
|
|
|
|
t.Run("should allow burn-on-read posts in direct messages with another user", func(t *testing.T) {
|
|
// Create a direct message channel between two different users
|
|
dmChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, model.ChannelTypeDirect, dmChannel.Type)
|
|
|
|
post := &model.Post{
|
|
ChannelId: dmChannel.Id,
|
|
Message: "This is a burn-on-read message in DM",
|
|
UserId: th.BasicUser.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
createdPost, _, err := th.App.CreatePost(th.Context, post, dmChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, createdPost)
|
|
require.Equal(t, model.PostTypeBurnOnRead, createdPost.Type)
|
|
})
|
|
|
|
t.Run("should allow burn-on-read posts in group messages", func(t *testing.T) {
|
|
// Create a group message channel with at least 3 users
|
|
user3 := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, user3, th.BasicTeam)
|
|
gmChannel := th.CreateGroupChannel(t, th.BasicUser2, user3)
|
|
require.Equal(t, model.ChannelTypeGroup, gmChannel.Type)
|
|
|
|
// This should succeed - group messages allow BoR
|
|
post := &model.Post{
|
|
ChannelId: gmChannel.Id,
|
|
Message: "This is a burn-on-read message in GM",
|
|
UserId: th.BasicUser.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
createdPost, _, err := th.App.CreatePost(th.Context, post, gmChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, createdPost)
|
|
require.Equal(t, model.PostTypeBurnOnRead, createdPost.Type)
|
|
})
|
|
|
|
t.Run("should allow burn-on-read posts from bot users", func(t *testing.T) {
|
|
// Create a bot user
|
|
bot := &model.Bot{
|
|
Username: "testbot",
|
|
DisplayName: "Test Bot",
|
|
Description: "Test Bot for burn-on-read (bots can send BoR for OTP, integrations, etc.)",
|
|
OwnerId: th.BasicUser.Id,
|
|
}
|
|
createdBot, appErr := th.App.CreateBot(th.Context, bot)
|
|
require.Nil(t, appErr)
|
|
|
|
// Get the bot user
|
|
botUser, appErr := th.App.GetUser(createdBot.UserId)
|
|
require.Nil(t, appErr)
|
|
require.True(t, botUser.IsBot)
|
|
|
|
// Create a burn-on-read post as bot (should succeed - bots can send BoR)
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "This is a burn-on-read message from bot",
|
|
UserId: botUser.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
createdPost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, createdPost)
|
|
require.Equal(t, model.PostTypeBurnOnRead, createdPost.Type)
|
|
})
|
|
|
|
t.Run("should reject burn-on-read posts in self DMs", func(t *testing.T) {
|
|
// Create a self DM channel (user messaging themselves)
|
|
selfDMChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, model.ChannelTypeDirect, selfDMChannel.Type)
|
|
|
|
// Try to create a burn-on-read post in self DM
|
|
post := &model.Post{
|
|
ChannelId: selfDMChannel.Id,
|
|
Message: "This is a burn-on-read message to myself",
|
|
UserId: th.BasicUser.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
_, _, err := th.App.CreatePost(th.Context, post, selfDMChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "api.post.fill_in_post_props.burn_on_read.self_dm.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("should reject burn-on-read posts in DMs with bots/AI agents", func(t *testing.T) {
|
|
// Create a bot user
|
|
bot := &model.Bot{
|
|
Username: "aiagent",
|
|
DisplayName: "AI Agent",
|
|
Description: "Test AI Agent for burn-on-read restrictions",
|
|
OwnerId: th.BasicUser.Id,
|
|
}
|
|
createdBot, appErr := th.App.CreateBot(th.Context, bot)
|
|
require.Nil(t, appErr)
|
|
|
|
// Get the bot user
|
|
botUser, appErr := th.App.GetUser(createdBot.UserId)
|
|
require.Nil(t, appErr)
|
|
require.True(t, botUser.IsBot)
|
|
|
|
// Create a DM channel between the regular user and the bot
|
|
dmWithBotChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, botUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, model.ChannelTypeDirect, dmWithBotChannel.Type)
|
|
|
|
// Try to create a burn-on-read post in DM with bot (regular user sending)
|
|
post := &model.Post{
|
|
ChannelId: dmWithBotChannel.Id,
|
|
Message: "This is a burn-on-read message to AI agent",
|
|
UserId: th.BasicUser.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
_, _, err := th.App.CreatePost(th.Context, post, dmWithBotChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "api.post.fill_in_post_props.burn_on_read.bot_dm.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("should reject burn-on-read posts in DMs with deleted users", func(t *testing.T) {
|
|
// Create a user that we'll delete
|
|
userToDelete := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, userToDelete, th.BasicTeam)
|
|
|
|
// Create a DM channel between the regular user and the user we'll delete
|
|
dmChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, userToDelete.Id)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, model.ChannelTypeDirect, dmChannel.Type)
|
|
|
|
// Delete the user
|
|
appErr = th.App.PermanentDeleteUser(th.Context, userToDelete)
|
|
require.Nil(t, appErr)
|
|
|
|
// Try to create a burn-on-read post in DM with deleted user
|
|
post := &model.Post{
|
|
ChannelId: dmChannel.Id,
|
|
Message: "This is a burn-on-read message to deleted user",
|
|
UserId: th.BasicUser.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
// This should fail because we can't validate the other user (deleted)
|
|
_, _, err := th.App.CreatePost(th.Context, post, dmChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "api.post.fill_in_post_props.burn_on_read.user.app_error", err.Id)
|
|
})
|
|
|
|
t.Run("should allow burn-on-read posts in public channels", func(t *testing.T) {
|
|
// This should succeed - public channel, regular user
|
|
require.Equal(t, model.ChannelTypeOpen, th.BasicChannel.Type)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "This is a burn-on-read message in public channel",
|
|
UserId: th.BasicUser.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
createdPost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, createdPost)
|
|
require.Equal(t, model.PostTypeBurnOnRead, createdPost.Type)
|
|
})
|
|
|
|
t.Run("should allow burn-on-read posts in private channels", func(t *testing.T) {
|
|
// Create a private channel using helper
|
|
createdPrivateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
require.Equal(t, model.ChannelTypePrivate, createdPrivateChannel.Type)
|
|
|
|
// This should succeed - private channel, regular user
|
|
post := &model.Post{
|
|
ChannelId: createdPrivateChannel.Id,
|
|
Message: "This is a burn-on-read message in private channel",
|
|
UserId: th.BasicUser.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
createdPost, _, err := th.App.CreatePost(th.Context, post, createdPrivateChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, createdPost)
|
|
require.Equal(t, model.PostTypeBurnOnRead, createdPost.Type)
|
|
})
|
|
}
|
|
|
|
func TestGetBurnOnReadPost(t *testing.T) {
|
|
t.Run("success - temporary post found", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "placeholder message",
|
|
FileIds: model.StringArray{"file1"},
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
temporaryPost := &model.TemporaryPost{
|
|
ID: post.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
ExpireAt: model.GetMillis() + 3600000,
|
|
Message: "actual secret message",
|
|
FileIDs: model.StringArray{"file2", "file3"},
|
|
}
|
|
|
|
_, err := th.App.Srv().Store().TemporaryPost().Save(th.Context, temporaryPost)
|
|
require.NoError(t, err)
|
|
|
|
resultPost, appErr := th.App.getBurnOnReadPost(th.Context, post)
|
|
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, resultPost)
|
|
assert.Equal(t, temporaryPost.Message, resultPost.Message)
|
|
assert.Equal(t, temporaryPost.FileIDs, resultPost.FileIds)
|
|
// Ensure original post is not modified
|
|
assert.Equal(t, "placeholder message", post.Message)
|
|
assert.Equal(t, model.StringArray{"file1"}, post.FileIds)
|
|
})
|
|
|
|
t.Run("temporary post not found - returns app error", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "placeholder message",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
|
|
resultPost, appErr := th.App.getBurnOnReadPost(th.Context, post)
|
|
|
|
require.NotNil(t, appErr)
|
|
require.Nil(t, resultPost)
|
|
assert.Equal(t, "app.post.get_post.app_error", appErr.Id)
|
|
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
|
})
|
|
}
|
|
|
|
func TestPostChannelMentionsWithPrivateChannels(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channel := th.BasicChannel
|
|
user := th.BasicUser
|
|
|
|
// Create context with session for the user to properly test sanitization
|
|
ctx := th.Context.WithSession(&model.Session{UserId: user.Id})
|
|
|
|
// Create a private channel where user IS a member
|
|
privateChannelMember, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
DisplayName: "Private Member",
|
|
Name: "private-member",
|
|
Type: model.ChannelTypePrivate,
|
|
TeamId: th.BasicTeam.Id,
|
|
}, false)
|
|
require.Nil(t, err)
|
|
th.AddUserToChannel(t, user, privateChannelMember)
|
|
|
|
// Create a private channel where user is NOT a member
|
|
privateChannelNonMember, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
DisplayName: "Private Non-Member",
|
|
Name: "private-non-member",
|
|
Type: model.ChannelTypePrivate,
|
|
TeamId: th.BasicTeam.Id,
|
|
}, false)
|
|
require.Nil(t, err)
|
|
|
|
// Create a public channel where user is NOT a member
|
|
publicChannel, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
DisplayName: "Public Channel",
|
|
Name: "public-channel",
|
|
Type: model.ChannelTypeOpen,
|
|
TeamId: th.BasicTeam.Id,
|
|
}, false)
|
|
require.Nil(t, err)
|
|
|
|
post := &model.Post{
|
|
Message: fmt.Sprintf("~%v and ~%v and ~%v", privateChannelMember.Name, privateChannelNonMember.Name, publicChannel.Name),
|
|
ChannelId: channel.Id,
|
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
|
UserId: user.Id,
|
|
CreateAt: 0,
|
|
}
|
|
|
|
post, _, err = th.App.CreatePostAsUser(ctx, post, "", true)
|
|
require.Nil(t, err)
|
|
|
|
mentions := post.GetProp(model.PostPropsChannelMentions)
|
|
require.NotNil(t, mentions)
|
|
mentionsMap, ok := mentions.(map[string]any)
|
|
require.True(t, ok)
|
|
|
|
// Should include private channel where user IS a member
|
|
assert.Contains(t, mentionsMap, privateChannelMember.Name)
|
|
|
|
// Should NOT include private channel where user is NOT a member
|
|
assert.NotContains(t, mentionsMap, privateChannelNonMember.Name)
|
|
|
|
// Should include public channel (user has team access)
|
|
assert.Contains(t, mentionsMap, publicChannel.Name)
|
|
}
|
|
|
|
func TestGetPostsForView(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("returns posts for channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Create a few posts
|
|
post1, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "post 1",
|
|
}, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
post2, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "post 2",
|
|
}, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
|
|
options := model.GetPostsOptions{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Page: 0,
|
|
PerPage: 10,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
postList, appErr := th.App.GetPostsForView(th.Context, options)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, postList)
|
|
assert.Contains(t, postList.Posts, post1.Id)
|
|
assert.Contains(t, postList.Posts, post2.Id)
|
|
})
|
|
|
|
t.Run("returns empty list for channel with no posts", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
options := model.GetPostsOptions{
|
|
ChannelId: channel.Id,
|
|
Page: 0,
|
|
PerPage: 10,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
postList, appErr := th.App.GetPostsForView(th.Context, options)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, postList)
|
|
assert.Empty(t, postList.Posts)
|
|
})
|
|
|
|
t.Run("respects pagination", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
for i := range 5 {
|
|
_, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
ChannelId: channel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: fmt.Sprintf("post %d", i),
|
|
}, channel, model.CreatePostFlags{})
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
options := model.GetPostsOptions{
|
|
ChannelId: channel.Id,
|
|
Page: 0,
|
|
PerPage: 2,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
postList, appErr := th.App.GetPostsForView(th.Context, options)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, postList)
|
|
assert.Len(t, postList.Posts, 2)
|
|
})
|
|
|
|
t.Run("invalid channel id returns error", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
options := model.GetPostsOptions{
|
|
ChannelId: model.NewId(),
|
|
Page: 0,
|
|
PerPage: 10,
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
postList, appErr := th.App.GetPostsForView(th.Context, options)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, postList)
|
|
assert.Empty(t, postList.Posts)
|
|
})
|
|
}
|