190 lines
5.1 KiB
Go
190 lines
5.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type usageRepoStub struct {
|
|
UsageLogRepository
|
|
stats *usagestats.DashboardStats
|
|
err error
|
|
calls int32
|
|
onCall chan struct{}
|
|
}
|
|
|
|
func (s *usageRepoStub) GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error) {
|
|
atomic.AddInt32(&s.calls, 1)
|
|
if s.onCall != nil {
|
|
select {
|
|
case s.onCall <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
if s.err != nil {
|
|
return nil, s.err
|
|
}
|
|
return s.stats, nil
|
|
}
|
|
|
|
type dashboardCacheStub struct {
|
|
get func(ctx context.Context) (string, error)
|
|
set func(ctx context.Context, data string, ttl time.Duration) error
|
|
getCalls int32
|
|
setCalls int32
|
|
lastSetMu sync.Mutex
|
|
lastSet string
|
|
}
|
|
|
|
func (c *dashboardCacheStub) GetDashboardStats(ctx context.Context) (string, error) {
|
|
atomic.AddInt32(&c.getCalls, 1)
|
|
if c.get != nil {
|
|
return c.get(ctx)
|
|
}
|
|
return "", ErrDashboardStatsCacheMiss
|
|
}
|
|
|
|
func (c *dashboardCacheStub) SetDashboardStats(ctx context.Context, data string, ttl time.Duration) error {
|
|
atomic.AddInt32(&c.setCalls, 1)
|
|
c.lastSetMu.Lock()
|
|
c.lastSet = data
|
|
c.lastSetMu.Unlock()
|
|
if c.set != nil {
|
|
return c.set(ctx, data, ttl)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *dashboardCacheStub) readLastEntry(t *testing.T) dashboardStatsCacheEntry {
|
|
t.Helper()
|
|
c.lastSetMu.Lock()
|
|
data := c.lastSet
|
|
c.lastSetMu.Unlock()
|
|
|
|
var entry dashboardStatsCacheEntry
|
|
err := json.Unmarshal([]byte(data), &entry)
|
|
require.NoError(t, err)
|
|
return entry
|
|
}
|
|
|
|
func TestDashboardService_CacheHitFresh(t *testing.T) {
|
|
stats := &usagestats.DashboardStats{
|
|
TotalUsers: 10,
|
|
}
|
|
entry := dashboardStatsCacheEntry{
|
|
Stats: stats,
|
|
UpdatedAt: time.Now().Unix(),
|
|
}
|
|
payload, err := json.Marshal(entry)
|
|
require.NoError(t, err)
|
|
|
|
cache := &dashboardCacheStub{
|
|
get: func(ctx context.Context) (string, error) {
|
|
return string(payload), nil
|
|
},
|
|
}
|
|
repo := &usageRepoStub{
|
|
stats: &usagestats.DashboardStats{TotalUsers: 99},
|
|
}
|
|
cfg := &config.Config{Dashboard: config.DashboardCacheConfig{Enabled: true}}
|
|
svc := NewDashboardService(repo, cache, cfg)
|
|
|
|
got, err := svc.GetDashboardStats(context.Background())
|
|
require.NoError(t, err)
|
|
require.Equal(t, stats, got)
|
|
require.Equal(t, int32(0), atomic.LoadInt32(&repo.calls))
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&cache.getCalls))
|
|
require.Equal(t, int32(0), atomic.LoadInt32(&cache.setCalls))
|
|
}
|
|
|
|
func TestDashboardService_CacheMiss_StoresCache(t *testing.T) {
|
|
stats := &usagestats.DashboardStats{
|
|
TotalUsers: 7,
|
|
}
|
|
cache := &dashboardCacheStub{
|
|
get: func(ctx context.Context) (string, error) {
|
|
return "", ErrDashboardStatsCacheMiss
|
|
},
|
|
}
|
|
repo := &usageRepoStub{stats: stats}
|
|
cfg := &config.Config{Dashboard: config.DashboardCacheConfig{Enabled: true}}
|
|
svc := NewDashboardService(repo, cache, cfg)
|
|
|
|
got, err := svc.GetDashboardStats(context.Background())
|
|
require.NoError(t, err)
|
|
require.Equal(t, stats, got)
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&repo.calls))
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&cache.getCalls))
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&cache.setCalls))
|
|
entry := cache.readLastEntry(t)
|
|
require.Equal(t, stats, entry.Stats)
|
|
require.WithinDuration(t, time.Now(), time.Unix(entry.UpdatedAt, 0), time.Second)
|
|
}
|
|
|
|
func TestDashboardService_CacheDisabled_SkipsCache(t *testing.T) {
|
|
stats := &usagestats.DashboardStats{
|
|
TotalUsers: 3,
|
|
}
|
|
cache := &dashboardCacheStub{
|
|
get: func(ctx context.Context) (string, error) {
|
|
return "", nil
|
|
},
|
|
}
|
|
repo := &usageRepoStub{stats: stats}
|
|
cfg := &config.Config{Dashboard: config.DashboardCacheConfig{Enabled: false}}
|
|
svc := NewDashboardService(repo, cache, cfg)
|
|
|
|
got, err := svc.GetDashboardStats(context.Background())
|
|
require.NoError(t, err)
|
|
require.Equal(t, stats, got)
|
|
require.Equal(t, int32(1), atomic.LoadInt32(&repo.calls))
|
|
require.Equal(t, int32(0), atomic.LoadInt32(&cache.getCalls))
|
|
require.Equal(t, int32(0), atomic.LoadInt32(&cache.setCalls))
|
|
}
|
|
|
|
func TestDashboardService_CacheHitStale_TriggersAsyncRefresh(t *testing.T) {
|
|
staleStats := &usagestats.DashboardStats{
|
|
TotalUsers: 11,
|
|
}
|
|
entry := dashboardStatsCacheEntry{
|
|
Stats: staleStats,
|
|
UpdatedAt: time.Now().Add(-defaultDashboardStatsFreshTTL * 2).Unix(),
|
|
}
|
|
payload, err := json.Marshal(entry)
|
|
require.NoError(t, err)
|
|
|
|
cache := &dashboardCacheStub{
|
|
get: func(ctx context.Context) (string, error) {
|
|
return string(payload), nil
|
|
},
|
|
}
|
|
refreshCh := make(chan struct{}, 1)
|
|
repo := &usageRepoStub{
|
|
stats: &usagestats.DashboardStats{TotalUsers: 22},
|
|
onCall: refreshCh,
|
|
}
|
|
cfg := &config.Config{Dashboard: config.DashboardCacheConfig{Enabled: true}}
|
|
svc := NewDashboardService(repo, cache, cfg)
|
|
|
|
got, err := svc.GetDashboardStats(context.Background())
|
|
require.NoError(t, err)
|
|
require.Equal(t, staleStats, got)
|
|
|
|
select {
|
|
case <-refreshCh:
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("等待异步刷新超时")
|
|
}
|
|
require.Eventually(t, func() bool {
|
|
return atomic.LoadInt32(&cache.setCalls) >= 1
|
|
}, 1*time.Second, 10*time.Millisecond)
|
|
}
|