196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type dailyUsageRepoStub struct {
|
|
service.UsageLogRepository
|
|
trend []usagestats.TrendDataPoint
|
|
|
|
called bool
|
|
startTime time.Time
|
|
endTime time.Time
|
|
granularity string
|
|
userID int64
|
|
apiKeyID int64
|
|
}
|
|
|
|
func (s *dailyUsageRepoStub) GetUsageTrendWithFilters(
|
|
ctx context.Context,
|
|
startTime, endTime time.Time,
|
|
granularity string,
|
|
userID, apiKeyID, accountID, groupID int64,
|
|
model string,
|
|
requestType *int16,
|
|
stream *bool,
|
|
billingType *int8,
|
|
) ([]usagestats.TrendDataPoint, error) {
|
|
s.called = true
|
|
s.startTime = startTime
|
|
s.endTime = endTime
|
|
s.granularity = granularity
|
|
s.userID = userID
|
|
s.apiKeyID = apiKeyID
|
|
return s.trend, nil
|
|
}
|
|
|
|
type dailyUsageAPIKeyRepoStub struct {
|
|
service.APIKeyRepository
|
|
keys map[int64]*service.APIKey
|
|
}
|
|
|
|
func (s *dailyUsageAPIKeyRepoStub) GetByID(ctx context.Context, id int64) (*service.APIKey, error) {
|
|
key, ok := s.keys[id]
|
|
if !ok {
|
|
return nil, service.ErrAPIKeyNotFound
|
|
}
|
|
clone := *key
|
|
return &clone, nil
|
|
}
|
|
|
|
func newDailyUsageTestRouter(usageRepo *dailyUsageRepoStub, apiKeyRepo *dailyUsageAPIKeyRepoStub, userID int64) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
usageSvc := service.NewUsageService(usageRepo, nil, nil, nil)
|
|
apiKeySvc := service.NewAPIKeyService(apiKeyRepo, nil, nil, nil, nil, nil, nil)
|
|
handler := NewUsageHandler(usageSvc, apiKeySvc)
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: userID})
|
|
c.Next()
|
|
})
|
|
router.GET("/user/api-keys/:id/usage/daily", handler.GetMyAPIKeyDailyUsage)
|
|
return router
|
|
}
|
|
|
|
type dailyUsageHandlerResponse struct {
|
|
Code int `json:"code"`
|
|
Data struct {
|
|
Items []usagestats.APIKeyDailyUsagePoint `json:"items"`
|
|
Days int `json:"days"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
func TestGetMyAPIKeyDailyUsageRejectsCrossUserAccess(t *testing.T) {
|
|
usageRepo := &dailyUsageRepoStub{}
|
|
apiKeyRepo := &dailyUsageAPIKeyRepoStub{
|
|
keys: map[int64]*service.APIKey{
|
|
7: {ID: 7, UserID: 99, Status: service.StatusAPIKeyActive},
|
|
},
|
|
}
|
|
router := newDailyUsageTestRouter(usageRepo, apiKeyRepo, 42)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/user/api-keys/7/usage/daily?days=30", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusForbidden, rec.Code)
|
|
require.False(t, usageRepo.called)
|
|
}
|
|
|
|
func TestGetMyAPIKeyDailyUsageRejectsInvalidDays(t *testing.T) {
|
|
for _, path := range []string{
|
|
"/user/api-keys/7/usage/daily?days=0",
|
|
"/user/api-keys/7/usage/daily?days=91",
|
|
} {
|
|
t.Run(path, func(t *testing.T) {
|
|
usageRepo := &dailyUsageRepoStub{}
|
|
apiKeyRepo := &dailyUsageAPIKeyRepoStub{
|
|
keys: map[int64]*service.APIKey{
|
|
7: {ID: 7, UserID: 42, Status: service.StatusAPIKeyActive},
|
|
},
|
|
}
|
|
router := newDailyUsageTestRouter(usageRepo, apiKeyRepo, 42)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusBadRequest, rec.Code)
|
|
require.False(t, usageRepo.called)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetMyAPIKeyDailyUsageReturnsEmptyData(t *testing.T) {
|
|
usageRepo := &dailyUsageRepoStub{trend: []usagestats.TrendDataPoint{}}
|
|
apiKeyRepo := &dailyUsageAPIKeyRepoStub{
|
|
keys: map[int64]*service.APIKey{
|
|
7: {ID: 7, UserID: 42, Status: service.StatusAPIKeyActive},
|
|
},
|
|
}
|
|
router := newDailyUsageTestRouter(usageRepo, apiKeyRepo, 42)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/user/api-keys/7/usage/daily", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
var got dailyUsageHandlerResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
|
require.Equal(t, 30, got.Data.Days)
|
|
require.Empty(t, got.Data.Items)
|
|
}
|
|
|
|
func TestGetMyAPIKeyDailyUsageAggregatesByDayForOwnedKey(t *testing.T) {
|
|
usageRepo := &dailyUsageRepoStub{
|
|
trend: []usagestats.TrendDataPoint{
|
|
{
|
|
Date: "2026-05-19",
|
|
Requests: 3,
|
|
InputTokens: 10,
|
|
OutputTokens: 20,
|
|
CacheCreationTokens: 4,
|
|
CacheReadTokens: 6,
|
|
TotalTokens: 40,
|
|
Cost: 0.5,
|
|
ActualCost: 0.4,
|
|
},
|
|
},
|
|
}
|
|
apiKeyRepo := &dailyUsageAPIKeyRepoStub{
|
|
keys: map[int64]*service.APIKey{
|
|
7: {ID: 7, UserID: 42, Status: service.StatusAPIKeyActive},
|
|
},
|
|
}
|
|
router := newDailyUsageTestRouter(usageRepo, apiKeyRepo, 42)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/user/api-keys/7/usage/daily?days=7", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.True(t, usageRepo.called)
|
|
require.Equal(t, "day", usageRepo.granularity)
|
|
require.Equal(t, int64(42), usageRepo.userID)
|
|
require.Equal(t, int64(7), usageRepo.apiKeyID)
|
|
require.True(t, usageRepo.startTime.Before(usageRepo.endTime))
|
|
|
|
var got dailyUsageHandlerResponse
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
|
require.Equal(t, 7, got.Data.Days)
|
|
require.Len(t, got.Data.Items, 1)
|
|
require.Equal(t, usagestats.APIKeyDailyUsagePoint{
|
|
Date: "2026-05-19",
|
|
Requests: 3,
|
|
InputTokens: 10,
|
|
OutputTokens: 20,
|
|
CacheReadTokens: 6,
|
|
CacheWriteTokens: 4,
|
|
TotalTokens: 40,
|
|
Cost: 0.5,
|
|
ActualCost: 0.4,
|
|
}, got.Data.Items[0])
|
|
}
|