mattermost-community-enterp.../channels/store/storetest/thread_store.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
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>
2025-12-17 23:59:07 +09:00

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)
})
}