user_platform_quota_repo_test.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. //go:build unit
  2. package repository
  3. import (
  4. "os"
  5. "strings"
  6. "testing"
  7. "time"
  8. )
  9. func TestMaybeReset(t *testing.T) {
  10. start := time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC)
  11. other := start.AddDate(0, 0, -1)
  12. cases := []struct {
  13. name string
  14. prevUsage float64
  15. prevStart *time.Time
  16. currStart time.Time
  17. cost float64
  18. want float64
  19. }{
  20. {"nil prev start resets", 10, nil, start, 1.5, 1.5},
  21. {"different start resets", 10, &other, start, 1.5, 1.5},
  22. {"same start accumulates", 10, &start, start, 1.5, 11.5},
  23. }
  24. for _, c := range cases {
  25. t.Run(c.name, func(t *testing.T) {
  26. if got := maybeReset(c.prevUsage, c.prevStart, c.currStart, c.cost); got != c.want {
  27. t.Errorf("maybeReset = %v, want %v", got, c.want)
  28. }
  29. })
  30. }
  31. }
  32. // TestMonthlyMaybeReset_NilStart 验证 prevStart=nil 时重置。
  33. func TestMonthlyMaybeReset_NilStart(t *testing.T) {
  34. now := time.Date(2026, 5, 22, 12, 0, 0, 0, time.UTC)
  35. usage, start := monthlyMaybeReset(10.0, nil, 1.5, now)
  36. if usage != 1.5 {
  37. t.Errorf("usage = %v, want 1.5", usage)
  38. }
  39. if !start.Equal(now) {
  40. t.Errorf("start = %v, want %v", start, now)
  41. }
  42. }
  43. // TestMonthlyMaybeReset_Expired 验证窗口满 30 天时重置(30 天恰好到期)。
  44. func TestMonthlyMaybeReset_Expired(t *testing.T) {
  45. windowStart := time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC)
  46. // now = windowStart + 30d(刚好到期)
  47. now := windowStart.Add(30 * 24 * time.Hour)
  48. usage, start := monthlyMaybeReset(8.0, &windowStart, 2.0, now)
  49. if usage != 2.0 {
  50. t.Errorf("usage = %v, want 2.0 (reset)", usage)
  51. }
  52. if !start.Equal(now) {
  53. t.Errorf("start = %v, want %v (new window)", start, now)
  54. }
  55. }
  56. // TestMonthlyMaybeReset_CrossMonthBoundary 验证跨自然月时也使用 30 天滚动(不提前重置)。
  57. // 旧行为:5 月 1 日跨月立即重置;新行为:窗口起始 4 月 20 日,5 月 1 日仅过了 11 天,应累加。
  58. func TestMonthlyMaybeReset_CrossMonthBoundary(t *testing.T) {
  59. windowStart := time.Date(2026, 4, 20, 0, 0, 0, 0, time.UTC)
  60. // 5 月 1 日:距起始 11 天,不足 30 天,应累加
  61. now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
  62. usage, start := monthlyMaybeReset(5.0, &windowStart, 1.0, now)
  63. if usage != 6.0 {
  64. t.Errorf("usage = %v, want 6.0 (accumulate, not reset at month boundary)", usage)
  65. }
  66. if !start.Equal(windowStart) {
  67. t.Errorf("start = %v, want %v (preserved)", start, windowStart)
  68. }
  69. }
  70. // TestMonthlyMaybeReset_Active 验证窗口内正常累加。
  71. func TestMonthlyMaybeReset_Active(t *testing.T) {
  72. windowStart := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
  73. // 15 天内,窗口有效
  74. now := windowStart.Add(15 * 24 * time.Hour)
  75. usage, start := monthlyMaybeReset(3.0, &windowStart, 0.5, now)
  76. if usage != 3.5 {
  77. t.Errorf("usage = %v, want 3.5", usage)
  78. }
  79. if !start.Equal(windowStart) {
  80. t.Errorf("start = %v, want %v", start, windowStart)
  81. }
  82. }
  83. // TestUpdateLimitsRowQuery_HasDeletedAtGuard 通过读取源文件验证 updateLimitsRow
  84. // 的 SQL WHERE 子句包含 deleted_at IS NULL 守卫(I-NEW-1)。
  85. // 此防回归测试可在无 DB 的 CI 环境中运行,防止意外删除该守卫。
  86. func TestUpdateLimitsRowQuery_HasDeletedAtGuard(t *testing.T) {
  87. src, err := os.ReadFile("user_platform_quota_repo.go")
  88. if err != nil {
  89. t.Fatalf("failed to read source file: %v", err)
  90. }
  91. const guard = "AND deleted_at IS NULL"
  92. if !strings.Contains(string(src), guard) {
  93. t.Errorf("updateLimitsRow SQL must contain %q to prevent bulk reactivation of soft-deleted rows (I-NEW-1)", guard)
  94. }
  95. }