github.com/mattermost/mattermost-server/v5@v5.39.3/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 "context" 8 "errors" 9 "sync" 10 "testing" 11 "time" 12 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 16 "github.com/mattermost/mattermost-server/v5/model" 17 "github.com/mattermost/mattermost-server/v5/store" 18 "github.com/mattermost/mattermost-server/v5/store/retrylayer" 19 ) 20 21 func TestReactionStore(t *testing.T, ss store.Store, s SqlStore) { 22 t.Run("ReactionSave", func(t *testing.T) { testReactionSave(t, ss) }) 23 t.Run("ReactionDelete", func(t *testing.T) { testReactionDelete(t, ss) }) 24 t.Run("ReactionGetForPost", func(t *testing.T) { testReactionGetForPost(t, ss) }) 25 t.Run("ReactionGetForPostSince", func(t *testing.T) { testReactionGetForPostSince(t, ss, s) }) 26 t.Run("ReactionDeleteAllWithEmojiName", func(t *testing.T) { testReactionDeleteAllWithEmojiName(t, ss, s) }) 27 t.Run("PermanentDeleteBatch", func(t *testing.T) { testReactionStorePermanentDeleteBatch(t, ss) }) 28 t.Run("ReactionBulkGetForPosts", func(t *testing.T) { testReactionBulkGetForPosts(t, ss) }) 29 t.Run("ReactionDeadlock", func(t *testing.T) { testReactionDeadlock(t, ss) }) 30 } 31 32 func testReactionSave(t *testing.T, ss store.Store) { 33 post, err := ss.Post().Save(&model.Post{ 34 ChannelId: model.NewId(), 35 UserId: model.NewId(), 36 }) 37 require.NoError(t, err) 38 firstUpdateAt := post.UpdateAt 39 40 reaction1 := &model.Reaction{ 41 UserId: model.NewId(), 42 PostId: post.Id, 43 EmojiName: model.NewId(), 44 } 45 46 time.Sleep(time.Millisecond) 47 reaction, nErr := ss.Reaction().Save(reaction1) 48 require.NoError(t, nErr) 49 50 saved := reaction 51 assert.Equal(t, saved.UserId, reaction1.UserId, "should've saved reaction user_id and returned it") 52 assert.Equal(t, saved.PostId, reaction1.PostId, "should've saved reaction post_id and returned it") 53 assert.Equal(t, saved.EmojiName, reaction1.EmojiName, "should've saved reaction emoji_name and returned it") 54 assert.NotZero(t, saved.UpdateAt, "should've saved reaction update_at and returned it") 55 assert.Zero(t, saved.DeleteAt, "should've saved reaction delete_at with zero value and returned it") 56 57 var secondUpdateAt int64 58 postList, err := ss.Post().Get(context.Background(), reaction1.PostId, false, false, false, "") 59 require.NoError(t, err) 60 61 assert.True(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = true on post") 62 assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should've marked post as updated when HasReactions changed") 63 64 if postList.Posts[post.Id].HasReactions && postList.Posts[post.Id].UpdateAt != firstUpdateAt { 65 secondUpdateAt = postList.Posts[post.Id].UpdateAt 66 } 67 68 _, nErr = ss.Reaction().Save(reaction1) 69 assert.NoError(t, nErr, "should've allowed saving a duplicate reaction") 70 71 // different user 72 reaction2 := &model.Reaction{ 73 UserId: model.NewId(), 74 PostId: reaction1.PostId, 75 EmojiName: reaction1.EmojiName, 76 } 77 78 time.Sleep(time.Millisecond) 79 _, nErr = ss.Reaction().Save(reaction2) 80 require.NoError(t, nErr) 81 82 postList, err = ss.Post().Get(context.Background(), reaction2.PostId, false, false, false, "") 83 require.NoError(t, err) 84 85 assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, secondUpdateAt, "should've marked post as updated even if HasReactions doesn't change") 86 87 // different post 88 reaction3 := &model.Reaction{ 89 UserId: reaction1.UserId, 90 PostId: model.NewId(), 91 EmojiName: reaction1.EmojiName, 92 } 93 _, nErr = ss.Reaction().Save(reaction3) 94 require.NoError(t, nErr) 95 96 // different emoji 97 reaction4 := &model.Reaction{ 98 UserId: reaction1.UserId, 99 PostId: reaction1.PostId, 100 EmojiName: model.NewId(), 101 } 102 _, nErr = ss.Reaction().Save(reaction4) 103 require.NoError(t, nErr) 104 105 // invalid reaction 106 reaction5 := &model.Reaction{ 107 UserId: reaction1.UserId, 108 PostId: reaction1.PostId, 109 } 110 _, nErr = ss.Reaction().Save(reaction5) 111 require.Error(t, nErr, "should've failed for invalid reaction") 112 113 } 114 115 func testReactionDelete(t *testing.T, ss store.Store) { 116 t.Run("Delete", func(t *testing.T) { 117 post, err := ss.Post().Save(&model.Post{ 118 ChannelId: model.NewId(), 119 UserId: model.NewId(), 120 }) 121 require.NoError(t, err) 122 123 reaction := &model.Reaction{ 124 UserId: model.NewId(), 125 PostId: post.Id, 126 EmojiName: model.NewId(), 127 } 128 129 _, nErr := ss.Reaction().Save(reaction) 130 require.NoError(t, nErr) 131 132 result, err := ss.Post().Get(context.Background(), reaction.PostId, false, false, false, "") 133 require.NoError(t, err) 134 135 firstUpdateAt := result.Posts[post.Id].UpdateAt 136 137 _, nErr = ss.Reaction().Delete(reaction) 138 require.NoError(t, nErr) 139 140 reactions, rErr := ss.Reaction().GetForPost(post.Id, false) 141 require.NoError(t, rErr) 142 143 assert.Empty(t, reactions, "should've deleted reaction") 144 145 postList, err := ss.Post().Get(context.Background(), post.Id, false, false, false, "") 146 require.NoError(t, err) 147 148 assert.False(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = false on post") 149 assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should mark post as updated after deleting reactions") 150 }) 151 152 t.Run("Undelete", func(t *testing.T) { 153 post, err := ss.Post().Save(&model.Post{ 154 ChannelId: model.NewId(), 155 UserId: model.NewId(), 156 }) 157 require.NoError(t, err) 158 159 reaction := &model.Reaction{ 160 UserId: model.NewId(), 161 PostId: post.Id, 162 EmojiName: model.NewId(), 163 } 164 165 savedReaction, nErr := ss.Reaction().Save(reaction) 166 require.NoError(t, nErr) 167 168 updateAt := savedReaction.UpdateAt 169 170 _, nErr = ss.Reaction().Delete(savedReaction) 171 require.NoError(t, nErr) 172 173 // add same reaction back and ensure update_at is set 174 _, nErr = ss.Reaction().Save(savedReaction) 175 require.NoError(t, nErr) 176 177 reactions, err := ss.Reaction().GetForPost(post.Id, false) 178 require.NoError(t, err) 179 180 assert.Len(t, reactions, 1) 181 assert.GreaterOrEqual(t, reactions[0].UpdateAt, updateAt) 182 }) 183 } 184 185 func testReactionGetForPost(t *testing.T, ss store.Store) { 186 postId := model.NewId() 187 188 userId := model.NewId() 189 190 reactions := []*model.Reaction{ 191 { 192 UserId: userId, 193 PostId: postId, 194 EmojiName: "smile", 195 }, 196 { 197 UserId: model.NewId(), 198 PostId: postId, 199 EmojiName: "smile", 200 }, 201 { 202 UserId: userId, 203 PostId: postId, 204 EmojiName: "sad", 205 }, 206 { 207 UserId: userId, 208 PostId: model.NewId(), 209 EmojiName: "angry", 210 }, 211 } 212 213 for _, reaction := range reactions { 214 _, err := ss.Reaction().Save(reaction) 215 require.NoError(t, err) 216 } 217 218 // save and delete an additional reaction to test soft deletion 219 temp := &model.Reaction{ 220 UserId: userId, 221 PostId: postId, 222 EmojiName: "grin", 223 } 224 savedTmp, err := ss.Reaction().Save(temp) 225 require.NoError(t, err) 226 _, err = ss.Reaction().Delete(savedTmp) 227 require.NoError(t, err) 228 229 returned, err := ss.Reaction().GetForPost(postId, false) 230 require.NoError(t, err) 231 require.Len(t, returned, 3, "should've returned 3 reactions") 232 233 for _, reaction := range reactions { 234 found := false 235 236 for _, returnedReaction := range returned { 237 if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId && 238 returnedReaction.EmojiName == reaction.EmojiName && returnedReaction.UpdateAt > 0 { 239 found = true 240 break 241 } 242 } 243 244 if !found { 245 assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction) 246 } else if found { 247 assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post") 248 } 249 } 250 251 // Should return cached item 252 returned, err = ss.Reaction().GetForPost(postId, true) 253 require.NoError(t, err) 254 require.Len(t, returned, 3, "should've returned 3 reactions") 255 256 for _, reaction := range reactions { 257 found := false 258 259 for _, returnedReaction := range returned { 260 if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId && 261 returnedReaction.EmojiName == reaction.EmojiName { 262 found = true 263 break 264 } 265 } 266 267 if !found { 268 assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction) 269 } else if found { 270 assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post") 271 } 272 } 273 } 274 275 func testReactionGetForPostSince(t *testing.T, ss store.Store, s SqlStore) { 276 now := model.GetMillis() 277 later := now + 1800000 // add 30 minutes 278 remoteId := model.NewId() 279 280 postId := model.NewId() 281 userId := model.NewId() 282 reactions := []*model.Reaction{ 283 { 284 UserId: userId, 285 PostId: postId, 286 EmojiName: "smile", 287 UpdateAt: later, 288 }, 289 { 290 UserId: model.NewId(), 291 PostId: postId, 292 EmojiName: "smile", 293 }, 294 { 295 UserId: userId, 296 PostId: postId, 297 EmojiName: "sad", 298 UpdateAt: later, 299 RemoteId: &remoteId, 300 }, 301 { 302 UserId: userId, 303 PostId: model.NewId(), 304 EmojiName: "angry", 305 }, 306 { 307 UserId: userId, 308 PostId: postId, 309 EmojiName: "angry", 310 DeleteAt: now + 1, 311 UpdateAt: later, 312 }, 313 } 314 315 for _, reaction := range reactions { 316 delete := reaction.DeleteAt 317 update := reaction.UpdateAt 318 319 _, err := ss.Reaction().Save(reaction) 320 require.NoError(t, err) 321 322 if delete > 0 { 323 _, err = ss.Reaction().Delete(reaction) 324 require.NoError(t, err) 325 } 326 if update > 0 { 327 err = forceUpdateAt(reaction, update, s) 328 require.NoError(t, err) 329 } 330 err = forceNULL(reaction, s) // test COALESCE 331 require.NoError(t, err) 332 } 333 334 t.Run("reactions since", func(t *testing.T) { 335 // should return 2 reactions that are not deleted for post 336 returned, err := ss.Reaction().GetForPostSince(postId, later-1, "", false) 337 require.NoError(t, err) 338 require.Len(t, returned, 2, "should've returned 2 non-deleted reactions") 339 for _, r := range returned { 340 assert.Zero(t, r.DeleteAt, "should not have returned deleted reaction") 341 } 342 343 }) 344 345 t.Run("reactions since, incl deleted", func(t *testing.T) { 346 // should return 3 reactions for post, including one deleted 347 returned, err := ss.Reaction().GetForPostSince(postId, later-1, "", true) 348 require.NoError(t, err) 349 require.Len(t, returned, 3, "should've returned 3 reactions") 350 var count int 351 for _, r := range returned { 352 if r.DeleteAt > 0 { 353 count++ 354 } 355 } 356 assert.Equal(t, 1, count, "should not have returned 1 deleted reaction") 357 358 }) 359 360 t.Run("reactions since, filter remoteId", func(t *testing.T) { 361 // should return 1 reactions that are not deleted for post and have no remoteId 362 returned, err := ss.Reaction().GetForPostSince(postId, later-1, remoteId, false) 363 require.NoError(t, err) 364 require.Len(t, returned, 1, "should've returned 1 filtered reactions") 365 for _, r := range returned { 366 assert.Zero(t, r.DeleteAt, "should not have returned deleted reaction") 367 } 368 }) 369 370 t.Run("reactions since, invalid post", func(t *testing.T) { 371 // should return 0 reactions for invalid post 372 returned, err := ss.Reaction().GetForPostSince(model.NewId(), later-1, "", true) 373 require.NoError(t, err) 374 require.Empty(t, returned, "should've returned 0 reactions") 375 }) 376 377 t.Run("reactions since, far future", func(t *testing.T) { 378 // should return 0 reactions for since far in the future 379 returned, err := ss.Reaction().GetForPostSince(postId, later*2, "", true) 380 require.NoError(t, err) 381 require.Empty(t, returned, "should've returned 0 reactions") 382 }) 383 } 384 385 func forceUpdateAt(reaction *model.Reaction, updateAt int64, s SqlStore) error { 386 params := map[string]interface{}{ 387 "UserId": reaction.UserId, 388 "PostId": reaction.PostId, 389 "EmojiName": reaction.EmojiName, 390 "UpdateAt": updateAt, 391 } 392 393 sqlResult, err := s.GetMaster().Exec(` 394 UPDATE 395 Reactions 396 SET 397 UpdateAt=:UpdateAt 398 WHERE 399 UserId = :UserId AND 400 PostId = :PostId AND 401 EmojiName = :EmojiName`, params, 402 ) 403 404 if err != nil { 405 return err 406 } 407 408 rows, err := sqlResult.RowsAffected() 409 if err != nil { 410 return err 411 } 412 413 if rows != 1 { 414 return errors.New("expected one row affected") 415 } 416 return nil 417 } 418 419 func forceNULL(reaction *model.Reaction, s SqlStore) error { 420 if _, err := s.GetMaster().Exec(`UPDATE Reactions SET UpdateAt = NULL WHERE UpdateAt = 0`); err != nil { 421 return err 422 } 423 if _, err := s.GetMaster().Exec(`UPDATE Reactions SET DeleteAt = NULL WHERE DeleteAt = 0`); err != nil { 424 return err 425 } 426 return nil 427 } 428 429 func testReactionDeleteAllWithEmojiName(t *testing.T, ss store.Store, s SqlStore) { 430 emojiToDelete := model.NewId() 431 432 post, err1 := ss.Post().Save(&model.Post{ 433 ChannelId: model.NewId(), 434 UserId: model.NewId(), 435 }) 436 require.NoError(t, err1) 437 post2, err2 := ss.Post().Save(&model.Post{ 438 ChannelId: model.NewId(), 439 UserId: model.NewId(), 440 }) 441 require.NoError(t, err2) 442 post3, err3 := ss.Post().Save(&model.Post{ 443 ChannelId: model.NewId(), 444 UserId: model.NewId(), 445 }) 446 require.NoError(t, err3) 447 448 userId := model.NewId() 449 450 reactions := []*model.Reaction{ 451 { 452 UserId: userId, 453 PostId: post.Id, 454 EmojiName: emojiToDelete, 455 }, 456 { 457 UserId: model.NewId(), 458 PostId: post.Id, 459 EmojiName: emojiToDelete, 460 }, 461 { 462 UserId: userId, 463 PostId: post.Id, 464 EmojiName: "sad", 465 }, 466 { 467 UserId: userId, 468 PostId: post2.Id, 469 EmojiName: "angry", 470 }, 471 { 472 UserId: userId, 473 PostId: post3.Id, 474 EmojiName: emojiToDelete, 475 }, 476 } 477 478 for _, reaction := range reactions { 479 _, err := ss.Reaction().Save(reaction) 480 require.NoError(t, err) 481 482 // make at least one Reaction record contain NULL for Update and DeleteAt to simulate post schema upgrade case. 483 if reaction.EmojiName == emojiToDelete { 484 err = forceNULL(reaction, s) 485 require.NoError(t, err) 486 } 487 } 488 489 err := ss.Reaction().DeleteAllWithEmojiName(emojiToDelete) 490 require.NoError(t, err) 491 492 // check that the reactions were deleted 493 returned, err := ss.Reaction().GetForPost(post.Id, false) 494 require.NoError(t, err) 495 require.Len(t, returned, 1, "should've only removed reactions with emoji name") 496 497 for _, reaction := range returned { 498 assert.NotEqual(t, reaction.EmojiName, "smile", "should've removed reaction with emoji name") 499 } 500 501 returned, err = ss.Reaction().GetForPost(post2.Id, false) 502 require.NoError(t, err) 503 assert.Len(t, returned, 1, "should've only removed reactions with emoji name") 504 505 returned, err = ss.Reaction().GetForPost(post3.Id, false) 506 require.NoError(t, err) 507 assert.Empty(t, returned, "should've only removed reactions with emoji name") 508 509 // check that the posts are updated 510 postList, err := ss.Post().Get(context.Background(), post.Id, false, false, false, "") 511 require.NoError(t, err) 512 assert.True(t, postList.Posts[post.Id].HasReactions, "post should still have reactions") 513 514 postList, err = ss.Post().Get(context.Background(), post2.Id, false, false, false, "") 515 require.NoError(t, err) 516 assert.True(t, postList.Posts[post2.Id].HasReactions, "post should still have reactions") 517 518 postList, err = ss.Post().Get(context.Background(), post3.Id, false, false, false, "") 519 require.NoError(t, err) 520 assert.False(t, postList.Posts[post3.Id].HasReactions, "post shouldn't have reactions any more") 521 522 } 523 524 func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) { 525 const limit = 1000 526 team, err := ss.Team().Save(&model.Team{ 527 DisplayName: "DisplayName", 528 Name: "team" + model.NewId(), 529 Email: MakeEmail(), 530 Type: model.TEAM_OPEN, 531 }) 532 require.NoError(t, err) 533 channel, err := ss.Channel().Save(&model.Channel{ 534 TeamId: team.Id, 535 DisplayName: "DisplayName", 536 Name: "channel" + model.NewId(), 537 Type: model.CHANNEL_OPEN, 538 }, -1) 539 require.NoError(t, err) 540 olderPost, err := ss.Post().Save(&model.Post{ 541 ChannelId: channel.Id, 542 UserId: model.NewId(), 543 CreateAt: 1000, 544 }) 545 require.NoError(t, err) 546 newerPost, err := ss.Post().Save(&model.Post{ 547 ChannelId: channel.Id, 548 UserId: model.NewId(), 549 CreateAt: 3000, 550 }) 551 require.NoError(t, err) 552 553 // Reactions will be deleted based on the timestamp of their post. So the time at 554 // which a reaction was created doesn't matter. 555 reactions := []*model.Reaction{ 556 { 557 UserId: model.NewId(), 558 PostId: olderPost.Id, 559 EmojiName: "sad", 560 }, 561 { 562 UserId: model.NewId(), 563 PostId: olderPost.Id, 564 EmojiName: "sad", 565 }, 566 { 567 UserId: model.NewId(), 568 PostId: newerPost.Id, 569 EmojiName: "smile", 570 }, 571 } 572 573 for _, reaction := range reactions { 574 _, err = ss.Reaction().Save(reaction) 575 require.NoError(t, err) 576 } 577 578 _, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{}) 579 require.NoError(t, err) 580 581 _, err = ss.Reaction().DeleteOrphanedRows(limit) 582 require.NoError(t, err) 583 584 returned, err := ss.Reaction().GetForPost(olderPost.Id, false) 585 require.NoError(t, err) 586 require.Len(t, returned, 0, "reactions for older post should have been deleted") 587 588 returned, err = ss.Reaction().GetForPost(newerPost.Id, false) 589 require.NoError(t, err) 590 require.Len(t, returned, 1, "reactions for newer post should not have been deleted") 591 } 592 593 func testReactionBulkGetForPosts(t *testing.T, ss store.Store) { 594 postId := model.NewId() 595 post2Id := model.NewId() 596 post3Id := model.NewId() 597 post4Id := model.NewId() 598 599 userId := model.NewId() 600 601 reactions := []*model.Reaction{ 602 { 603 UserId: userId, 604 PostId: postId, 605 EmojiName: "smile", 606 }, 607 { 608 UserId: model.NewId(), 609 PostId: post2Id, 610 EmojiName: "smile", 611 }, 612 { 613 UserId: userId, 614 PostId: post3Id, 615 EmojiName: "sad", 616 }, 617 { 618 UserId: userId, 619 PostId: postId, 620 EmojiName: "angry", 621 }, 622 { 623 UserId: userId, 624 PostId: post2Id, 625 EmojiName: "angry", 626 }, 627 { 628 UserId: userId, 629 PostId: post4Id, 630 EmojiName: "angry", 631 }, 632 } 633 634 for _, reaction := range reactions { 635 _, err := ss.Reaction().Save(reaction) 636 require.NoError(t, err) 637 } 638 639 postIds := []string{postId, post2Id, post3Id} 640 returned, err := ss.Reaction().BulkGetForPosts(postIds) 641 require.NoError(t, err) 642 require.Len(t, returned, 5, "should've returned 5 reactions") 643 644 post4IdFound := false 645 for _, reaction := range returned { 646 if reaction.PostId == post4Id { 647 post4IdFound = true 648 break 649 } 650 } 651 652 require.False(t, post4IdFound, "Wrong reaction returned") 653 654 } 655 656 // testReactionDeadlock is a best-case attempt to recreate the deadlock scenario. 657 // It at least deadlocks 2 times out of 5. 658 func testReactionDeadlock(t *testing.T, ss store.Store) { 659 ss = retrylayer.New(ss) 660 661 post, err := ss.Post().Save(&model.Post{ 662 ChannelId: model.NewId(), 663 UserId: model.NewId(), 664 }) 665 require.NoError(t, err) 666 667 reaction1 := &model.Reaction{ 668 UserId: model.NewId(), 669 PostId: post.Id, 670 EmojiName: model.NewId(), 671 } 672 _, nErr := ss.Reaction().Save(reaction1) 673 require.NoError(t, nErr) 674 675 // different user 676 reaction2 := &model.Reaction{ 677 UserId: model.NewId(), 678 PostId: reaction1.PostId, 679 EmojiName: reaction1.EmojiName, 680 } 681 _, nErr = ss.Reaction().Save(reaction2) 682 require.NoError(t, nErr) 683 684 // different post 685 reaction3 := &model.Reaction{ 686 UserId: reaction1.UserId, 687 PostId: model.NewId(), 688 EmojiName: reaction1.EmojiName, 689 } 690 _, nErr = ss.Reaction().Save(reaction3) 691 require.NoError(t, nErr) 692 693 // different emoji 694 reaction4 := &model.Reaction{ 695 UserId: reaction1.UserId, 696 PostId: reaction1.PostId, 697 EmojiName: model.NewId(), 698 } 699 _, nErr = ss.Reaction().Save(reaction4) 700 require.NoError(t, nErr) 701 702 var wg sync.WaitGroup 703 wg.Add(2) 704 // 1st tx 705 go func() { 706 defer wg.Done() 707 err := ss.Reaction().DeleteAllWithEmojiName(reaction1.EmojiName) 708 require.NoError(t, err) 709 }() 710 711 // 2nd tx 712 go func() { 713 defer wg.Done() 714 _, err := ss.Reaction().Delete(reaction2) 715 require.NoError(t, err) 716 }() 717 wg.Wait() 718 }