gateway_key_billing_test.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. package routes
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. "github.com/Wei-Shaw/sub2api/internal/handler"
  11. servermiddleware "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  12. "github.com/Wei-Shaw/sub2api/internal/service"
  13. "github.com/Wei-Shaw/sub2api/internal/web"
  14. "github.com/gin-gonic/gin"
  15. "github.com/stretchr/testify/require"
  16. )
  17. type keyBillingRouteAPIKeyRepo struct {
  18. service.APIKeyRepository
  19. apiKey *service.APIKey
  20. }
  21. func (r *keyBillingRouteAPIKeyRepo) GetByKeyForAuth(_ context.Context, key string) (*service.APIKey, error) {
  22. if r.apiKey == nil || key != r.apiKey.Key {
  23. return nil, service.ErrAPIKeyNotFound
  24. }
  25. clone := *r.apiKey
  26. return &clone, nil
  27. }
  28. type keyBillingRouteRateRepo struct {
  29. service.UserGroupRateRepository
  30. lookupCalls int
  31. }
  32. func (r *keyBillingRouteRateRepo) GetByUserAndGroup(context.Context, int64, int64) (*float64, error) {
  33. r.lookupCalls++
  34. return nil, nil
  35. }
  36. func (r *keyBillingRouteRateRepo) GetRPMOverrideByUserAndGroup(context.Context, int64, int64) (*int, error) {
  37. return nil, nil
  38. }
  39. func newKeyBillingRouteTestRouter(runMode string) (*gin.Engine, *keyBillingRouteRateRepo, string) {
  40. gin.SetMode(gin.TestMode)
  41. group := &service.Group{
  42. ID: 42,
  43. Status: service.StatusActive,
  44. Hydrated: true,
  45. Platform: service.PlatformOpenAI,
  46. SubscriptionType: service.SubscriptionTypeStandard,
  47. RateMultiplier: 0.75,
  48. }
  49. user := &service.User{ID: 7, Role: service.RoleUser, Status: service.StatusActive, Balance: 10}
  50. var groupID *int64
  51. var apiKeyGroup *service.Group
  52. if runMode != config.RunModeSimple {
  53. groupID = &group.ID
  54. apiKeyGroup = group
  55. }
  56. apiKey := &service.APIKey{
  57. ID: 100,
  58. UserID: user.ID,
  59. Key: "billing-route-test-key",
  60. Status: service.StatusActive,
  61. User: user,
  62. GroupID: groupID,
  63. Group: apiKeyGroup,
  64. }
  65. cfg := &config.Config{RunMode: runMode}
  66. rateRepo := &keyBillingRouteRateRepo{}
  67. apiKeyService := service.NewAPIKeyService(
  68. &keyBillingRouteAPIKeyRepo{apiKey: apiKey}, nil, nil, nil, rateRepo, nil, cfg,
  69. )
  70. gatewayService := service.NewGatewayService(
  71. nil, nil, nil, nil, nil, nil, rateRepo, nil, cfg, nil, nil, nil, nil, nil,
  72. nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
  73. )
  74. openAIGatewayService := service.NewOpenAIGatewayService(
  75. nil, nil, nil, nil, nil, rateRepo, nil, cfg, nil, nil, nil,
  76. nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
  77. )
  78. gatewayHandler := handler.NewGatewayHandler(
  79. gatewayService, openAIGatewayService, nil, nil, nil, nil, nil, nil,
  80. apiKeyService, nil, nil, nil, nil, cfg, nil,
  81. )
  82. router := gin.New()
  83. if web.HasEmbeddedFrontend() {
  84. router.Use(web.ServeEmbeddedFrontend())
  85. }
  86. RegisterGatewayRoutes(
  87. router,
  88. &handler.Handlers{Gateway: gatewayHandler, OpenAIGateway: &handler.OpenAIGatewayHandler{}},
  89. servermiddleware.NewAPIKeyAuthMiddleware(apiKeyService, nil, cfg),
  90. apiKeyService,
  91. nil,
  92. nil,
  93. nil,
  94. nil,
  95. cfg,
  96. )
  97. return router, rateRepo, apiKey.Key
  98. }
  99. func TestGatewayRoutesKeyBillingInfoPathIsRegistered(t *testing.T) {
  100. router := newGatewayRoutesTestRouter()
  101. for _, route := range router.Routes() {
  102. if route.Method == http.MethodGet && route.Path == "/v1/sub2api/billing" {
  103. return
  104. }
  105. }
  106. t.Fatal("GET /v1/sub2api/billing should be registered")
  107. }
  108. func TestGatewayRoutesKeyBillingInfoEndToEnd(t *testing.T) {
  109. t.Run("missing credentials", func(t *testing.T) {
  110. router, rateRepo, _ := newKeyBillingRouteTestRouter(config.RunModeStandard)
  111. w := httptest.NewRecorder()
  112. router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/v1/sub2api/billing", nil))
  113. require.Equal(t, http.StatusUnauthorized, w.Code)
  114. require.Contains(t, w.Header().Get("Content-Type"), "application/json")
  115. require.NotContains(t, strings.ToLower(w.Body.String()), "<!doctype html>")
  116. require.Zero(t, rateRepo.lookupCalls)
  117. })
  118. t.Run("standard mode", func(t *testing.T) {
  119. router, rateRepo, key := newKeyBillingRouteTestRouter(config.RunModeStandard)
  120. req := httptest.NewRequest(http.MethodGet, "/v1/sub2api/billing", nil)
  121. req.Header.Set("Authorization", "Bearer "+key)
  122. w := httptest.NewRecorder()
  123. router.ServeHTTP(w, req)
  124. require.Equal(t, http.StatusOK, w.Code)
  125. require.Contains(t, w.Header().Get("Content-Type"), "application/json")
  126. require.Equal(t, "no-store", w.Header().Get("Cache-Control"))
  127. require.NotContains(t, strings.ToLower(w.Body.String()), "<!doctype html>")
  128. var body map[string]any
  129. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  130. require.Equal(t, "sub2api.key_billing", body["object"])
  131. require.Equal(t, 0.75, body["effective_rate_multiplier"])
  132. require.Equal(t, 1, rateRepo.lookupCalls)
  133. })
  134. t.Run("simple mode", func(t *testing.T) {
  135. router, rateRepo, key := newKeyBillingRouteTestRouter(config.RunModeSimple)
  136. req := httptest.NewRequest(http.MethodGet, "/v1/sub2api/billing", nil)
  137. req.Header.Set("x-api-key", key)
  138. w := httptest.NewRecorder()
  139. router.ServeHTTP(w, req)
  140. require.Equal(t, http.StatusNotFound, w.Code)
  141. require.Contains(t, w.Header().Get("Content-Type"), "application/json")
  142. require.NotContains(t, strings.ToLower(w.Body.String()), "<!doctype html>")
  143. require.JSONEq(t, `{
  144. "type": "error",
  145. "error": {
  146. "type": "not_found_error",
  147. "message": "Billing information is not supported in simple mode"
  148. }
  149. }`, w.Body.String())
  150. require.Zero(t, rateRepo.lookupCalls)
  151. })
  152. }