| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- //go:build unit
- package service
- import (
- "context"
- "errors"
- "sync/atomic"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/stretchr/testify/require"
- )
- type balanceEligibilityCacheStub struct {
- billingCacheWorkerStub
- balance float64
- cacheMissAfterInvalidate bool
- invalidated atomic.Bool
- deductCalls atomic.Int64
- invalidateCalls atomic.Int64
- }
- func (s *balanceEligibilityCacheStub) GetUserBalance(context.Context, int64) (float64, error) {
- if s.cacheMissAfterInvalidate && s.invalidated.Load() {
- return 0, errors.New("cache miss")
- }
- return s.balance, nil
- }
- func (s *balanceEligibilityCacheStub) DeductUserBalance(context.Context, int64, float64) error {
- s.deductCalls.Add(1)
- return nil
- }
- func (s *balanceEligibilityCacheStub) InvalidateUserBalance(context.Context, int64) error {
- s.invalidateCalls.Add(1)
- s.invalidated.Store(true)
- return nil
- }
- func TestCheckBillingEligibility_RejectsBalanceBelowMinimumReserve(t *testing.T) {
- cache := &balanceEligibilityCacheStub{balance: 0.005}
- cfg := &config.Config{}
- cfg.Billing.MinimumBalanceReserve = 0.01
- svc := NewBillingCacheService(cache, nil, nil, nil, nil, nil, cfg, nil)
- t.Cleanup(svc.Stop)
- err := svc.CheckBillingEligibility(context.Background(), &User{ID: 1}, nil, nil, nil, "")
- require.ErrorIs(t, err, ErrInsufficientBalance)
- }
- func TestCheckBillingEligibility_AllowsBalanceAtMinimumReserve(t *testing.T) {
- cache := &balanceEligibilityCacheStub{balance: 0.01}
- cfg := &config.Config{}
- cfg.Billing.MinimumBalanceReserve = 0.01
- svc := NewBillingCacheService(cache, nil, nil, nil, nil, nil, cfg, nil)
- t.Cleanup(svc.Stop)
- err := svc.CheckBillingEligibility(context.Background(), &User{ID: 1}, nil, nil, nil, "")
- require.NoError(t, err)
- }
- func TestSyncBalanceCacheAfterDeduction_InvalidatesExhaustedBalance(t *testing.T) {
- cache := &balanceEligibilityCacheStub{
- balance: 0.50,
- cacheMissAfterInvalidate: true,
- }
- userRepo := &balanceLoadUserRepoStub{balance: -0.25}
- cfg := &config.Config{}
- cfg.Billing.MinimumBalanceReserve = 0.01
- svc := NewBillingCacheService(cache, userRepo, nil, nil, nil, nil, cfg, nil)
- t.Cleanup(svc.Stop)
- newBalance := -0.25
- syncBalanceCacheAfterDeduction(context.Background(), &postUsageBillingParams{
- Cost: &CostBreakdown{ActualCost: 0.75},
- User: &User{ID: 1},
- }, &billingDeps{billingCacheService: svc}, &UsageBillingApplyResult{
- NewBalance: &newBalance,
- BalanceOverdrafted: true,
- })
- require.Equal(t, int64(1), cache.invalidateCalls.Load())
- require.Equal(t, int64(0), cache.deductCalls.Load())
- err := svc.CheckBillingEligibility(context.Background(), &User{ID: 1}, nil, nil, nil, "")
- require.ErrorIs(t, err, ErrInsufficientBalance)
- require.Equal(t, int64(1), userRepo.calls.Load())
- }
- func TestSyncBalanceCacheAfterDeduction_InvalidatesWhenBalanceFallsBelowReserve(t *testing.T) {
- cache := &balanceEligibilityCacheStub{balance: 0.50}
- cfg := &config.Config{}
- cfg.Billing.MinimumBalanceReserve = 0.01
- svc := NewBillingCacheService(cache, nil, nil, nil, nil, nil, cfg, nil)
- t.Cleanup(svc.Stop)
- newBalance := 0.005
- syncBalanceCacheAfterDeduction(context.Background(), &postUsageBillingParams{
- Cost: &CostBreakdown{ActualCost: 0.495},
- User: &User{ID: 1},
- }, &billingDeps{billingCacheService: svc}, &UsageBillingApplyResult{NewBalance: &newBalance})
- require.Equal(t, int64(1), cache.invalidateCalls.Load())
- require.Equal(t, int64(0), cache.deductCalls.Load())
- }
- func TestSyncBalanceCacheAfterDeduction_QueuesDeductWhenBalanceStillEligible(t *testing.T) {
- cache := &balanceEligibilityCacheStub{balance: 1}
- cfg := &config.Config{}
- cfg.Billing.MinimumBalanceReserve = 0.01
- svc := NewBillingCacheService(cache, nil, nil, nil, nil, nil, cfg, nil)
- t.Cleanup(svc.Stop)
- newBalance := 0.75
- syncBalanceCacheAfterDeduction(context.Background(), &postUsageBillingParams{
- Cost: &CostBreakdown{ActualCost: 0.25},
- User: &User{ID: 1},
- }, &billingDeps{billingCacheService: svc}, &UsageBillingApplyResult{NewBalance: &newBalance})
- require.Equal(t, int64(0), cache.invalidateCalls.Load())
- require.Eventually(t, func() bool {
- return cache.deductCalls.Load() == 1
- }, 2*time.Second, 10*time.Millisecond)
- }
|