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