Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type dailyResetTrackingUserSubRepo struct {
|
||||
userSubRepoNoop
|
||||
|
||||
resetDailyCalled bool
|
||||
}
|
||||
|
||||
func (r *dailyResetTrackingUserSubRepo) ResetDailyUsage(context.Context, int64, *time.Time, time.Time) error {
|
||||
r.resetDailyCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAssignOrExtendSubscription_ExpiredDailyCardStartsNewOneTimeQuota(t *testing.T) {
|
||||
groupRepo := &subscriptionGroupRepoStub{
|
||||
group: &Group{ID: 1, SubscriptionType: SubscriptionTypeSubscription},
|
||||
}
|
||||
subRepo := newSubscriptionUserSubRepoStub()
|
||||
oldStart := time.Now().AddDate(0, 0, -3)
|
||||
oldWindowStart := startOfDay(oldStart)
|
||||
subRepo.seed(&UserSubscription{
|
||||
ID: 100,
|
||||
UserID: 200,
|
||||
GroupID: 1,
|
||||
StartsAt: oldStart,
|
||||
ExpiresAt: oldStart.AddDate(0, 0, 1),
|
||||
Status: SubscriptionStatusExpired,
|
||||
DailyWindowStart: &oldWindowStart,
|
||||
WeeklyWindowStart: &oldWindowStart,
|
||||
MonthlyWindowStart: &oldWindowStart,
|
||||
DailyUsageUSD: 10,
|
||||
WeeklyUsageUSD: 20,
|
||||
MonthlyUsageUSD: 30,
|
||||
Notes: "old",
|
||||
})
|
||||
svc := NewSubscriptionService(groupRepo, subRepo, nil, nil, nil)
|
||||
|
||||
renewed, reused, err := svc.AssignOrExtendSubscription(context.Background(), &AssignSubscriptionInput{
|
||||
UserID: 200,
|
||||
GroupID: 1,
|
||||
ValidityDays: 1,
|
||||
Notes: "new",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, reused)
|
||||
require.True(t, renewed.HasOneTimeDailyQuota(), "过期后重新购买 1 日卡仍应被识别为一次性日额度")
|
||||
require.Equal(t, SubscriptionStatusActive, renewed.Status)
|
||||
require.True(t, renewed.StartsAt.After(oldStart), "重新购买过期订阅时应重置当前周期 StartsAt")
|
||||
require.False(t, renewed.ExpiresAt.After(renewed.StartsAt.AddDate(0, 0, 1)))
|
||||
require.NotNil(t, renewed.DailyWindowStart)
|
||||
require.Equal(t, timezone.StartOfDay(renewed.StartsAt), *renewed.DailyWindowStart, "续期后日窗口应锚定当天 0 点")
|
||||
require.Equal(t, 0.0, renewed.DailyUsageUSD)
|
||||
require.Equal(t, 0.0, renewed.WeeklyUsageUSD)
|
||||
require.Equal(t, 0.0, renewed.MonthlyUsageUSD)
|
||||
require.Equal(t, "old\nnew", renewed.Notes)
|
||||
}
|
||||
|
||||
func TestAssignOrExtendSubscription_ExpiredSubscriptionAppendsMatchingNotes(t *testing.T) {
|
||||
groupRepo := &subscriptionGroupRepoStub{
|
||||
group: &Group{ID: 1, SubscriptionType: SubscriptionTypeSubscription},
|
||||
}
|
||||
subRepo := newSubscriptionUserSubRepoStub()
|
||||
oldStart := time.Now().AddDate(0, 0, -3)
|
||||
subRepo.seed(&UserSubscription{
|
||||
ID: 101,
|
||||
UserID: 201,
|
||||
GroupID: 1,
|
||||
StartsAt: oldStart,
|
||||
ExpiresAt: oldStart.AddDate(0, 0, 1),
|
||||
Status: SubscriptionStatusExpired,
|
||||
Notes: "same",
|
||||
})
|
||||
svc := NewSubscriptionService(groupRepo, subRepo, nil, nil, nil)
|
||||
|
||||
renewed, reused, err := svc.AssignOrExtendSubscription(context.Background(), &AssignSubscriptionInput{
|
||||
UserID: 201,
|
||||
GroupID: 1,
|
||||
ValidityDays: 1,
|
||||
Notes: "same",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, reused)
|
||||
require.Equal(t, "same\nsame", renewed.Notes)
|
||||
}
|
||||
|
||||
func TestUserSubscriptionNeedsDailyReset_DailyCardKeepsOneTimeQuota(t *testing.T) {
|
||||
start := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC)
|
||||
dailyWindowStart := time.Date(2026, 5, 18, 0, 0, 0, 0, time.UTC)
|
||||
sub := &UserSubscription{
|
||||
StartsAt: start,
|
||||
ExpiresAt: start.Add(24 * time.Hour),
|
||||
DailyWindowStart: &dailyWindowStart,
|
||||
DailyUsageUSD: 10,
|
||||
}
|
||||
|
||||
require.True(t, sub.HasOneTimeDailyQuota())
|
||||
require.False(t, sub.NeedsDailyResetAt(dailyWindowStart.Add(25*time.Hour)), "日卡应作为一次性配额,跨 0 点后不再刷新日额度")
|
||||
}
|
||||
|
||||
func TestUserSubscriptionNeedsDailyReset_MultiDaySubscriptionStillRefreshes(t *testing.T) {
|
||||
start := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC)
|
||||
dailyWindowStart := time.Date(2026, 5, 18, 0, 0, 0, 0, time.UTC)
|
||||
sub := &UserSubscription{
|
||||
StartsAt: start,
|
||||
ExpiresAt: start.AddDate(0, 0, 2),
|
||||
DailyWindowStart: &dailyWindowStart,
|
||||
}
|
||||
|
||||
require.False(t, sub.HasOneTimeDailyQuota())
|
||||
require.True(t, sub.NeedsDailyResetAt(dailyWindowStart.Add(24*time.Hour)), "多日订阅仍应按 24 小时日窗口刷新")
|
||||
}
|
||||
|
||||
func TestUserSubscriptionDailyResetTime_DailyCardReturnsExpiry(t *testing.T) {
|
||||
start := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC)
|
||||
dailyWindowStart := time.Date(2026, 5, 18, 0, 0, 0, 0, time.UTC)
|
||||
expiresAt := start.Add(24 * time.Hour)
|
||||
sub := &UserSubscription{
|
||||
StartsAt: start,
|
||||
ExpiresAt: expiresAt,
|
||||
DailyWindowStart: &dailyWindowStart,
|
||||
}
|
||||
|
||||
resetAt := sub.DailyResetTime()
|
||||
require.NotNil(t, resetAt)
|
||||
require.Equal(t, expiresAt, *resetAt, "日卡展示的日额度结束时间应为订阅过期时间")
|
||||
}
|
||||
|
||||
func TestCheckAndResetWindows_DailyCardDoesNotResetDailyUsage(t *testing.T) {
|
||||
now := time.Now()
|
||||
startsAt := now.Add(-23 * time.Hour)
|
||||
dailyWindowStart := now.Add(-25 * time.Hour)
|
||||
repo := &dailyResetTrackingUserSubRepo{}
|
||||
svc := NewSubscriptionService(groupRepoNoop{}, repo, nil, nil, nil)
|
||||
sub := &UserSubscription{
|
||||
ID: 1,
|
||||
UserID: 10,
|
||||
GroupID: 20,
|
||||
StartsAt: startsAt,
|
||||
ExpiresAt: startsAt.Add(24 * time.Hour),
|
||||
DailyUsageUSD: 10,
|
||||
DailyWindowStart: &dailyWindowStart,
|
||||
}
|
||||
|
||||
err := svc.CheckAndResetWindows(context.Background(), sub)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.False(t, repo.resetDailyCalled, "日卡作为一次性配额,过了 24 小时日窗口也不应重置 daily usage")
|
||||
require.Equal(t, 10.0, sub.DailyUsageUSD)
|
||||
}
|
||||
|
||||
func TestCheckAndResetWindows_MultiDaySubscriptionStillResetsDailyUsage(t *testing.T) {
|
||||
now := time.Date(2026, 7, 29, 12, 0, 0, 0, time.UTC)
|
||||
startsAt := now.Add(-48 * time.Hour)
|
||||
dailyWindowStart := now.Add(-25 * time.Hour)
|
||||
repo := &dailyResetTrackingUserSubRepo{}
|
||||
svc := NewSubscriptionService(groupRepoNoop{}, repo, nil, nil, nil)
|
||||
svc.now = func() time.Time { return now }
|
||||
sub := &UserSubscription{
|
||||
ID: 1,
|
||||
UserID: 10,
|
||||
GroupID: 20,
|
||||
StartsAt: startsAt,
|
||||
ExpiresAt: startsAt.AddDate(0, 0, 4),
|
||||
DailyUsageUSD: 10,
|
||||
DailyWindowStart: &dailyWindowStart,
|
||||
}
|
||||
|
||||
err := svc.CheckAndResetWindows(context.Background(), sub)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, repo.resetDailyCalled, "多日订阅仍应重置过期 daily window")
|
||||
require.Equal(t, 0.0, sub.DailyUsageUSD)
|
||||
}
|
||||
|
||||
func TestValidateAndCheckLimits_DailyCardDoesNotAllowSecondQuotaAfterMidnight(t *testing.T) {
|
||||
start := time.Now().Add(-23 * time.Hour)
|
||||
dailyWindowStart := time.Now().Add(-25 * time.Hour)
|
||||
dailyLimit := 10.0
|
||||
sub := &UserSubscription{
|
||||
Status: SubscriptionStatusActive,
|
||||
StartsAt: start,
|
||||
ExpiresAt: start.Add(24 * time.Hour),
|
||||
DailyWindowStart: &dailyWindowStart,
|
||||
DailyUsageUSD: dailyLimit + 0.01,
|
||||
}
|
||||
group := &Group{
|
||||
SubscriptionType: SubscriptionTypeSubscription,
|
||||
DailyLimitUSD: &dailyLimit,
|
||||
}
|
||||
svc := NewSubscriptionService(groupRepoNoop{}, userSubRepoNoop{}, nil, nil, nil)
|
||||
|
||||
needsMaintenance, err := svc.ValidateAndCheckLimits(sub, group)
|
||||
|
||||
require.False(t, needsMaintenance, "日卡跨过日窗口后不应触发 daily reset 维护")
|
||||
require.True(t, errors.Is(err, ErrDailyLimitExceeded))
|
||||
require.Equal(t, dailyLimit+0.01, sub.DailyUsageUSD, "热路径不应清零日卡已用额度")
|
||||
}
|
||||
Reference in New Issue
Block a user