- 前端: 所有界面显示、i18n 文本、组件中的品牌名称 - 后端: 服务层、设置默认值、邮件模板、安装向导 - 数据库: 迁移脚本注释 - 保持功能完全一致,仅更改品牌名称 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
284 lines
9.8 KiB
Go
284 lines
9.8 KiB
Go
//go:build integration
|
|
|
|
package repository
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
type BillingCacheSuite struct {
|
|
IntegrationRedisSuite
|
|
}
|
|
|
|
func (s *BillingCacheSuite) TestUserBalance() {
|
|
tests := []struct {
|
|
name string
|
|
fn func(ctx context.Context, rdb *redis.Client, cache service.BillingCache)
|
|
}{
|
|
{
|
|
name: "missing_key_returns_redis_nil",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
_, err := cache.GetUserBalance(ctx, 1)
|
|
require.ErrorIs(s.T(), err, redis.Nil, "expected redis.Nil for missing balance key")
|
|
},
|
|
},
|
|
{
|
|
name: "deduct_on_nonexistent_is_noop",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(1)
|
|
balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID)
|
|
|
|
require.NoError(s.T(), cache.DeductUserBalance(ctx, userID, 1), "DeductUserBalance should not error")
|
|
|
|
_, err := rdb.Get(ctx, balanceKey).Result()
|
|
require.ErrorIs(s.T(), err, redis.Nil, "expected missing key after deduct on non-existent")
|
|
},
|
|
},
|
|
{
|
|
name: "set_and_get_with_ttl",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(2)
|
|
balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID)
|
|
|
|
require.NoError(s.T(), cache.SetUserBalance(ctx, userID, 10.5), "SetUserBalance")
|
|
|
|
got, err := cache.GetUserBalance(ctx, userID)
|
|
require.NoError(s.T(), err, "GetUserBalance")
|
|
require.Equal(s.T(), 10.5, got, "balance mismatch")
|
|
|
|
ttl, err := rdb.TTL(ctx, balanceKey).Result()
|
|
require.NoError(s.T(), err, "TTL")
|
|
s.AssertTTLWithin(ttl, 1*time.Second, billingCacheTTL)
|
|
},
|
|
},
|
|
{
|
|
name: "deduct_reduces_balance",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(3)
|
|
|
|
require.NoError(s.T(), cache.SetUserBalance(ctx, userID, 10.5), "SetUserBalance")
|
|
require.NoError(s.T(), cache.DeductUserBalance(ctx, userID, 2.25), "DeductUserBalance")
|
|
|
|
got, err := cache.GetUserBalance(ctx, userID)
|
|
require.NoError(s.T(), err, "GetUserBalance after deduct")
|
|
require.Equal(s.T(), 8.25, got, "deduct mismatch")
|
|
},
|
|
},
|
|
{
|
|
name: "invalidate_removes_key",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(100)
|
|
balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID)
|
|
|
|
require.NoError(s.T(), cache.SetUserBalance(ctx, userID, 50.0), "SetUserBalance")
|
|
|
|
exists, err := rdb.Exists(ctx, balanceKey).Result()
|
|
require.NoError(s.T(), err, "Exists")
|
|
require.Equal(s.T(), int64(1), exists, "expected balance key to exist")
|
|
|
|
require.NoError(s.T(), cache.InvalidateUserBalance(ctx, userID), "InvalidateUserBalance")
|
|
|
|
exists, err = rdb.Exists(ctx, balanceKey).Result()
|
|
require.NoError(s.T(), err, "Exists after invalidate")
|
|
require.Equal(s.T(), int64(0), exists, "expected balance key to be removed after invalidate")
|
|
|
|
_, err = cache.GetUserBalance(ctx, userID)
|
|
require.ErrorIs(s.T(), err, redis.Nil, "expected redis.Nil after invalidate")
|
|
},
|
|
},
|
|
{
|
|
name: "deduct_refreshes_ttl",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(103)
|
|
balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID)
|
|
|
|
require.NoError(s.T(), cache.SetUserBalance(ctx, userID, 100.0), "SetUserBalance")
|
|
|
|
ttl1, err := rdb.TTL(ctx, balanceKey).Result()
|
|
require.NoError(s.T(), err, "TTL before deduct")
|
|
s.AssertTTLWithin(ttl1, 1*time.Second, billingCacheTTL)
|
|
|
|
require.NoError(s.T(), cache.DeductUserBalance(ctx, userID, 25.0), "DeductUserBalance")
|
|
|
|
balance, err := cache.GetUserBalance(ctx, userID)
|
|
require.NoError(s.T(), err, "GetUserBalance")
|
|
require.Equal(s.T(), 75.0, balance, "expected balance 75.0")
|
|
|
|
ttl2, err := rdb.TTL(ctx, balanceKey).Result()
|
|
require.NoError(s.T(), err, "TTL after deduct")
|
|
s.AssertTTLWithin(ttl2, 1*time.Second, billingCacheTTL)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
rdb := testRedis(s.T())
|
|
cache := NewBillingCache(rdb)
|
|
ctx := context.Background()
|
|
|
|
tt.fn(ctx, rdb, cache)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *BillingCacheSuite) TestSubscriptionCache() {
|
|
tests := []struct {
|
|
name string
|
|
fn func(ctx context.Context, rdb *redis.Client, cache service.BillingCache)
|
|
}{
|
|
{
|
|
name: "missing_key_returns_redis_nil",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(10)
|
|
groupID := int64(20)
|
|
|
|
_, err := cache.GetSubscriptionCache(ctx, userID, groupID)
|
|
require.ErrorIs(s.T(), err, redis.Nil, "expected redis.Nil for missing subscription key")
|
|
},
|
|
},
|
|
{
|
|
name: "update_usage_on_nonexistent_is_noop",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(11)
|
|
groupID := int64(21)
|
|
subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID)
|
|
|
|
require.NoError(s.T(), cache.UpdateSubscriptionUsage(ctx, userID, groupID, 1.0), "UpdateSubscriptionUsage should not error")
|
|
|
|
exists, err := rdb.Exists(ctx, subKey).Result()
|
|
require.NoError(s.T(), err, "Exists")
|
|
require.Equal(s.T(), int64(0), exists, "expected missing subscription key after UpdateSubscriptionUsage on non-existent")
|
|
},
|
|
},
|
|
{
|
|
name: "set_and_get_with_ttl",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(12)
|
|
groupID := int64(22)
|
|
subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID)
|
|
|
|
data := &service.SubscriptionCacheData{
|
|
Status: "active",
|
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
|
DailyUsage: 1.0,
|
|
WeeklyUsage: 2.0,
|
|
MonthlyUsage: 3.0,
|
|
Version: 7,
|
|
}
|
|
require.NoError(s.T(), cache.SetSubscriptionCache(ctx, userID, groupID, data), "SetSubscriptionCache")
|
|
|
|
gotSub, err := cache.GetSubscriptionCache(ctx, userID, groupID)
|
|
require.NoError(s.T(), err, "GetSubscriptionCache")
|
|
require.Equal(s.T(), "active", gotSub.Status)
|
|
require.Equal(s.T(), int64(7), gotSub.Version)
|
|
require.Equal(s.T(), 1.0, gotSub.DailyUsage)
|
|
|
|
ttl, err := rdb.TTL(ctx, subKey).Result()
|
|
require.NoError(s.T(), err, "TTL subKey")
|
|
s.AssertTTLWithin(ttl, 1*time.Second, billingCacheTTL)
|
|
},
|
|
},
|
|
{
|
|
name: "update_usage_increments_all_fields",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(13)
|
|
groupID := int64(23)
|
|
|
|
data := &service.SubscriptionCacheData{
|
|
Status: "active",
|
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
|
DailyUsage: 1.0,
|
|
WeeklyUsage: 2.0,
|
|
MonthlyUsage: 3.0,
|
|
Version: 1,
|
|
}
|
|
require.NoError(s.T(), cache.SetSubscriptionCache(ctx, userID, groupID, data), "SetSubscriptionCache")
|
|
|
|
require.NoError(s.T(), cache.UpdateSubscriptionUsage(ctx, userID, groupID, 0.5), "UpdateSubscriptionUsage")
|
|
|
|
gotSub, err := cache.GetSubscriptionCache(ctx, userID, groupID)
|
|
require.NoError(s.T(), err, "GetSubscriptionCache after update")
|
|
require.Equal(s.T(), 1.5, gotSub.DailyUsage)
|
|
require.Equal(s.T(), 2.5, gotSub.WeeklyUsage)
|
|
require.Equal(s.T(), 3.5, gotSub.MonthlyUsage)
|
|
},
|
|
},
|
|
{
|
|
name: "invalidate_removes_key",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(101)
|
|
groupID := int64(10)
|
|
subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID)
|
|
|
|
data := &service.SubscriptionCacheData{
|
|
Status: "active",
|
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
|
DailyUsage: 1.0,
|
|
WeeklyUsage: 2.0,
|
|
MonthlyUsage: 3.0,
|
|
Version: 1,
|
|
}
|
|
require.NoError(s.T(), cache.SetSubscriptionCache(ctx, userID, groupID, data), "SetSubscriptionCache")
|
|
|
|
exists, err := rdb.Exists(ctx, subKey).Result()
|
|
require.NoError(s.T(), err, "Exists")
|
|
require.Equal(s.T(), int64(1), exists, "expected subscription key to exist")
|
|
|
|
require.NoError(s.T(), cache.InvalidateSubscriptionCache(ctx, userID, groupID), "InvalidateSubscriptionCache")
|
|
|
|
exists, err = rdb.Exists(ctx, subKey).Result()
|
|
require.NoError(s.T(), err, "Exists after invalidate")
|
|
require.Equal(s.T(), int64(0), exists, "expected subscription key to be removed after invalidate")
|
|
|
|
_, err = cache.GetSubscriptionCache(ctx, userID, groupID)
|
|
require.ErrorIs(s.T(), err, redis.Nil, "expected redis.Nil after invalidate")
|
|
},
|
|
},
|
|
{
|
|
name: "missing_status_returns_parsing_error",
|
|
fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) {
|
|
userID := int64(102)
|
|
groupID := int64(11)
|
|
subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID)
|
|
|
|
fields := map[string]any{
|
|
"expires_at": time.Now().Add(1 * time.Hour).Unix(),
|
|
"daily_usage": 1.0,
|
|
"weekly_usage": 2.0,
|
|
"monthly_usage": 3.0,
|
|
"version": 1,
|
|
}
|
|
require.NoError(s.T(), rdb.HSet(ctx, subKey, fields).Err(), "HSet")
|
|
|
|
_, err := cache.GetSubscriptionCache(ctx, userID, groupID)
|
|
require.Error(s.T(), err, "expected error for missing status field")
|
|
require.NotErrorIs(s.T(), err, redis.Nil, "expected parsing error, not redis.Nil")
|
|
require.Equal(s.T(), "invalid cache: missing status", err.Error())
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
rdb := testRedis(s.T())
|
|
cache := NewBillingCache(rdb)
|
|
ctx := context.Background()
|
|
|
|
tt.fn(ctx, rdb, cache)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBillingCacheSuite(t *testing.T) {
|
|
suite.Run(t, new(BillingCacheSuite))
|
|
}
|