* ci: shard server Postgres tests into 4 parallel runners (#35739) * ci: add test sharding plumbing to server CI Add infrastructure for upcoming test sharding without changing behavior: - Add shard-index and shard-total inputs to server-test-template.yml (defaults preserve existing single-runner behavior) - Add timing cache restore step (activates only when shard-total > 1) - Add merge-postgres-test-results job to server-ci.yml that: - Merges JUnit XML reports from shard artifacts - Saves timing data cache for future shard balancing - Handles both single-artifact and multi-shard scenarios - Add .gitignore entries for timing cache and shard work files Co-authored-by: Claude <claude@anthropic.com> * ci: shard server Postgres tests into 4 parallel runners Extract sharding logic into standalone, tested scripts and enable 4-shard parallel test execution for server Postgres CI: Scripts: - server/scripts/shard-split.js: Node.js bin-packing solver that assigns test packages to shards using timing data from previous runs. Two-tier strategy: light packages (<2min) whole, heavy packages (api4, app) split at individual test level. - server/scripts/run-shard-tests.sh: Multi-run wrapper that calls gotestsum directly for each package group with -run regex filters. - server/scripts/shard-split.test.js: 8 test cases covering round-robin fallback, timing-based balancing, heavy package splitting, JUnit XML fallback, and enterprise package separation. Workflow changes: - server-test-template.yml: Add shard splitting step that discovers test packages and runs the solver. Modified Run Tests step to use wrapper script when sharding is active. - server-ci.yml: Add 4-shard matrix to test-postgres-normal. Update merge job artifact patterns for shard-specific names. Performance: 7.2 min with timing cache vs 62.5 min baseline = 88% wall-time improvement. First run without cache uses JUnit XML fallback or round-robin, then populates the cache for subsequent runs. Co-authored-by: Claude <claude@anthropic.com> * fix: raise heavy package threshold to 5 min to preserve test isolation sqlstore integrity tests scan the entire database and fail when other packages' test data is present. At 182s, sqlstore was just over the 120s threshold and getting split at test level. Raising to 300s keeps only api4 (~38 min) and app (~15 min) as heavy — where the real sharding gains are — while sqlstore, elasticsearch, etc. stay whole and maintain their test isolation guarantees. Co-authored-by: Claude <claude@anthropic.com> * ci: only save test timing cache on default branch PR branches always restore from master's timing cache via restore-keys prefix matching. Timing data is stable day-to-day so this eliminates cache misses on first PR runs and reduces cache storage. Co-authored-by: Claude <claude@anthropic.com> * ci: skip FIPS tests on PRs (enterprise CI handles compile check) Per review feedback: the enterprise CI already runs a FIPS compile check on every PR. Running the full FIPS test suite on PRs is redundant since it uses the identical test suite as non-FIPS — the only FIPS-specific failure mode is a build failure from non-approved crypto imports, which the enterprise compile check catches. Full FIPS tests continue to run on every push to master. Co-authored-by: Claude <claude@anthropic.com> * fix: address review feedback on run-shard-tests.sh - Remove set -e so all test runs execute even if earlier ones fail; track failures and exit with error at the end (wiggin77) - Remove unused top-level COVERAGE_FLAG variable (wiggin77) - Fix RUN_IDX increment position so report, json, and coverage files share the same index (wiggin77) - Update workflow comment: heavy threshold is 5 min, not 2 min (wiggin77) Co-authored-by: Claude <claude@anthropic.com> * style: use node: prefix for built-in fs module in shard-split.js Co-authored-by: Claude <claude@anthropic.com> * fix: avoid interpolating file paths into generated shell script Read shard package lists from files at runtime instead of interpolating them into the generated script via printf. This prevents theoretical shell metacharacter injection from directory names, as flagged by DryRun Security. Co-authored-by: Claude <claude@anthropic.com> * fix(ci): rename merged artifact to match server-ci-report glob The merged artifact was named postgres-server-test-logs-merged which does not match the *-test-logs pattern in server-ci-report.yml, causing Postgres test results to be missing from PR/commit reports. Also pins junit-report-merger to exact version 7.0.0 for supply chain safety. Co-authored-by: Claude <claude@anthropic.com> * fix(ci): pass RACE_MODE env into Docker container RACE_MODE was set on the host runner but never included in the docker run --env list. The light-package path worked because the heredoc expanded on the host, but run-shard-tests.sh reads RACE_MODE at runtime inside the container where it was unset. This caused heavy packages (api4, app) to silently lose -race detection. Co-authored-by: Claude <claude@anthropic.com> * fix(ci): discover new tests in heavy packages not in timing cache Tests not present in the timing cache (newly added or renamed) would not appear in any shard -run regex, causing them to silently skip. After building items from the cache, run go test -list to discover current test names and assign any cache-missing tests to shards via the normal bin-packing algorithm with a small default duration. Co-authored-by: Claude <claude@anthropic.com> * fix(ci): add missing line continuation backslash in docker run The previous --env FIPS_ENABLED line was missing a trailing backslash after adding --env RACE_MODE, causing docker run to see a truncated command and fail with "requires at least 1 argument". Co-authored-by: Claude <claude@anthropic.com> * fix(ci): add setup-go step for shard test discovery go test -list in shard-split.js runs on the host runner via execSync, but Go is only available inside the Docker container. Without this step, every invocation fails silently and new-test discovery is a no-op. Adding actions/setup-go before the shard split step ensures the Go toolchain is available on the host. Co-authored-by: Claude <claude@anthropic.com> --------- Co-authored-by: Claude <claude@anthropic.com> * updated go to version 1.25.8 (#35817) * updated go to version 1.25.8 * updated gotestsum version to work with go 1.25.8 go 1.25 does not work with indirect tools 0.11 dependency pulled by gotestsum. * Use sync.WaitGroup.Go to simplify goroutine creation Replace the wg.Add(1) + go func() { defer wg.Done() }() pattern with wg.Go(), which was introduced in Go 1.25. * pushes fips image on workflow dispatch to allow fips test to run on go version update * fix new requirements for FIPS compliance imposed on updating to go 1.25.8 * updates openssl symbol check for library shipped with FIPS new versions go-openssl v2 shipped with FIPS versions starting from 1.25 uses mkcgo to generate bindings causing symbol names to be different. * removes temp workflow-dispatch condition * keep versions out of agents md file * upgrade golangci-lint (#35845) * test: clean up channel store data after TestChannelStore (#36066) TestChannelStore sub-tests create channels, members, and team members using fake TeamIds and UserIds (model.NewId() for non-existent rows). These records are left in the database and cause integrity tests (TestCheck*) running in the same binary to fail their full-table scans. Register a t.Cleanup on TestChannelStore that purges the affected tables entirely. A blanket purge is safe: the schema enforces no FK constraints, and every test suite creates its own data independently. * Fix command injection in server-test-template workflow (#36080) Replace the unquoted heredoc (which embedded GITHUB_HEAD_REF into a generated script) with a cp of the existing run-shard-tests.sh, which already handles the light-only case. Pass BUILD_NUMBER and TEST_TARGET as explicit docker env vars instead of interpolating them into script content. * fix(ci): restore testname format in sharded gotestsum runs (#36078) run-shard-tests.sh called gotestsum directly without --format, so it fell back to gotestsum's default (pkgname) instead of the testname format set by the Makefile. Pass --format "${GOTESTSUM_FORMAT:-testname}" to match the Makefile default. Co-authored-by: Mattermost Build <build@mattermost.com> * fix(lint): fix pre-existing golangci-lint v2.11.4 issues Fix misspelling in comment and redundant nil check flagged by the upgraded linter. * ci: use golang image for test runner on release-10.11 mattermost-build-server images are not built for release branches. Use the official golang image which is always available for any Go version. * ci: use mattermost/mattermost-build-server for release-10.11 The mattermostdevelopment/ images are only built for master. The production mattermost/ images are built for release branches. * ci: use mattermost/mattermost-build-server in mmctl test template The mattermostdevelopment/ images are only built for master. The production mattermost/ images are built for release branches. --------- Co-authored-by: Pavel Zeman <pavel.zeman@mattermost.com> Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Carlos Garcia <carlos.garcia@mattermost.com> Co-authored-by: Mattermost Build <build@mattermost.com>
4415 строки
144 KiB
Go
4415 строки
144 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"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"
|
|
eMocks "github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
|
"github.com/mattermost/mattermost/server/v8/platform/services/imageproxy"
|
|
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine/mocks"
|
|
)
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
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}
|
|
|
|
appErr := th.App.attachFilesToPost(th.Context, post)
|
|
assert.Nil(t, appErr)
|
|
|
|
infos, _, appErr := th.App.GetFileInfosForPost(th.Context, post.Id, false, false)
|
|
assert.Nil(t, appErr)
|
|
assert.Len(t, infos, 2)
|
|
})
|
|
|
|
t.Run("should update File.PostIds after failing to add files", func(t *testing.T) {
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
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}
|
|
|
|
appErr := th.App.attachFilesToPost(th.Context, post)
|
|
assert.Nil(t, appErr)
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
archivedChannel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
post := th.CreatePost(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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
channel := th.BasicChannel
|
|
user := th.BasicUser
|
|
|
|
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(th.Context, 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(th.Context, 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(th.Context, 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)
|
|
defer th.TearDown()
|
|
|
|
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())
|
|
|
|
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: "foo",
|
|
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: "foo",
|
|
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: "foo",
|
|
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: "foo",
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
// 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 TestDeletePostInArchivedChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
archivedChannel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
post := th.CreatePost(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()
|
|
defer th.TearDown()
|
|
|
|
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 = "foo"
|
|
})
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
th.AddUserToChannel(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(model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId)
|
|
th.RemovePermissionFromRole(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(model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId)
|
|
th.AddPermissionToRole(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()
|
|
defer th.TearDown()
|
|
th.AddUserToChannel(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(th.Context, 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()
|
|
defer th.TearDown()
|
|
|
|
channelForPreview := th.CreateChannel(th.Context, 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()
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.AddUserToChannel(th.BasicUser, th.BasicChannel)
|
|
|
|
user1 := th.CreateUser()
|
|
user2 := th.CreateUser()
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.CreateUser()
|
|
user2 := th.CreateUser()
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.CreateUser()
|
|
user2 := th.CreateUser()
|
|
user3 := th.CreateUser()
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
channelForPreview := th.CreateChannel(th.Context, th.BasicTeam)
|
|
|
|
for i := 0; i < 20; i++ {
|
|
user := th.CreateUser()
|
|
th.LinkUserToTeam(user, th.BasicTeam)
|
|
th.AddUserToChannel(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 i := 0; i < n; i++ {
|
|
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()
|
|
defer th.TearDown()
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
th.AddUserToChannel(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))
|
|
})
|
|
}
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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 = "foo"
|
|
})
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
th.AddUserToChannel(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(model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId)
|
|
th.RemovePermissionFromRole(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(model.PermissionUseChannelMentions.Id, model.ChannelUserRoleId)
|
|
th.AddPermissionToRole(model.PermissionUseChannelMentions.Id, model.ChannelAdminRoleId)
|
|
})
|
|
})
|
|
}
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test",
|
|
UserId: th.BasicUser.Id,
|
|
}
|
|
|
|
channelMemberBefore, err := th.App.Srv().Store().Channel().GetMember(context.Background(), 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(context.Background(), 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()
|
|
defer th.TearDown()
|
|
|
|
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(context.Background(), 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(context.Background(), 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()
|
|
defer th.TearDown()
|
|
|
|
bot := th.CreateBot()
|
|
|
|
botUser, appErr := th.App.GetUser(bot.UserId)
|
|
require.Nil(t, appErr)
|
|
|
|
th.LinkUserToTeam(botUser, th.BasicTeam)
|
|
th.AddUserToChannel(botUser, th.BasicChannel)
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test",
|
|
UserId: bot.UserId,
|
|
}
|
|
|
|
channelMemberBefore, err := th.App.Srv().Store().Channel().GetMember(context.Background(), 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(context.Background(), 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()
|
|
defer th.TearDown()
|
|
|
|
bot := th.CreateBot()
|
|
|
|
botUser, appErr := th.App.GetUser(bot.UserId)
|
|
require.Nil(t, appErr)
|
|
|
|
th.LinkUserToTeam(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()
|
|
defer th.TearDown()
|
|
|
|
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(context.Background(), 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(context.Background(), 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()
|
|
defer th.TearDown()
|
|
|
|
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(context.Background(), 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(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, channelMemberAfter.LastViewedAt, channelMemberBefore.LastViewedAt)
|
|
})
|
|
}
|
|
|
|
func TestPatchPostInArchivedChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
archivedChannel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
post := th.CreatePost(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()
|
|
defer th.TearDown()
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
privateChannel := th.CreatePrivateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(th.BasicUser, privateChannel)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
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 = "foo"
|
|
})
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
th.AddUserToChannel(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(th.Context, 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()
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.AddUserToChannel(th.BasicUser, th.BasicChannel)
|
|
|
|
user1 := th.CreateUser()
|
|
user2 := th.CreateUser()
|
|
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()
|
|
|
|
th.AddUserToChannel(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")
|
|
})
|
|
}
|
|
|
|
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()
|
|
|
|
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)
|
|
defer th.TearDown()
|
|
|
|
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)
|
|
defer th.TearDown()
|
|
|
|
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)
|
|
defer th.TearDown()
|
|
|
|
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)
|
|
defer th.TearDown()
|
|
|
|
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)
|
|
defer th.TearDown()
|
|
|
|
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)
|
|
defer th.TearDown()
|
|
|
|
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)
|
|
defer th.TearDown()
|
|
|
|
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)
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.PostPriority = true
|
|
})
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
|
|
|
|
user1 := th.BasicUser
|
|
|
|
channel := th.CreateChannel(th.Context, 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()
|
|
defer th.TearDown()
|
|
|
|
id := model.NewId()
|
|
guest := &model.User{
|
|
Email: "success+" + id + "@simulator.amazonses.com",
|
|
Username: "un_" + id,
|
|
Nickname: "nn_" + id,
|
|
Password: "Password1",
|
|
EmailVerified: true,
|
|
}
|
|
guest, err := th.App.CreateGuest(th.Context, guest)
|
|
require.Nil(t, err)
|
|
th.LinkUserToTeam(guest, th.BasicTeam)
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
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: "Password1",
|
|
EmailVerified: true,
|
|
}
|
|
guest, err := th.App.CreateGuest(th.Context, guest)
|
|
require.Nil(t, err)
|
|
th.LinkUserToTeam(guest, th.BasicTeam)
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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 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()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
team2 := th.CreateTeam()
|
|
th.LinkUserToTeam(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(user2)
|
|
|
|
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()
|
|
|
|
user1 := th.BasicUser
|
|
user2 := th.BasicUser2
|
|
|
|
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
|
|
|
dmChannel := th.CreateDmChannel(user2)
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
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(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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()
|
|
defer th.TearDown()
|
|
|
|
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(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(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(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(threadMembership, false)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
|
|
|
threadMembership.Following = false
|
|
threadMembership.PostId = oldID
|
|
_, appErr = th.App.GetThreadForUser(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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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(th.Context, th.BasicTeam)
|
|
th.AddUserToChannel(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(th.Context, th.BasicTeam)
|
|
defer func() {
|
|
appErr = th.App.DeleteChannel(th.Context, channel, user1.Id)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
th.LinkUserToTeam(user3, th.BasicTeam)
|
|
th.AddUserToChannel(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(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(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 TestReplyToPostWithLag(t *testing.T) {
|
|
if !replicaFlag {
|
|
t.Skipf("requires test flag -mysql-replica")
|
|
}
|
|
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
if *th.App.Config().SqlSettings.DriverName != model.DatabaseDriverMysql {
|
|
t.Skipf("requires %q database driver", model.DatabaseDriverMysql)
|
|
}
|
|
|
|
mainHelper.SQLStore.UpdateLicense(model.NewTestLicense("somelicense"))
|
|
|
|
t.Run("replication lag time great than reply time", func(t *testing.T) {
|
|
err := mainHelper.SetReplicationLagForTesting(5)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
err := mainHelper.SetReplicationLagForTesting(0)
|
|
require.NoError(t, err)
|
|
}()
|
|
mainHelper.ToggleReplicasOn()
|
|
defer mainHelper.ToggleReplicasOff()
|
|
|
|
root, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "root post",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
|
|
reply, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser2.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
RootId: root.Id,
|
|
Message: fmt.Sprintf("@%s", th.BasicUser2.Username),
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, reply)
|
|
})
|
|
}
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
sharedChannelService := NewMockSharedChannelService(th.Server.GetSharedChannelSyncService())
|
|
th.Server.SetSharedChannelSyncService(sharedChannelService)
|
|
testCluster := &testlib.FakeClusterInterface{}
|
|
th.Server.Platform().SetCluster(testCluster)
|
|
|
|
user := th.BasicUser
|
|
|
|
channel := th.CreateChannel(th.Context, 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()
|
|
defer th.TearDown()
|
|
|
|
sharedChannelService := NewMockSharedChannelService(th.Server.GetSharedChannelSyncService())
|
|
th.Server.SetSharedChannelSyncService(sharedChannelService)
|
|
testCluster := &testlib.FakeClusterInterface{}
|
|
th.Server.Platform().SetCluster(testCluster)
|
|
|
|
user := th.BasicUser
|
|
|
|
channel := th.CreateChannel(th.Context, 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()
|
|
defer th.TearDown()
|
|
|
|
sharedChannelService := NewMockSharedChannelService(th.Server.GetSharedChannelSyncService())
|
|
th.Server.SetSharedChannelSyncService(sharedChannelService)
|
|
testCluster := &testlib.FakeClusterInterface{}
|
|
th.Server.Platform().SetCluster(testCluster)
|
|
|
|
user := th.BasicUser
|
|
|
|
channel := th.CreateChannel(th.Context, 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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
t.Run("Private channel", func(t *testing.T) {
|
|
privateChannel := th.CreatePrivateChannel(th.Context, 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(th.Context, 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)
|
|
})
|
|
}
|
|
|
|
func TestShouldNotRefollowOnOthersReply(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.ThreadAutoFollow = true
|
|
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
|
|
})
|
|
|
|
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)
|
|
defer th.TearDown()
|
|
|
|
r, err := th.App.GetLastAccessiblePostTime()
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, int64(0), r)
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
|
|
|
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
|
|
|
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)
|
|
|
|
mockSystemStore = storemocks.SystemStore{}
|
|
mockStore.On("System").Return(&mockSystemStore)
|
|
mockSystemStore.On("GetByName", mock.Anything).Return(nil, errors.New("test"))
|
|
_, err = th.App.GetLastAccessiblePostTime()
|
|
assert.NotNil(t, err)
|
|
|
|
mockSystemStore = storemocks.SystemStore{}
|
|
mockStore.On("System").Return(&mockSystemStore)
|
|
mockSystemStore.On("GetByName", mock.Anything).Return(&model.System{Name: model.SystemLastAccessiblePostTime, Value: "10"}, nil)
|
|
r, err = th.App.GetLastAccessiblePostTime()
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, int64(10), r)
|
|
}
|
|
|
|
func TestComputeLastAccessiblePostTime(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("Updates the time, if cloud limit is applicable", func(t *testing.T) {
|
|
th := SetupWithStoreMock(t)
|
|
defer th.TearDown()
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
|
|
|
cloud := &eMocks.CloudInterface{}
|
|
th.App.Srv().Cloud = cloud
|
|
|
|
// cloud-starter, limit is applicable
|
|
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(&model.ProductLimits{
|
|
Messages: &model.MessagesLimits{
|
|
History: model.NewPointer(1),
|
|
},
|
|
}, nil)
|
|
|
|
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
|
mockPostStore := storemocks.PostStore{}
|
|
mockPostStore.On("GetNthRecentPostTime", mock.Anything).Return(int64(1), 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)
|
|
|
|
mockSystemStore.AssertCalled(t, "SaveOrUpdate", mock.Anything)
|
|
})
|
|
|
|
t.Run("Remove the time if cloud limit is NOT applicable", func(t *testing.T) {
|
|
th := SetupWithStoreMock(t)
|
|
defer th.TearDown()
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
|
|
|
cloud := &eMocks.CloudInterface{}
|
|
th.App.Srv().Cloud = cloud
|
|
|
|
// enterprise, limit is NOT applicable
|
|
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, nil)
|
|
|
|
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
|
mockSystemStore := storemocks.SystemStore{}
|
|
mockSystemStore.On("GetByName", mock.Anything).Return(&model.System{Name: model.SystemLastAccessiblePostTime, Value: "10"}, nil)
|
|
mockSystemStore.On("PermanentDeleteByName", mock.Anything).Return(nil, nil)
|
|
mockStore.On("System").Return(&mockSystemStore)
|
|
|
|
err := th.App.ComputeLastAccessiblePostTime()
|
|
assert.NoError(t, err)
|
|
|
|
mockSystemStore.AssertNotCalled(t, "SaveOrUpdate", mock.Anything)
|
|
mockSystemStore.AssertCalled(t, "PermanentDeleteByName", mock.Anything)
|
|
})
|
|
}
|
|
|
|
func TestGetEditHistoryForPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
// 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()
|
|
defer th.TearDown()
|
|
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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)
|
|
})
|
|
}
|
|
|
|
func TestSendTestMessage(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
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()
|
|
defer th.TearDown()
|
|
|
|
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(th.BasicChannel, func(post *model.Post) {
|
|
post.FileIds = model.StringArray{fileInfo1.Id}
|
|
})
|
|
|
|
post2 := th.CreatePost(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(th.BasicChannel, func(post *model.Post) {
|
|
post.FileIds = model.StringArray{fileInfo1.Id}
|
|
})
|
|
|
|
post2 := th.CreatePost(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(th.BasicChannel, func(post *model.Post) {
|
|
post.FileIds = model.StringArray{fileInfo1.Id}
|
|
})
|
|
|
|
post2 := th.CreatePost(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()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.GuestAccountsSettings.Enable = true
|
|
})
|
|
|
|
guestUser := th.CreateGuest()
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guestUser.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
privateChannel := th.CreatePrivateChannel(th.Context, 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(th.BasicChannel)
|
|
post2 := th.CreatePost(privateChannel)
|
|
post3 := th.CreatePost(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}
|
|
|
|
_, 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)
|
|
})
|
|
|
|
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}
|
|
|
|
_, 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)
|
|
})
|
|
|
|
t.Run("should filter posts when guest does not have read_channel_content permission", func(t *testing.T) {
|
|
channelGuestRole, appErr := th.App.GetRoleByName(context.Background(), 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}
|
|
|
|
_, 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)
|
|
})
|
|
|
|
t.Run("should handle empty post list", func(t *testing.T) {
|
|
postList := model.NewPostList()
|
|
_, 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)
|
|
})
|
|
|
|
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}
|
|
|
|
_, 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)
|
|
})
|
|
|
|
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}
|
|
|
|
_, 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)
|
|
})
|
|
}
|