billing_cache_service_balance_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "sync/atomic"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. "github.com/stretchr/testify/require"
  11. )
  12. type balanceEligibilityCacheStub struct {
  13. billingCacheWorkerStub
  14. balance float64
  15. cacheMissAfterInvalidate bool
  16. invalidated atomic.Bool
  17. deductCalls atomic.Int64
  18. invalidateCalls atomic.Int64
  19. }
  20. func (s *balanceEligibilityCacheStub) GetUserBalance(context.Context, int64) (float64, error) {
  21. if s.cacheMissAfterInvalidate && s.invalidated.Load() {
  22. return 0, errors.New("cache miss")
  23. }
  24. return s.balance, nil
  25. }
  26. func (s *balanceEligibilityCacheStub) DeductUserBalance(context.Context, int64, float64) error {
  27. s.deductCalls.Add(1)
  28. return nil
  29. }
  30. func (s *balanceEligibilityCacheStub) InvalidateUserBalance(context.Context, int64) error {
  31. s.invalidateCalls.Add(1)
  32. s.invalidated.Store(true)
  33. return nil
  34. }
  35. func TestCheckBillingEligibility_RejectsBalanceBelowMinimumReserve(t *testing.T) {
  36. cache := &balanceEligibilityCacheStub{balance: 0.005}
  37. cfg := &config.Config{}
  38. cfg.Billing.MinimumBalanceReserve = 0.01
  39. svc := NewBillingCacheService(cache, nil, nil, nil, nil, nil, cfg, nil)
  40. t.Cleanup(svc.Stop)
  41. err := svc.CheckBillingEligibility(context.Background(), &User{ID: 1}, nil, nil, nil, "")
  42. require.ErrorIs(t, err, ErrInsufficientBalance)
  43. }
  44. func TestCheckBillingEligibility_AllowsBalanceAtMinimumReserve(t *testing.T) {
  45. cache := &balanceEligibilityCacheStub{balance: 0.01}
  46. cfg := &config.Config{}
  47. cfg.Billing.MinimumBalanceReserve = 0.01
  48. svc := NewBillingCacheService(cache, nil, nil, nil, nil, nil, cfg, nil)
  49. t.Cleanup(svc.Stop)
  50. err := svc.CheckBillingEligibility(context.Background(), &User{ID: 1}, nil, nil, nil, "")
  51. require.NoError(t, err)
  52. }
  53. func TestSyncBalanceCacheAfterDeduction_InvalidatesExhaustedBalance(t *testing.T) {
  54. cache := &balanceEligibilityCacheStub{
  55. balance: 0.50,
  56. cacheMissAfterInvalidate: true,
  57. }
  58. userRepo := &balanceLoadUserRepoStub{balance: -0.25}
  59. cfg := &config.Config{}
  60. cfg.Billing.MinimumBalanceReserve = 0.01
  61. svc := NewBillingCacheService(cache, userRepo, nil, nil, nil, nil, cfg, nil)
  62. t.Cleanup(svc.Stop)
  63. newBalance := -0.25
  64. syncBalanceCacheAfterDeduction(context.Background(), &postUsageBillingParams{
  65. Cost: &CostBreakdown{ActualCost: 0.75},
  66. User: &User{ID: 1},
  67. }, &billingDeps{billingCacheService: svc}, &UsageBillingApplyResult{
  68. NewBalance: &newBalance,
  69. BalanceOverdrafted: true,
  70. })
  71. require.Equal(t, int64(1), cache.invalidateCalls.Load())
  72. require.Equal(t, int64(0), cache.deductCalls.Load())
  73. err := svc.CheckBillingEligibility(context.Background(), &User{ID: 1}, nil, nil, nil, "")
  74. require.ErrorIs(t, err, ErrInsufficientBalance)
  75. require.Equal(t, int64(1), userRepo.calls.Load())
  76. }
  77. func TestSyncBalanceCacheAfterDeduction_InvalidatesWhenBalanceFallsBelowReserve(t *testing.T) {
  78. cache := &balanceEligibilityCacheStub{balance: 0.50}
  79. cfg := &config.Config{}
  80. cfg.Billing.MinimumBalanceReserve = 0.01
  81. svc := NewBillingCacheService(cache, nil, nil, nil, nil, nil, cfg, nil)
  82. t.Cleanup(svc.Stop)
  83. newBalance := 0.005
  84. syncBalanceCacheAfterDeduction(context.Background(), &postUsageBillingParams{
  85. Cost: &CostBreakdown{ActualCost: 0.495},
  86. User: &User{ID: 1},
  87. }, &billingDeps{billingCacheService: svc}, &UsageBillingApplyResult{NewBalance: &newBalance})
  88. require.Equal(t, int64(1), cache.invalidateCalls.Load())
  89. require.Equal(t, int64(0), cache.deductCalls.Load())
  90. }
  91. func TestSyncBalanceCacheAfterDeduction_QueuesDeductWhenBalanceStillEligible(t *testing.T) {
  92. cache := &balanceEligibilityCacheStub{balance: 1}
  93. cfg := &config.Config{}
  94. cfg.Billing.MinimumBalanceReserve = 0.01
  95. svc := NewBillingCacheService(cache, nil, nil, nil, nil, nil, cfg, nil)
  96. t.Cleanup(svc.Stop)
  97. newBalance := 0.75
  98. syncBalanceCacheAfterDeduction(context.Background(), &postUsageBillingParams{
  99. Cost: &CostBreakdown{ActualCost: 0.25},
  100. User: &User{ID: 1},
  101. }, &billingDeps{billingCacheService: svc}, &UsageBillingApplyResult{NewBalance: &newBalance})
  102. require.Equal(t, int64(0), cache.invalidateCalls.Load())
  103. require.Eventually(t, func() bool {
  104. return cache.deductCalls.Load() == 1
  105. }, 2*time.Second, 10*time.Millisecond)
  106. }