* refactor: advanced custom channel route editor * fix(channels): show raw balance response from balance cell
645 lines
19 KiB
Go
645 lines
19 KiB
Go
package dto
|
|
|
|
import (
|
|
"encoding/json"
|
|
"regexp"
|
|
"testing"
|
|
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAdvancedCustomValidateResponsesToChatConverterPath(t *testing.T) {
|
|
valid := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, valid.Validate())
|
|
|
|
validGemini := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, validGemini.Validate())
|
|
|
|
tests := []struct {
|
|
name string
|
|
incomingPath string
|
|
}{
|
|
{name: "chat completions", incomingPath: "/v1/chat/completions"},
|
|
{name: "responses compact", incomingPath: "/v1/responses/compact"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: tt.incomingPath,
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
},
|
|
},
|
|
}
|
|
err := config.Validate()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "converter does not match incoming_path")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdvancedCustomValidateModelListRouteConstraints(t *testing.T) {
|
|
valid := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: AdvancedCustomModelListPath,
|
|
UpstreamPath: "https://upstream.example/custom/models",
|
|
Converter: advancedCustomConverterNone,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, valid.Validate())
|
|
|
|
tests := []struct {
|
|
name string
|
|
routes []AdvancedCustomRoute
|
|
want string
|
|
}{
|
|
{
|
|
name: "model matching rules",
|
|
routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: AdvancedCustomModelListPath,
|
|
UpstreamPath: "/v1/models",
|
|
Models: []string{"gpt-4o"},
|
|
},
|
|
},
|
|
want: "models must be empty",
|
|
},
|
|
{
|
|
name: "converter",
|
|
routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: AdvancedCustomModelListPath,
|
|
UpstreamPath: "/v1/models",
|
|
Converter: advancedCustomConverterOpenAIChatToOpenAIResponses,
|
|
},
|
|
},
|
|
want: "converter must be none",
|
|
},
|
|
{
|
|
name: "model placeholder",
|
|
routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: AdvancedCustomModelListPath,
|
|
UpstreamPath: "/v1/models/{model}",
|
|
},
|
|
},
|
|
want: "upstream_path must not contain {model}",
|
|
},
|
|
{
|
|
name: "duplicate routes",
|
|
routes: []AdvancedCustomRoute{
|
|
{IncomingPath: AdvancedCustomModelListPath, UpstreamPath: "/v1/models"},
|
|
{IncomingPath: AdvancedCustomModelListPath, UpstreamPath: "/provider/models"},
|
|
},
|
|
want: "duplicates the /v1/models route",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := (&AdvancedCustomConfig{Routes: tt.routes}).Validate()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdvancedCustomModelListRouteRequiresExactIncomingPath(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/{model}",
|
|
UpstreamPath: "/generic/{model}",
|
|
},
|
|
{
|
|
IncomingPath: AdvancedCustomModelListPath,
|
|
UpstreamPath: "/provider/models",
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, config.Validate())
|
|
|
|
route, ok := config.ModelListRoute()
|
|
require.True(t, ok)
|
|
assert.Equal(t, "/provider/models", route.UpstreamPath)
|
|
}
|
|
|
|
func TestAdvancedCustomValidateBalanceRouteConstraints(t *testing.T) {
|
|
valid := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{{
|
|
IncomingPath: AdvancedCustomBalancePath,
|
|
UpstreamPath: "/provider/balance",
|
|
Converter: advancedCustomConverterNone,
|
|
}},
|
|
}
|
|
require.NoError(t, valid.Validate())
|
|
|
|
route, ok := valid.BalanceRoute()
|
|
require.True(t, ok)
|
|
assert.Equal(t, "/provider/balance", route.UpstreamPath)
|
|
|
|
tests := []struct {
|
|
name string
|
|
routes []AdvancedCustomRoute
|
|
want string
|
|
}{
|
|
{
|
|
name: "model matching rules",
|
|
routes: []AdvancedCustomRoute{{
|
|
IncomingPath: AdvancedCustomBalancePath,
|
|
UpstreamPath: "/provider/balance",
|
|
Models: []string{"gpt-4o"},
|
|
}},
|
|
want: "models must be empty",
|
|
},
|
|
{
|
|
name: "converter",
|
|
routes: []AdvancedCustomRoute{{
|
|
IncomingPath: AdvancedCustomBalancePath,
|
|
UpstreamPath: "/provider/balance",
|
|
Converter: advancedCustomConverterOpenAIChatToOpenAIResponses,
|
|
}},
|
|
want: "converter must be none",
|
|
},
|
|
{
|
|
name: "model placeholder",
|
|
routes: []AdvancedCustomRoute{{
|
|
IncomingPath: AdvancedCustomBalancePath,
|
|
UpstreamPath: "/provider/{model}/balance",
|
|
}},
|
|
want: "upstream_path must not contain {model}",
|
|
},
|
|
{
|
|
name: "duplicate routes",
|
|
routes: []AdvancedCustomRoute{
|
|
{IncomingPath: AdvancedCustomBalancePath, UpstreamPath: "/provider/balance"},
|
|
{IncomingPath: AdvancedCustomBalancePath, UpstreamPath: "/provider/credits"},
|
|
},
|
|
want: "duplicates the /v1/dashboard/billing/credit_grants route",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := (&AdvancedCustomConfig{Routes: tt.routes}).Validate()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdvancedCustomValidateDuplicateIncomingPathWithDisjointModels(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
Models: []string{"gpt-4o"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: []string{"gemini-2.5-flash"},
|
|
},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, config.Validate())
|
|
}
|
|
|
|
func TestAdvancedCustomValidateDuplicateIncomingPathRejectsOverlappingModels(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
Models: []string{"shared-model"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: []string{"shared-model"},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := config.Validate()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "models overlaps")
|
|
}
|
|
|
|
func TestAdvancedCustomValidateDuplicateIncomingPathRejectsMultipleCatchAllRoutes(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := config.Validate()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "catch-all already exists")
|
|
}
|
|
|
|
func TestAdvancedCustomValidateDuplicateIncomingPathRequiresCatchAllLast(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: []string{"gemini-2.5-flash"},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := config.Validate()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "catch-all route must be last")
|
|
}
|
|
|
|
func TestAdvancedCustomMatchPathForModel(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: []string{"gemini-2.5-flash"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
Models: []string{"gpt-4o"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/responses",
|
|
Converter: advancedCustomConverterNone,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, config.Validate())
|
|
|
|
geminiRoute, ok := config.MatchPathForModel("/v1/responses", "gemini-2.5-flash")
|
|
require.True(t, ok)
|
|
assert.Equal(t, advancedCustomConverterOpenAIResponsesToGemini, geminiRoute.Converter)
|
|
|
|
chatRoute, ok := config.MatchPathForModel("/v1/responses", "gpt-4o")
|
|
require.True(t, ok)
|
|
assert.Equal(t, advancedCustomConverterOpenAIResponsesToOpenAIChat, chatRoute.Converter)
|
|
|
|
fallbackRoute, ok := config.MatchPathForModel("/v1/responses", "unknown-model")
|
|
require.True(t, ok)
|
|
assert.Equal(t, advancedCustomConverterNone, fallbackRoute.Converter)
|
|
}
|
|
|
|
func TestAdvancedCustomMatchPathForModelRegexRules(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: []string{"re:^gemini-"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
Models: []string{"re:(?i)^OAI-"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/responses",
|
|
Converter: advancedCustomConverterNone,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, config.Validate())
|
|
|
|
geminiRoute, ok := config.MatchPathForModel("/v1/responses", "gemini-2.5-flash")
|
|
require.True(t, ok)
|
|
assert.Equal(t, advancedCustomConverterOpenAIResponsesToGemini, geminiRoute.Converter)
|
|
|
|
chatRoute, ok := config.MatchPathForModel("/v1/responses", "oai-test")
|
|
require.True(t, ok)
|
|
assert.Equal(t, advancedCustomConverterOpenAIResponsesToOpenAIChat, chatRoute.Converter)
|
|
|
|
fallbackRoute, ok := config.MatchPathForModel("/v1/responses", "gpt-4o")
|
|
require.True(t, ok)
|
|
assert.Equal(t, advancedCustomConverterNone, fallbackRoute.Converter)
|
|
}
|
|
|
|
func TestAdvancedCustomRouteModelRegexRulesAreCachedCompiled(t *testing.T) {
|
|
require.True(t, matchAdvancedCustomRouteModelRule("re:^cache-probe-", "cache-probe-model"))
|
|
|
|
cached, ok := advancedCustomModelRegexCache.Load("^cache-probe-")
|
|
require.True(t, ok)
|
|
require.NotNil(t, cached)
|
|
_, isRegexp := cached.(*regexp.Regexp)
|
|
require.True(t, isRegexp)
|
|
|
|
// Invalid patterns never match and are cached as nil so they are not recompiled.
|
|
require.False(t, matchAdvancedCustomRouteModelRule("re:(", "anything"))
|
|
cached, ok = advancedCustomModelRegexCache.Load("(")
|
|
require.True(t, ok)
|
|
re, _ := cached.(*regexp.Regexp)
|
|
require.Nil(t, re)
|
|
|
|
// Cached entries keep matching correctly on subsequent calls.
|
|
require.True(t, matchAdvancedCustomRouteModelRule("re:^cache-probe-", "cache-probe-other"))
|
|
require.False(t, matchAdvancedCustomRouteModelRule("re:^cache-probe-", "other-model"))
|
|
}
|
|
|
|
func TestAdvancedCustomMatchPathForModelExactRuleDoesNotMatchPrefix(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: []string{"gemini"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/responses",
|
|
Converter: advancedCustomConverterNone,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, config.Validate())
|
|
|
|
fallbackRoute, ok := config.MatchPathForModel("/v1/responses", "gemini-2.5-flash")
|
|
require.True(t, ok)
|
|
assert.Equal(t, advancedCustomConverterNone, fallbackRoute.Converter)
|
|
}
|
|
|
|
func TestAdvancedCustomValidateDuplicateIncomingPathRejectsInvalidRegexModels(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
models []string
|
|
want string
|
|
}{
|
|
{name: "empty regex", models: []string{"re:"}, want: "regex is empty"},
|
|
{name: "invalid regex", models: []string{"re:["}, want: "regex is invalid"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: tt.models,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := config.Validate()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdvancedCustomValidateDuplicateIncomingPathRejectsDuplicateRegexModels(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: []string{"re:^gemini-"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
Models: []string{"re:^gemini-"},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := config.Validate()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "models overlaps")
|
|
}
|
|
|
|
func TestAdvancedCustomMatchPathForModelUsesFirstMatchingRegexRoute(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: []string{"re:^gemini-"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
Models: []string{"gemini-2.5-flash"},
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, config.Validate())
|
|
|
|
route, ok := config.MatchPathForModel("/v1/responses", "gemini-2.5-flash")
|
|
require.True(t, ok)
|
|
assert.Equal(t, advancedCustomConverterOpenAIResponsesToGemini, route.Converter)
|
|
}
|
|
|
|
func TestAdvancedCustomSupportedEndpointTypesForModel(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/responses",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Converter: advancedCustomConverterOpenAIResponsesToGemini,
|
|
Models: []string{"re:^gemini-"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1beta/models/{model}:generateContent",
|
|
UpstreamPath: "/v1beta/models/{model}:generateContent",
|
|
Models: []string{"re:^gemini-"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1beta/models/{model}:streamGenerateContent",
|
|
UpstreamPath: "/v1beta/models/{model}:streamGenerateContent",
|
|
Models: []string{"re:^gemini-"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/chat/completions",
|
|
UpstreamPath: "/v1/chat/completions",
|
|
Models: []string{"gpt-4o"},
|
|
},
|
|
{
|
|
IncomingPath: "/v1/messages",
|
|
UpstreamPath: "/v1/messages",
|
|
},
|
|
{
|
|
IncomingPath: "/custom/endpoint",
|
|
UpstreamPath: "/custom/endpoint",
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, config.Validate())
|
|
|
|
assert.Equal(t, []types.EndpointType{
|
|
types.EndpointTypeOpenAIResponse,
|
|
types.EndpointTypeGemini,
|
|
types.EndpointTypeAnthropic,
|
|
}, config.SupportedEndpointTypesForModel("gemini-2.5-flash"))
|
|
assert.Equal(t, []types.EndpointType{
|
|
types.EndpointTypeOpenAI,
|
|
types.EndpointTypeAnthropic,
|
|
}, config.SupportedEndpointTypesForModel("gpt-4o"))
|
|
assert.Equal(t, []types.EndpointType{
|
|
types.EndpointTypeAnthropic,
|
|
}, config.SupportedEndpointTypesForModel("other-model"))
|
|
}
|
|
|
|
func TestAdvancedCustomValidateAlphaSearchConverterPath(t *testing.T) {
|
|
valid := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/alpha/search",
|
|
UpstreamPath: "/v1/alpha/search",
|
|
Converter: advancedCustomConverterNone,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, valid.Validate())
|
|
assert.Equal(t, []types.EndpointType{
|
|
types.EndpointTypeOpenAIAlphaSearch,
|
|
}, valid.SupportedEndpointTypesForModel("gpt-5.1"))
|
|
|
|
nonNoneConverters := []string{
|
|
advancedCustomConverterClaudeMessagesToOpenAIChat,
|
|
advancedCustomConverterOpenAIChatToClaudeMessages,
|
|
advancedCustomConverterOpenAIChatToOpenAIResponses,
|
|
advancedCustomConverterOpenAIResponsesToOpenAIChat,
|
|
advancedCustomConverterOpenAIResponsesToGemini,
|
|
advancedCustomConverterGeminiContentToOpenAIChat,
|
|
advancedCustomConverterOpenAIChatToGeminiContent,
|
|
}
|
|
for _, converter := range nonNoneConverters {
|
|
t.Run(converter, func(t *testing.T) {
|
|
config := &AdvancedCustomConfig{
|
|
Routes: []AdvancedCustomRoute{
|
|
{
|
|
IncomingPath: "/v1/alpha/search",
|
|
UpstreamPath: "/v1/alpha/search",
|
|
Converter: converter,
|
|
},
|
|
},
|
|
}
|
|
err := config.Validate()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "converter does not match incoming_path")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChannelSettingsHTTPTransportJSONRoundTrip(t *testing.T) {
|
|
legacy := `{"proxy":"http://127.0.0.1:8080","force_format":true}`
|
|
var settings ChannelSettings
|
|
require.NoError(t, json.Unmarshal([]byte(legacy), &settings))
|
|
assert.Equal(t, "http://127.0.0.1:8080", settings.Proxy)
|
|
assert.True(t, settings.ForceFormat)
|
|
assert.Empty(t, settings.HTTPProtocol)
|
|
assert.Zero(t, settings.HTTP2ConnectionShards)
|
|
|
|
encoded, err := json.Marshal(settings)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, string(encoded), "http_protocol")
|
|
assert.NotContains(t, string(encoded), "http2_connection_shards")
|
|
|
|
explicit := ChannelSettings{
|
|
Proxy: "socks5://127.0.0.1:1080",
|
|
HTTPProtocol: HTTPProtocolHTTP1,
|
|
HTTP2ConnectionShards: 1,
|
|
}
|
|
encoded, err = json.Marshal(explicit)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(encoded), `"http_protocol":"http1"`)
|
|
|
|
var decoded ChannelSettings
|
|
require.NoError(t, json.Unmarshal(encoded, &decoded))
|
|
assert.Equal(t, explicit.HTTPProtocol, decoded.HTTPProtocol)
|
|
assert.Equal(t, 1, decoded.HTTP2ConnectionShards)
|
|
|
|
sharded := ChannelSettings{HTTP2ConnectionShards: 4}
|
|
encoded, err = json.Marshal(sharded)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(encoded), `"http2_connection_shards":4`)
|
|
assert.NotContains(t, string(encoded), "http_protocol")
|
|
}
|
|
|
|
func TestChannelSettingsValidateHTTPTransport(t *testing.T) {
|
|
require.NoError(t, (&ChannelSettings{}).ValidateHTTPTransport())
|
|
require.NoError(t, (&ChannelSettings{HTTPProtocol: "AUTO"}).ValidateHTTPTransport())
|
|
require.NoError(t, (&ChannelSettings{HTTPProtocol: "http1"}).ValidateHTTPTransport())
|
|
require.NoError(t, (&ChannelSettings{HTTP2ConnectionShards: 8}).ValidateHTTPTransport())
|
|
|
|
err := (&ChannelSettings{HTTPProtocol: "http2"}).ValidateHTTPTransport()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "http_protocol")
|
|
|
|
err = (&ChannelSettings{HTTP2ConnectionShards: -1}).ValidateHTTPTransport()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "http2_connection_shards")
|
|
|
|
err = (&ChannelSettings{HTTP2ConnectionShards: 9}).ValidateHTTPTransport()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "http2_connection_shards")
|
|
|
|
err = (&ChannelSettings{HTTPProtocol: "http1", HTTP2ConnectionShards: 2}).ValidateHTTPTransport()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "http2_connection_shards")
|
|
}
|