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>
1035 lines
28 KiB
Go
1035 lines
28 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package storetest
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store/retrylayer"
|
|
)
|
|
|
|
func TestReactionStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
|
t.Run("ReactionSave", func(t *testing.T) { testReactionSave(t, rctx, ss) })
|
|
t.Run("ReactionDelete", func(t *testing.T) { testReactionDelete(t, rctx, ss) })
|
|
t.Run("ReactionGetForPost", func(t *testing.T) { testReactionGetForPost(t, rctx, ss) })
|
|
t.Run("ReactionGetForPostSince", func(t *testing.T) { testReactionGetForPostSince(t, rctx, ss, s) })
|
|
t.Run("ReactionDeleteAllWithEmojiName", func(t *testing.T) { testReactionDeleteAllWithEmojiName(t, rctx, ss, s) })
|
|
t.Run("PermanentDeleteByUser", func(t *testing.T) { testPermanentDeleteByUser(t, rctx, ss) })
|
|
t.Run("PermanentDeleteBatch", func(t *testing.T) { testReactionStorePermanentDeleteBatch(t, rctx, ss) })
|
|
t.Run("ReactionBulkGetForPosts", func(t *testing.T) { testReactionBulkGetForPosts(t, rctx, ss) })
|
|
t.Run("ReactionDeadlock", func(t *testing.T) { testReactionDeadlock(t, rctx, ss) })
|
|
t.Run("ExistsOnPost", func(t *testing.T) { testExistsOnPost(t, rctx, ss) })
|
|
t.Run("GetUniqueCountForPost", func(t *testing.T) { testGetUniqueCountForPost(t, rctx, ss) })
|
|
t.Run("ReactionGetSingle", func(t *testing.T) { testReactionGetSingle(t, rctx, ss) })
|
|
}
|
|
|
|
func testReactionSave(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
firstUpdateAt := post.UpdateAt
|
|
|
|
reaction1 := &model.Reaction{
|
|
UserId: model.NewId(),
|
|
PostId: post.Id,
|
|
EmojiName: model.NewId(),
|
|
}
|
|
|
|
time.Sleep(time.Millisecond)
|
|
reaction, nErr := ss.Reaction().Save(reaction1)
|
|
require.NoError(t, nErr)
|
|
|
|
saved := reaction
|
|
assert.Equal(t, saved.UserId, reaction1.UserId, "should've saved reaction user_id and returned it")
|
|
assert.Equal(t, saved.PostId, reaction1.PostId, "should've saved reaction post_id and returned it")
|
|
assert.Equal(t, saved.EmojiName, reaction1.EmojiName, "should've saved reaction emoji_name and returned it")
|
|
assert.NotZero(t, saved.UpdateAt, "should've saved reaction update_at and returned it")
|
|
assert.Equal(t, saved.ChannelId, post.ChannelId, "should've saved reaction update_at and returned it")
|
|
assert.Zero(t, saved.DeleteAt, "should've saved reaction delete_at with zero value and returned it")
|
|
|
|
var secondUpdateAt int64
|
|
postList, err := ss.Post().Get(rctx, reaction1.PostId, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = true on post")
|
|
assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should've marked post as updated when HasReactions changed")
|
|
|
|
if postList.Posts[post.Id].HasReactions && postList.Posts[post.Id].UpdateAt != firstUpdateAt {
|
|
secondUpdateAt = postList.Posts[post.Id].UpdateAt
|
|
}
|
|
|
|
_, nErr = ss.Reaction().Save(reaction1)
|
|
assert.NoError(t, nErr, "should've allowed saving a duplicate reaction")
|
|
|
|
// different user
|
|
reaction2 := &model.Reaction{
|
|
UserId: model.NewId(),
|
|
PostId: reaction1.PostId,
|
|
EmojiName: reaction1.EmojiName,
|
|
}
|
|
|
|
time.Sleep(time.Millisecond)
|
|
_, nErr = ss.Reaction().Save(reaction2)
|
|
require.NoError(t, nErr)
|
|
|
|
postList, err = ss.Post().Get(rctx, reaction2.PostId, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, secondUpdateAt, "should've marked post as updated even if HasReactions doesn't change")
|
|
|
|
// different post
|
|
// create post1
|
|
post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
reaction3 := &model.Reaction{
|
|
UserId: reaction1.UserId,
|
|
PostId: post1.Id,
|
|
EmojiName: reaction1.EmojiName,
|
|
}
|
|
_, nErr = ss.Reaction().Save(reaction3)
|
|
require.NoError(t, nErr)
|
|
|
|
// different emoji
|
|
reaction4 := &model.Reaction{
|
|
UserId: reaction1.UserId,
|
|
PostId: reaction1.PostId,
|
|
EmojiName: model.NewId(),
|
|
}
|
|
_, nErr = ss.Reaction().Save(reaction4)
|
|
require.NoError(t, nErr)
|
|
|
|
// invalid reaction
|
|
reaction5 := &model.Reaction{
|
|
UserId: reaction1.UserId,
|
|
PostId: reaction1.PostId,
|
|
}
|
|
_, nErr = ss.Reaction().Save(reaction5)
|
|
require.Error(t, nErr, "should've failed for invalid reaction")
|
|
|
|
t.Run("channel not found", func(t *testing.T) {
|
|
// invalid reaction
|
|
reaction5 := &model.Reaction{
|
|
UserId: reaction1.UserId,
|
|
PostId: model.NewId(), // Unknown PostId
|
|
EmojiName: model.NewId(),
|
|
}
|
|
_, nErr = ss.Reaction().Save(reaction5)
|
|
require.Error(t, nErr, "should've failed because postId doesn't belong to a stored post")
|
|
})
|
|
}
|
|
|
|
func testReactionDelete(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
t.Run("Delete", func(t *testing.T) {
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
reaction := &model.Reaction{
|
|
UserId: model.NewId(),
|
|
PostId: post.Id,
|
|
EmojiName: model.NewId(),
|
|
}
|
|
|
|
_, nErr := ss.Reaction().Save(reaction)
|
|
require.NoError(t, nErr)
|
|
|
|
result, err := ss.Post().Get(rctx, reaction.PostId, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
|
|
firstUpdateAt := result.Posts[post.Id].UpdateAt
|
|
|
|
_, nErr = ss.Reaction().Delete(reaction)
|
|
require.NoError(t, nErr)
|
|
|
|
reactions, rErr := ss.Reaction().GetForPost(post.Id, false)
|
|
require.NoError(t, rErr)
|
|
|
|
assert.Empty(t, reactions, "should've deleted reaction")
|
|
|
|
postList, err := ss.Post().Get(rctx, post.Id, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
|
|
assert.False(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = false on post")
|
|
assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should mark post as updated after deleting reactions")
|
|
})
|
|
|
|
t.Run("Undelete", func(t *testing.T) {
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
reaction := &model.Reaction{
|
|
UserId: model.NewId(),
|
|
PostId: post.Id,
|
|
EmojiName: model.NewId(),
|
|
}
|
|
|
|
savedReaction, nErr := ss.Reaction().Save(reaction)
|
|
require.NoError(t, nErr)
|
|
|
|
updateAt := savedReaction.UpdateAt
|
|
|
|
_, nErr = ss.Reaction().Delete(savedReaction)
|
|
require.NoError(t, nErr)
|
|
|
|
// add same reaction back and ensure update_at is set
|
|
_, nErr = ss.Reaction().Save(savedReaction)
|
|
require.NoError(t, nErr)
|
|
|
|
reactions, err := ss.Reaction().GetForPost(post.Id, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, reactions, 1)
|
|
assert.GreaterOrEqual(t, reactions[0].UpdateAt, updateAt)
|
|
})
|
|
}
|
|
|
|
func testReactionGetForPost(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
userId := model.NewId()
|
|
// create post
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: userId,
|
|
})
|
|
require.NoError(t, err)
|
|
post1, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: userId,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
postId := post.Id
|
|
post1Id := post1.Id
|
|
|
|
reactions := []*model.Reaction{
|
|
{
|
|
UserId: userId,
|
|
PostId: postId,
|
|
EmojiName: "smile",
|
|
},
|
|
{
|
|
UserId: post1Id,
|
|
PostId: postId,
|
|
EmojiName: "smile",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: postId,
|
|
EmojiName: "sad",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post1Id,
|
|
EmojiName: "angry",
|
|
},
|
|
}
|
|
|
|
for _, reaction := range reactions {
|
|
_, err = ss.Reaction().Save(reaction)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// save and delete an additional reaction to test soft deletion
|
|
temp := &model.Reaction{
|
|
UserId: userId,
|
|
PostId: postId,
|
|
EmojiName: "grin",
|
|
}
|
|
savedTmp, err := ss.Reaction().Save(temp)
|
|
require.NoError(t, err)
|
|
_, err = ss.Reaction().Delete(savedTmp)
|
|
require.NoError(t, err)
|
|
|
|
returned, err := ss.Reaction().GetForPost(postId, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 3, "should've returned 3 reactions")
|
|
|
|
for _, reaction := range reactions {
|
|
found := false
|
|
|
|
for _, returnedReaction := range returned {
|
|
if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId &&
|
|
returnedReaction.EmojiName == reaction.EmojiName && returnedReaction.UpdateAt > 0 {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction)
|
|
} else if found {
|
|
assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post")
|
|
}
|
|
}
|
|
|
|
// Should return cached item
|
|
returned, err = ss.Reaction().GetForPost(postId, true)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 3, "should've returned 3 reactions")
|
|
|
|
for _, reaction := range reactions {
|
|
found := false
|
|
|
|
for _, returnedReaction := range returned {
|
|
if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId &&
|
|
returnedReaction.EmojiName == reaction.EmojiName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction)
|
|
} else if found {
|
|
assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testReactionGetForPostSince(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
|
now := model.GetMillis()
|
|
later := now + 1800000 // add 30 minutes
|
|
remoteId := model.NewId()
|
|
userId := model.NewId()
|
|
|
|
// create post
|
|
post, _ := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: userId,
|
|
})
|
|
post1, _ := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: userId,
|
|
})
|
|
|
|
postId := post.Id
|
|
post1Id := post1.Id
|
|
|
|
reactions := []*model.Reaction{
|
|
{
|
|
UserId: userId,
|
|
PostId: postId,
|
|
EmojiName: "smile",
|
|
UpdateAt: later,
|
|
},
|
|
{
|
|
UserId: model.NewId(),
|
|
PostId: postId,
|
|
EmojiName: "smile",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: postId,
|
|
EmojiName: "sad",
|
|
UpdateAt: later,
|
|
RemoteId: &remoteId,
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post1Id,
|
|
EmojiName: "angry",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: postId,
|
|
EmojiName: "angry",
|
|
DeleteAt: now + 1,
|
|
UpdateAt: later,
|
|
},
|
|
}
|
|
|
|
for _, reaction := range reactions {
|
|
del := reaction.DeleteAt
|
|
update := reaction.UpdateAt
|
|
|
|
_, err := ss.Reaction().Save(reaction)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
if del > 0 {
|
|
_, err = ss.Reaction().Delete(reaction)
|
|
require.NoError(t, err)
|
|
}
|
|
if update > 0 {
|
|
err = forceUpdateAt(reaction, update, s)
|
|
require.NoError(t, err)
|
|
}
|
|
err = forceNULL(reaction, s) // test COALESCE
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("reactions since", func(t *testing.T) {
|
|
// should return 2 reactions that are not deleted for post
|
|
returned, err := ss.Reaction().GetForPostSince(postId, later-1, "", false)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 2, "should've returned 2 non-deleted reactions")
|
|
assert.Less(t, returned[0].CreateAt, returned[1].CreateAt)
|
|
for _, r := range returned {
|
|
assert.Zero(t, r.DeleteAt, "should not have returned deleted reaction")
|
|
}
|
|
})
|
|
|
|
t.Run("reactions since, incl deleted", func(t *testing.T) {
|
|
// should return 3 reactions for post, including one deleted
|
|
returned, err := ss.Reaction().GetForPostSince(postId, later-1, "", true)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 3, "should've returned 3 reactions")
|
|
var count int
|
|
for i, r := range returned {
|
|
if i > 0 {
|
|
assert.Less(t, returned[i-1].CreateAt, returned[i].CreateAt)
|
|
}
|
|
if r.DeleteAt > 0 {
|
|
count++
|
|
}
|
|
}
|
|
assert.Equal(t, 1, count, "should not have returned 1 deleted reaction")
|
|
})
|
|
|
|
t.Run("reactions since, filter remoteId", func(t *testing.T) {
|
|
// should return 1 reactions that are not deleted for post and have no remoteId
|
|
returned, err := ss.Reaction().GetForPostSince(postId, later-1, remoteId, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 1, "should've returned 1 filtered reactions")
|
|
for _, r := range returned {
|
|
assert.Zero(t, r.DeleteAt, "should not have returned deleted reaction")
|
|
}
|
|
})
|
|
|
|
t.Run("reactions since, invalid post", func(t *testing.T) {
|
|
// should return 0 reactions for invalid post
|
|
returned, err := ss.Reaction().GetForPostSince(model.NewId(), later-1, "", true)
|
|
require.NoError(t, err)
|
|
require.Empty(t, returned, "should've returned 0 reactions")
|
|
})
|
|
|
|
t.Run("reactions since, far future", func(t *testing.T) {
|
|
// should return 0 reactions for since far in the future
|
|
returned, err := ss.Reaction().GetForPostSince(postId, later*2, "", true)
|
|
require.NoError(t, err)
|
|
require.Empty(t, returned, "should've returned 0 reactions")
|
|
})
|
|
}
|
|
|
|
func forceUpdateAt(reaction *model.Reaction, updateAt int64, s SqlStore) error {
|
|
params := map[string]any{
|
|
"userid": reaction.UserId,
|
|
"postid": reaction.PostId,
|
|
"emojiname": reaction.EmojiName,
|
|
"updateat": updateAt,
|
|
}
|
|
|
|
sqlResult, err := s.GetMaster().NamedExec(`
|
|
UPDATE
|
|
Reactions
|
|
SET
|
|
UpdateAt=:updateat
|
|
WHERE
|
|
UserId = :userid AND
|
|
PostId = :postid AND
|
|
EmojiName = :emojiname`, params,
|
|
)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, err := sqlResult.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if rows != 1 {
|
|
return errors.New("expected one row affected")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func forceNULL(reaction *model.Reaction, s SqlStore) error {
|
|
if _, err := s.GetMaster().Exec(`UPDATE Reactions SET UpdateAt = NULL WHERE UpdateAt = 0`); err != nil {
|
|
return err
|
|
}
|
|
if _, err := s.GetMaster().Exec(`UPDATE Reactions SET DeleteAt = NULL WHERE DeleteAt = 0`); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func testReactionDeleteAllWithEmojiName(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
|
emojiToDelete := model.NewId()
|
|
|
|
post, err1 := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err1)
|
|
post2, err2 := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err2)
|
|
post3, err3 := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err3)
|
|
|
|
userId := model.NewId()
|
|
|
|
reactions := []*model.Reaction{
|
|
{
|
|
UserId: userId,
|
|
PostId: post.Id,
|
|
EmojiName: emojiToDelete,
|
|
},
|
|
{
|
|
UserId: model.NewId(),
|
|
PostId: post.Id,
|
|
EmojiName: emojiToDelete,
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post.Id,
|
|
EmojiName: "sad",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post2.Id,
|
|
EmojiName: "angry",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post3.Id,
|
|
EmojiName: emojiToDelete,
|
|
},
|
|
}
|
|
|
|
for _, reaction := range reactions {
|
|
_, err := ss.Reaction().Save(reaction)
|
|
require.NoError(t, err)
|
|
|
|
// make at least one Reaction record contain NULL for Update and DeleteAt to simulate post schema upgrade case.
|
|
if reaction.EmojiName == emojiToDelete {
|
|
err = forceNULL(reaction, s)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
err := ss.Reaction().DeleteAllWithEmojiName(emojiToDelete)
|
|
require.NoError(t, err)
|
|
|
|
// check that the reactions were deleted
|
|
returned, err := ss.Reaction().GetForPost(post.Id, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 1, "should've only removed reactions with emoji name")
|
|
|
|
for _, reaction := range returned {
|
|
assert.NotEqual(t, reaction.EmojiName, "smile", "should've removed reaction with emoji name")
|
|
}
|
|
|
|
returned, err = ss.Reaction().GetForPost(post2.Id, false)
|
|
require.NoError(t, err)
|
|
assert.Len(t, returned, 1, "should've only removed reactions with emoji name")
|
|
|
|
returned, err = ss.Reaction().GetForPost(post3.Id, false)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, returned, "should've only removed reactions with emoji name")
|
|
|
|
// check that the posts are updated
|
|
postList, err := ss.Post().Get(rctx, post.Id, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
assert.True(t, postList.Posts[post.Id].HasReactions, "post should still have reactions")
|
|
|
|
postList, err = ss.Post().Get(rctx, post2.Id, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
assert.True(t, postList.Posts[post2.Id].HasReactions, "post should still have reactions")
|
|
|
|
postList, err = ss.Post().Get(rctx, post3.Id, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
assert.False(t, postList.Posts[post3.Id].HasReactions, "post shouldn't have reactions any more")
|
|
}
|
|
|
|
func testPermanentDeleteByUser(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
userId := model.NewId()
|
|
post, err1 := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err1)
|
|
post2, err2 := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err2)
|
|
post3, err3 := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err3)
|
|
|
|
reactions := []*model.Reaction{
|
|
{
|
|
UserId: userId,
|
|
PostId: post.Id,
|
|
EmojiName: "happy",
|
|
},
|
|
{
|
|
UserId: model.NewId(),
|
|
PostId: post.Id,
|
|
EmojiName: "smile",
|
|
},
|
|
{
|
|
UserId: model.NewId(),
|
|
PostId: post.Id,
|
|
EmojiName: "sad",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post2.Id,
|
|
EmojiName: "angry",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post3.Id,
|
|
EmojiName: "joy",
|
|
},
|
|
}
|
|
|
|
for _, reaction := range reactions {
|
|
_, err := ss.Reaction().Save(reaction)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
err := ss.Reaction().PermanentDeleteByUser(userId)
|
|
require.NoError(t, err)
|
|
|
|
// check that the reactions were deleted
|
|
returned, err := ss.Reaction().GetForPost(post.Id, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 2, "should only have removed reaction for user")
|
|
|
|
for _, reaction := range returned {
|
|
assert.NotEqual(t, reaction.EmojiName, "happy", "should've removed reaction with emoji name")
|
|
}
|
|
|
|
returned, err = ss.Reaction().GetForPost(post2.Id, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 0, "should have removed reaction for user")
|
|
|
|
returned, err = ss.Reaction().GetForPost(post3.Id, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 0, "should remove reaction for user")
|
|
|
|
// check that the posts are updated
|
|
postList, err := ss.Post().Get(rctx, post.Id, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
assert.True(t, postList.Posts[post.Id].HasReactions, "post should still have reactions")
|
|
|
|
postList, err = ss.Post().Get(rctx, post2.Id, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
assert.False(t, postList.Posts[post2.Id].HasReactions, "post shouldn't have reactions any more")
|
|
|
|
postList, err = ss.Post().Get(rctx, post3.Id, model.GetPostsOptions{}, "", map[string]bool{})
|
|
require.NoError(t, err)
|
|
assert.False(t, postList.Posts[post3.Id].HasReactions, "post shouldn't have reactions any more")
|
|
}
|
|
|
|
func testReactionStorePermanentDeleteBatch(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)
|
|
olderPost, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel.Id,
|
|
UserId: model.NewId(),
|
|
CreateAt: 1000,
|
|
})
|
|
require.NoError(t, err)
|
|
newerPost, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: channel.Id,
|
|
UserId: model.NewId(),
|
|
CreateAt: 3000,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Reactions will be deleted based on the timestamp of their post. So the time at
|
|
// which a reaction was created doesn't matter.
|
|
reactions := []*model.Reaction{
|
|
{
|
|
UserId: model.NewId(),
|
|
PostId: olderPost.Id,
|
|
EmojiName: "sad",
|
|
},
|
|
{
|
|
UserId: model.NewId(),
|
|
PostId: olderPost.Id,
|
|
EmojiName: "sad",
|
|
},
|
|
{
|
|
UserId: model.NewId(),
|
|
PostId: newerPost.Id,
|
|
EmojiName: "smile",
|
|
},
|
|
}
|
|
|
|
for _, reaction := range reactions {
|
|
_, err = ss.Reaction().Save(reaction)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
|
|
Now: 0,
|
|
GlobalPolicyEndTime: 2000,
|
|
Limit: limit,
|
|
}, model.RetentionPolicyCursor{})
|
|
require.NoError(t, err)
|
|
|
|
rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(rows))
|
|
require.Equal(t, 1, len(rows[0].Ids))
|
|
require.Contains(t, rows[0].Ids, olderPost.Id)
|
|
|
|
for _, row := range rows {
|
|
var deleted int64
|
|
deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(row)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(2), deleted)
|
|
}
|
|
|
|
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(rows))
|
|
|
|
returned, err := ss.Reaction().GetForPost(olderPost.Id, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 0, "reactions for older post should have been deleted")
|
|
|
|
returned, err = ss.Reaction().GetForPost(newerPost.Id, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 1, "reactions for newer post should not have been deleted")
|
|
}
|
|
|
|
func testReactionBulkGetForPosts(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
userId := model.NewId()
|
|
post, _ := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: userId,
|
|
})
|
|
postId := post.Id
|
|
post, _ = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: userId,
|
|
})
|
|
post2Id := post.Id
|
|
post, _ = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: userId,
|
|
})
|
|
post3Id := post.Id
|
|
post, _ = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: userId,
|
|
})
|
|
post4Id := post.Id
|
|
|
|
reactions := []*model.Reaction{
|
|
{
|
|
UserId: userId,
|
|
PostId: postId,
|
|
EmojiName: "smile",
|
|
},
|
|
{
|
|
UserId: model.NewId(),
|
|
PostId: post2Id,
|
|
EmojiName: "smile",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post3Id,
|
|
EmojiName: "sad",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: postId,
|
|
EmojiName: "angry",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post2Id,
|
|
EmojiName: "angry",
|
|
},
|
|
{
|
|
UserId: userId,
|
|
PostId: post4Id,
|
|
EmojiName: "angry",
|
|
},
|
|
}
|
|
|
|
for _, reaction := range reactions {
|
|
_, err := ss.Reaction().Save(reaction)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
postIds := []string{postId, post2Id, post3Id}
|
|
returned, err := ss.Reaction().BulkGetForPosts(postIds)
|
|
require.NoError(t, err)
|
|
require.Len(t, returned, 5, "should've returned 5 reactions")
|
|
|
|
post4IdFound := false
|
|
for _, reaction := range returned {
|
|
if reaction.PostId == post4Id {
|
|
post4IdFound = true
|
|
break
|
|
}
|
|
}
|
|
|
|
require.False(t, post4IdFound, "Wrong reaction returned")
|
|
}
|
|
|
|
// testReactionDeadlock is a best-case attempt to recreate the deadlock scenario.
|
|
// It at least deadlocks 2 times out of 5.
|
|
func testReactionDeadlock(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
ss = retrylayer.New(ss)
|
|
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
postId := post.Id
|
|
post, err = ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
require.NoError(t, err)
|
|
reaction1 := &model.Reaction{
|
|
UserId: model.NewId(),
|
|
PostId: post.Id,
|
|
EmojiName: model.NewId(),
|
|
}
|
|
_, nErr := ss.Reaction().Save(reaction1)
|
|
require.NoError(t, nErr)
|
|
|
|
// different user
|
|
reaction2 := &model.Reaction{
|
|
UserId: model.NewId(),
|
|
PostId: reaction1.PostId,
|
|
EmojiName: reaction1.EmojiName,
|
|
}
|
|
_, nErr = ss.Reaction().Save(reaction2)
|
|
require.NoError(t, nErr)
|
|
|
|
// different post
|
|
reaction3 := &model.Reaction{
|
|
UserId: reaction1.UserId,
|
|
PostId: postId,
|
|
EmojiName: reaction1.EmojiName,
|
|
}
|
|
_, nErr = ss.Reaction().Save(reaction3)
|
|
require.NoError(t, nErr)
|
|
|
|
// different emoji
|
|
reaction4 := &model.Reaction{
|
|
UserId: reaction1.UserId,
|
|
PostId: reaction1.PostId,
|
|
EmojiName: model.NewId(),
|
|
}
|
|
_, nErr = ss.Reaction().Save(reaction4)
|
|
require.NoError(t, nErr)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
// 1st tx
|
|
go func() {
|
|
defer wg.Done()
|
|
err := ss.Reaction().DeleteAllWithEmojiName(reaction1.EmojiName)
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
// 2nd tx
|
|
go func() {
|
|
defer wg.Done()
|
|
_, err := ss.Reaction().Delete(reaction2)
|
|
require.NoError(t, err)
|
|
}()
|
|
wg.Wait()
|
|
}
|
|
|
|
func testExistsOnPost(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
post, _ := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
emojiName := model.NewId()
|
|
reaction := &model.Reaction{
|
|
UserId: model.NewId(),
|
|
PostId: post.Id,
|
|
EmojiName: emojiName,
|
|
}
|
|
_, nErr := ss.Reaction().Save(reaction)
|
|
require.NoError(t, nErr)
|
|
exists, err := ss.Reaction().ExistsOnPost(post.Id, emojiName)
|
|
require.NoError(t, err)
|
|
require.True(t, exists)
|
|
exists, err = ss.Reaction().ExistsOnPost(post.Id, model.NewId())
|
|
require.NoError(t, err)
|
|
require.False(t, exists)
|
|
}
|
|
|
|
func testGetUniqueCountForPost(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
post, _ := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: model.NewId(),
|
|
})
|
|
|
|
userId := model.NewId()
|
|
emojiName := model.NewId()
|
|
|
|
reaction := &model.Reaction{
|
|
UserId: userId,
|
|
PostId: post.Id,
|
|
EmojiName: emojiName,
|
|
}
|
|
_, nErr := ss.Reaction().Save(reaction)
|
|
require.NoError(t, nErr)
|
|
|
|
sameReaction := &model.Reaction{
|
|
UserId: model.NewId(),
|
|
PostId: post.Id,
|
|
EmojiName: emojiName,
|
|
}
|
|
_, nErr = ss.Reaction().Save(sameReaction)
|
|
require.NoError(t, nErr)
|
|
|
|
newReaction := &model.Reaction{
|
|
UserId: userId,
|
|
PostId: post.Id,
|
|
EmojiName: model.NewId(),
|
|
}
|
|
_, nErr = ss.Reaction().Save(newReaction)
|
|
require.NoError(t, nErr)
|
|
|
|
totalReactions, err := ss.Reaction().GetForPost(post.Id, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, len(totalReactions))
|
|
|
|
count, err := ss.Reaction().GetUniqueCountForPost(post.Id)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, count)
|
|
}
|
|
|
|
func testReactionGetSingle(t *testing.T, rctx request.CTX, ss store.Store) {
|
|
var (
|
|
testUserID = model.NewId()
|
|
testEmojiName = "smile"
|
|
testRemoteID = model.NewId()
|
|
)
|
|
|
|
t.Run("get without remoteId", func(t *testing.T) {
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: testUserID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
reaction := &model.Reaction{
|
|
UserId: testUserID,
|
|
PostId: post.Id,
|
|
EmojiName: testEmojiName,
|
|
}
|
|
|
|
_, nErr := ss.Reaction().Save(reaction)
|
|
require.NoError(t, nErr)
|
|
|
|
reactionFound, err := ss.Reaction().GetSingle(testUserID, post.Id, "", testEmojiName)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, testUserID, reactionFound.UserId)
|
|
assert.Equal(t, post.Id, reactionFound.PostId)
|
|
assert.Equal(t, "", reactionFound.GetRemoteID())
|
|
assert.Equal(t, testEmojiName, reactionFound.EmojiName)
|
|
})
|
|
|
|
t.Run("get with remoteId", func(t *testing.T) {
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: testUserID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
reaction := &model.Reaction{
|
|
UserId: testUserID,
|
|
PostId: post.Id,
|
|
EmojiName: testEmojiName,
|
|
RemoteId: model.NewPointer(testRemoteID),
|
|
}
|
|
|
|
_, nErr := ss.Reaction().Save(reaction)
|
|
require.NoError(t, nErr)
|
|
|
|
reactionFound, err := ss.Reaction().GetSingle(testUserID, post.Id, testRemoteID, testEmojiName)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, testUserID, reactionFound.UserId)
|
|
assert.Equal(t, post.Id, reactionFound.PostId)
|
|
assert.Equal(t, testRemoteID, reactionFound.GetRemoteID())
|
|
assert.Equal(t, testEmojiName, reactionFound.EmojiName)
|
|
})
|
|
|
|
t.Run("not found - wrong remoteID", func(t *testing.T) {
|
|
post, err := ss.Post().Save(rctx, &model.Post{
|
|
ChannelId: model.NewId(),
|
|
UserId: testUserID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
reaction := &model.Reaction{
|
|
UserId: testUserID,
|
|
PostId: post.Id,
|
|
EmojiName: testEmojiName,
|
|
RemoteId: model.NewPointer(testRemoteID),
|
|
}
|
|
|
|
_, nErr := ss.Reaction().Save(reaction)
|
|
require.NoError(t, nErr)
|
|
|
|
reactionFound, err := ss.Reaction().GetSingle(testUserID, post.Id, "bogus-remoteId", testEmojiName)
|
|
require.Error(t, err)
|
|
assert.Nil(t, reactionFound)
|
|
|
|
var errNotFound *store.ErrNotFound
|
|
assert.ErrorAs(t, err, &errNotFound)
|
|
})
|
|
}
|