usage_handler_daily_test.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package handler
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
  10. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  11. "github.com/Wei-Shaw/sub2api/internal/service"
  12. "github.com/gin-gonic/gin"
  13. "github.com/stretchr/testify/require"
  14. )
  15. type dailyUsageRepoStub struct {
  16. service.UsageLogRepository
  17. trend []usagestats.TrendDataPoint
  18. called bool
  19. startTime time.Time
  20. endTime time.Time
  21. granularity string
  22. userID int64
  23. apiKeyID int64
  24. }
  25. func (s *dailyUsageRepoStub) GetUsageTrendWithFilters(
  26. ctx context.Context,
  27. startTime, endTime time.Time,
  28. granularity string,
  29. userID, apiKeyID, accountID, groupID int64,
  30. model string,
  31. requestType *int16,
  32. stream *bool,
  33. billingType *int8,
  34. ) ([]usagestats.TrendDataPoint, error) {
  35. s.called = true
  36. s.startTime = startTime
  37. s.endTime = endTime
  38. s.granularity = granularity
  39. s.userID = userID
  40. s.apiKeyID = apiKeyID
  41. return s.trend, nil
  42. }
  43. type dailyUsageAPIKeyRepoStub struct {
  44. service.APIKeyRepository
  45. keys map[int64]*service.APIKey
  46. }
  47. func (s *dailyUsageAPIKeyRepoStub) GetByID(ctx context.Context, id int64) (*service.APIKey, error) {
  48. key, ok := s.keys[id]
  49. if !ok {
  50. return nil, service.ErrAPIKeyNotFound
  51. }
  52. clone := *key
  53. return &clone, nil
  54. }
  55. func newDailyUsageTestRouter(usageRepo *dailyUsageRepoStub, apiKeyRepo *dailyUsageAPIKeyRepoStub, userID int64) *gin.Engine {
  56. gin.SetMode(gin.TestMode)
  57. usageSvc := service.NewUsageService(usageRepo, nil, nil, nil)
  58. apiKeySvc := service.NewAPIKeyService(apiKeyRepo, nil, nil, nil, nil, nil, nil)
  59. handler := NewUsageHandler(usageSvc, apiKeySvc, nil, nil)
  60. router := gin.New()
  61. router.Use(func(c *gin.Context) {
  62. c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: userID})
  63. c.Next()
  64. })
  65. router.GET("/user/api-keys/:id/usage/daily", handler.GetMyAPIKeyDailyUsage)
  66. return router
  67. }
  68. type dailyUsageHandlerResponse struct {
  69. Code int `json:"code"`
  70. Data struct {
  71. Items []usagestats.APIKeyDailyUsagePoint `json:"items"`
  72. Days int `json:"days"`
  73. } `json:"data"`
  74. }
  75. func TestGetMyAPIKeyDailyUsageRejectsCrossUserAccess(t *testing.T) {
  76. usageRepo := &dailyUsageRepoStub{}
  77. apiKeyRepo := &dailyUsageAPIKeyRepoStub{
  78. keys: map[int64]*service.APIKey{
  79. 7: {ID: 7, UserID: 99, Status: service.StatusAPIKeyActive},
  80. },
  81. }
  82. router := newDailyUsageTestRouter(usageRepo, apiKeyRepo, 42)
  83. req := httptest.NewRequest(http.MethodGet, "/user/api-keys/7/usage/daily?days=30", nil)
  84. rec := httptest.NewRecorder()
  85. router.ServeHTTP(rec, req)
  86. require.Equal(t, http.StatusForbidden, rec.Code)
  87. require.False(t, usageRepo.called)
  88. }
  89. func TestGetMyAPIKeyDailyUsageRejectsInvalidDays(t *testing.T) {
  90. for _, path := range []string{
  91. "/user/api-keys/7/usage/daily?days=0",
  92. "/user/api-keys/7/usage/daily?days=91",
  93. } {
  94. t.Run(path, func(t *testing.T) {
  95. usageRepo := &dailyUsageRepoStub{}
  96. apiKeyRepo := &dailyUsageAPIKeyRepoStub{
  97. keys: map[int64]*service.APIKey{
  98. 7: {ID: 7, UserID: 42, Status: service.StatusAPIKeyActive},
  99. },
  100. }
  101. router := newDailyUsageTestRouter(usageRepo, apiKeyRepo, 42)
  102. req := httptest.NewRequest(http.MethodGet, path, nil)
  103. rec := httptest.NewRecorder()
  104. router.ServeHTTP(rec, req)
  105. require.Equal(t, http.StatusBadRequest, rec.Code)
  106. require.False(t, usageRepo.called)
  107. })
  108. }
  109. }
  110. func TestGetMyAPIKeyDailyUsageReturnsEmptyData(t *testing.T) {
  111. usageRepo := &dailyUsageRepoStub{trend: []usagestats.TrendDataPoint{}}
  112. apiKeyRepo := &dailyUsageAPIKeyRepoStub{
  113. keys: map[int64]*service.APIKey{
  114. 7: {ID: 7, UserID: 42, Status: service.StatusAPIKeyActive},
  115. },
  116. }
  117. router := newDailyUsageTestRouter(usageRepo, apiKeyRepo, 42)
  118. req := httptest.NewRequest(http.MethodGet, "/user/api-keys/7/usage/daily", nil)
  119. rec := httptest.NewRecorder()
  120. router.ServeHTTP(rec, req)
  121. require.Equal(t, http.StatusOK, rec.Code)
  122. var got dailyUsageHandlerResponse
  123. require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
  124. require.Equal(t, 30, got.Data.Days)
  125. require.Empty(t, got.Data.Items)
  126. }
  127. func TestGetMyAPIKeyDailyUsageAggregatesByDayForOwnedKey(t *testing.T) {
  128. usageRepo := &dailyUsageRepoStub{
  129. trend: []usagestats.TrendDataPoint{
  130. {
  131. Date: "2026-05-19",
  132. Requests: 3,
  133. InputTokens: 10,
  134. OutputTokens: 20,
  135. CacheCreationTokens: 4,
  136. CacheReadTokens: 6,
  137. TotalTokens: 40,
  138. Cost: 0.5,
  139. ActualCost: 0.4,
  140. },
  141. },
  142. }
  143. apiKeyRepo := &dailyUsageAPIKeyRepoStub{
  144. keys: map[int64]*service.APIKey{
  145. 7: {ID: 7, UserID: 42, Status: service.StatusAPIKeyActive},
  146. },
  147. }
  148. router := newDailyUsageTestRouter(usageRepo, apiKeyRepo, 42)
  149. req := httptest.NewRequest(http.MethodGet, "/user/api-keys/7/usage/daily?days=7", nil)
  150. rec := httptest.NewRecorder()
  151. router.ServeHTTP(rec, req)
  152. require.Equal(t, http.StatusOK, rec.Code)
  153. require.True(t, usageRepo.called)
  154. require.Equal(t, "day", usageRepo.granularity)
  155. require.Equal(t, int64(42), usageRepo.userID)
  156. require.Equal(t, int64(7), usageRepo.apiKeyID)
  157. require.True(t, usageRepo.startTime.Before(usageRepo.endTime))
  158. var got dailyUsageHandlerResponse
  159. require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
  160. require.Equal(t, 7, got.Data.Days)
  161. require.Len(t, got.Data.Items, 1)
  162. require.Equal(t, usagestats.APIKeyDailyUsagePoint{
  163. Date: "2026-05-19",
  164. Requests: 3,
  165. InputTokens: 10,
  166. OutputTokens: 20,
  167. CacheReadTokens: 6,
  168. CacheWriteTokens: 4,
  169. TotalTokens: 40,
  170. Cost: 0.5,
  171. ActualCost: 0.4,
  172. }, got.Data.Items[0])
  173. }