| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- //go:build unit
- package handler
- import (
- "bytes"
- "context"
- "io"
- "net/http"
- "net/http/httptest"
- "sync"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- 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"
- "github.com/tidwall/gjson"
- "go.uber.org/zap"
- "go.uber.org/zap/zaptest/observer"
- )
- type openAIImagesFailoverAccountRepo struct {
- service.AccountRepository
- accounts []service.Account
- }
- func (r openAIImagesFailoverAccountRepo) GetByID(_ context.Context, id int64) (*service.Account, error) {
- for i := range r.accounts {
- if r.accounts[i].ID == id {
- account := r.accounts[i]
- return &account, nil
- }
- }
- return nil, service.ErrNoAvailableAccounts
- }
- func (r openAIImagesFailoverAccountRepo) ListSchedulableByGroupIDAndPlatform(_ context.Context, _ int64, platform string) ([]service.Account, error) {
- return r.accountsForPlatform(platform), nil
- }
- func (r openAIImagesFailoverAccountRepo) ListSchedulableByPlatform(_ context.Context, platform string) ([]service.Account, error) {
- return r.accountsForPlatform(platform), nil
- }
- func (r openAIImagesFailoverAccountRepo) ListSchedulableUngroupedByPlatform(_ context.Context, platform string) ([]service.Account, error) {
- return r.accountsForPlatform(platform), nil
- }
- func (r openAIImagesFailoverAccountRepo) accountsForPlatform(platform string) []service.Account {
- out := make([]service.Account, 0, len(r.accounts))
- for _, account := range r.accounts {
- if account.Platform == platform {
- out = append(out, account)
- }
- }
- return out
- }
- type openAIImagesFailoverHTTPUpstream struct {
- service.HTTPUpstream
- mu sync.Mutex
- accountIDs []int64
- }
- func (u *openAIImagesFailoverHTTPUpstream) Do(_ *http.Request, _ string, accountID int64, _ int) (*http.Response, error) {
- u.mu.Lock()
- u.accountIDs = append(u.accountIDs, accountID)
- u.mu.Unlock()
- return &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{
- "Content-Type": []string{"text/event-stream"},
- "X-Request-Id": []string{"req_img_failover"},
- },
- Body: io.NopCloser(bytes.NewBufferString(
- "data: {\"type\":\"error\",\"error\":{\"type\":\"server_error\",\"code\":\"server_error\",\"message\":\"image backend unavailable\"}}\n\n",
- )),
- }, nil
- }
- func (u *openAIImagesFailoverHTTPUpstream) calls() []int64 {
- u.mu.Lock()
- defer u.mu.Unlock()
- return append([]int64(nil), u.accountIDs...)
- }
- func TestOpenAIGatewayHandlerImages_ServerErrorFailsOverAndReturnsClearErrorWhenExhausted(t *testing.T) {
- gin.SetMode(gin.TestMode)
- groupID := int64(3130)
- accounts := []service.Account{
- {
- ID: 1,
- Name: "image-account-1",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeOAuth,
- Status: service.StatusActive,
- Schedulable: true,
- Concurrency: 0,
- Priority: 0,
- Credentials: map[string]any{"access_token": "token-1"},
- },
- {
- ID: 2,
- Name: "image-account-2",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeOAuth,
- Status: service.StatusActive,
- Schedulable: true,
- Concurrency: 0,
- Priority: 1,
- Credentials: map[string]any{"access_token": "token-2"},
- },
- }
- accountRepo := openAIImagesFailoverAccountRepo{accounts: accounts}
- upstream := &openAIImagesFailoverHTTPUpstream{}
- cfg := &config.Config{RunMode: config.RunModeSimple}
- gatewayService := service.NewOpenAIGatewayService(
- accountRepo,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- cfg,
- nil,
- nil,
- nil,
- nil,
- nil,
- upstream,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- )
- billingService := service.NewBillingCacheService(nil, nil, nil, nil, nil, nil, cfg, nil)
- t.Cleanup(billingService.Stop)
- concurrencyService := service.NewConcurrencyService(nil)
- handler := NewOpenAIGatewayHandler(
- gatewayService,
- concurrencyService,
- billingService,
- service.NewAPIKeyService(nil, nil, nil, nil, nil, nil, cfg),
- nil,
- nil,
- nil,
- nil,
- cfg,
- )
- handler.maxAccountSwitches = 10
- body := []byte(`{"model":"gpt-image-2","prompt":"draw a cat","quality":"high","size":"1536x1024"}`)
- core, observedLogs := observer.New(zap.DebugLevel)
- requestCtx := logger.IntoContext(context.Background(), zap.New(core))
- req := httptest.NewRequest(http.MethodPost, "/v1/images/generations", bytes.NewReader(body)).WithContext(requestCtx)
- req.Header.Set("Content-Type", "application/json")
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = req
- c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{
- ID: 99,
- GroupID: &groupID,
- Group: &service.Group{
- ID: groupID,
- AllowImageGeneration: true,
- },
- User: &service.User{ID: 100},
- })
- c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: 100, Concurrency: 0})
- handler.Images(c)
- accountSelectingLogs := observedLogs.FilterMessage("openai.images.account_selecting").All()
- require.NotEmpty(t, accountSelectingLogs)
- loggedFields := make(map[string]string)
- for _, field := range accountSelectingLogs[0].Context {
- loggedFields[field.Key] = field.String
- }
- require.Equal(t, "high", loggedFields["img_quality"])
- require.Equal(t, "1536x1024", loggedFields["img_size"])
- require.NotContains(t, loggedFields, "prompt")
- require.Equal(t, []int64{1, 2}, upstream.calls())
- require.Equal(t, http.StatusBadGateway, rec.Code)
- require.Equal(t, "upstream_error", gjson.GetBytes(rec.Body.Bytes(), "error.type").String())
- require.Equal(t, "Upstream service temporarily unavailable", gjson.GetBytes(rec.Body.Bytes(), "error.message").String())
- rawEvents, ok := c.Get(service.OpsUpstreamErrorsKey)
- require.True(t, ok)
- events, ok := rawEvents.([]*service.OpsUpstreamErrorEvent)
- require.True(t, ok)
- require.Len(t, events, 2)
- require.Equal(t, "failover", events[0].Kind)
- require.Equal(t, "failover", events[1].Kind)
- }
|