github.com/haalcala/mattermost-server-change-repo/v5@v5.33.2/store/storetest/reaction_store.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package storetest
     5  
     6  import (
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/mattermost/mattermost-server/v5/model"
    15  	"github.com/mattermost/mattermost-server/v5/store"
    16  	"github.com/mattermost/mattermost-server/v5/store/retrylayer"
    17  )
    18  
    19  func TestReactionStore(t *testing.T, ss store.Store, s SqlStore) {
    20  	t.Run("ReactionSave", func(t *testing.T) { testReactionSave(t, ss) })
    21  	t.Run("ReactionDelete", func(t *testing.T) { testReactionDelete(t, ss) })
    22  	t.Run("ReactionGetForPost", func(t *testing.T) { testReactionGetForPost(t, ss) })
    23  	t.Run("ReactionDeleteAllWithEmojiName", func(t *testing.T) { testReactionDeleteAllWithEmojiName(t, ss, s) })
    24  	t.Run("PermanentDeleteBatch", func(t *testing.T) { testReactionStorePermanentDeleteBatch(t, ss) })
    25  	t.Run("ReactionBulkGetForPosts", func(t *testing.T) { testReactionBulkGetForPosts(t, ss) })
    26  	t.Run("ReactionDeadlock", func(t *testing.T) { testReactionDeadlock(t, ss) })
    27  }
    28  
    29  func testReactionSave(t *testing.T, ss store.Store) {
    30  	post, err := ss.Post().Save(&model.Post{
    31  		ChannelId: model.NewId(),
    32  		UserId:    model.NewId(),
    33  	})
    34  	require.NoError(t, err)
    35  	firstUpdateAt := post.UpdateAt
    36  
    37  	reaction1 := &model.Reaction{
    38  		UserId:    model.NewId(),
    39  		PostId:    post.Id,
    40  		EmojiName: model.NewId(),
    41  	}
    42  
    43  	time.Sleep(time.Millisecond)
    44  	reaction, nErr := ss.Reaction().Save(reaction1)
    45  	require.NoError(t, nErr)
    46  
    47  	saved := reaction
    48  	assert.Equal(t, saved.UserId, reaction1.UserId, "should've saved reaction user_id and returned it")
    49  	assert.Equal(t, saved.PostId, reaction1.PostId, "should've saved reaction post_id and returned it")
    50  	assert.Equal(t, saved.EmojiName, reaction1.EmojiName, "should've saved reaction emoji_name and returned it")
    51  	assert.NotZero(t, saved.UpdateAt, "should've saved reaction update_at and returned it")
    52  	assert.Zero(t, saved.DeleteAt, "should've saved reaction delete_at with zero value and returned it")
    53  
    54  	var secondUpdateAt int64
    55  	postList, err := ss.Post().Get(reaction1.PostId, false, false, false)
    56  	require.NoError(t, err)
    57  
    58  	assert.True(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = true on post")
    59  	assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should've marked post as updated when HasReactions changed")
    60  
    61  	if postList.Posts[post.Id].HasReactions && postList.Posts[post.Id].UpdateAt != firstUpdateAt {
    62  		secondUpdateAt = postList.Posts[post.Id].UpdateAt
    63  	}
    64  
    65  	_, nErr = ss.Reaction().Save(reaction1)
    66  	assert.NoError(t, nErr, "should've allowed saving a duplicate reaction")
    67  
    68  	// different user
    69  	reaction2 := &model.Reaction{
    70  		UserId:    model.NewId(),
    71  		PostId:    reaction1.PostId,
    72  		EmojiName: reaction1.EmojiName,
    73  	}
    74  
    75  	time.Sleep(time.Millisecond)
    76  	_, nErr = ss.Reaction().Save(reaction2)
    77  	require.NoError(t, nErr)
    78  
    79  	postList, err = ss.Post().Get(reaction2.PostId, false, false, false)
    80  	require.NoError(t, err)
    81  
    82  	assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, secondUpdateAt, "should've marked post as updated even if HasReactions doesn't change")
    83  
    84  	// different post
    85  	reaction3 := &model.Reaction{
    86  		UserId:    reaction1.UserId,
    87  		PostId:    model.NewId(),
    88  		EmojiName: reaction1.EmojiName,
    89  	}
    90  	_, nErr = ss.Reaction().Save(reaction3)
    91  	require.NoError(t, nErr)
    92  
    93  	// different emoji
    94  	reaction4 := &model.Reaction{
    95  		UserId:    reaction1.UserId,
    96  		PostId:    reaction1.PostId,
    97  		EmojiName: model.NewId(),
    98  	}
    99  	_, nErr = ss.Reaction().Save(reaction4)
   100  	require.NoError(t, nErr)
   101  
   102  	// invalid reaction
   103  	reaction5 := &model.Reaction{
   104  		UserId: reaction1.UserId,
   105  		PostId: reaction1.PostId,
   106  	}
   107  	_, nErr = ss.Reaction().Save(reaction5)
   108  	require.Error(t, nErr, "should've failed for invalid reaction")
   109  
   110  }
   111  
   112  func testReactionDelete(t *testing.T, ss store.Store) {
   113  	t.Run("Delete", func(t *testing.T) {
   114  		post, err := ss.Post().Save(&model.Post{
   115  			ChannelId: model.NewId(),
   116  			UserId:    model.NewId(),
   117  		})
   118  		require.NoError(t, err)
   119  
   120  		reaction := &model.Reaction{
   121  			UserId:    model.NewId(),
   122  			PostId:    post.Id,
   123  			EmojiName: model.NewId(),
   124  		}
   125  
   126  		_, nErr := ss.Reaction().Save(reaction)
   127  		require.NoError(t, nErr)
   128  
   129  		result, err := ss.Post().Get(reaction.PostId, false, false, false)
   130  		require.NoError(t, err)
   131  
   132  		firstUpdateAt := result.Posts[post.Id].UpdateAt
   133  
   134  		_, nErr = ss.Reaction().Delete(reaction)
   135  		require.NoError(t, nErr)
   136  
   137  		reactions, rErr := ss.Reaction().GetForPost(post.Id, false)
   138  		require.NoError(t, rErr)
   139  
   140  		assert.Empty(t, reactions, "should've deleted reaction")
   141  
   142  		postList, err := ss.Post().Get(post.Id, false, false, false)
   143  		require.NoError(t, err)
   144  
   145  		assert.False(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = false on post")
   146  		assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should mark post as updated after deleting reactions")
   147  	})
   148  
   149  	t.Run("Undelete", func(t *testing.T) {
   150  		post, err := ss.Post().Save(&model.Post{
   151  			ChannelId: model.NewId(),
   152  			UserId:    model.NewId(),
   153  		})
   154  		require.NoError(t, err)
   155  
   156  		reaction := &model.Reaction{
   157  			UserId:    model.NewId(),
   158  			PostId:    post.Id,
   159  			EmojiName: model.NewId(),
   160  		}
   161  
   162  		savedReaction, nErr := ss.Reaction().Save(reaction)
   163  		require.NoError(t, nErr)
   164  
   165  		updateAt := savedReaction.UpdateAt
   166  
   167  		_, nErr = ss.Reaction().Delete(savedReaction)
   168  		require.NoError(t, nErr)
   169  
   170  		// add same reaction back and ensure update_at is set
   171  		_, nErr = ss.Reaction().Save(savedReaction)
   172  		require.NoError(t, nErr)
   173  
   174  		reactions, err := ss.Reaction().GetForPost(post.Id, false)
   175  		require.NoError(t, err)
   176  
   177  		assert.Len(t, reactions, 1)
   178  		assert.GreaterOrEqual(t, reactions[0].UpdateAt, updateAt)
   179  	})
   180  }
   181  
   182  func testReactionGetForPost(t *testing.T, ss store.Store) {
   183  	postId := model.NewId()
   184  
   185  	userId := model.NewId()
   186  
   187  	reactions := []*model.Reaction{
   188  		{
   189  			UserId:    userId,
   190  			PostId:    postId,
   191  			EmojiName: "smile",
   192  		},
   193  		{
   194  			UserId:    model.NewId(),
   195  			PostId:    postId,
   196  			EmojiName: "smile",
   197  		},
   198  		{
   199  			UserId:    userId,
   200  			PostId:    postId,
   201  			EmojiName: "sad",
   202  		},
   203  		{
   204  			UserId:    userId,
   205  			PostId:    model.NewId(),
   206  			EmojiName: "angry",
   207  		},
   208  	}
   209  
   210  	for _, reaction := range reactions {
   211  		_, err := ss.Reaction().Save(reaction)
   212  		require.NoError(t, err)
   213  	}
   214  
   215  	// save and delete an additional reaction to test soft deletion
   216  	temp := &model.Reaction{
   217  		UserId:    userId,
   218  		PostId:    postId,
   219  		EmojiName: "grin",
   220  	}
   221  	savedTmp, err := ss.Reaction().Save(temp)
   222  	require.NoError(t, err)
   223  	_, err = ss.Reaction().Delete(savedTmp)
   224  	require.NoError(t, err)
   225  
   226  	returned, err := ss.Reaction().GetForPost(postId, false)
   227  	require.NoError(t, err)
   228  	require.Len(t, returned, 3, "should've returned 3 reactions")
   229  
   230  	for _, reaction := range reactions {
   231  		found := false
   232  
   233  		for _, returnedReaction := range returned {
   234  			if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId &&
   235  				returnedReaction.EmojiName == reaction.EmojiName && returnedReaction.UpdateAt > 0 {
   236  				found = true
   237  				break
   238  			}
   239  		}
   240  
   241  		if !found {
   242  			assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction)
   243  		} else if found {
   244  			assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post")
   245  		}
   246  	}
   247  
   248  	// Should return cached item
   249  	returned, err = ss.Reaction().GetForPost(postId, true)
   250  	require.NoError(t, err)
   251  	require.Len(t, returned, 3, "should've returned 3 reactions")
   252  
   253  	for _, reaction := range reactions {
   254  		found := false
   255  
   256  		for _, returnedReaction := range returned {
   257  			if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId &&
   258  				returnedReaction.EmojiName == reaction.EmojiName {
   259  				found = true
   260  				break
   261  			}
   262  		}
   263  
   264  		if !found {
   265  			assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction)
   266  		} else if found {
   267  			assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post")
   268  		}
   269  	}
   270  }
   271  
   272  func testReactionDeleteAllWithEmojiName(t *testing.T, ss store.Store, s SqlStore) {
   273  	emojiToDelete := model.NewId()
   274  
   275  	post, err1 := ss.Post().Save(&model.Post{
   276  		ChannelId: model.NewId(),
   277  		UserId:    model.NewId(),
   278  	})
   279  	require.NoError(t, err1)
   280  	post2, err2 := ss.Post().Save(&model.Post{
   281  		ChannelId: model.NewId(),
   282  		UserId:    model.NewId(),
   283  	})
   284  	require.NoError(t, err2)
   285  	post3, err3 := ss.Post().Save(&model.Post{
   286  		ChannelId: model.NewId(),
   287  		UserId:    model.NewId(),
   288  	})
   289  	require.NoError(t, err3)
   290  
   291  	userId := model.NewId()
   292  
   293  	reactions := []*model.Reaction{
   294  		{
   295  			UserId:    userId,
   296  			PostId:    post.Id,
   297  			EmojiName: emojiToDelete,
   298  		},
   299  		{
   300  			UserId:    model.NewId(),
   301  			PostId:    post.Id,
   302  			EmojiName: emojiToDelete,
   303  		},
   304  		{
   305  			UserId:    userId,
   306  			PostId:    post.Id,
   307  			EmojiName: "sad",
   308  		},
   309  		{
   310  			UserId:    userId,
   311  			PostId:    post2.Id,
   312  			EmojiName: "angry",
   313  		},
   314  		{
   315  			UserId:    userId,
   316  			PostId:    post3.Id,
   317  			EmojiName: emojiToDelete,
   318  		},
   319  	}
   320  
   321  	for _, reaction := range reactions {
   322  		_, err := ss.Reaction().Save(reaction)
   323  		require.NoError(t, err)
   324  	}
   325  
   326  	// make at least one Reaction record contain NULL for Update and DeleteAt to simulate post schema upgrade case.
   327  	sqlResult, err := s.GetMaster().Exec(`
   328  		UPDATE 
   329  				Reactions 
   330  			SET 
   331  				UpdateAt=NULL, DeleteAt=NULL 
   332  			WHERE 
   333  				UserId = :UserId AND PostId = :PostId AND EmojiName = :EmojiName`,
   334  		map[string]interface{}{
   335  			"UserId":    userId,
   336  			"PostId":    post.Id,
   337  			"EmojiName": emojiToDelete,
   338  		})
   339  	require.NoError(t, err)
   340  	rowsAffected, err := sqlResult.RowsAffected()
   341  	require.NoError(t, err)
   342  	require.NotZero(t, rowsAffected)
   343  
   344  	err = ss.Reaction().DeleteAllWithEmojiName(emojiToDelete)
   345  	require.NoError(t, err)
   346  
   347  	// check that the reactions were deleted
   348  	returned, err := ss.Reaction().GetForPost(post.Id, false)
   349  	require.NoError(t, err)
   350  	require.Len(t, returned, 1, "should've only removed reactions with emoji name")
   351  
   352  	for _, reaction := range returned {
   353  		assert.NotEqual(t, reaction.EmojiName, "smile", "should've removed reaction with emoji name")
   354  	}
   355  
   356  	returned, err = ss.Reaction().GetForPost(post2.Id, false)
   357  	require.NoError(t, err)
   358  	assert.Len(t, returned, 1, "should've only removed reactions with emoji name")
   359  
   360  	returned, err = ss.Reaction().GetForPost(post3.Id, false)
   361  	require.NoError(t, err)
   362  	assert.Empty(t, returned, "should've only removed reactions with emoji name")
   363  
   364  	// check that the posts are updated
   365  	postList, err := ss.Post().Get(post.Id, false, false, false)
   366  	require.NoError(t, err)
   367  	assert.True(t, postList.Posts[post.Id].HasReactions, "post should still have reactions")
   368  
   369  	postList, err = ss.Post().Get(post2.Id, false, false, false)
   370  	require.NoError(t, err)
   371  	assert.True(t, postList.Posts[post2.Id].HasReactions, "post should still have reactions")
   372  
   373  	postList, err = ss.Post().Get(post3.Id, false, false, false)
   374  	require.NoError(t, err)
   375  	assert.False(t, postList.Posts[post3.Id].HasReactions, "post shouldn't have reactions any more")
   376  
   377  }
   378  
   379  func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
   380  	post, err1 := ss.Post().Save(&model.Post{
   381  		ChannelId: model.NewId(),
   382  		UserId:    model.NewId(),
   383  	})
   384  	require.NoError(t, err1)
   385  
   386  	reactions := []*model.Reaction{
   387  		{
   388  			UserId:    model.NewId(),
   389  			PostId:    post.Id,
   390  			EmojiName: "sad",
   391  			CreateAt:  1000,
   392  		},
   393  		{
   394  			UserId:    model.NewId(),
   395  			PostId:    post.Id,
   396  			EmojiName: "sad",
   397  			CreateAt:  1500,
   398  		},
   399  		{
   400  			UserId:    model.NewId(),
   401  			PostId:    post.Id,
   402  			EmojiName: "sad",
   403  			CreateAt:  2000,
   404  		},
   405  		{
   406  			UserId:    model.NewId(),
   407  			PostId:    post.Id,
   408  			EmojiName: "sad",
   409  			CreateAt:  2000,
   410  		},
   411  	}
   412  
   413  	// Need to hang on to a reaction to delete later in order to clear the cache, as "allowFromCache" isn't honoured any more.
   414  	var lastReaction *model.Reaction
   415  	for _, reaction := range reactions {
   416  		var nErr error
   417  		lastReaction, nErr = ss.Reaction().Save(reaction)
   418  		require.NoError(t, nErr)
   419  	}
   420  
   421  	returned, err := ss.Reaction().GetForPost(post.Id, false)
   422  	require.NoError(t, err)
   423  	require.Len(t, returned, 4, "expected 4 reactions")
   424  
   425  	_, err = ss.Reaction().PermanentDeleteBatch(1800, 1000)
   426  	require.NoError(t, err)
   427  
   428  	// This is to force a clear of the cache.
   429  	_, err = ss.Reaction().Delete(lastReaction)
   430  	require.NoError(t, err)
   431  
   432  	returned, err = ss.Reaction().GetForPost(post.Id, false)
   433  	require.NoError(t, err)
   434  	require.Len(t, returned, 1, "expected 1 reaction. Got: %v", len(returned))
   435  }
   436  
   437  func testReactionBulkGetForPosts(t *testing.T, ss store.Store) {
   438  	postId := model.NewId()
   439  	post2Id := model.NewId()
   440  	post3Id := model.NewId()
   441  	post4Id := model.NewId()
   442  
   443  	userId := model.NewId()
   444  
   445  	reactions := []*model.Reaction{
   446  		{
   447  			UserId:    userId,
   448  			PostId:    postId,
   449  			EmojiName: "smile",
   450  		},
   451  		{
   452  			UserId:    model.NewId(),
   453  			PostId:    post2Id,
   454  			EmojiName: "smile",
   455  		},
   456  		{
   457  			UserId:    userId,
   458  			PostId:    post3Id,
   459  			EmojiName: "sad",
   460  		},
   461  		{
   462  			UserId:    userId,
   463  			PostId:    postId,
   464  			EmojiName: "angry",
   465  		},
   466  		{
   467  			UserId:    userId,
   468  			PostId:    post2Id,
   469  			EmojiName: "angry",
   470  		},
   471  		{
   472  			UserId:    userId,
   473  			PostId:    post4Id,
   474  			EmojiName: "angry",
   475  		},
   476  	}
   477  
   478  	for _, reaction := range reactions {
   479  		_, err := ss.Reaction().Save(reaction)
   480  		require.NoError(t, err)
   481  	}
   482  
   483  	postIds := []string{postId, post2Id, post3Id}
   484  	returned, err := ss.Reaction().BulkGetForPosts(postIds)
   485  	require.NoError(t, err)
   486  	require.Len(t, returned, 5, "should've returned 5 reactions")
   487  
   488  	post4IdFound := false
   489  	for _, reaction := range returned {
   490  		if reaction.PostId == post4Id {
   491  			post4IdFound = true
   492  			break
   493  		}
   494  	}
   495  
   496  	require.False(t, post4IdFound, "Wrong reaction returned")
   497  
   498  }
   499  
   500  // testReactionDeadlock is a best-case attempt to recreate the deadlock scenario.
   501  // It at least deadlocks 2 times out of 5.
   502  func testReactionDeadlock(t *testing.T, ss store.Store) {
   503  	ss = retrylayer.New(ss)
   504  
   505  	post, err := ss.Post().Save(&model.Post{
   506  		ChannelId: model.NewId(),
   507  		UserId:    model.NewId(),
   508  	})
   509  	require.NoError(t, err)
   510  
   511  	reaction1 := &model.Reaction{
   512  		UserId:    model.NewId(),
   513  		PostId:    post.Id,
   514  		EmojiName: model.NewId(),
   515  	}
   516  	_, nErr := ss.Reaction().Save(reaction1)
   517  	require.NoError(t, nErr)
   518  
   519  	// different user
   520  	reaction2 := &model.Reaction{
   521  		UserId:    model.NewId(),
   522  		PostId:    reaction1.PostId,
   523  		EmojiName: reaction1.EmojiName,
   524  	}
   525  	_, nErr = ss.Reaction().Save(reaction2)
   526  	require.NoError(t, nErr)
   527  
   528  	// different post
   529  	reaction3 := &model.Reaction{
   530  		UserId:    reaction1.UserId,
   531  		PostId:    model.NewId(),
   532  		EmojiName: reaction1.EmojiName,
   533  	}
   534  	_, nErr = ss.Reaction().Save(reaction3)
   535  	require.NoError(t, nErr)
   536  
   537  	// different emoji
   538  	reaction4 := &model.Reaction{
   539  		UserId:    reaction1.UserId,
   540  		PostId:    reaction1.PostId,
   541  		EmojiName: model.NewId(),
   542  	}
   543  	_, nErr = ss.Reaction().Save(reaction4)
   544  	require.NoError(t, nErr)
   545  
   546  	var wg sync.WaitGroup
   547  	wg.Add(2)
   548  	// 1st tx
   549  	go func() {
   550  		defer wg.Done()
   551  		err := ss.Reaction().DeleteAllWithEmojiName(reaction1.EmojiName)
   552  		require.NoError(t, err)
   553  	}()
   554  
   555  	// 2nd tx
   556  	go func() {
   557  		defer wg.Done()
   558  		_, err := ss.Reaction().Delete(reaction2)
   559  		require.NoError(t, err)
   560  	}()
   561  	wg.Wait()
   562  }