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 }