| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- //go:build unit
- package service
- import (
- "context"
- "errors"
- "sync"
- "sync/atomic"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/stretchr/testify/require"
- )
- type billingCacheMissStub struct {
- setBalanceCalls atomic.Int64
- }
- func (s *billingCacheMissStub) GetUserBalance(ctx context.Context, userID int64) (float64, error) {
- return 0, errors.New("cache miss")
- }
- func (s *billingCacheMissStub) SetUserBalance(ctx context.Context, userID int64, balance float64) error {
- s.setBalanceCalls.Add(1)
- return nil
- }
- func (s *billingCacheMissStub) DeductUserBalance(ctx context.Context, userID int64, amount float64) error {
- return nil
- }
- func (s *billingCacheMissStub) InvalidateUserBalance(ctx context.Context, userID int64) error {
- return nil
- }
- func (s *billingCacheMissStub) GetSubscriptionCache(ctx context.Context, userID, groupID int64) (*SubscriptionCacheData, error) {
- return nil, errors.New("cache miss")
- }
- func (s *billingCacheMissStub) SetSubscriptionCache(ctx context.Context, userID, groupID int64, data *SubscriptionCacheData) error {
- return nil
- }
- func (s *billingCacheMissStub) UpdateSubscriptionUsage(ctx context.Context, userID, groupID int64, cost float64) error {
- return nil
- }
- func (s *billingCacheMissStub) InvalidateSubscriptionCache(ctx context.Context, userID, groupID int64) error {
- return nil
- }
- func (s *billingCacheMissStub) GetAPIKeyRateLimit(ctx context.Context, keyID int64) (*APIKeyRateLimitCacheData, error) {
- return nil, errors.New("cache miss")
- }
- func (s *billingCacheMissStub) SetAPIKeyRateLimit(ctx context.Context, keyID int64, data *APIKeyRateLimitCacheData) error {
- return nil
- }
- func (s *billingCacheMissStub) UpdateAPIKeyRateLimitUsage(ctx context.Context, keyID int64, cost float64) error {
- return nil
- }
- func (s *billingCacheMissStub) InvalidateAPIKeyRateLimit(ctx context.Context, keyID int64) error {
- return nil
- }
- func (s *billingCacheMissStub) GetUserPlatformQuotaCache(ctx context.Context, userID int64, platform string) (*UserPlatformQuotaCacheEntry, bool, error) {
- return nil, false, nil
- }
- func (s *billingCacheMissStub) SetUserPlatformQuotaCache(ctx context.Context, userID int64, platform string, entry *UserPlatformQuotaCacheEntry, ttl time.Duration) error {
- return nil
- }
- func (s *billingCacheMissStub) DeleteUserPlatformQuotaCache(ctx context.Context, userID int64, platform string) error {
- return nil
- }
- func (s *billingCacheMissStub) IncrUserPlatformQuotaUsageCache(ctx context.Context, userID int64, platform string, cost float64, ttl time.Duration, markDirty bool) error {
- return nil
- }
- func (s *billingCacheMissStub) PopDirtyUserPlatformQuotaKeys(ctx context.Context, n int) ([]UserPlatformQuotaKey, error) {
- return nil, nil
- }
- func (s *billingCacheMissStub) ReaddDirtyUserPlatformQuotaKeys(ctx context.Context, keys []UserPlatformQuotaKey) error {
- return nil
- }
- func (s *billingCacheMissStub) BatchGetUserPlatformQuotaCache(ctx context.Context, keys []UserPlatformQuotaKey) ([]*UserPlatformQuotaCacheEntry, error) {
- return nil, nil
- }
- type balanceLoadUserRepoStub struct {
- mockUserRepo
- calls atomic.Int64
- delay time.Duration
- balance float64
- }
- func (s *balanceLoadUserRepoStub) GetByID(ctx context.Context, id int64) (*User, error) {
- s.calls.Add(1)
- if s.delay > 0 {
- select {
- case <-time.After(s.delay):
- case <-ctx.Done():
- return nil, ctx.Err()
- }
- }
- return &User{ID: id, Balance: s.balance}, nil
- }
- func (s *balanceLoadUserRepoStub) ListUserAuthIdentities(context.Context, int64) ([]UserAuthIdentityRecord, error) {
- return nil, nil
- }
- func (s *balanceLoadUserRepoStub) UnbindUserAuthProvider(context.Context, int64, string) error {
- return nil
- }
- func TestBillingCacheServiceGetUserBalance_Singleflight(t *testing.T) {
- cache := &billingCacheMissStub{}
- userRepo := &balanceLoadUserRepoStub{
- delay: 80 * time.Millisecond,
- balance: 12.34,
- }
- svc := NewBillingCacheService(cache, userRepo, nil, nil, nil, nil, &config.Config{}, nil)
- t.Cleanup(svc.Stop)
- const goroutines = 16
- start := make(chan struct{})
- var wg sync.WaitGroup
- errCh := make(chan error, goroutines)
- balCh := make(chan float64, goroutines)
- for i := 0; i < goroutines; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- <-start
- bal, err := svc.GetUserBalance(context.Background(), 99)
- errCh <- err
- balCh <- bal
- }()
- }
- close(start)
- wg.Wait()
- close(errCh)
- close(balCh)
- for err := range errCh {
- require.NoError(t, err)
- }
- for bal := range balCh {
- require.Equal(t, 12.34, bal)
- }
- require.Equal(t, int64(1), userRepo.calls.Load(), "并发穿透应被 singleflight 合并")
- require.Eventually(t, func() bool {
- return cache.setBalanceCalls.Load() >= 1
- }, time.Second, 10*time.Millisecond)
- }
|