Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2154 lines
68 KiB
Go
2154 lines
68 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package storetest
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"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/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
)
|
|
|
|
func TestThreadStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
|
t.Run("ThreadStorePopulation", func(t *testing.T) { testThreadStorePopulation(t, rctx, ss) })
|
|
t.Run("ThreadStorePermanentDeleteBatchForRetentionPolicies", func(t *testing.T) {
|
|
testThreadStorePermanentDeleteBatchForRetentionPolicies(t, rctx, ss)
|
|
})
|
|
t.Run("ThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies", func(t *testing.T) {
|
|
testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t, rctx, ss, s)
|
|
})
|
|
t.Run("GetTeamsUnreadForUser", func(t *testing.T) { testGetTeamsUnreadForUser(t, rctx, ss) })
|
|
t.Run("GetVarious", func(t *testing.T) { testVarious(t, rctx, ss) })
|
|
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, rctx, ss) })
|
|
t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, rctx, ss) })
|
|
t.Run("DeleteMembershipsForChannel", func(t *testing.T) { testDeleteMembershipsForChannel(t, rctx, ss) })
|
|
t.Run("SaveMultipleMemberships", func(t *testing.T) { testSaveMultipleMemberships(t, ss) })
|
|
t.Run("MaintainMultipleFromImport", func(t *testing.T) { testMaintainMultipleFromImport(t, rctx, ss) })
|
|
t.Run("UpdateTeamIdForChannelThreads", func(t *testing.T) { testUpdateTeamIdForChannelThreads(t, rctx, ss) })
|
|
}
|
|
|
|
func testThreadStorePopulation(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
makeSomePosts := func(urgent bool) []*model.Post {
|
|
u1 := model.User{
|
|
Email: MakeEmail(),
|
|
Username: model.NewUsername(),
|
|
}
|
|
|
|
u, err := ss.User().Save(rctx, &u1)
|
|
require.NoError(t, err)
|
|
|
|
c, err2 := ss.Channel().Save(rctx, &model.Channel{
|
|
DisplayName: model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
Name: model.NewId(),
|
|
}, 999)
|
|
require.NoError(t, err2)
|
|
|
|
_, err44 := ss.Channel().SaveMember(rctx, &model.ChannelMember{
|
|
ChannelId: c.Id,
|
|
UserId: u1.Id,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
MsgCount: 0,
|
|
})
|
|
require.NoError(t, err44)
|
|
o := model.Post{}
|
|
o.ChannelId = c.Id
|
|
o.UserId = u.Id
|
|
o.Message = NewTestID()
|
|
|
|
if urgent {
|
|
o.Metadata = &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
RequestedAck: model.NewPointer(false),
|
|
PersistentNotifications: model.NewPointer(false),
|
|
},
|
|
}
|
|
}
|
|
|
|
otmp, err3 := ss.Post().Save(rctx, &o)
|
|
require.NoError(t, err3)
|
|
o2 := model.Post{}
|
|
o2.ChannelId = c.Id
|
|
o2.UserId = model.NewId()
|
|
o2.RootId = otmp.Id
|
|
o2.Message = NewTestID()
|
|
|
|
o3 := model.Post{}
|
|
o3.ChannelId = c.Id
|
|
o3.UserId = u.Id
|
|
o3.RootId = otmp.Id
|
|
o3.Message = NewTestID()
|
|
|
|
o4 := model.Post{}
|
|
o4.ChannelId = c.Id
|
|
o4.UserId = model.NewId()
|
|
o4.Message = NewTestID()
|
|
|
|
newPosts, errIdx, err3 := ss.Post().SaveMultiple(rctx, []*model.Post{&o2, &o3, &o4})
|
|
|
|
opts := model.GetPostsOptions{
|
|
SkipFetchThreads: true,
|
|
}
|
|
olist, _ := ss.Post().Get(rctx, otmp.Id, opts, "", map[string]bool{})
|
|
o1 := olist.Posts[olist.Order[0]]
|
|
|
|
newPosts = append([]*model.Post{o1}, newPosts...)
|
|
require.NoError(t, err3, "couldn't save item")
|
|
require.Equal(t, -1, errIdx)
|
|
require.Len(t, newPosts, 4)
|
|
require.Equal(t, int64(2), newPosts[0].ReplyCount)
|
|
require.Equal(t, int64(2), newPosts[1].ReplyCount)
|
|
require.Equal(t, int64(2), newPosts[2].ReplyCount)
|
|
require.Equal(t, int64(0), newPosts[3].ReplyCount)
|
|
|
|
return newPosts
|
|
}
|
|
t.Run("Save replies creates a thread", func(t *testing.T) {
|
|
newPosts := makeSomePosts(false)
|
|
thread, err := ss.Thread().Get(newPosts[0].Id)
|
|
require.NoError(t, err, "couldn't get thread")
|
|
require.NotNil(t, thread)
|
|
require.Equal(t, int64(2), thread.ReplyCount)
|
|
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, thread.Participants)
|
|
|
|
teamId := model.NewId()
|
|
channel, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: teamId,
|
|
DisplayName: "DisplayName1",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
o5 := model.Post{}
|
|
o5.ChannelId = channel.Id
|
|
o5.UserId = model.NewId()
|
|
o5.RootId = newPosts[0].Id
|
|
o5.Message = NewTestID()
|
|
|
|
_, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&o5})
|
|
require.NoError(t, err, "couldn't save item")
|
|
|
|
thread, err = ss.Thread().Get(newPosts[0].Id)
|
|
require.NoError(t, err, "couldn't get thread")
|
|
require.NotNil(t, thread)
|
|
require.Equal(t, int64(3), thread.ReplyCount)
|
|
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId, o5.UserId}, thread.Participants)
|
|
})
|
|
|
|
t.Run("Delete a reply updates count on a thread", func(t *testing.T) {
|
|
newPosts := makeSomePosts(false)
|
|
thread, err := ss.Thread().Get(newPosts[0].Id)
|
|
require.NoError(t, err, "couldn't get thread")
|
|
require.NotNil(t, thread)
|
|
require.Equal(t, int64(2), thread.ReplyCount)
|
|
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, thread.Participants)
|
|
|
|
err = ss.Post().Delete(rctx, newPosts[1].Id, 1234, model.NewId())
|
|
require.NoError(t, err, "couldn't delete post")
|
|
|
|
thread, err = ss.Thread().Get(newPosts[0].Id)
|
|
require.NoError(t, err, "couldn't get thread")
|
|
require.NotNil(t, thread)
|
|
require.Equal(t, int64(1), thread.ReplyCount)
|
|
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId}, thread.Participants)
|
|
})
|
|
|
|
t.Run("Update reply should update the UpdateAt of the thread", func(t *testing.T) {
|
|
teamId := model.NewId()
|
|
channel, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: teamId,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
rootPost := model.Post{}
|
|
rootPost.RootId = model.NewId()
|
|
rootPost.ChannelId = channel.Id
|
|
rootPost.UserId = model.NewId()
|
|
rootPost.Message = NewTestID()
|
|
|
|
replyPost := model.Post{}
|
|
replyPost.ChannelId = rootPost.ChannelId
|
|
replyPost.UserId = model.NewId()
|
|
replyPost.Message = NewTestID()
|
|
replyPost.RootId = rootPost.RootId
|
|
|
|
newPosts, _, err := ss.Post().SaveMultiple(rctx, []*model.Post{&rootPost, &replyPost})
|
|
require.NoError(t, err)
|
|
|
|
thread1, err := ss.Thread().Get(newPosts[0].RootId)
|
|
require.NoError(t, err)
|
|
|
|
rrootPost, err := ss.Post().GetSingle(rctx, rootPost.Id, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, rrootPost.UpdateAt, rootPost.UpdateAt)
|
|
|
|
replyPost2 := model.Post{}
|
|
replyPost2.ChannelId = rootPost.ChannelId
|
|
replyPost2.UserId = model.NewId()
|
|
replyPost2.Message = NewTestID()
|
|
replyPost2.RootId = rootPost.Id
|
|
|
|
replyPost3 := model.Post{}
|
|
replyPost3.ChannelId = rootPost.ChannelId
|
|
replyPost3.UserId = model.NewId()
|
|
replyPost3.Message = NewTestID()
|
|
replyPost3.RootId = rootPost.Id
|
|
|
|
_, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&replyPost2, &replyPost3})
|
|
require.NoError(t, err)
|
|
|
|
rrootPost2, err := ss.Post().GetSingle(rctx, rootPost.Id, false)
|
|
require.NoError(t, err)
|
|
require.Greater(t, rrootPost2.UpdateAt, rrootPost.UpdateAt)
|
|
|
|
thread2, err := ss.Thread().Get(rootPost.Id)
|
|
require.NoError(t, err)
|
|
require.Greater(t, thread2.LastReplyAt, thread1.LastReplyAt)
|
|
})
|
|
|
|
t.Run("Deleting reply should update the thread", func(t *testing.T) {
|
|
teamId := model.NewId()
|
|
channel, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: teamId,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
o1 := model.Post{}
|
|
o1.ChannelId = channel.Id
|
|
o1.UserId = model.NewId()
|
|
o1.Message = NewTestID()
|
|
rootPost, err := ss.Post().Save(rctx, &o1)
|
|
require.NoError(t, err)
|
|
|
|
o2 := model.Post{}
|
|
o2.RootId = rootPost.Id
|
|
o2.ChannelId = rootPost.ChannelId
|
|
o2.UserId = model.NewId()
|
|
o2.Message = NewTestID()
|
|
replyPost, err := ss.Post().Save(rctx, &o2)
|
|
require.NoError(t, err)
|
|
|
|
o3 := model.Post{}
|
|
o3.RootId = rootPost.Id
|
|
o3.ChannelId = rootPost.ChannelId
|
|
o3.UserId = o2.UserId
|
|
o3.Message = NewTestID()
|
|
replyPost2, err := ss.Post().Save(rctx, &o3)
|
|
require.NoError(t, err)
|
|
|
|
o4 := model.Post{}
|
|
o4.RootId = rootPost.Id
|
|
o4.ChannelId = rootPost.ChannelId
|
|
o4.UserId = model.NewId()
|
|
o4.Message = NewTestID()
|
|
replyPost3, err := ss.Post().Save(rctx, &o4)
|
|
require.NoError(t, err)
|
|
|
|
thread, err := ss.Thread().Get(rootPost.Id)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, thread.ReplyCount, 3)
|
|
require.EqualValues(t, thread.Participants, model.StringArray{replyPost.UserId, replyPost3.UserId})
|
|
|
|
err = ss.Post().Delete(rctx, replyPost2.Id, 123, model.NewId())
|
|
require.NoError(t, err)
|
|
thread, err = ss.Thread().Get(rootPost.Id)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, thread.ReplyCount, 2)
|
|
require.EqualValues(t, thread.Participants, model.StringArray{replyPost.UserId, replyPost3.UserId})
|
|
|
|
err = ss.Post().Delete(rctx, replyPost.Id, 123, model.NewId())
|
|
require.NoError(t, err)
|
|
thread, err = ss.Thread().Get(rootPost.Id)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, thread.ReplyCount, 1)
|
|
require.EqualValues(t, thread.Participants, model.StringArray{replyPost3.UserId})
|
|
})
|
|
|
|
t.Run("Deleting root post should delete the thread", func(t *testing.T) {
|
|
teamId := model.NewId()
|
|
channel, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: teamId,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
rootPost := model.Post{}
|
|
rootPost.ChannelId = channel.Id
|
|
rootPost.UserId = model.NewId()
|
|
rootPost.Message = NewTestID()
|
|
|
|
newPosts1, _, err := ss.Post().SaveMultiple(rctx, []*model.Post{&rootPost})
|
|
require.NoError(t, err)
|
|
|
|
replyPost := model.Post{}
|
|
replyPost.ChannelId = rootPost.ChannelId
|
|
replyPost.UserId = model.NewId()
|
|
replyPost.Message = NewTestID()
|
|
replyPost.RootId = newPosts1[0].Id
|
|
|
|
_, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&replyPost})
|
|
require.NoError(t, err)
|
|
|
|
thread1, err := ss.Thread().Get(newPosts1[0].Id)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, thread1.ReplyCount, 1)
|
|
require.Len(t, thread1.Participants, 1)
|
|
|
|
err = ss.Post().PermanentDeleteByUser(rctx, rootPost.UserId)
|
|
require.NoError(t, err)
|
|
|
|
thread2, _ := ss.Thread().Get(rootPost.Id)
|
|
require.Nil(t, thread2)
|
|
})
|
|
|
|
t.Run("Thread membership 'viewed' timestamp is updated properly", func(t *testing.T) {
|
|
newPosts := makeSomePosts(false)
|
|
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: true,
|
|
}
|
|
tm, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
|
|
require.NoError(t, e)
|
|
require.Equal(t, int64(0), tm.LastViewed)
|
|
|
|
// No update since array has same elements.
|
|
th, e := ss.Thread().Get(newPosts[0].Id)
|
|
require.NoError(t, e)
|
|
assert.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, th.Participants)
|
|
|
|
opts.UpdateViewedTimestamp = true
|
|
_, e = ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
|
|
require.NoError(t, e)
|
|
m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
|
|
require.NoError(t, err2)
|
|
require.Greater(t, m2.LastViewed, int64(0))
|
|
|
|
// Adding a new participant
|
|
_, e = ss.Thread().MaintainMembership("newuser", newPosts[0].Id, opts)
|
|
require.NoError(t, e)
|
|
th, e = ss.Thread().Get(newPosts[0].Id)
|
|
require.NoError(t, e)
|
|
assert.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId, "newuser"}, th.Participants)
|
|
})
|
|
|
|
t.Run("Thread membership 'viewed' timestamp is updated properly for new membership", func(t *testing.T) {
|
|
newPosts := makeSomePosts(false)
|
|
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: false,
|
|
UpdateViewedTimestamp: true,
|
|
UpdateParticipants: false,
|
|
}
|
|
tm, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
|
|
require.NoError(t, e)
|
|
require.NotEqual(t, int64(0), tm.LastViewed)
|
|
})
|
|
|
|
t.Run("Updating post does not make thread unread", func(t *testing.T) {
|
|
newPosts := makeSomePosts(false)
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
m, err := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
|
|
require.NoError(t, err)
|
|
th, err := ss.Thread().GetThreadForUser(rctx, m, false, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(2), th.UnreadReplies)
|
|
|
|
m.LastViewed = newPosts[2].UpdateAt + 1
|
|
_, err = ss.Thread().UpdateMembership(m)
|
|
require.NoError(t, err)
|
|
th, err = ss.Thread().GetThreadForUser(rctx, m, false, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(0), th.UnreadReplies)
|
|
|
|
editedPost := newPosts[2].Clone()
|
|
editedPost.Message = "This is an edited post"
|
|
_, err = ss.Post().Update(rctx, editedPost, newPosts[2])
|
|
require.NoError(t, err)
|
|
|
|
th, err = ss.Thread().GetThreadForUser(rctx, m, false, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(0), th.UnreadReplies)
|
|
})
|
|
|
|
t.Run("Empty participantID should not appear in thread response", func(t *testing.T) {
|
|
newPosts := makeSomePosts(false)
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: true,
|
|
}
|
|
m, err := ss.Thread().MaintainMembership("", newPosts[0].Id, opts)
|
|
require.NoError(t, err)
|
|
m.UserId = newPosts[0].UserId
|
|
th, err := ss.Thread().GetThreadForUser(rctx, m, true, false)
|
|
require.NoError(t, err)
|
|
for _, user := range th.Participants {
|
|
require.NotNil(t, user)
|
|
}
|
|
})
|
|
t.Run("Get unread reply counts for thread", func(t *testing.T) {
|
|
t.Skip("MM-41797")
|
|
newPosts := makeSomePosts(false)
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: true,
|
|
UpdateParticipants: false,
|
|
}
|
|
|
|
_, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
|
|
require.NoError(t, e)
|
|
|
|
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
|
|
require.NoError(t, err1)
|
|
|
|
unreads, err := ss.Thread().GetThreadUnreadReplyCount(m)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(0), unreads)
|
|
|
|
err = ss.Thread().MarkAsRead(newPosts[0].UserId, newPosts[0].Id, newPosts[0].CreateAt)
|
|
require.NoError(t, err)
|
|
m, err = ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
|
|
require.NoError(t, err)
|
|
|
|
unreads, err = ss.Thread().GetThreadUnreadReplyCount(m)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(2), unreads)
|
|
})
|
|
|
|
testCases := []bool{true, false}
|
|
|
|
for _, isUrgent := range testCases {
|
|
t.Run("Return is urgent for user thread/s", func(t *testing.T) {
|
|
newPosts := makeSomePosts(isUrgent)
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: true,
|
|
UpdateParticipants: false,
|
|
}
|
|
|
|
userID := newPosts[0].UserId
|
|
_, e := ss.Thread().MaintainMembership(userID, newPosts[0].Id, opts)
|
|
require.NoError(t, e)
|
|
|
|
m, e := ss.Thread().GetMembershipForUser(userID, newPosts[0].Id)
|
|
require.NoError(t, e)
|
|
|
|
th, e := ss.Thread().GetThreadForUser(rctx, m, false, true)
|
|
require.NoError(t, e)
|
|
require.Equal(t, isUrgent, th.IsUrgent)
|
|
|
|
threads, e := ss.Thread().GetThreadsForUser(rctx, userID, "", model.GetUserThreadsOpts{IncludeIsUrgent: true})
|
|
require.NoError(t, e)
|
|
require.Equal(t, isUrgent, threads[0].IsUrgent)
|
|
})
|
|
}
|
|
}
|
|
|
|
func threadStoreCreateReply(t *testing.T, rctx request.CTX, ss store.Store, channelID, postID, userID string, createAt int64) *model.Post {
|
|
t.Helper()
|
|
reply, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channelID,
|
|
UserId: userID,
|
|
CreateAt: createAt,
|
|
RootId: postID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
return reply
|
|
}
|
|
|
|
func testThreadStorePermanentDeleteBatchForRetentionPolicies(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
const limit = 1000
|
|
team, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "DisplayName",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
channel, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team.Id,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel.Id,
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
threadStoreCreateReply(t, rctx, ss, channel.Id, post.Id, post.UserId, 2000)
|
|
|
|
thread, err := ss.Thread().Get(post.Id)
|
|
require.NoError(t, err)
|
|
|
|
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
|
|
RetentionPolicy: model.RetentionPolicy{
|
|
DisplayName: "DisplayName",
|
|
PostDurationDays: model.NewPointer(int64(30)),
|
|
},
|
|
ChannelIDs: []string{channel.Id},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
nowMillis := thread.LastReplyAt + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
|
|
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
|
|
Now: nowMillis,
|
|
GlobalPolicyEndTime: 0,
|
|
Limit: limit,
|
|
}, model.RetentionPolicyCursor{})
|
|
require.NoError(t, err)
|
|
thread, err = ss.Thread().Get(post.Id)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, thread, "thread should have been deleted by channel policy")
|
|
|
|
// create a new thread
|
|
threadStoreCreateReply(t, rctx, ss, channel.Id, post.Id, post.UserId, 2000)
|
|
thread, err = ss.Thread().Get(post.Id)
|
|
require.NoError(t, err)
|
|
|
|
// Create a team policy which is stricter than the channel policy
|
|
teamPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
|
|
RetentionPolicy: model.RetentionPolicy{
|
|
DisplayName: "DisplayName",
|
|
PostDurationDays: model.NewPointer(int64(20)),
|
|
},
|
|
TeamIDs: []string{team.Id},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
nowMillis = thread.LastReplyAt + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1
|
|
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
|
|
Now: nowMillis,
|
|
GlobalPolicyEndTime: 0,
|
|
Limit: limit,
|
|
}, model.RetentionPolicyCursor{})
|
|
require.NoError(t, err)
|
|
_, err = ss.Thread().Get(post.Id)
|
|
require.NoError(t, err, "channel policy should have overridden team policy")
|
|
|
|
// Delete channel policy and re-run team policy
|
|
err = ss.RetentionPolicy().Delete(channelPolicy.ID)
|
|
require.NoError(t, err)
|
|
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
|
|
Now: nowMillis,
|
|
GlobalPolicyEndTime: 0,
|
|
Limit: limit,
|
|
}, model.RetentionPolicyCursor{})
|
|
require.NoError(t, err)
|
|
thread, err = ss.Thread().Get(post.Id)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, thread, "thread should have been deleted by team policy")
|
|
}
|
|
|
|
func testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
|
const limit = 1000
|
|
userID := model.NewId()
|
|
createThreadMembership := func(userID, postID string) *model.ThreadMembership {
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
require.NoError(t, err)
|
|
threadMembership, err := ss.Thread().GetMembershipForUser(userID, postID)
|
|
require.NoError(t, err)
|
|
return threadMembership
|
|
}
|
|
team, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "DisplayName",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
channel, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team.Id,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel.Id,
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
threadStoreCreateReply(t, rctx, ss, channel.Id, post.Id, post.UserId, 2000)
|
|
|
|
threadMembership := createThreadMembership(userID, post.Id)
|
|
|
|
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
|
|
RetentionPolicy: model.RetentionPolicy{
|
|
DisplayName: "DisplayName",
|
|
PostDurationDays: model.NewPointer(int64(30)),
|
|
},
|
|
ChannelIDs: []string{channel.Id},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
nowMillis := threadMembership.LastUpdated + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
|
|
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(model.RetentionPolicyBatchConfigs{
|
|
Now: nowMillis,
|
|
GlobalPolicyEndTime: 0,
|
|
Limit: limit,
|
|
}, model.RetentionPolicyCursor{})
|
|
require.NoError(t, err)
|
|
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
|
|
require.Error(t, err, "thread membership should have been deleted by channel policy")
|
|
|
|
// create a new thread membership
|
|
threadMembership = createThreadMembership(userID, post.Id)
|
|
|
|
// Create a team policy which is stricter than the channel policy
|
|
teamPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
|
|
RetentionPolicy: model.RetentionPolicy{
|
|
DisplayName: "DisplayName",
|
|
PostDurationDays: model.NewPointer(int64(20)),
|
|
},
|
|
TeamIDs: []string{team.Id},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
nowMillis = threadMembership.LastUpdated + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1
|
|
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(model.RetentionPolicyBatchConfigs{
|
|
Now: nowMillis,
|
|
GlobalPolicyEndTime: 0,
|
|
Limit: limit,
|
|
}, model.RetentionPolicyCursor{})
|
|
require.NoError(t, err)
|
|
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
|
|
require.NoError(t, err, "channel policy should have overridden team policy")
|
|
|
|
// Delete channel policy and re-run team policy
|
|
err = ss.RetentionPolicy().Delete(channelPolicy.ID)
|
|
require.NoError(t, err)
|
|
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(model.RetentionPolicyBatchConfigs{
|
|
Now: nowMillis,
|
|
GlobalPolicyEndTime: 0,
|
|
Limit: limit,
|
|
}, model.RetentionPolicyCursor{})
|
|
require.NoError(t, err)
|
|
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
|
|
require.Error(t, err, "thread membership should have been deleted by team policy")
|
|
|
|
// create a new thread membership
|
|
createThreadMembership(userID, post.Id)
|
|
|
|
// Delete team policy and thread
|
|
err = ss.RetentionPolicy().Delete(teamPolicy.ID)
|
|
require.NoError(t, err)
|
|
_, err = s.GetMaster().Exec("DELETE FROM Threads WHERE PostId='" + post.Id + "'")
|
|
require.NoError(t, err)
|
|
|
|
deleted, err := ss.Thread().DeleteOrphanedRows(1000)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, deleted)
|
|
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
|
|
require.Error(t, err, "thread membership should have been deleted because thread no longer exists")
|
|
}
|
|
|
|
func testGetTeamsUnreadForUser(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
userID := model.NewId()
|
|
createThreadMembership := func(userID, postID string) {
|
|
t.Helper()
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
require.NoError(t, err)
|
|
}
|
|
team1, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "DisplayName",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
channel1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team1.Id,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: userID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
threadStoreCreateReply(t, rctx, ss, channel1.Id, post.Id, post.UserId, model.GetMillis())
|
|
createThreadMembership(userID, post.Id)
|
|
|
|
teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, true)
|
|
require.NoError(t, err)
|
|
assert.Len(t, teamsUnread, 1)
|
|
assert.Equal(t, int64(1), teamsUnread[team1.Id].ThreadCount)
|
|
|
|
post, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: userID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
threadStoreCreateReply(t, rctx, ss, channel1.Id, post.Id, post.UserId, model.GetMillis())
|
|
createThreadMembership(userID, post.Id)
|
|
|
|
teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, true)
|
|
require.NoError(t, err)
|
|
assert.Len(t, teamsUnread, 1)
|
|
assert.Equal(t, int64(2), teamsUnread[team1.Id].ThreadCount)
|
|
|
|
team2, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "DisplayName",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
channel2, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team2.Id,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
post2, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel2.Id,
|
|
UserId: userID,
|
|
Message: model.NewRandomString(10),
|
|
Metadata: &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
RequestedAck: model.NewPointer(false),
|
|
PersistentNotifications: model.NewPointer(false),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
threadStoreCreateReply(t, rctx, ss, channel2.Id, post2.Id, post2.UserId, model.GetMillis())
|
|
createThreadMembership(userID, post2.Id)
|
|
|
|
teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id, team2.Id}, true)
|
|
require.NoError(t, err)
|
|
assert.Len(t, teamsUnread, 2)
|
|
assert.Equal(t, int64(2), teamsUnread[team1.Id].ThreadCount)
|
|
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadCount)
|
|
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: true,
|
|
}
|
|
_, err = ss.Thread().MaintainMembership(userID, post2.Id, opts)
|
|
require.NoError(t, err)
|
|
|
|
teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team2.Id}, true)
|
|
require.NoError(t, err)
|
|
assert.Len(t, teamsUnread, 1)
|
|
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadCount)
|
|
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadMentionCount)
|
|
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadUrgentMentionCount)
|
|
}
|
|
|
|
type byPostId []*model.Post
|
|
|
|
func (a byPostId) Len() int { return len(a) }
|
|
func (a byPostId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a byPostId) Less(i, j int) bool { return a[i].Id < a[j].Id }
|
|
|
|
func testVarious(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
createThreadMembership := func(userID, postID string, isMention bool) {
|
|
t.Helper()
|
|
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: isMention,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
viewThread := func(userID, postID string) {
|
|
t.Helper()
|
|
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: true,
|
|
UpdateParticipants: false,
|
|
}
|
|
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
user1, err := ss.User().Save(rctx, &model.User{
|
|
Username: "user1" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
})
|
|
require.NoError(t, err)
|
|
user2, err := ss.User().Save(rctx, &model.User{
|
|
Username: "user2" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
user1ID := user1.Id
|
|
user2ID := user2.Id
|
|
|
|
team1, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "Team1",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team2, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "Team2",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team1channel1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team1.Id,
|
|
DisplayName: "Channel1",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
team2channel1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team2.Id,
|
|
DisplayName: "Channel2",
|
|
Name: "channel" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
dm1, err := ss.Channel().CreateDirectChannel(rctx, &model.User{Id: user1ID}, &model.User{Id: user2ID})
|
|
require.NoError(t, err)
|
|
|
|
gm1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
DisplayName: "GM",
|
|
Name: "gm" + model.NewId(),
|
|
Type: model.ChannelTypeGroup,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
team1channel1post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team1channel1.Id,
|
|
UserId: user1ID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team1channel1post2, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team1channel1.Id,
|
|
UserId: user1ID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team1channel1post3, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team1channel1.Id,
|
|
UserId: user1ID,
|
|
Message: model.NewRandomString(10),
|
|
Metadata: &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
RequestedAck: model.NewPointer(false),
|
|
PersistentNotifications: model.NewPointer(false),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team2channel1post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team2channel1.Id,
|
|
UserId: user1ID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team2channel1post2deleted, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team2channel1.Id,
|
|
UserId: user1ID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
dm1post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: dm1.Id,
|
|
UserId: user1ID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gm1post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: gm1.Id,
|
|
UserId: user1ID,
|
|
Message: model.NewRandomString(10),
|
|
Metadata: &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
RequestedAck: model.NewPointer(false),
|
|
PersistentNotifications: model.NewPointer(false),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
postNames := map[string]string{
|
|
team1channel1post1.Id: "team1channel1post1",
|
|
team1channel1post2.Id: "team1channel1post2",
|
|
team1channel1post3.Id: "team1channel1post3",
|
|
team2channel1post1.Id: "team2channel1post1",
|
|
team2channel1post2deleted.Id: "team2channel1post2deleted",
|
|
dm1post1.Id: "dm1post1",
|
|
gm1post1.Id: "gm1post1",
|
|
}
|
|
|
|
threadStoreCreateReply(t, rctx, ss, team1channel1.Id, team1channel1post1.Id, user2ID, model.GetMillis())
|
|
threadStoreCreateReply(t, rctx, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis())
|
|
threadStoreCreateReply(t, rctx, ss, team1channel1.Id, team1channel1post3.Id, user2ID, model.GetMillis())
|
|
threadStoreCreateReply(t, rctx, ss, team2channel1.Id, team2channel1post1.Id, user2ID, model.GetMillis())
|
|
threadStoreCreateReply(t, rctx, ss, team2channel1.Id, team2channel1post2deleted.Id, user2ID, model.GetMillis())
|
|
threadStoreCreateReply(t, rctx, ss, dm1.Id, dm1post1.Id, user2ID, model.GetMillis())
|
|
threadStoreCreateReply(t, rctx, ss, gm1.Id, gm1post1.Id, user2ID, model.GetMillis())
|
|
|
|
// Create thread memberships, with simulated unread mentions.
|
|
createThreadMembership(user1ID, team1channel1post1.Id, false)
|
|
createThreadMembership(user1ID, team1channel1post2.Id, false)
|
|
createThreadMembership(user1ID, team1channel1post3.Id, true)
|
|
createThreadMembership(user1ID, team2channel1post1.Id, false)
|
|
createThreadMembership(user1ID, team2channel1post2deleted.Id, false)
|
|
createThreadMembership(user1ID, dm1post1.Id, false)
|
|
createThreadMembership(user1ID, gm1post1.Id, true)
|
|
|
|
// Have user1 view a subset of the threads
|
|
viewThread(user1ID, team1channel1post1.Id)
|
|
viewThread(user2ID, team1channel1post2.Id)
|
|
viewThread(user1ID, team2channel1post1.Id)
|
|
viewThread(user1ID, dm1post1.Id)
|
|
|
|
// Add reply to a viewed thread to confirm it's unread again.
|
|
time.Sleep(2 * time.Millisecond)
|
|
threadStoreCreateReply(t, rctx, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis())
|
|
|
|
// Actually make team2channel1post2deleted deleted
|
|
err = ss.Post().Delete(rctx, team2channel1post2deleted.Id, model.GetMillis(), user1ID)
|
|
require.NoError(t, err)
|
|
|
|
// Re-fetch posts to ensure metadata up-to-date
|
|
allPosts := []*model.Post{
|
|
team1channel1post1,
|
|
team1channel1post2,
|
|
team1channel1post3,
|
|
team2channel1post1,
|
|
team2channel1post2deleted,
|
|
dm1post1,
|
|
gm1post1,
|
|
}
|
|
for i := range allPosts {
|
|
updatedPost, err := ss.Post().GetSingle(rctx, allPosts[i].Id, true)
|
|
require.NoError(t, err)
|
|
|
|
// Fix some inconsistencies with how the post store returns posts vs. how the
|
|
// thread store returns it.
|
|
if updatedPost.RemoteId == nil {
|
|
updatedPost.RemoteId = new(string)
|
|
}
|
|
|
|
// Also, we don't populate ReplyCount for posts when querying threads, so don't
|
|
// assert same.
|
|
updatedPost.ReplyCount = 0
|
|
|
|
updatedPost.ShallowCopy(allPosts[i])
|
|
}
|
|
|
|
t.Run("GetTotalUnreadThreads", func(t *testing.T) {
|
|
testCases := []struct {
|
|
Description string
|
|
UserID string
|
|
TeamID string
|
|
Options model.GetUserThreadsOpts
|
|
|
|
ExpectedThreads []*model.Post
|
|
}{
|
|
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post2, team1channel1post3, gm1post1,
|
|
}},
|
|
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post2, team1channel1post3, gm1post1,
|
|
}},
|
|
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
|
|
team1channel1post2, team1channel1post3, gm1post1, // (no deleted threads in team1)
|
|
}},
|
|
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
|
gm1post1, // (no unread threads in team2)
|
|
}},
|
|
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
|
|
team2channel1post2deleted, gm1post1,
|
|
}},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
totalUnreadThreads, err := ss.Thread().GetTotalUnreadThreads(testCase.UserID, testCase.TeamID, testCase.Options)
|
|
require.NoError(t, err)
|
|
|
|
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadThreads)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("GetTotalThreads", func(t *testing.T) {
|
|
testCases := []struct {
|
|
Description string
|
|
UserID string
|
|
TeamID string
|
|
Options model.GetUserThreadsOpts
|
|
|
|
ExpectedThreads []*model.Post
|
|
}{
|
|
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post1, team1channel1post2, team1channel1post3, team2channel1post1, dm1post1, gm1post1,
|
|
}},
|
|
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1,
|
|
}},
|
|
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
|
|
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1, // (no deleted threads in team1)
|
|
}},
|
|
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
|
team2channel1post1, dm1post1, gm1post1,
|
|
}},
|
|
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
|
|
team2channel1post1, team2channel1post2deleted, dm1post1, gm1post1,
|
|
}},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
totalThreads, err := ss.Thread().GetTotalThreads(testCase.UserID, testCase.TeamID, testCase.Options)
|
|
require.NoError(t, err)
|
|
|
|
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalThreads)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("GetTotalUnreadMentions", func(t *testing.T) {
|
|
testCases := []struct {
|
|
Description string
|
|
UserID string
|
|
TeamID string
|
|
Options model.GetUserThreadsOpts
|
|
|
|
ExpectedThreads []*model.Post
|
|
}{
|
|
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post3, gm1post1,
|
|
}},
|
|
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post3, gm1post1,
|
|
}},
|
|
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
|
gm1post1,
|
|
}},
|
|
{"team1, user1, exclude direct", user1ID, team1.Id, model.GetUserThreadsOpts{ExcludeDirect: true}, []*model.Post{
|
|
team1channel1post3,
|
|
}},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
totalUnreadMentions, err := ss.Thread().GetTotalUnreadMentions(testCase.UserID, testCase.TeamID, testCase.Options)
|
|
require.NoError(t, err)
|
|
|
|
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadMentions)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("GetTotalUnreadUrgentMentions", func(t *testing.T) {
|
|
testCases := []struct {
|
|
Description string
|
|
UserID string
|
|
TeamID string
|
|
Options model.GetUserThreadsOpts
|
|
ExpectedThreads []*model.Post
|
|
}{
|
|
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post3, gm1post1,
|
|
}},
|
|
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post3, gm1post1,
|
|
}},
|
|
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{gm1post1}},
|
|
{"team1, user1, exclude direct", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{team1channel1post3}},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
totalUnreadUrgentMentions, err := ss.Thread().GetTotalUnreadUrgentMentions(testCase.UserID, testCase.TeamID, testCase.Options)
|
|
require.NoError(t, err)
|
|
|
|
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadUrgentMentions)
|
|
})
|
|
}
|
|
})
|
|
|
|
assertThreadPosts := func(t *testing.T, threads []*model.ThreadResponse, expectedPosts []*model.Post) {
|
|
t.Helper()
|
|
|
|
actualPosts := make([]*model.Post, 0, len(threads))
|
|
actualPostNames := make([]string, 0, len(threads))
|
|
for _, thread := range threads {
|
|
actualPosts = append(actualPosts, thread.Post)
|
|
postName, ok := postNames[thread.PostId]
|
|
require.True(t, ok, "failed to find actual %s in post names", thread.PostId)
|
|
actualPostNames = append(actualPostNames, postName)
|
|
}
|
|
sort.Strings(actualPostNames)
|
|
|
|
expectedPostNames := make([]string, 0, len(expectedPosts))
|
|
for _, post := range expectedPosts {
|
|
postName, ok := postNames[post.Id]
|
|
require.True(t, ok, "failed to find expected %s in post names", post.Id)
|
|
expectedPostNames = append(expectedPostNames, postName)
|
|
}
|
|
sort.Strings(expectedPostNames)
|
|
|
|
assert.Equal(t, expectedPostNames, actualPostNames)
|
|
|
|
// Check posts themselves
|
|
sort.Sort(byPostId(expectedPosts))
|
|
sort.Sort(byPostId(actualPosts))
|
|
if assert.Len(t, actualPosts, len(expectedPosts)) {
|
|
for i := range actualPosts {
|
|
assert.Equal(t, expectedPosts[i], actualPosts[i], "mismatch comparing expected post %s with actual post %s", postNames[expectedPosts[i].Id], postNames[actualPosts[i].Id])
|
|
}
|
|
} else {
|
|
assert.Equal(t, expectedPosts, actualPosts)
|
|
}
|
|
|
|
// Check common fields between threads and posts.
|
|
for _, thread := range threads {
|
|
assert.Equal(t, thread.DeleteAt, thread.Post.DeleteAt, "expected Thread.DeleteAt == Post.DeleteAt")
|
|
}
|
|
}
|
|
|
|
t.Run("GetThreadsForUser", func(t *testing.T) {
|
|
testCases := []struct {
|
|
Description string
|
|
UserID string
|
|
TeamID string
|
|
Options model.GetUserThreadsOpts
|
|
|
|
ExpectedThreads []*model.Post
|
|
}{
|
|
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post1, team1channel1post2, team1channel1post3, team2channel1post1, dm1post1, gm1post1,
|
|
}},
|
|
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
|
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1,
|
|
}},
|
|
{"team1, user1, unread", user1ID, team1.Id, model.GetUserThreadsOpts{Unread: true}, []*model.Post{
|
|
team1channel1post2, team1channel1post3, gm1post1,
|
|
}},
|
|
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
|
|
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1, // (no deleted threads in team1)
|
|
}},
|
|
{"team1, user1, unread + deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Unread: true, Deleted: true}, []*model.Post{
|
|
team1channel1post2, team1channel1post3, gm1post1, // (no deleted threads in team1)
|
|
}},
|
|
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
|
team2channel1post1, dm1post1, gm1post1,
|
|
}},
|
|
{"team2, user1, exclude direct", user1ID, team2.Id, model.GetUserThreadsOpts{ExcludeDirect: true}, []*model.Post{
|
|
team2channel1post1,
|
|
}},
|
|
{"team2, user1, unread", user1ID, team2.Id, model.GetUserThreadsOpts{Unread: true}, []*model.Post{
|
|
gm1post1, // (no unread in team2)
|
|
}},
|
|
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
|
|
team2channel1post1, team2channel1post2deleted, dm1post1, gm1post1,
|
|
}},
|
|
{"team2, user1, unread + deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Unread: true, Deleted: true}, []*model.Post{
|
|
team2channel1post2deleted, gm1post1,
|
|
}},
|
|
{"team2, user1, unread + deleted + exclude direct", user1ID, team2.Id, model.GetUserThreadsOpts{Unread: true, Deleted: true, ExcludeDirect: true}, []*model.Post{
|
|
team2channel1post2deleted,
|
|
}},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
threads, err := ss.Thread().GetThreadsForUser(rctx, testCase.UserID, testCase.TeamID, testCase.Options)
|
|
require.NoError(t, err)
|
|
|
|
assertThreadPosts(t, threads, testCase.ExpectedThreads)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run(("GetThreadMembershipsForExport"), func(t *testing.T) {
|
|
t.Run("Get members for thread, ensure usernames", func(t *testing.T) {
|
|
members, err := ss.Thread().GetThreadMembershipsForExport(team1channel1post1.Id)
|
|
require.NoError(t, err)
|
|
|
|
// team1channel1post1 has 1 member
|
|
assert.Len(t, members, 1)
|
|
|
|
userIDs, err := ss.Thread().GetThreadFollowers(team1channel1post1.Id, true)
|
|
require.NoError(t, err)
|
|
require.Len(t, userIDs, 1)
|
|
|
|
u, err := ss.User().Get(context.Background(), userIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, u.Username, members[0].Username)
|
|
|
|
members, err = ss.Thread().GetThreadMembershipsForExport(team1channel1post2.Id)
|
|
require.NoError(t, err)
|
|
|
|
// team1channel1post2 has 2 members
|
|
assert.Len(t, members, 2)
|
|
|
|
userIDs, err = ss.Thread().GetThreadFollowers(team1channel1post2.Id, true)
|
|
require.NoError(t, err)
|
|
require.Len(t, userIDs, 2)
|
|
|
|
for i := range userIDs {
|
|
u, err := ss.User().Get(context.Background(), userIDs[i])
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, u.Username, members[i].Username)
|
|
}
|
|
})
|
|
|
|
t.Run("Get members for a thread, ensure only following members are exported", func(t *testing.T) {
|
|
createThreadMembership(user2ID, team1channel1post1.Id, false)
|
|
|
|
members, err := ss.Thread().GetThreadMembershipsForExport(team1channel1post1.Id)
|
|
require.NoError(t, err)
|
|
|
|
// team1channel1post1 should have 2 members
|
|
assert.Len(t, members, 2)
|
|
|
|
_, err = ss.Thread().MaintainMembership(user2ID, team1channel1post1.Id, store.ThreadMembershipOpts{
|
|
Following: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
members, err = ss.Thread().GetThreadMembershipsForExport(team1channel1post1.Id)
|
|
require.NoError(t, err)
|
|
|
|
// team1channel1post1 should have 1 following member
|
|
assert.Len(t, members, 1)
|
|
|
|
userIDs, err := ss.Thread().GetThreadFollowers(team1channel1post1.Id, true)
|
|
require.NoError(t, err)
|
|
require.Len(t, userIDs, 1)
|
|
|
|
u, err := ss.User().Get(context.Background(), userIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, u.Username, members[0].Username)
|
|
})
|
|
})
|
|
}
|
|
|
|
func testMarkAllAsReadByChannels(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
postingUserId := model.NewId()
|
|
userAID := model.NewId()
|
|
userBID := model.NewId()
|
|
|
|
team1, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "Team1",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
channel1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team1.Id,
|
|
DisplayName: "Channel1",
|
|
Name: "channel1" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
channel2, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team1.Id,
|
|
DisplayName: "Channel2",
|
|
Name: "channel2" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
createThreadMembership := func(userID, postID string) {
|
|
t.Helper()
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
assertThreadReplyCount := func(t *testing.T, userID string, count int64) {
|
|
t.Helper()
|
|
|
|
teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, teamsUnread, 1, "unexpected unread teams count")
|
|
assert.Equal(t, count, teamsUnread[team1.Id].ThreadCount, "unexpected thread count")
|
|
}
|
|
|
|
t.Run("empty set of channels", func(t *testing.T) {
|
|
err := ss.Thread().MarkAllAsReadByChannels(model.NewId(), []string{})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("single channel", func(t *testing.T) {
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserId,
|
|
Message: "Root",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserId,
|
|
RootId: post.Id,
|
|
Message: "Reply",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createThreadMembership(userAID, post.Id)
|
|
createThreadMembership(userBID, post.Id)
|
|
|
|
assertThreadReplyCount(t, userAID, 1)
|
|
assertThreadReplyCount(t, userBID, 1)
|
|
|
|
err = ss.Thread().MarkAllAsReadByChannels(userAID, []string{channel1.Id})
|
|
require.NoError(t, err)
|
|
|
|
assertThreadReplyCount(t, userAID, 0)
|
|
assertThreadReplyCount(t, userBID, 1)
|
|
|
|
err = ss.Thread().MarkAllAsReadByChannels(userBID, []string{channel1.Id})
|
|
require.NoError(t, err)
|
|
|
|
assertThreadReplyCount(t, userAID, 0)
|
|
assertThreadReplyCount(t, userBID, 0)
|
|
})
|
|
|
|
t.Run("multiple channels", func(t *testing.T) {
|
|
post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserId,
|
|
Message: "Root",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserId,
|
|
RootId: post1.Id,
|
|
Message: "Reply",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
post2, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel2.Id,
|
|
UserId: postingUserId,
|
|
Message: "Root",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel2.Id,
|
|
UserId: postingUserId,
|
|
RootId: post2.Id,
|
|
Message: "Reply",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createThreadMembership(userAID, post1.Id)
|
|
createThreadMembership(userBID, post1.Id)
|
|
createThreadMembership(userAID, post2.Id)
|
|
createThreadMembership(userBID, post2.Id)
|
|
|
|
assertThreadReplyCount(t, userAID, 2)
|
|
assertThreadReplyCount(t, userBID, 2)
|
|
|
|
err = ss.Thread().MarkAllAsReadByChannels(userAID, []string{channel1.Id, channel2.Id})
|
|
require.NoError(t, err)
|
|
|
|
assertThreadReplyCount(t, userAID, 0)
|
|
assertThreadReplyCount(t, userBID, 2)
|
|
|
|
err = ss.Thread().MarkAllAsReadByChannels(userBID, []string{channel1.Id, channel2.Id})
|
|
require.NoError(t, err)
|
|
|
|
assertThreadReplyCount(t, userAID, 0)
|
|
assertThreadReplyCount(t, userBID, 0)
|
|
})
|
|
}
|
|
|
|
func testMarkAllAsReadByTeam(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
createThreadMembership := func(userID, postID string) {
|
|
t.Helper()
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
assertThreadReplyCount := func(t *testing.T, userID, teamID string, count int64, message string) {
|
|
t.Helper()
|
|
|
|
teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{teamID}, true)
|
|
require.NoError(t, err)
|
|
require.Lenf(t, teamsUnread, 1, "unexpected unread teams count: %s", message)
|
|
assert.Equalf(t, count, teamsUnread[teamID].ThreadCount, "unexpected thread count: %s", message)
|
|
}
|
|
|
|
postingUserId := model.NewId()
|
|
userAID := model.NewId()
|
|
userBID := model.NewId()
|
|
|
|
team1, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "Team1",
|
|
Name: "team1" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team1channel1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team1.Id,
|
|
DisplayName: "Team1: Channel1",
|
|
Name: "team1channel1" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
team1channel2, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team1.Id,
|
|
DisplayName: "Team1: Channel2",
|
|
Name: "team1channel2" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
team2, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "Team2",
|
|
Name: "team2" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team2channel1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team2.Id,
|
|
DisplayName: "Team2: Channel1",
|
|
Name: "team2channel1" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
team2channel2, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team2.Id,
|
|
DisplayName: "Team2: Channel2",
|
|
Name: "team2channel2" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
team1channel1post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team1channel1.Id,
|
|
UserId: postingUserId,
|
|
Message: "Root",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team1channel1.Id,
|
|
UserId: postingUserId,
|
|
RootId: team1channel1post1.Id,
|
|
Message: "Reply",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team1channel2post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team1channel2.Id,
|
|
UserId: postingUserId,
|
|
Message: "Root",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team1channel1.Id,
|
|
UserId: postingUserId,
|
|
RootId: team1channel2post1.Id,
|
|
Message: "Reply",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team2channel1post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team2channel1.Id,
|
|
UserId: postingUserId,
|
|
Message: "Root",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team2channel1.Id,
|
|
UserId: postingUserId,
|
|
RootId: team2channel1post1.Id,
|
|
Message: "Reply",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team2channel2post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team2channel2.Id,
|
|
UserId: postingUserId,
|
|
Message: "Root",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: team2channel1.Id,
|
|
UserId: postingUserId,
|
|
RootId: team2channel2post1.Id,
|
|
Message: "Reply",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gm1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
DisplayName: "GM1",
|
|
Name: "gm1" + model.NewId(),
|
|
Type: model.ChannelTypeGroup,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
gm1post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: gm1.Id,
|
|
UserId: postingUserId,
|
|
Message: "Root",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: gm1.Id,
|
|
UserId: postingUserId,
|
|
RootId: gm1post1.Id,
|
|
Message: "Reply",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gm2, err := ss.Channel().Save(rctx, &model.Channel{
|
|
DisplayName: "GM1",
|
|
Name: "gm1" + model.NewId(),
|
|
Type: model.ChannelTypeGroup,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
gm2post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: gm2.Id,
|
|
UserId: postingUserId,
|
|
Message: "Root",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: gm2.Id,
|
|
UserId: postingUserId,
|
|
RootId: gm2post1.Id,
|
|
Message: "Reply",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("empty team", func(t *testing.T) {
|
|
err = ss.Thread().MarkAllAsReadByTeam(model.NewId(), "")
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("unknown team", func(t *testing.T) {
|
|
err = ss.Thread().MarkAllAsReadByTeam(model.NewId(), model.NewId())
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("team1", func(t *testing.T) {
|
|
createThreadMembership(userAID, team1channel1post1.Id)
|
|
createThreadMembership(userBID, team1channel1post1.Id)
|
|
createThreadMembership(userAID, team1channel2post1.Id)
|
|
createThreadMembership(userBID, team1channel2post1.Id)
|
|
createThreadMembership(userAID, team2channel1post1.Id)
|
|
createThreadMembership(userBID, team2channel1post1.Id)
|
|
|
|
// Note that GMs (and similarly, DMs) don't count towards this API.
|
|
createThreadMembership(userAID, gm1.Id)
|
|
createThreadMembership(userBID, gm1.Id)
|
|
createThreadMembership(userAID, gm2.Id)
|
|
createThreadMembership(userBID, gm2.Id)
|
|
|
|
assertThreadReplyCount(t, userAID, team1.Id, 2, "expected 2 unread messages in team1 for userA")
|
|
assertThreadReplyCount(t, userBID, team1.Id, 2, "expected 2 unread messages in team1 for userB")
|
|
assertThreadReplyCount(t, userAID, team2.Id, 1, "expected 1 unread message in team2 for userA")
|
|
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
|
|
|
|
err = ss.Thread().MarkAllAsReadByTeam(userAID, team1.Id)
|
|
require.NoError(t, err)
|
|
|
|
assertThreadReplyCount(t, userAID, team1.Id, 0, "expected 0 unread messages in team1 for userA")
|
|
assertThreadReplyCount(t, userBID, team1.Id, 2, "expected 2 unread messages in team1 for userB")
|
|
assertThreadReplyCount(t, userAID, team2.Id, 1, "expected 1 unread message in team2 for userA")
|
|
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
|
|
|
|
err = ss.Thread().MarkAllAsReadByTeam(userBID, team1.Id)
|
|
require.NoError(t, err)
|
|
|
|
assertThreadReplyCount(t, userAID, team1.Id, 0, "expected 0 unread messages in team1 for userA")
|
|
assertThreadReplyCount(t, userBID, team1.Id, 0, "expected 0 unread messages in team1 for userB")
|
|
assertThreadReplyCount(t, userAID, team2.Id, 1, "expected 1 unread message in team2 for userA")
|
|
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
|
|
})
|
|
}
|
|
|
|
func testDeleteMembershipsForChannel(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
createThreadMembership := func(userID, postID string) (*model.ThreadMembership, func()) {
|
|
t.Helper()
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
mem, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
require.NoError(t, err)
|
|
|
|
return mem, func() {
|
|
err := ss.Thread().DeleteMembershipForUser(userID, postID)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
postingUserID := model.NewId()
|
|
userAID := model.NewId()
|
|
userBID := model.NewId()
|
|
|
|
team, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "DisplayName",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
channel1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team.Id,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel1" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
channel2, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team.Id,
|
|
DisplayName: "DisplayName2",
|
|
Name: "channel2" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
rootPost1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserID,
|
|
Message: model.NewRandomString(10),
|
|
RootId: rootPost1.Id,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
rootPost2, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel2.Id,
|
|
UserId: postingUserID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel2.Id,
|
|
UserId: postingUserID,
|
|
Message: model.NewRandomString(10),
|
|
RootId: rootPost2.Id,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("should return memberships for user", func(t *testing.T) {
|
|
memA1, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
|
defer cleanupA1()
|
|
memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
|
defer cleanupA2()
|
|
|
|
membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, membershipsA, 2)
|
|
require.ElementsMatch(t, []*model.ThreadMembership{memA1, memA2}, membershipsA)
|
|
})
|
|
|
|
t.Run("should delete memberships for user for channel", func(t *testing.T) {
|
|
_, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
|
defer cleanupA1()
|
|
memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
|
defer cleanupA2()
|
|
|
|
ss.Thread().DeleteMembershipsForChannel(userAID, channel1.Id)
|
|
membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, membershipsA, 1)
|
|
require.ElementsMatch(t, []*model.ThreadMembership{memA2}, membershipsA)
|
|
})
|
|
|
|
t.Run("deleting memberships for channel for userA should not affect userB", func(t *testing.T) {
|
|
_, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
|
defer cleanupA1()
|
|
_, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
|
defer cleanupA2()
|
|
memB1, cleanupB2 := createThreadMembership(userBID, rootPost1.Id)
|
|
defer cleanupB2()
|
|
|
|
membershipsB, err := ss.Thread().GetMembershipsForUser(userBID, team.Id)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, membershipsB, 1)
|
|
require.ElementsMatch(t, []*model.ThreadMembership{memB1}, membershipsB)
|
|
})
|
|
}
|
|
|
|
func testSaveMultipleMemberships(t *testing.T, ss store.Store) {
|
|
t.Run("should save multiple memberships", func(t *testing.T) {
|
|
memberships := []*model.ThreadMembership{
|
|
{
|
|
PostId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
Following: true,
|
|
},
|
|
{
|
|
PostId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
Following: true,
|
|
},
|
|
}
|
|
|
|
_, err := ss.Thread().SaveMultipleMemberships(memberships)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should return error if any of the memberships is invalid", func(t *testing.T) {
|
|
memberships := []*model.ThreadMembership{
|
|
{
|
|
PostId: model.NewId(),
|
|
UserId: "invalid",
|
|
Following: true,
|
|
},
|
|
{
|
|
PostId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
Following: true,
|
|
},
|
|
}
|
|
|
|
_, err := ss.Thread().SaveMultipleMemberships(memberships)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("should not fail if the list is empty", func(t *testing.T) {
|
|
_, err := ss.Thread().SaveMultipleMemberships([]*model.ThreadMembership{})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail if there is a conflict", func(t *testing.T) {
|
|
postID := model.NewId()
|
|
userID := model.NewId()
|
|
|
|
memberships := []*model.ThreadMembership{
|
|
{
|
|
PostId: postID,
|
|
UserId: userID,
|
|
Following: true,
|
|
},
|
|
{
|
|
PostId: postID,
|
|
UserId: userID,
|
|
Following: true,
|
|
},
|
|
}
|
|
|
|
_, err := ss.Thread().SaveMultipleMemberships(memberships)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func testMaintainMultipleFromImport(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
createThreadMembership := func(userID, postID string, following bool) (*model.ThreadMembership, func()) {
|
|
t.Helper()
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: following,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
mem, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
require.NoError(t, err)
|
|
|
|
return mem, func() {
|
|
err := ss.Thread().DeleteMembershipForUser(userID, postID)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
cleanMembers := func(userIDs []string, postID string) error {
|
|
// clean the thread memberships
|
|
for _, id := range userIDs {
|
|
err := ss.Thread().DeleteMembershipForUser(id, postID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
postingUserID := model.NewId()
|
|
|
|
team, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "DisplayName",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
channel1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team.Id,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel1" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
rootPost1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserID,
|
|
Message: model.NewRandomString(10),
|
|
RootId: rootPost1.Id,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Should create new memberships from new list", func(t *testing.T) {
|
|
userAID := model.NewId()
|
|
userBID := model.NewId()
|
|
|
|
_, err := ss.Thread().MaintainMultipleFromImport([]*model.ThreadMembership{
|
|
{
|
|
UserId: userAID,
|
|
PostId: rootPost1.Id,
|
|
Following: true,
|
|
},
|
|
{
|
|
UserId: userBID,
|
|
PostId: rootPost1.Id,
|
|
Following: true,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
followers, err := ss.Thread().GetThreadFollowers(rootPost1.Id, true)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, followers, []string{userAID, userBID})
|
|
|
|
// clean the thread memberships
|
|
err = cleanMembers(followers, rootPost1.Id)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Should add incoming memberships from the list", func(t *testing.T) {
|
|
userAID := model.NewId()
|
|
userBID := model.NewId()
|
|
|
|
_, clean := createThreadMembership(userAID, rootPost1.Id, true)
|
|
defer clean()
|
|
|
|
_, err := ss.Thread().MaintainMultipleFromImport([]*model.ThreadMembership{
|
|
{
|
|
UserId: userBID,
|
|
PostId: rootPost1.Id,
|
|
Following: true,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
followers, err := ss.Thread().GetThreadFollowers(rootPost1.Id, true)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, followers, []string{userAID, userBID})
|
|
|
|
// clean the thread memberships
|
|
err = cleanMembers(followers, rootPost1.Id)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Should update memberships if they are newer", func(t *testing.T) {
|
|
userAID := model.NewId()
|
|
|
|
old, clean := createThreadMembership(userAID, rootPost1.Id, true)
|
|
defer clean()
|
|
|
|
_, err := ss.Thread().MaintainMultipleFromImport([]*model.ThreadMembership{
|
|
{
|
|
UserId: userAID,
|
|
PostId: rootPost1.Id,
|
|
Following: true,
|
|
LastViewed: time.Now().Add(time.Minute).UnixMilli(),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
followers, err := ss.Thread().GetThreadFollowers(rootPost1.Id, true)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, followers, []string{userAID})
|
|
|
|
updated, err := ss.Thread().GetMembershipForUser(userAID, rootPost1.Id)
|
|
require.NoError(t, err)
|
|
require.Greater(t, updated.LastViewed, old.LastViewed)
|
|
|
|
// clean the thread memberships
|
|
err = cleanMembers(followers, rootPost1.Id)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Should not update membership if incoming is not newer", func(t *testing.T) {
|
|
userAID := model.NewId()
|
|
|
|
_, clean := createThreadMembership(userAID, rootPost1.Id, false)
|
|
defer clean()
|
|
|
|
_, err := ss.Thread().MaintainMultipleFromImport([]*model.ThreadMembership{
|
|
{
|
|
UserId: userAID,
|
|
PostId: rootPost1.Id,
|
|
Following: true,
|
|
LastViewed: time.Now().Add(-1 * time.Hour).UnixMilli(),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
followers, err := ss.Thread().GetThreadFollowers(rootPost1.Id, true)
|
|
require.NoError(t, err)
|
|
require.Empty(t, followers)
|
|
|
|
m, err := ss.Thread().GetMembershipForUser(userAID, rootPost1.Id)
|
|
require.NoError(t, err)
|
|
require.False(t, m.Following)
|
|
|
|
// clean the thread memberships
|
|
err = cleanMembers(followers, rootPost1.Id)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func testUpdateTeamIdForChannelThreads(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
createThreadMembership := func(userID, postID string, following bool) (*model.ThreadMembership, func()) {
|
|
t.Helper()
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: following,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: true,
|
|
}
|
|
mem, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
|
require.NoError(t, err)
|
|
|
|
return mem, func() {
|
|
err := ss.Thread().DeleteMembershipForUser(userID, postID)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
postingUserID := model.NewId()
|
|
|
|
team1, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "DisplayName",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
team2, err := ss.Team().Save(&model.Team{
|
|
DisplayName: "DisplayNameTwo",
|
|
Name: "team" + model.NewId(),
|
|
Email: MakeEmail(),
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
channel1, err := ss.Channel().Save(rctx, &model.Channel{
|
|
TeamId: team1.Id,
|
|
DisplayName: "DisplayName",
|
|
Name: "channel1" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
}, -1)
|
|
require.NoError(t, err)
|
|
|
|
rootPost1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserID,
|
|
Message: model.NewRandomString(10),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel1.Id,
|
|
UserId: postingUserID,
|
|
Message: model.NewRandomString(10),
|
|
RootId: rootPost1.Id,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Should move threads to the new team", func(t *testing.T) {
|
|
userA, err := ss.User().Save(request.TestContext(t), &model.User{
|
|
Username: model.NewId(),
|
|
Email: MakeEmail(),
|
|
Password: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, clean := createThreadMembership(userA.Id, rootPost1.Id, true)
|
|
defer clean()
|
|
|
|
err = ss.Thread().UpdateTeamIdForChannelThreads(channel1.Id, team2.Id)
|
|
require.NoError(t, err)
|
|
|
|
defer func() {
|
|
err = ss.Thread().UpdateTeamIdForChannelThreads(channel1.Id, team1.Id)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
threads, err := ss.Thread().GetThreadsForUser(rctx, userA.Id, team2.Id, model.GetUserThreadsOpts{})
|
|
require.NoError(t, err)
|
|
require.Len(t, threads, 1)
|
|
})
|
|
|
|
t.Run("Should not move threads to a non existent team", func(t *testing.T) {
|
|
userA, err := ss.User().Save(request.TestContext(t), &model.User{
|
|
Username: model.NewId(),
|
|
Email: MakeEmail(),
|
|
Password: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
newTeamID := model.NewId()
|
|
|
|
_, clean := createThreadMembership(userA.Id, rootPost1.Id, true)
|
|
t.Cleanup(clean)
|
|
|
|
err = ss.Thread().UpdateTeamIdForChannelThreads(channel1.Id, newTeamID)
|
|
require.NoError(t, err)
|
|
|
|
threads, err := ss.Thread().GetThreadsForUser(rctx, userA.Id, newTeamID, model.GetUserThreadsOpts{})
|
|
require.NoError(t, err)
|
|
require.Len(t, threads, 0)
|
|
|
|
threads, err = ss.Thread().GetThreadsForUser(rctx, userA.Id, team1.Id, model.GetUserThreadsOpts{})
|
|
require.NoError(t, err)
|
|
require.Len(t, threads, 1)
|
|
})
|
|
}
|