Files
new-api/controller/channel_multi_key_test.go
KoSukeWork 9fe0457ee1 Add Responses WebSocket relay support (#5062)
* Add Responses WebSocket relay support

* Fix Responses WebSocket relay protocol issues

* Add status to Responses WebSocket errors

* Fix Responses WebSocket local error handling

* Use Responses error payload for control write failures

* Fix WebSocket subprotocol auth handling

* fix: address responses websocket CR feedback

* fix: truncate responses websocket relay errors

* fix: preserve websocket error message

* test: cover responses usage output fallback

* feat: close active websockets for disabled channels

* chore: address websocket manager review feedback

* fix: adapt responses websocket relay after rebase

* feat: add per-channel Responses WebSocket switch

* fix: address responses websocket relay review findings

* fix: share Responses WebSocket billing with HTTP and retain existing rate limits
2026-09-14 21:04:26 +08:00

112 lines
5.2 KiB
Go

package controller
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMultiKeyEnableRestoresOnlyExhaustedChannels(t *testing.T) {
previousDB, previousLogDB := model.DB, model.LOG_DB
previousType, previousLogType := common.MainDatabaseType(), common.LogDatabaseType()
previousMaster, previousCache, previousRedis, previousSQLite := common.IsMasterNode, common.MemoryCacheEnabled, common.RedisEnabled, common.SQLitePath
t.Cleanup(func() {
model.DB, model.LOG_DB = previousDB, previousLogDB
common.SetDatabaseTypes(previousType, previousLogType)
common.IsMasterNode, common.MemoryCacheEnabled, common.RedisEnabled, common.SQLitePath = previousMaster, previousCache, previousRedis, previousSQLite
})
t.Setenv("SQL_DSN", os.Getenv("TEST_CHANNEL_SQL_DSN"))
t.Setenv("LOG_SQL_DSN", "")
common.IsMasterNode, common.MemoryCacheEnabled, common.RedisEnabled = false, false, false
common.SQLitePath = filepath.Join(t.TempDir(), "channel.db")
require.NoError(t, model.InitDB())
database := model.DB
sqlDB, err := database.DB()
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
model.LOG_DB = database
common.SetLogDatabaseType(common.MainDatabaseType())
require.NoError(t, database.AutoMigrate(&model.Channel{}, &model.Ability{}, &model.User{}, &model.Log{}, &model.AuditLog{}))
root := &model.User{Username: "multi-key-review-root", Role: common.RoleRootUser, Status: common.UserStatusEnabled}
require.NoError(t, database.Create(root).Error)
t.Cleanup(func() { require.NoError(t, database.Unscoped().Delete(root).Error) })
versionQuery := "SELECT VERSION()"
if common.UsingMainDatabase(common.DatabaseTypeSQLite) {
versionQuery = "SELECT sqlite_version()"
}
var version string
require.NoError(t, database.Raw(versionQuery).Scan(&version).Error)
t.Logf("database=%s version=%s", common.MainDatabaseType(), version)
for _, cacheEnabled := range []bool{false, true} {
for _, action := range []string{"enable_key", "enable_all_keys"} {
for _, tc := range []struct {
name string
initialStatus int
manualOverride string
wantStatus int
}{
{name: "key exhaustion restores", initialStatus: common.ChannelStatusEnabled, wantStatus: common.ChannelStatusEnabled},
{name: "manual disable is preserved", initialStatus: common.ChannelStatusManuallyDisabled, wantStatus: common.ChannelStatusManuallyDisabled},
{name: "manual disable after exhaustion is preserved", initialStatus: common.ChannelStatusEnabled, manualOverride: "status", wantStatus: common.ChannelStatusManuallyDisabled},
{name: "tag disable after exhaustion is preserved", initialStatus: common.ChannelStatusEnabled, manualOverride: "tag", wantStatus: common.ChannelStatusManuallyDisabled},
} {
t.Run(fmt.Sprintf("cache=%t/%s/%s", cacheEnabled, action, tc.name), func(t *testing.T) {
common.MemoryCacheEnabled = cacheEnabled
tag := t.Name()
channel := &model.Channel{Name: t.Name(), Type: 1, Key: "key-one\nkey-two", Status: tc.initialStatus, Models: "test-model", Group: "default", Tag: &tag,
ChannelInfo: model.ChannelInfo{IsMultiKey: true, MultiKeySize: 2, MultiKeyStatusList: map[int]int{1: common.ChannelStatusManuallyDisabled}},
}
require.NoError(t, channel.Insert())
t.Cleanup(func() {
require.NoError(t, channel.Delete())
model.InitChannelCache()
})
for _, operation := range []string{"disable_key", action} {
if operation == action {
if tc.manualOverride == "status" {
model.UpdateChannelStatus(channel.Id, "", common.ChannelStatusManuallyDisabled, "manual operation")
} else if tc.manualOverride == "tag" {
require.NoError(t, model.DisableChannelByTag(tag))
}
}
payload, err := common.Marshal(MultiKeyManageRequest{ChannelId: channel.Id, Action: operation, KeyIndex: common.GetPointer(0)})
require.NoError(t, err)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Set("id", root.Id)
c.Set("role", common.RoleRootUser)
c.Request = httptest.NewRequest(http.MethodPost, "/api/channel/multi_key", bytes.NewReader(payload))
c.Request.Header.Set("Content-Type", "application/json")
ManageMultiKeys(c)
var result struct {
Success bool `json:"success"`
}
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &result))
require.True(t, result.Success, recorder.Body.String())
}
loaded, err := model.GetChannelById(channel.Id, true)
require.NoError(t, err)
assert.Equal(t, tc.wantStatus, loaded.Status)
assert.NotContains(t, loaded.ChannelInfo.MultiKeyStatusList, 0)
assert.NotContains(t, loaded.ChannelInfo.MultiKeyDisabledReason, 0)
assert.NotContains(t, loaded.ChannelInfo.MultiKeyDisabledTime, 0)
var ability model.Ability
require.NoError(t, database.Where("channel_id = ?", channel.Id).First(&ability).Error)
assert.Equal(t, tc.wantStatus == common.ChannelStatusEnabled, ability.Enabled)
})
}
}
}
}