gateway_handler_cancellation_test.go 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. //go:build unit
  2. package handler
  3. import (
  4. "bytes"
  5. "context"
  6. "net/http"
  7. "net/http/httptest"
  8. "sync/atomic"
  9. "testing"
  10. "github.com/Wei-Shaw/sub2api/internal/config"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
  12. middleware "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  13. "github.com/Wei-Shaw/sub2api/internal/service"
  14. "github.com/gin-gonic/gin"
  15. "github.com/stretchr/testify/require"
  16. )
  17. type countingGatewaySchedulerCache struct {
  18. *fakeSchedulerCache
  19. snapshotCalls atomic.Int64
  20. }
  21. func (c *countingGatewaySchedulerCache) GetSnapshot(ctx context.Context, bucket service.SchedulerBucket) ([]*service.Account, bool, error) {
  22. c.snapshotCalls.Add(1)
  23. return c.fakeSchedulerCache.GetSnapshot(ctx, bucket)
  24. }
  25. func TestGatewayHandlerPreCancelledCompatibleRequestsDoNotSelectAccount(t *testing.T) {
  26. gin.SetMode(gin.TestMode)
  27. groupID := int64(9100)
  28. group := &service.Group{ID: groupID, Hydrated: true, Platform: service.PlatformAnthropic, Status: service.StatusActive}
  29. account := &service.Account{
  30. ID: 9101, Platform: service.PlatformAnthropic, Type: service.AccountTypeAPIKey,
  31. Status: service.StatusActive, Schedulable: true, Concurrency: 1,
  32. AccountGroups: []service.AccountGroup{{AccountID: 9101, GroupID: groupID}},
  33. }
  34. schedulerCache := &countingGatewaySchedulerCache{fakeSchedulerCache: &fakeSchedulerCache{accounts: []*service.Account{account}}}
  35. schedulerSnapshot := service.NewSchedulerSnapshotService(schedulerCache, nil, nil, nil, nil)
  36. gatewayService := service.NewGatewayService(
  37. nil, &fakeGroupRepo{group: group}, nil, nil, nil, nil, nil, nil, nil,
  38. schedulerSnapshot, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
  39. )
  40. cfg := &config.Config{RunMode: config.RunModeSimple}
  41. billingCacheService := service.NewBillingCacheService(nil, nil, nil, nil, nil, nil, cfg, nil)
  42. t.Cleanup(billingCacheService.Stop)
  43. h := &GatewayHandler{
  44. gatewayService: gatewayService,
  45. billingCacheService: billingCacheService,
  46. concurrencyHelper: NewConcurrencyHelper(service.NewConcurrencyService(&fakeConcurrencyCache{}), SSEPingFormatClaude, 0),
  47. maxAccountSwitches: 1,
  48. cfg: cfg,
  49. }
  50. apiKey := &service.APIKey{
  51. ID: 9102, UserID: 9103, GroupID: &groupID, Group: group, Status: service.StatusActive,
  52. User: &service.User{ID: 9103, Concurrency: 10, Balance: 100},
  53. }
  54. tests := []struct {
  55. name string
  56. path string
  57. body string
  58. call func(*gin.Context)
  59. }{
  60. {
  61. name: "responses", path: "/v1/responses", body: `{"model":"claude-test","input":"hello","stream":false}`,
  62. call: h.Responses,
  63. },
  64. {
  65. name: "chat completions", path: "/v1/chat/completions", body: `{"model":"claude-test","messages":[{"role":"user","content":"hello"}],"stream":false}`,
  66. call: h.ChatCompletions,
  67. },
  68. }
  69. for _, tt := range tests {
  70. t.Run(tt.name, func(t *testing.T) {
  71. schedulerCache.snapshotCalls.Store(0)
  72. recorder := httptest.NewRecorder()
  73. c, _ := gin.CreateTestContext(recorder)
  74. ctx, cancel := context.WithCancel(context.Background())
  75. cancel()
  76. ctx = context.WithValue(ctx, ctxkey.Group, group)
  77. req := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewBufferString(tt.body)).WithContext(ctx)
  78. req.Header.Set("Content-Type", "application/json")
  79. c.Request = req
  80. c.Set(string(middleware.ContextKeyAPIKey), apiKey)
  81. c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{UserID: apiKey.UserID, Concurrency: 10})
  82. tt.call(c)
  83. require.Zero(t, schedulerCache.snapshotCalls.Load(), "a cancelled request must stop before the account selector")
  84. _, selected := c.Get(opsAccountIDKey)
  85. require.False(t, selected)
  86. })
  87. }
  88. }