subscription_calculate_progress_test.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. package service
  2. import (
  3. "testing"
  4. "time"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. )
  8. // --- Task 5: 验证 calculateProgress 纯函数行为正确 ---
  9. func newTestSubscriptionService() *SubscriptionService {
  10. return &SubscriptionService{}
  11. }
  12. func ptrFloat64(v float64) *float64 { return &v }
  13. func ptrTime(t time.Time) *time.Time { return &t }
  14. func TestCalculateProgress_BasicFields(t *testing.T) {
  15. svc := newTestSubscriptionService()
  16. now := time.Now()
  17. sub := &UserSubscription{
  18. ID: 100,
  19. ExpiresAt: now.Add(30 * 24 * time.Hour),
  20. }
  21. group := &Group{
  22. Name: "Premium",
  23. }
  24. progress := svc.calculateProgress(sub, group)
  25. assert.Equal(t, int64(100), progress.ID)
  26. assert.Equal(t, "Premium", progress.GroupName)
  27. assert.Equal(t, sub.ExpiresAt, progress.ExpiresAt)
  28. assert.Equal(t, 30, progress.ExpiresInDays)
  29. assert.Nil(t, progress.Daily, "无日限额时 Daily 应为 nil")
  30. assert.Nil(t, progress.Weekly, "无周限额时 Weekly 应为 nil")
  31. assert.Nil(t, progress.Monthly, "无月限额时 Monthly 应为 nil")
  32. }
  33. func TestCalculateProgress_DailyUsage(t *testing.T) {
  34. svc := newTestSubscriptionService()
  35. now := time.Now()
  36. dailyStart := now.Add(-12 * time.Hour)
  37. sub := &UserSubscription{
  38. ID: 1,
  39. ExpiresAt: now.Add(10 * 24 * time.Hour),
  40. DailyUsageUSD: 3.0,
  41. DailyWindowStart: ptrTime(dailyStart),
  42. }
  43. group := &Group{
  44. Name: "Pro",
  45. DailyLimitUSD: ptrFloat64(10.0),
  46. }
  47. progress := svc.calculateProgress(sub, group)
  48. require.NotNil(t, progress.Daily, "有日限额和窗口时 Daily 不应为 nil")
  49. assert.Equal(t, 10.0, progress.Daily.LimitUSD)
  50. assert.Equal(t, 3.0, progress.Daily.UsedUSD)
  51. assert.Equal(t, 7.0, progress.Daily.RemainingUSD)
  52. assert.Equal(t, 30.0, progress.Daily.Percentage)
  53. assert.Equal(t, dailyStart, progress.Daily.WindowStart)
  54. }
  55. func TestCalculateProgress_DailyCardUsesExpiryAsDailyResetTime(t *testing.T) {
  56. svc := newTestSubscriptionService()
  57. startsAt := time.Now().Add(-12 * time.Hour)
  58. dailyStart := time.Date(startsAt.Year(), startsAt.Month(), startsAt.Day(), 0, 0, 0, 0, startsAt.Location())
  59. expiresAt := startsAt.Add(24 * time.Hour)
  60. sub := &UserSubscription{
  61. ID: 1,
  62. StartsAt: startsAt,
  63. ExpiresAt: expiresAt,
  64. DailyUsageUSD: 3.0,
  65. DailyWindowStart: ptrTime(dailyStart),
  66. }
  67. group := &Group{
  68. Name: "Daily",
  69. DailyLimitUSD: ptrFloat64(10.0),
  70. }
  71. progress := svc.calculateProgress(sub, group)
  72. require.NotNil(t, progress.Daily, "日卡有日限额和窗口时 Daily 不应为 nil")
  73. assert.Equal(t, expiresAt, progress.Daily.ResetsAt, "日卡的一次性日额度结束时间应为订阅过期时间")
  74. }
  75. func TestCalculateProgress_WeeklyUsage(t *testing.T) {
  76. svc := newTestSubscriptionService()
  77. now := time.Now()
  78. weeklyStart := now.Add(-3 * 24 * time.Hour)
  79. sub := &UserSubscription{
  80. ID: 1,
  81. ExpiresAt: now.Add(10 * 24 * time.Hour),
  82. WeeklyUsageUSD: 25.0,
  83. WeeklyWindowStart: ptrTime(weeklyStart),
  84. }
  85. group := &Group{
  86. Name: "Pro",
  87. WeeklyLimitUSD: ptrFloat64(50.0),
  88. }
  89. progress := svc.calculateProgress(sub, group)
  90. require.NotNil(t, progress.Weekly, "有周限额和窗口时 Weekly 不应为 nil")
  91. assert.Equal(t, 50.0, progress.Weekly.LimitUSD)
  92. assert.Equal(t, 25.0, progress.Weekly.UsedUSD)
  93. assert.Equal(t, 25.0, progress.Weekly.RemainingUSD)
  94. assert.Equal(t, 50.0, progress.Weekly.Percentage)
  95. }
  96. // 周窗口初始化在开通日零点(legacy anchor)时,展示的 ResetsAt 应与
  97. // automaticWindowStartAt 的实际推进时间一致(StartsAt+7d),而非窗口起点+7d。
  98. func TestCalculateProgress_WeeklyResetsAt_LegacyMidnightAnchor(t *testing.T) {
  99. svc := newTestSubscriptionService()
  100. startsAt := time.Date(2026, 7, 31, 13, 37, 6, 0, time.FixedZone("UTC+8", 8*3600))
  101. weeklyStart := time.Date(startsAt.Year(), startsAt.Month(), startsAt.Day(), 0, 0, 0, 0, startsAt.Location())
  102. sub := &UserSubscription{
  103. ID: 1,
  104. StartsAt: startsAt,
  105. ExpiresAt: startsAt.AddDate(0, 0, 30),
  106. WeeklyUsageUSD: 2000.18,
  107. WeeklyWindowStart: ptrTime(weeklyStart),
  108. }
  109. group := &Group{
  110. Name: "Pro",
  111. WeeklyLimitUSD: ptrFloat64(2000.0),
  112. }
  113. progress := svc.calculateProgress(sub, group)
  114. require.NotNil(t, progress.Weekly)
  115. assert.True(t, startsAt.Add(7*24*time.Hour).Equal(progress.Weekly.ResetsAt),
  116. "legacy 午夜锚点的周窗口 ResetsAt 应为 StartsAt+7d,与实际推进时间一致")
  117. }
  118. func TestCalculateProgress_MonthlyUsage(t *testing.T) {
  119. svc := newTestSubscriptionService()
  120. now := time.Now()
  121. monthlyStart := now.Add(-15 * 24 * time.Hour)
  122. sub := &UserSubscription{
  123. ID: 1,
  124. ExpiresAt: now.Add(10 * 24 * time.Hour),
  125. MonthlyUsageUSD: 80.0,
  126. MonthlyWindowStart: ptrTime(monthlyStart),
  127. }
  128. group := &Group{
  129. Name: "Enterprise",
  130. MonthlyLimitUSD: ptrFloat64(100.0),
  131. }
  132. progress := svc.calculateProgress(sub, group)
  133. require.NotNil(t, progress.Monthly, "有月限额和窗口时 Monthly 不应为 nil")
  134. assert.Equal(t, 100.0, progress.Monthly.LimitUSD)
  135. assert.Equal(t, 80.0, progress.Monthly.UsedUSD)
  136. assert.Equal(t, 20.0, progress.Monthly.RemainingUSD)
  137. assert.Equal(t, 80.0, progress.Monthly.Percentage)
  138. }
  139. func TestCalculateProgress_OverLimit_ClampedTo100Percent(t *testing.T) {
  140. svc := newTestSubscriptionService()
  141. now := time.Now()
  142. sub := &UserSubscription{
  143. ID: 1,
  144. ExpiresAt: now.Add(10 * 24 * time.Hour),
  145. DailyUsageUSD: 15.0, // 超过限额
  146. DailyWindowStart: ptrTime(now.Add(-1 * time.Hour)),
  147. }
  148. group := &Group{
  149. Name: "Pro",
  150. DailyLimitUSD: ptrFloat64(10.0),
  151. }
  152. progress := svc.calculateProgress(sub, group)
  153. require.NotNil(t, progress.Daily)
  154. assert.Equal(t, 100.0, progress.Daily.Percentage, "超额使用应被截断为 100%")
  155. assert.Equal(t, 0.0, progress.Daily.RemainingUSD, "超额使用时剩余应为 0")
  156. }
  157. func TestCalculateProgress_NoWindowStart_NoProgress(t *testing.T) {
  158. svc := newTestSubscriptionService()
  159. now := time.Now()
  160. // 有限额但无窗口起始时间(订阅未激活)
  161. sub := &UserSubscription{
  162. ID: 1,
  163. ExpiresAt: now.Add(10 * 24 * time.Hour),
  164. DailyUsageUSD: 0,
  165. WeeklyUsageUSD: 0,
  166. }
  167. group := &Group{
  168. Name: "Pro",
  169. DailyLimitUSD: ptrFloat64(10.0),
  170. WeeklyLimitUSD: ptrFloat64(50.0),
  171. }
  172. progress := svc.calculateProgress(sub, group)
  173. assert.Nil(t, progress.Daily, "无 DailyWindowStart 时 Daily 应为 nil")
  174. assert.Nil(t, progress.Weekly, "无 WeeklyWindowStart 时 Weekly 应为 nil")
  175. }
  176. func TestCalculateProgress_AllLimits(t *testing.T) {
  177. svc := newTestSubscriptionService()
  178. now := time.Now()
  179. sub := &UserSubscription{
  180. ID: 1,
  181. ExpiresAt: now.Add(10 * 24 * time.Hour),
  182. DailyUsageUSD: 5.0,
  183. WeeklyUsageUSD: 20.0,
  184. MonthlyUsageUSD: 60.0,
  185. DailyWindowStart: ptrTime(now.Add(-6 * time.Hour)),
  186. WeeklyWindowStart: ptrTime(now.Add(-3 * 24 * time.Hour)),
  187. MonthlyWindowStart: ptrTime(now.Add(-15 * 24 * time.Hour)),
  188. }
  189. group := &Group{
  190. Name: "Full",
  191. DailyLimitUSD: ptrFloat64(10.0),
  192. WeeklyLimitUSD: ptrFloat64(50.0),
  193. MonthlyLimitUSD: ptrFloat64(100.0),
  194. }
  195. progress := svc.calculateProgress(sub, group)
  196. require.NotNil(t, progress.Daily)
  197. require.NotNil(t, progress.Weekly)
  198. require.NotNil(t, progress.Monthly)
  199. assert.Equal(t, 50.0, progress.Daily.Percentage)
  200. assert.Equal(t, 40.0, progress.Weekly.Percentage)
  201. assert.Equal(t, 60.0, progress.Monthly.Percentage)
  202. }
  203. func TestCalculateProgress_ExpiredSubscription(t *testing.T) {
  204. svc := newTestSubscriptionService()
  205. sub := &UserSubscription{
  206. ID: 1,
  207. ExpiresAt: time.Now().Add(-24 * time.Hour), // 已过期
  208. }
  209. group := &Group{Name: "Expired"}
  210. progress := svc.calculateProgress(sub, group)
  211. assert.Equal(t, 0, progress.ExpiresInDays, "过期订阅的剩余天数应为 0")
  212. }
  213. func TestCalculateProgress_ResetsInSeconds_NotNegative(t *testing.T) {
  214. svc := newTestSubscriptionService()
  215. // 使用过去的窗口起始时间,使得重置时间已过
  216. pastStart := time.Now().Add(-48 * time.Hour)
  217. sub := &UserSubscription{
  218. ID: 1,
  219. ExpiresAt: time.Now().Add(10 * 24 * time.Hour),
  220. DailyUsageUSD: 1.0,
  221. DailyWindowStart: ptrTime(pastStart),
  222. }
  223. group := &Group{
  224. Name: "Test",
  225. DailyLimitUSD: ptrFloat64(10.0),
  226. }
  227. progress := svc.calculateProgress(sub, group)
  228. require.NotNil(t, progress.Daily)
  229. assert.GreaterOrEqual(t, progress.Daily.ResetsInSeconds, int64(0),
  230. "ResetsInSeconds 不应为负数")
  231. }