redeem_cache_integration_test.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. //go:build integration
  2. package repository
  3. import (
  4. "fmt"
  5. "testing"
  6. "time"
  7. "github.com/stretchr/testify/require"
  8. "github.com/stretchr/testify/suite"
  9. )
  10. type RedeemCacheSuite struct {
  11. IntegrationRedisSuite
  12. cache *redeemCache
  13. }
  14. func (s *RedeemCacheSuite) SetupTest() {
  15. s.IntegrationRedisSuite.SetupTest()
  16. s.cache = NewRedeemCache(s.rdb).(*redeemCache)
  17. }
  18. func (s *RedeemCacheSuite) TestGetRedeemAttemptCount_Missing() {
  19. missingUserID := int64(99999)
  20. count, err := s.cache.GetRedeemAttemptCount(s.ctx, missingUserID)
  21. require.NoError(s.T(), err, "expected nil error for missing rate-limit key")
  22. require.Equal(s.T(), 0, count, "expected zero count for missing key")
  23. }
  24. func (s *RedeemCacheSuite) TestIncrementAndGetRedeemAttemptCount() {
  25. userID := int64(1)
  26. key := fmt.Sprintf("%s%d", redeemRateLimitKeyPrefix, userID)
  27. require.NoError(s.T(), s.cache.IncrementRedeemAttemptCount(s.ctx, userID), "IncrementRedeemAttemptCount")
  28. count, err := s.cache.GetRedeemAttemptCount(s.ctx, userID)
  29. require.NoError(s.T(), err, "GetRedeemAttemptCount")
  30. require.Equal(s.T(), 1, count, "count mismatch")
  31. ttl, err := s.rdb.TTL(s.ctx, key).Result()
  32. require.NoError(s.T(), err, "TTL")
  33. s.AssertTTLWithin(ttl, redeemRateLimitWindow-time.Second, redeemRateLimitWindow)
  34. }
  35. func (s *RedeemCacheSuite) TestIncrementDoesNotRefreshFixedWindow() {
  36. userID := int64(3)
  37. key := redeemRateLimitKey(userID)
  38. shortTTL := 5 * time.Minute
  39. require.NoError(s.T(), s.cache.IncrementRedeemAttemptCount(s.ctx, userID))
  40. require.NoError(s.T(), s.rdb.PExpire(s.ctx, key, shortTTL).Err())
  41. require.NoError(s.T(), s.cache.IncrementRedeemAttemptCount(s.ctx, userID))
  42. ttl, err := s.rdb.PTTL(s.ctx, key).Result()
  43. require.NoError(s.T(), err)
  44. s.AssertTTLWithin(ttl, shortTTL-time.Second, shortTTL)
  45. }
  46. func (s *RedeemCacheSuite) TestIncrementRepairsMissingTTL() {
  47. userID := int64(4)
  48. key := redeemRateLimitKey(userID)
  49. require.NoError(s.T(), s.rdb.Set(s.ctx, key, 5, 0).Err())
  50. ttl, err := s.rdb.PTTL(s.ctx, key).Result()
  51. require.NoError(s.T(), err)
  52. require.Equal(s.T(), time.Duration(-1), ttl)
  53. require.NoError(s.T(), s.cache.IncrementRedeemAttemptCount(s.ctx, userID))
  54. count, err := s.cache.GetRedeemAttemptCount(s.ctx, userID)
  55. require.NoError(s.T(), err)
  56. require.Equal(s.T(), 6, count)
  57. ttl, err = s.rdb.PTTL(s.ctx, key).Result()
  58. require.NoError(s.T(), err)
  59. s.AssertTTLWithin(ttl, redeemRateLimitWindow-time.Second, redeemRateLimitWindow)
  60. }
  61. func (s *RedeemCacheSuite) TestMultipleIncrements() {
  62. userID := int64(2)
  63. require.NoError(s.T(), s.cache.IncrementRedeemAttemptCount(s.ctx, userID))
  64. require.NoError(s.T(), s.cache.IncrementRedeemAttemptCount(s.ctx, userID))
  65. require.NoError(s.T(), s.cache.IncrementRedeemAttemptCount(s.ctx, userID))
  66. count, err := s.cache.GetRedeemAttemptCount(s.ctx, userID)
  67. require.NoError(s.T(), err)
  68. require.Equal(s.T(), 3, count, "count after 3 increments")
  69. }
  70. func (s *RedeemCacheSuite) TestAcquireAndReleaseRedeemLock() {
  71. ok, err := s.cache.AcquireRedeemLock(s.ctx, "CODE", 10*time.Second)
  72. require.NoError(s.T(), err, "AcquireRedeemLock")
  73. require.True(s.T(), ok)
  74. // Second acquire should fail
  75. ok, err = s.cache.AcquireRedeemLock(s.ctx, "CODE", 10*time.Second)
  76. require.NoError(s.T(), err, "AcquireRedeemLock 2")
  77. require.False(s.T(), ok, "expected lock to be held")
  78. // Release
  79. require.NoError(s.T(), s.cache.ReleaseRedeemLock(s.ctx, "CODE"), "ReleaseRedeemLock")
  80. // Now acquire should succeed
  81. ok, err = s.cache.AcquireRedeemLock(s.ctx, "CODE", 10*time.Second)
  82. require.NoError(s.T(), err, "AcquireRedeemLock after release")
  83. require.True(s.T(), ok)
  84. }
  85. func (s *RedeemCacheSuite) TestAcquireRedeemLock_TTL() {
  86. lockKey := redeemLockKeyPrefix + "CODE2"
  87. lockTTL := 15 * time.Second
  88. ok, err := s.cache.AcquireRedeemLock(s.ctx, "CODE2", lockTTL)
  89. require.NoError(s.T(), err, "AcquireRedeemLock CODE2")
  90. require.True(s.T(), ok)
  91. ttl, err := s.rdb.TTL(s.ctx, lockKey).Result()
  92. require.NoError(s.T(), err, "TTL lock key")
  93. s.AssertTTLWithin(ttl, 1*time.Second, lockTTL)
  94. }
  95. func (s *RedeemCacheSuite) TestReleaseRedeemLock_Idempotent() {
  96. // Release a lock that doesn't exist should not error
  97. require.NoError(s.T(), s.cache.ReleaseRedeemLock(s.ctx, "NONEXISTENT"))
  98. // Acquire, release, release again
  99. ok, err := s.cache.AcquireRedeemLock(s.ctx, "IDEMPOTENT", 10*time.Second)
  100. require.NoError(s.T(), err)
  101. require.True(s.T(), ok)
  102. require.NoError(s.T(), s.cache.ReleaseRedeemLock(s.ctx, "IDEMPOTENT"))
  103. require.NoError(s.T(), s.cache.ReleaseRedeemLock(s.ctx, "IDEMPOTENT"), "second release should be idempotent")
  104. }
  105. func TestRedeemCacheSuite(t *testing.T) {
  106. suite.Run(t, new(RedeemCacheSuite))
  107. }