mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-25 17:14:45 +08:00
feat: 实现固定时间重置模式的 SQL 表达式,并添加相关单元测试
This commit is contained in:
@@ -1747,21 +1747,70 @@ const weeklyExpiredExpr = `(
|
|||||||
)`
|
)`
|
||||||
|
|
||||||
// nextDailyResetAtExpr is a SQL expression to compute the next daily reset_at when a reset occurs.
|
// nextDailyResetAtExpr is a SQL expression to compute the next daily reset_at when a reset occurs.
|
||||||
// For fixed mode: advances current reset_at by 1 day. For rolling mode: not used (NULL).
|
// For fixed mode: computes the next future reset time based on NOW(), timezone, and configured hour.
|
||||||
|
// This correctly handles long-inactive accounts by jumping directly to the next valid reset point.
|
||||||
const nextDailyResetAtExpr = `(
|
const nextDailyResetAtExpr = `(
|
||||||
CASE WHEN COALESCE(extra->>'quota_daily_reset_mode', 'rolling') = 'fixed'
|
CASE WHEN COALESCE(extra->>'quota_daily_reset_mode', 'rolling') = 'fixed'
|
||||||
THEN to_char(
|
THEN to_char((
|
||||||
COALESCE((extra->>'quota_daily_reset_at')::timestamptz, NOW()) + '1 day'::interval
|
-- Compute today's reset point in the configured timezone, then pick next future one
|
||||||
AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')
|
CASE WHEN NOW() >= (
|
||||||
|
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
|
||||||
|
+ (COALESCE((extra->>'quota_daily_reset_hour')::int, 0) || ' hours')::interval
|
||||||
|
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
|
||||||
|
-- NOW() is at or past today's reset point → next reset is tomorrow
|
||||||
|
THEN (
|
||||||
|
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
|
||||||
|
+ (COALESCE((extra->>'quota_daily_reset_hour')::int, 0) || ' hours')::interval
|
||||||
|
+ '1 day'::interval
|
||||||
|
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
|
||||||
|
-- NOW() is before today's reset point → next reset is today
|
||||||
|
ELSE (
|
||||||
|
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
|
||||||
|
+ (COALESCE((extra->>'quota_daily_reset_hour')::int, 0) || ' hours')::interval
|
||||||
|
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
|
||||||
|
END
|
||||||
|
) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')
|
||||||
ELSE NULL END
|
ELSE NULL END
|
||||||
)`
|
)`
|
||||||
|
|
||||||
// nextWeeklyResetAtExpr is a SQL expression to compute the next weekly reset_at when a reset occurs.
|
// nextWeeklyResetAtExpr is a SQL expression to compute the next weekly reset_at when a reset occurs.
|
||||||
|
// For fixed mode: computes the next future reset time based on NOW(), timezone, configured day and hour.
|
||||||
|
// This correctly handles long-inactive accounts by jumping directly to the next valid reset point.
|
||||||
const nextWeeklyResetAtExpr = `(
|
const nextWeeklyResetAtExpr = `(
|
||||||
CASE WHEN COALESCE(extra->>'quota_weekly_reset_mode', 'rolling') = 'fixed'
|
CASE WHEN COALESCE(extra->>'quota_weekly_reset_mode', 'rolling') = 'fixed'
|
||||||
THEN to_char(
|
THEN to_char((
|
||||||
COALESCE((extra->>'quota_weekly_reset_at')::timestamptz, NOW()) + '7 days'::interval
|
-- Compute this week's reset point in the configured timezone
|
||||||
AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')
|
-- Step 1: get today's date at reset hour in configured tz
|
||||||
|
-- Step 2: compute days forward to target weekday
|
||||||
|
-- Step 3: if same day but past reset hour, advance 7 days
|
||||||
|
CASE
|
||||||
|
WHEN (
|
||||||
|
-- days_forward = (target_day - current_day + 7) % 7
|
||||||
|
(COALESCE((extra->>'quota_weekly_reset_day')::int, 1)
|
||||||
|
- EXTRACT(DOW FROM NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))::int
|
||||||
|
+ 7) % 7
|
||||||
|
) = 0 AND NOW() >= (
|
||||||
|
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
|
||||||
|
+ (COALESCE((extra->>'quota_weekly_reset_hour')::int, 0) || ' hours')::interval
|
||||||
|
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
|
||||||
|
-- Same weekday and past reset hour → next week
|
||||||
|
THEN (
|
||||||
|
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
|
||||||
|
+ (COALESCE((extra->>'quota_weekly_reset_hour')::int, 0) || ' hours')::interval
|
||||||
|
+ '7 days'::interval
|
||||||
|
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
|
||||||
|
ELSE (
|
||||||
|
-- Advance to target weekday this week (or next if days_forward > 0)
|
||||||
|
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
|
||||||
|
+ (COALESCE((extra->>'quota_weekly_reset_hour')::int, 0) || ' hours')::interval
|
||||||
|
+ ((
|
||||||
|
(COALESCE((extra->>'quota_weekly_reset_day')::int, 1)
|
||||||
|
- EXTRACT(DOW FROM NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))::int
|
||||||
|
+ 7) % 7
|
||||||
|
) || ' days')::interval
|
||||||
|
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
|
||||||
|
END
|
||||||
|
) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')
|
||||||
ELSE NULL END
|
ELSE NULL END
|
||||||
)`
|
)`
|
||||||
|
|
||||||
|
|||||||
516
backend/internal/service/account_quota_reset_test.go
Normal file
516
backend/internal/service/account_quota_reset_test.go
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
//go:build unit
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// nextFixedDailyReset
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestNextFixedDailyReset_BeforeResetHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-14 06:00 UTC, reset hour = 9
|
||||||
|
after := time.Date(2026, 3, 14, 6, 0, 0, 0, tz)
|
||||||
|
got := nextFixedDailyReset(9, tz, after)
|
||||||
|
want := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFixedDailyReset_AtResetHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// Exactly at reset hour → should return tomorrow
|
||||||
|
after := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
|
||||||
|
got := nextFixedDailyReset(9, tz, after)
|
||||||
|
want := time.Date(2026, 3, 15, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFixedDailyReset_AfterResetHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// After reset hour → should return tomorrow
|
||||||
|
after := time.Date(2026, 3, 14, 15, 30, 0, 0, tz)
|
||||||
|
got := nextFixedDailyReset(9, tz, after)
|
||||||
|
want := time.Date(2026, 3, 15, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFixedDailyReset_MidnightReset(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// Reset at hour 0 (midnight), currently 23:59
|
||||||
|
after := time.Date(2026, 3, 14, 23, 59, 0, 0, tz)
|
||||||
|
got := nextFixedDailyReset(0, tz, after)
|
||||||
|
want := time.Date(2026, 3, 15, 0, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFixedDailyReset_NonUTCTimezone(t *testing.T) {
|
||||||
|
tz, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 2026-03-14 07:00 UTC = 2026-03-14 15:00 CST, reset hour = 9 (CST)
|
||||||
|
after := time.Date(2026, 3, 14, 7, 0, 0, 0, time.UTC)
|
||||||
|
got := nextFixedDailyReset(9, tz, after)
|
||||||
|
// Already past 9:00 CST today → tomorrow 9:00 CST = 2026-03-15 01:00 UTC
|
||||||
|
want := time.Date(2026, 3, 15, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// lastFixedDailyReset
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestLastFixedDailyReset_BeforeResetHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
now := time.Date(2026, 3, 14, 6, 0, 0, 0, tz)
|
||||||
|
got := lastFixedDailyReset(9, tz, now)
|
||||||
|
// Before today's 9:00 → yesterday 9:00
|
||||||
|
want := time.Date(2026, 3, 13, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLastFixedDailyReset_AtResetHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
now := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
|
||||||
|
got := lastFixedDailyReset(9, tz, now)
|
||||||
|
// At exactly 9:00 → today 9:00
|
||||||
|
want := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLastFixedDailyReset_AfterResetHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
now := time.Date(2026, 3, 14, 15, 0, 0, 0, tz)
|
||||||
|
got := lastFixedDailyReset(9, tz, now)
|
||||||
|
// After 9:00 → today 9:00
|
||||||
|
want := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// nextFixedWeeklyReset
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestNextFixedWeeklyReset_TargetDayAhead(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-14 is Saturday (day=6), target = Monday (day=1), hour = 9
|
||||||
|
after := time.Date(2026, 3, 14, 10, 0, 0, 0, tz)
|
||||||
|
got := nextFixedWeeklyReset(1, 9, tz, after)
|
||||||
|
// Next Monday = 2026-03-16
|
||||||
|
want := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFixedWeeklyReset_TargetDayToday_BeforeHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-16 is Monday (day=1), target = Monday, hour = 9, before 9:00
|
||||||
|
after := time.Date(2026, 3, 16, 6, 0, 0, 0, tz)
|
||||||
|
got := nextFixedWeeklyReset(1, 9, tz, after)
|
||||||
|
// Today at 9:00
|
||||||
|
want := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFixedWeeklyReset_TargetDayToday_AtHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-16 is Monday, target = Monday, hour = 9, exactly at 9:00
|
||||||
|
after := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
|
||||||
|
got := nextFixedWeeklyReset(1, 9, tz, after)
|
||||||
|
// Next Monday at 9:00
|
||||||
|
want := time.Date(2026, 3, 23, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFixedWeeklyReset_TargetDayToday_AfterHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-16 is Monday, target = Monday, hour = 9, after 9:00
|
||||||
|
after := time.Date(2026, 3, 16, 15, 0, 0, 0, tz)
|
||||||
|
got := nextFixedWeeklyReset(1, 9, tz, after)
|
||||||
|
// Next Monday at 9:00
|
||||||
|
want := time.Date(2026, 3, 23, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFixedWeeklyReset_TargetDayPast(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-18 is Wednesday (day=3), target = Monday (day=1)
|
||||||
|
after := time.Date(2026, 3, 18, 10, 0, 0, 0, tz)
|
||||||
|
got := nextFixedWeeklyReset(1, 9, tz, after)
|
||||||
|
// Next Monday = 2026-03-23
|
||||||
|
want := time.Date(2026, 3, 23, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFixedWeeklyReset_Sunday(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-14 is Saturday (day=6), target = Sunday (day=0)
|
||||||
|
after := time.Date(2026, 3, 14, 10, 0, 0, 0, tz)
|
||||||
|
got := nextFixedWeeklyReset(0, 0, tz, after)
|
||||||
|
// Next Sunday = 2026-03-15
|
||||||
|
want := time.Date(2026, 3, 15, 0, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// lastFixedWeeklyReset
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestLastFixedWeeklyReset_SameDay_AfterHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-16 is Monday (day=1), target = Monday, hour = 9, now = 15:00
|
||||||
|
now := time.Date(2026, 3, 16, 15, 0, 0, 0, tz)
|
||||||
|
got := lastFixedWeeklyReset(1, 9, tz, now)
|
||||||
|
// Today at 9:00
|
||||||
|
want := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLastFixedWeeklyReset_SameDay_BeforeHour(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-16 is Monday, target = Monday, hour = 9, now = 06:00
|
||||||
|
now := time.Date(2026, 3, 16, 6, 0, 0, 0, tz)
|
||||||
|
got := lastFixedWeeklyReset(1, 9, tz, now)
|
||||||
|
// Last Monday at 9:00 = 2026-03-09
|
||||||
|
want := time.Date(2026, 3, 9, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLastFixedWeeklyReset_DifferentDay(t *testing.T) {
|
||||||
|
tz := time.UTC
|
||||||
|
// 2026-03-18 is Wednesday (day=3), target = Monday (day=1)
|
||||||
|
now := time.Date(2026, 3, 18, 10, 0, 0, 0, tz)
|
||||||
|
got := lastFixedWeeklyReset(1, 9, tz, now)
|
||||||
|
// Last Monday = 2026-03-16
|
||||||
|
want := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// isFixedDailyPeriodExpired
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestIsFixedDailyPeriodExpired_ZeroPeriodStart(t *testing.T) {
|
||||||
|
a := &Account{Extra: map[string]any{
|
||||||
|
"quota_daily_reset_mode": "fixed",
|
||||||
|
"quota_daily_reset_hour": float64(9),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}}
|
||||||
|
assert.True(t, a.isFixedDailyPeriodExpired(time.Time{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFixedDailyPeriodExpired_NotExpired(t *testing.T) {
|
||||||
|
a := &Account{Extra: map[string]any{
|
||||||
|
"quota_daily_reset_mode": "fixed",
|
||||||
|
"quota_daily_reset_hour": float64(9),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}}
|
||||||
|
// Period started after the most recent reset → not expired
|
||||||
|
// (This test uses a time very close to "now", which is after the last reset)
|
||||||
|
periodStart := time.Now().Add(-1 * time.Minute)
|
||||||
|
assert.False(t, a.isFixedDailyPeriodExpired(periodStart))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFixedDailyPeriodExpired_Expired(t *testing.T) {
|
||||||
|
a := &Account{Extra: map[string]any{
|
||||||
|
"quota_daily_reset_mode": "fixed",
|
||||||
|
"quota_daily_reset_hour": float64(9),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}}
|
||||||
|
// Period started 3 days ago → definitely expired
|
||||||
|
periodStart := time.Now().Add(-72 * time.Hour)
|
||||||
|
assert.True(t, a.isFixedDailyPeriodExpired(periodStart))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFixedDailyPeriodExpired_InvalidTimezone(t *testing.T) {
|
||||||
|
a := &Account{Extra: map[string]any{
|
||||||
|
"quota_daily_reset_mode": "fixed",
|
||||||
|
"quota_daily_reset_hour": float64(9),
|
||||||
|
"quota_reset_timezone": "Invalid/Timezone",
|
||||||
|
}}
|
||||||
|
// Invalid timezone falls back to UTC
|
||||||
|
periodStart := time.Now().Add(-72 * time.Hour)
|
||||||
|
assert.True(t, a.isFixedDailyPeriodExpired(periodStart))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// isFixedWeeklyPeriodExpired
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestIsFixedWeeklyPeriodExpired_ZeroPeriodStart(t *testing.T) {
|
||||||
|
a := &Account{Extra: map[string]any{
|
||||||
|
"quota_weekly_reset_mode": "fixed",
|
||||||
|
"quota_weekly_reset_day": float64(1),
|
||||||
|
"quota_weekly_reset_hour": float64(9),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}}
|
||||||
|
assert.True(t, a.isFixedWeeklyPeriodExpired(time.Time{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFixedWeeklyPeriodExpired_NotExpired(t *testing.T) {
|
||||||
|
a := &Account{Extra: map[string]any{
|
||||||
|
"quota_weekly_reset_mode": "fixed",
|
||||||
|
"quota_weekly_reset_day": float64(1),
|
||||||
|
"quota_weekly_reset_hour": float64(9),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}}
|
||||||
|
// Period started 1 minute ago → not expired
|
||||||
|
periodStart := time.Now().Add(-1 * time.Minute)
|
||||||
|
assert.False(t, a.isFixedWeeklyPeriodExpired(periodStart))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFixedWeeklyPeriodExpired_Expired(t *testing.T) {
|
||||||
|
a := &Account{Extra: map[string]any{
|
||||||
|
"quota_weekly_reset_mode": "fixed",
|
||||||
|
"quota_weekly_reset_day": float64(1),
|
||||||
|
"quota_weekly_reset_hour": float64(9),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}}
|
||||||
|
// Period started 10 days ago → definitely expired
|
||||||
|
periodStart := time.Now().Add(-240 * time.Hour)
|
||||||
|
assert.True(t, a.isFixedWeeklyPeriodExpired(periodStart))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ValidateQuotaResetConfig
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_NilExtra(t *testing.T) {
|
||||||
|
assert.NoError(t, ValidateQuotaResetConfig(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_EmptyExtra(t *testing.T) {
|
||||||
|
assert.NoError(t, ValidateQuotaResetConfig(map[string]any{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_ValidFixed(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_mode": "fixed",
|
||||||
|
"quota_daily_reset_hour": float64(9),
|
||||||
|
"quota_weekly_reset_mode": "fixed",
|
||||||
|
"quota_weekly_reset_day": float64(1),
|
||||||
|
"quota_weekly_reset_hour": float64(0),
|
||||||
|
"quota_reset_timezone": "Asia/Shanghai",
|
||||||
|
}
|
||||||
|
assert.NoError(t, ValidateQuotaResetConfig(extra))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_ValidRolling(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_mode": "rolling",
|
||||||
|
"quota_weekly_reset_mode": "rolling",
|
||||||
|
}
|
||||||
|
assert.NoError(t, ValidateQuotaResetConfig(extra))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_InvalidTimezone(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_reset_timezone": "Not/A/Timezone",
|
||||||
|
}
|
||||||
|
err := ValidateQuotaResetConfig(extra)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "quota_reset_timezone")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_InvalidDailyMode(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_mode": "invalid",
|
||||||
|
}
|
||||||
|
err := ValidateQuotaResetConfig(extra)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "quota_daily_reset_mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_InvalidDailyHour_TooHigh(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_hour": float64(24),
|
||||||
|
}
|
||||||
|
err := ValidateQuotaResetConfig(extra)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "quota_daily_reset_hour")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_InvalidDailyHour_Negative(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_hour": float64(-1),
|
||||||
|
}
|
||||||
|
err := ValidateQuotaResetConfig(extra)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "quota_daily_reset_hour")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_InvalidWeeklyMode(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_weekly_reset_mode": "unknown",
|
||||||
|
}
|
||||||
|
err := ValidateQuotaResetConfig(extra)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "quota_weekly_reset_mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_InvalidWeeklyDay_TooHigh(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_weekly_reset_day": float64(7),
|
||||||
|
}
|
||||||
|
err := ValidateQuotaResetConfig(extra)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "quota_weekly_reset_day")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_InvalidWeeklyDay_Negative(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_weekly_reset_day": float64(-1),
|
||||||
|
}
|
||||||
|
err := ValidateQuotaResetConfig(extra)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "quota_weekly_reset_day")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_InvalidWeeklyHour(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_weekly_reset_hour": float64(25),
|
||||||
|
}
|
||||||
|
err := ValidateQuotaResetConfig(extra)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "quota_weekly_reset_hour")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateQuotaResetConfig_BoundaryValues(t *testing.T) {
|
||||||
|
// All boundary values should be valid
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_hour": float64(23),
|
||||||
|
"quota_weekly_reset_day": float64(0), // Sunday
|
||||||
|
"quota_weekly_reset_hour": float64(0),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}
|
||||||
|
assert.NoError(t, ValidateQuotaResetConfig(extra))
|
||||||
|
|
||||||
|
extra2 := map[string]any{
|
||||||
|
"quota_daily_reset_hour": float64(0),
|
||||||
|
"quota_weekly_reset_day": float64(6), // Saturday
|
||||||
|
"quota_weekly_reset_hour": float64(23),
|
||||||
|
}
|
||||||
|
assert.NoError(t, ValidateQuotaResetConfig(extra2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ComputeQuotaResetAt
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestComputeQuotaResetAt_RollingMode_NoResetAt(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_mode": "rolling",
|
||||||
|
"quota_weekly_reset_mode": "rolling",
|
||||||
|
}
|
||||||
|
ComputeQuotaResetAt(extra)
|
||||||
|
_, hasDailyResetAt := extra["quota_daily_reset_at"]
|
||||||
|
_, hasWeeklyResetAt := extra["quota_weekly_reset_at"]
|
||||||
|
assert.False(t, hasDailyResetAt, "rolling mode should not set quota_daily_reset_at")
|
||||||
|
assert.False(t, hasWeeklyResetAt, "rolling mode should not set quota_weekly_reset_at")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeQuotaResetAt_RollingMode_ClearsExistingResetAt(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_mode": "rolling",
|
||||||
|
"quota_weekly_reset_mode": "rolling",
|
||||||
|
"quota_daily_reset_at": "2026-03-14T09:00:00Z",
|
||||||
|
"quota_weekly_reset_at": "2026-03-16T09:00:00Z",
|
||||||
|
}
|
||||||
|
ComputeQuotaResetAt(extra)
|
||||||
|
_, hasDailyResetAt := extra["quota_daily_reset_at"]
|
||||||
|
_, hasWeeklyResetAt := extra["quota_weekly_reset_at"]
|
||||||
|
assert.False(t, hasDailyResetAt, "rolling mode should remove quota_daily_reset_at")
|
||||||
|
assert.False(t, hasWeeklyResetAt, "rolling mode should remove quota_weekly_reset_at")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeQuotaResetAt_FixedDaily_SetsResetAt(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_mode": "fixed",
|
||||||
|
"quota_daily_reset_hour": float64(9),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}
|
||||||
|
ComputeQuotaResetAt(extra)
|
||||||
|
resetAtStr, ok := extra["quota_daily_reset_at"].(string)
|
||||||
|
require.True(t, ok, "quota_daily_reset_at should be set")
|
||||||
|
|
||||||
|
resetAt, err := time.Parse(time.RFC3339, resetAtStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Reset time should be in the future
|
||||||
|
assert.True(t, resetAt.After(time.Now()), "reset_at should be in the future")
|
||||||
|
// Reset hour should be 9 UTC
|
||||||
|
assert.Equal(t, 9, resetAt.UTC().Hour())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeQuotaResetAt_FixedWeekly_SetsResetAt(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_weekly_reset_mode": "fixed",
|
||||||
|
"quota_weekly_reset_day": float64(1), // Monday
|
||||||
|
"quota_weekly_reset_hour": float64(0),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}
|
||||||
|
ComputeQuotaResetAt(extra)
|
||||||
|
resetAtStr, ok := extra["quota_weekly_reset_at"].(string)
|
||||||
|
require.True(t, ok, "quota_weekly_reset_at should be set")
|
||||||
|
|
||||||
|
resetAt, err := time.Parse(time.RFC3339, resetAtStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Reset time should be in the future
|
||||||
|
assert.True(t, resetAt.After(time.Now()), "reset_at should be in the future")
|
||||||
|
// Reset day should be Monday
|
||||||
|
assert.Equal(t, time.Monday, resetAt.UTC().Weekday())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeQuotaResetAt_FixedDaily_WithTimezone(t *testing.T) {
|
||||||
|
tz, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_mode": "fixed",
|
||||||
|
"quota_daily_reset_hour": float64(9),
|
||||||
|
"quota_reset_timezone": "Asia/Shanghai",
|
||||||
|
}
|
||||||
|
ComputeQuotaResetAt(extra)
|
||||||
|
resetAtStr, ok := extra["quota_daily_reset_at"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
resetAt, err := time.Parse(time.RFC3339, resetAtStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// In Shanghai timezone, the hour should be 9
|
||||||
|
assert.Equal(t, 9, resetAt.In(tz).Hour())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeQuotaResetAt_DefaultTimezone(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_mode": "fixed",
|
||||||
|
"quota_daily_reset_hour": float64(12),
|
||||||
|
}
|
||||||
|
ComputeQuotaResetAt(extra)
|
||||||
|
resetAtStr, ok := extra["quota_daily_reset_at"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
resetAt, err := time.Parse(time.RFC3339, resetAtStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Default timezone is UTC
|
||||||
|
assert.Equal(t, 12, resetAt.UTC().Hour())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeQuotaResetAt_InvalidHour_ClampedToZero(t *testing.T) {
|
||||||
|
extra := map[string]any{
|
||||||
|
"quota_daily_reset_mode": "fixed",
|
||||||
|
"quota_daily_reset_hour": float64(99),
|
||||||
|
"quota_reset_timezone": "UTC",
|
||||||
|
}
|
||||||
|
ComputeQuotaResetAt(extra)
|
||||||
|
resetAtStr, ok := extra["quota_daily_reset_at"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
resetAt, err := time.Parse(time.RFC3339, resetAtStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Invalid hour → clamped to 0
|
||||||
|
assert.Equal(t, 0, resetAt.UTC().Hour())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user