631 lines
26 KiB
Go
631 lines
26 KiB
Go
package diff
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
|
|
)
|
|
|
|
func TestBuildConfigChangeDetails(t *testing.T) {
|
|
oldCfg := &config.Config{
|
|
Port: 8080,
|
|
AuthDir: "/tmp/auth-old",
|
|
GeminiKey: []config.GeminiKey{
|
|
{APIKey: "old", BaseURL: "http://old", ExcludedModels: []string{"old-model"}},
|
|
},
|
|
RemoteManagement: config.RemoteManagement{
|
|
AllowRemote: false,
|
|
SecretKey: "old",
|
|
DisableControlPanel: false,
|
|
},
|
|
OAuthExcludedModels: map[string][]string{
|
|
"providerA": {"m1"},
|
|
},
|
|
OpenAICompatibility: []config.OpenAICompatibility{
|
|
{
|
|
Name: "compat-a",
|
|
APIKeyEntries: []config.OpenAICompatibilityAPIKey{
|
|
{APIKey: "k1"},
|
|
},
|
|
Models: []config.OpenAICompatibilityModel{{Name: "m1"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
newCfg := &config.Config{
|
|
Port: 9090,
|
|
AuthDir: "/tmp/auth-new",
|
|
Codex: config.CodexConfig{DisableCodexCloaking: true},
|
|
GeminiKey: []config.GeminiKey{
|
|
{APIKey: "old", BaseURL: "http://old", ExcludedModels: []string{"old-model", "extra"}},
|
|
},
|
|
RemoteManagement: config.RemoteManagement{
|
|
AllowRemote: true,
|
|
SecretKey: "new",
|
|
DisableControlPanel: true,
|
|
},
|
|
OAuthExcludedModels: map[string][]string{
|
|
"providerA": {"m1", "m2"},
|
|
"providerB": {"x"},
|
|
},
|
|
OpenAICompatibility: []config.OpenAICompatibility{
|
|
{
|
|
Name: "compat-a",
|
|
APIKeyEntries: []config.OpenAICompatibilityAPIKey{
|
|
{APIKey: "k1"},
|
|
},
|
|
Models: []config.OpenAICompatibilityModel{{Name: "m1"}, {Name: "m2"}},
|
|
},
|
|
{
|
|
Name: "compat-b",
|
|
APIKeyEntries: []config.OpenAICompatibilityAPIKey{
|
|
{APIKey: "k2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
details := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
|
|
expectContains(t, details, "port: 8080 -> 9090")
|
|
expectContains(t, details, "auth-dir: /tmp/auth-old -> /tmp/auth-new")
|
|
expectContains(t, details, "gemini[0].excluded-models: updated (1 -> 2 entries)")
|
|
expectContains(t, details, "remote-management.allow-remote: false -> true")
|
|
expectContains(t, details, "remote-management.secret-key: updated")
|
|
expectContains(t, details, "codex.disable-codex-cloaking: false -> true")
|
|
expectContains(t, details, "oauth-excluded-models[providera]: updated (1 -> 2 entries)")
|
|
expectContains(t, details, "oauth-excluded-models[providerb]: added (1 entries)")
|
|
expectContains(t, details, "openai-compatibility:")
|
|
expectContains(t, details, " provider added: compat-b (api-keys=1, models=0)")
|
|
expectContains(t, details, " provider updated: compat-a (models 1 -> 2)")
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_NoChanges(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Port: 8080,
|
|
}
|
|
if details := BuildConfigChangeDetails(cfg, cfg); len(details) != 0 {
|
|
t.Fatalf("expected no change entries, got %v", details)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_CodexLiveMediaRelay(t *testing.T) {
|
|
oldCfg := &config.Config{Codex: config.CodexConfig{LiveMediaRelay: config.CodexLiveMediaRelayConfig{
|
|
Enabled: false,
|
|
MaxSessions: 16,
|
|
ICEServers: []config.CodexLiveICEServer{{
|
|
URLs: []string{"turn:old.example.com"},
|
|
Username: "old-user",
|
|
Credential: "old-secret",
|
|
}},
|
|
}}}
|
|
newCfg := &config.Config{Codex: config.CodexConfig{LiveMediaRelay: config.CodexLiveMediaRelayConfig{
|
|
Enabled: true,
|
|
MaxSessions: 32,
|
|
DisablePrivateRemoteIPs: true,
|
|
PublicIP: "203.0.113.10",
|
|
UDPPortMin: 40000,
|
|
UDPPortMax: 40063,
|
|
ICEServers: []config.CodexLiveICEServer{{
|
|
URLs: []string{"turn:new.example.com"},
|
|
Username: "new-user",
|
|
Credential: "new-secret",
|
|
}},
|
|
}}}
|
|
|
|
details := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, details, "codex.live-media-relay.enabled: false -> true")
|
|
expectContains(t, details, "codex.live-media-relay.max-sessions: 16 -> 32")
|
|
expectContains(t, details, "codex.live-media-relay.disable-private-remote-ips: false -> true")
|
|
expectContains(t, details, "codex.live-media-relay.public-ip: <none> -> 203.0.113.10")
|
|
expectContains(t, details, "codex.live-media-relay.udp-port-min: 0 -> 40000")
|
|
expectContains(t, details, "codex.live-media-relay.udp-port-max: 0 -> 40063")
|
|
expectContains(t, details, "codex.live-media-relay.ice-servers: updated (1 -> 1 entries, credentials redacted)")
|
|
joined := strings.Join(details, "\n")
|
|
for _, secret := range []string{"old-secret", "new-secret", "old-user", "new-user"} {
|
|
if strings.Contains(joined, secret) {
|
|
t.Fatalf("config change details leaked %q: %s", secret, joined)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_GeminiVertexHeaders(t *testing.T) {
|
|
oldCfg := &config.Config{
|
|
GeminiKey: []config.GeminiKey{
|
|
{APIKey: "g1", Headers: map[string]string{"H": "1"}, ExcludedModels: []string{"a"}},
|
|
},
|
|
VertexCompatAPIKey: []config.VertexCompatKey{
|
|
{APIKey: "v1", BaseURL: "http://v-old", Models: []config.VertexCompatModel{{Name: "m1"}}},
|
|
},
|
|
}
|
|
newCfg := &config.Config{
|
|
GeminiKey: []config.GeminiKey{
|
|
{APIKey: "g1", Headers: map[string]string{"H": "2"}, ExcludedModels: []string{"a", "b"}},
|
|
},
|
|
VertexCompatAPIKey: []config.VertexCompatKey{
|
|
{APIKey: "v1", BaseURL: "http://v-new", Models: []config.VertexCompatModel{{Name: "m1"}, {Name: "m2"}}},
|
|
},
|
|
}
|
|
|
|
details := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, details, "gemini[0].headers: updated")
|
|
expectContains(t, details, "gemini[0].excluded-models: updated (1 -> 2 entries)")
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_ModelPrefixes(t *testing.T) {
|
|
oldCfg := &config.Config{
|
|
GeminiKey: []config.GeminiKey{
|
|
{APIKey: "g1", Prefix: "old-g", BaseURL: "http://g", ProxyURL: "http://gp"},
|
|
},
|
|
ClaudeKey: []config.ClaudeKey{
|
|
{APIKey: "c1", Prefix: "old-c", BaseURL: "http://c", ProxyURL: "http://cp"},
|
|
},
|
|
CodexKey: []config.CodexKey{
|
|
{APIKey: "x1", Prefix: "old-x", BaseURL: "http://x", ProxyURL: "http://xp"},
|
|
},
|
|
VertexCompatAPIKey: []config.VertexCompatKey{
|
|
{APIKey: "v1", Prefix: "old-v", BaseURL: "http://v", ProxyURL: "http://vp"},
|
|
},
|
|
}
|
|
newCfg := &config.Config{
|
|
GeminiKey: []config.GeminiKey{
|
|
{APIKey: "g1", Prefix: "new-g", BaseURL: "http://g", ProxyURL: "http://gp"},
|
|
},
|
|
ClaudeKey: []config.ClaudeKey{
|
|
{APIKey: "c1", Prefix: "new-c", BaseURL: "http://c", ProxyURL: "http://cp"},
|
|
},
|
|
CodexKey: []config.CodexKey{
|
|
{APIKey: "x1", Prefix: "new-x", BaseURL: "http://x", ProxyURL: "http://xp"},
|
|
},
|
|
VertexCompatAPIKey: []config.VertexCompatKey{
|
|
{APIKey: "v1", Prefix: "new-v", BaseURL: "http://v", ProxyURL: "http://vp"},
|
|
},
|
|
}
|
|
|
|
changes := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, changes, "gemini[0].prefix: old-g -> new-g")
|
|
expectContains(t, changes, "claude[0].prefix: old-c -> new-c")
|
|
expectContains(t, changes, "codex[0].prefix: old-x -> new-x")
|
|
expectContains(t, changes, "vertex[0].prefix: old-v -> new-v")
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_CodexAlphaSearch(t *testing.T) {
|
|
oldCfg := &config.Config{CodexKey: []config.CodexKey{{APIKey: "key", BaseURL: "https://codex.example.com"}}}
|
|
newCfg := &config.Config{CodexKey: []config.CodexKey{{APIKey: "key", BaseURL: "https://codex.example.com", AlphaSearch: true}}}
|
|
|
|
changes := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, changes, "codex[0].alpha-search: false -> true")
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_XAIKeys(t *testing.T) {
|
|
oldRetry := 1
|
|
newRetry := 0
|
|
oldDisableCooling := false
|
|
newDisableCooling := true
|
|
oldCfg := &config.Config{XAIKey: []config.XAIKey{{
|
|
APIKey: "old-key",
|
|
Priority: 1,
|
|
Prefix: "old",
|
|
BaseURL: "https://old.example.com/v1",
|
|
ProxyURL: "http://old-proxy",
|
|
Websockets: false,
|
|
DisableCooling: &oldDisableCooling,
|
|
RequestRetry: &oldRetry,
|
|
Headers: map[string]string{"X-Test": "old"},
|
|
Models: []config.XAIModel{{Name: "grok-old", Alias: "grok"}},
|
|
ExcludedModels: []string{"grok-hidden"},
|
|
}}}
|
|
newCfg := &config.Config{XAIKey: []config.XAIKey{{
|
|
APIKey: "new-key",
|
|
Priority: 2,
|
|
Prefix: "new",
|
|
BaseURL: "https://new.example.com/v1",
|
|
ProxyURL: "http://new-proxy",
|
|
Websockets: true,
|
|
DisableCooling: &newDisableCooling,
|
|
RequestRetry: &newRetry,
|
|
Headers: map[string]string{"X-Test": "new"},
|
|
Models: []config.XAIModel{{Name: "grok-new", Alias: "grok"}},
|
|
ExcludedModels: []string{"grok-other"},
|
|
}}}
|
|
|
|
changes := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, changes, "xai[0].base-url: https://old.example.com -> https://new.example.com")
|
|
expectContains(t, changes, "xai[0].proxy-url: http://old-proxy -> http://new-proxy")
|
|
expectContains(t, changes, "xai[0].prefix: old -> new")
|
|
expectContains(t, changes, "xai[0].priority: 1 -> 2")
|
|
expectContains(t, changes, "xai[0].websockets: false -> true")
|
|
expectContains(t, changes, "xai[0].disable-cooling: false -> true")
|
|
expectContains(t, changes, "xai[0].request-retry: 1 -> 0")
|
|
expectContains(t, changes, "xai[0].api-key: updated")
|
|
expectContains(t, changes, "xai[0].headers: updated")
|
|
expectContains(t, changes, "xai[0].models: updated (1 -> 1 entries)")
|
|
expectContains(t, changes, "xai[0].excluded-models: updated (1 -> 1 entries)")
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_XAIForceMappingOnly(t *testing.T) {
|
|
oldCfg := &config.Config{XAIKey: []config.XAIKey{{
|
|
APIKey: "xai-key",
|
|
BaseURL: "https://api.x.ai/v1",
|
|
Models: []config.XAIModel{{Name: "grok-4.5", Alias: "grok-latest"}},
|
|
}}}
|
|
newCfg := &config.Config{XAIKey: []config.XAIKey{{
|
|
APIKey: "xai-key",
|
|
BaseURL: "https://api.x.ai/v1",
|
|
Models: []config.XAIModel{{Name: "grok-4.5", Alias: "grok-latest", ForceMapping: true}},
|
|
}}}
|
|
|
|
changes := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, changes, "xai[0].models: updated (1 -> 1 entries)")
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_NilSafe(t *testing.T) {
|
|
if details := BuildConfigChangeDetails(nil, &config.Config{}); len(details) != 0 {
|
|
t.Fatalf("expected empty change list when old nil, got %v", details)
|
|
}
|
|
if details := BuildConfigChangeDetails(&config.Config{}, nil); len(details) != 0 {
|
|
t.Fatalf("expected empty change list when new nil, got %v", details)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_SecretsAndCounts(t *testing.T) {
|
|
oldCfg := &config.Config{
|
|
SDKConfig: sdkconfig.SDKConfig{
|
|
APIKeys: []string{"a"},
|
|
},
|
|
RemoteManagement: config.RemoteManagement{
|
|
SecretKey: "",
|
|
},
|
|
}
|
|
newCfg := &config.Config{
|
|
SDKConfig: sdkconfig.SDKConfig{
|
|
APIKeys: []string{"a", "b", "c"},
|
|
},
|
|
RemoteManagement: config.RemoteManagement{
|
|
SecretKey: "new-secret",
|
|
},
|
|
}
|
|
|
|
details := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, details, "api-keys count: 1 -> 3")
|
|
expectContains(t, details, "remote-management.secret-key: created")
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_RedactsEndpointURLs(t *testing.T) {
|
|
oldCfg := &config.Config{
|
|
GeminiKey: []config.GeminiKey{{BaseURL: "https://old-user:old-pass@old.example/v1?token=old-token"}},
|
|
OpenAICompatibility: []config.OpenAICompatibility{{
|
|
BaseURL: "https://old-user:old-pass@old-compat.example/v1?token=old-token",
|
|
}},
|
|
}
|
|
newCfg := &config.Config{
|
|
GeminiKey: []config.GeminiKey{{BaseURL: "https://new-user:new-pass@new.example/v1?token=new-token"}},
|
|
OpenAICompatibility: []config.OpenAICompatibility{{
|
|
BaseURL: "https://new-user:new-pass@new-compat.example/v1?token=new-token",
|
|
}},
|
|
}
|
|
|
|
details := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, details, "gemini[0].base-url: https://old.example -> https://new.example")
|
|
joined := strings.Join(details, "\n")
|
|
for _, sensitive := range []string{"old-user", "new-user", "old-pass", "new-pass", "old-token", "new-token", "/v1"} {
|
|
if strings.Contains(joined, sensitive) {
|
|
t.Fatalf("config change details leaked %q: %s", sensitive, joined)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) {
|
|
oldCfg := &config.Config{
|
|
Port: 1000,
|
|
AuthDir: "/old",
|
|
Debug: false,
|
|
LoggingToFile: false,
|
|
UsageStatisticsEnabled: false,
|
|
DisableCooling: false,
|
|
SaveCooldownStatus: false,
|
|
TransientErrorCooldownSeconds: 0,
|
|
RequestRetry: 1,
|
|
MaxRetryCredentials: 1,
|
|
MaxRetryInterval: 1,
|
|
WebsocketAuth: false,
|
|
QuotaExceeded: config.QuotaExceeded{SwitchProject: false, SwitchPreviewModel: false, AntigravityCredits: false},
|
|
Antigravity: config.AntigravityConfig{SensitiveWords: []string{"old-word"}},
|
|
ClaudeKey: []config.ClaudeKey{{APIKey: "c1"}},
|
|
CodexKey: []config.CodexKey{{APIKey: "x1"}},
|
|
RemoteManagement: config.RemoteManagement{DisableControlPanel: false, SecretKey: "keep"},
|
|
SDKConfig: sdkconfig.SDKConfig{
|
|
RequestLog: false,
|
|
ProxyURL: "http://old-proxy",
|
|
APIKeys: []string{"key-1"},
|
|
ForceModelPrefix: false,
|
|
NonStreamKeepAliveInterval: 0,
|
|
},
|
|
}
|
|
newCfg := &config.Config{
|
|
Port: 2000,
|
|
AuthDir: "/new",
|
|
Debug: true,
|
|
LoggingToFile: true,
|
|
UsageStatisticsEnabled: true,
|
|
DisableCooling: true,
|
|
SaveCooldownStatus: true,
|
|
TransientErrorCooldownSeconds: -1,
|
|
RequestRetry: 2,
|
|
MaxRetryCredentials: 3,
|
|
MaxRetryInterval: 3,
|
|
WebsocketAuth: true,
|
|
QuotaExceeded: config.QuotaExceeded{SwitchProject: true, SwitchPreviewModel: true, AntigravityCredits: true},
|
|
Antigravity: config.AntigravityConfig{SensitiveWords: []string{"new-word-1", "new-word-2"}},
|
|
XAI: config.XAIConfig{InjectXSearch: true},
|
|
ClaudeKey: []config.ClaudeKey{
|
|
{APIKey: "c1", BaseURL: "http://new", ProxyURL: "http://p", Headers: map[string]string{"H": "1"}, ExcludedModels: []string{"a"}},
|
|
{APIKey: "c2"},
|
|
},
|
|
CodexKey: []config.CodexKey{
|
|
{APIKey: "x1", BaseURL: "http://x", ProxyURL: "http://px", Headers: map[string]string{"H": "2"}, ExcludedModels: []string{"b"}},
|
|
{APIKey: "x2"},
|
|
},
|
|
RemoteManagement: config.RemoteManagement{
|
|
DisableControlPanel: true,
|
|
SecretKey: "",
|
|
},
|
|
SDKConfig: sdkconfig.SDKConfig{
|
|
RequestLog: true,
|
|
ProxyURL: "http://new-proxy",
|
|
APIKeys: []string{" key-1 ", "key-2"},
|
|
ForceModelPrefix: true,
|
|
NonStreamKeepAliveInterval: 5,
|
|
DisableImageGeneration: config.DisableImageGenerationAll,
|
|
ClaudeCode: sdkconfig.ClaudeCodeConfig{
|
|
DisableCloakingModelList: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
details := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, details, "debug: false -> true")
|
|
expectContains(t, details, "logging-to-file: false -> true")
|
|
expectContains(t, details, "usage-statistics-enabled: false -> true")
|
|
expectContains(t, details, "disable-cooling: false -> true")
|
|
expectContains(t, details, "save-cooldown-status: false -> true")
|
|
expectContains(t, details, "transient-error-cooldown-seconds: 0 -> -1")
|
|
expectContains(t, details, "disable-image-generation: false -> true")
|
|
expectContains(t, details, "claude-code.disable-cloaking-model-list: false -> true")
|
|
expectContains(t, details, "request-log: false -> true")
|
|
expectContains(t, details, "request-retry: 1 -> 2")
|
|
expectContains(t, details, "max-retry-credentials: 1 -> 3")
|
|
expectContains(t, details, "max-retry-interval: 1 -> 3")
|
|
expectContains(t, details, "proxy-url: http://old-proxy -> http://new-proxy")
|
|
expectContains(t, details, "ws-auth: false -> true")
|
|
expectContains(t, details, "force-model-prefix: false -> true")
|
|
expectContains(t, details, "nonstream-keepalive-interval: 0 -> 5")
|
|
expectContains(t, details, "quota-exceeded.switch-project: false -> true")
|
|
expectContains(t, details, "quota-exceeded.switch-preview-model: false -> true")
|
|
expectContains(t, details, "quota-exceeded.antigravity-credits: false -> true")
|
|
expectContains(t, details, "antigravity.sensitive-words: 1 -> 2")
|
|
expectContains(t, details, "xai.inject-x-search: false -> true")
|
|
expectContains(t, details, "api-keys count: 1 -> 2")
|
|
expectContains(t, details, "claude-api-key count: 1 -> 2")
|
|
expectContains(t, details, "codex-api-key count: 1 -> 2")
|
|
expectContains(t, details, "remote-management.disable-control-panel: false -> true")
|
|
expectContains(t, details, "remote-management.secret-key: deleted")
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_AllBranches(t *testing.T) {
|
|
oldCfg := &config.Config{
|
|
Port: 1,
|
|
AuthDir: "/a",
|
|
Debug: false,
|
|
LoggingToFile: false,
|
|
UsageStatisticsEnabled: false,
|
|
DisableCooling: false,
|
|
SaveCooldownStatus: false,
|
|
TransientErrorCooldownSeconds: 0,
|
|
RequestRetry: 1,
|
|
MaxRetryCredentials: 1,
|
|
MaxRetryInterval: 1,
|
|
WebsocketAuth: false,
|
|
QuotaExceeded: config.QuotaExceeded{SwitchProject: false, SwitchPreviewModel: false, AntigravityCredits: false},
|
|
GeminiKey: []config.GeminiKey{
|
|
{APIKey: "g-old", BaseURL: "http://g-old", ProxyURL: "http://gp-old", Headers: map[string]string{"A": "1"}},
|
|
},
|
|
ClaudeKey: []config.ClaudeKey{
|
|
{APIKey: "c-old", BaseURL: "http://c-old", ProxyURL: "http://cp-old", Headers: map[string]string{"H": "1"}, ExcludedModels: []string{"x"}},
|
|
},
|
|
CodexKey: []config.CodexKey{
|
|
{APIKey: "x-old", BaseURL: "http://x-old", ProxyURL: "http://xp-old", Headers: map[string]string{"H": "1"}, ExcludedModels: []string{"x"}},
|
|
},
|
|
VertexCompatAPIKey: []config.VertexCompatKey{
|
|
{APIKey: "v-old", BaseURL: "http://v-old", ProxyURL: "http://vp-old", Headers: map[string]string{"H": "1"}, Models: []config.VertexCompatModel{{Name: "m1"}}},
|
|
},
|
|
RemoteManagement: config.RemoteManagement{
|
|
AllowRemote: false,
|
|
DisableControlPanel: false,
|
|
SecretKey: "old",
|
|
},
|
|
SDKConfig: sdkconfig.SDKConfig{
|
|
RequestLog: false,
|
|
ProxyURL: "http://old-proxy",
|
|
APIKeys: []string{" keyA "},
|
|
},
|
|
OAuthExcludedModels: map[string][]string{"p1": {"a"}},
|
|
OpenAICompatibility: []config.OpenAICompatibility{
|
|
{
|
|
Name: "prov-old",
|
|
APIKeyEntries: []config.OpenAICompatibilityAPIKey{
|
|
{APIKey: "k1"},
|
|
},
|
|
Models: []config.OpenAICompatibilityModel{{Name: "m1"}},
|
|
},
|
|
},
|
|
}
|
|
newCfg := &config.Config{
|
|
Port: 2,
|
|
AuthDir: "/b",
|
|
Debug: true,
|
|
LoggingToFile: true,
|
|
UsageStatisticsEnabled: true,
|
|
DisableCooling: true,
|
|
SaveCooldownStatus: true,
|
|
TransientErrorCooldownSeconds: -1,
|
|
RequestRetry: 2,
|
|
MaxRetryCredentials: 3,
|
|
MaxRetryInterval: 3,
|
|
WebsocketAuth: true,
|
|
QuotaExceeded: config.QuotaExceeded{SwitchProject: true, SwitchPreviewModel: true, AntigravityCredits: true},
|
|
GeminiKey: []config.GeminiKey{
|
|
{APIKey: "g-new", BaseURL: "http://g-new", ProxyURL: "http://gp-new", Headers: map[string]string{"A": "2"}, ExcludedModels: []string{"x", "y"}},
|
|
},
|
|
ClaudeKey: []config.ClaudeKey{
|
|
{APIKey: "c-new", BaseURL: "http://c-new", ProxyURL: "http://cp-new", Headers: map[string]string{"H": "2"}, ExcludedModels: []string{"x", "y"}},
|
|
},
|
|
CodexKey: []config.CodexKey{
|
|
{APIKey: "x-new", BaseURL: "http://x-new", ProxyURL: "http://xp-new", Headers: map[string]string{"H": "2"}, ExcludedModels: []string{"x", "y"}},
|
|
},
|
|
VertexCompatAPIKey: []config.VertexCompatKey{
|
|
{APIKey: "v-new", BaseURL: "http://v-new", ProxyURL: "http://vp-new", Headers: map[string]string{"H": "2"}, Models: []config.VertexCompatModel{{Name: "m1"}, {Name: "m2"}}},
|
|
},
|
|
RemoteManagement: config.RemoteManagement{
|
|
AllowRemote: true,
|
|
DisableControlPanel: true,
|
|
SecretKey: "",
|
|
},
|
|
SDKConfig: sdkconfig.SDKConfig{
|
|
RequestLog: true,
|
|
ProxyURL: "http://new-proxy",
|
|
APIKeys: []string{"keyB"},
|
|
DisableImageGeneration: config.DisableImageGenerationAll,
|
|
},
|
|
OAuthExcludedModels: map[string][]string{"p1": {"b", "c"}, "p2": {"d"}},
|
|
OpenAICompatibility: []config.OpenAICompatibility{
|
|
{
|
|
Name: "prov-old",
|
|
APIKeyEntries: []config.OpenAICompatibilityAPIKey{
|
|
{APIKey: "k1"},
|
|
{APIKey: "k2"},
|
|
},
|
|
Models: []config.OpenAICompatibilityModel{{Name: "m1"}, {Name: "m2"}},
|
|
},
|
|
{
|
|
Name: "prov-new",
|
|
APIKeyEntries: []config.OpenAICompatibilityAPIKey{{APIKey: "k3"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
changes := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, changes, "port: 1 -> 2")
|
|
expectContains(t, changes, "auth-dir: /a -> /b")
|
|
expectContains(t, changes, "debug: false -> true")
|
|
expectContains(t, changes, "logging-to-file: false -> true")
|
|
expectContains(t, changes, "usage-statistics-enabled: false -> true")
|
|
expectContains(t, changes, "disable-cooling: false -> true")
|
|
expectContains(t, changes, "save-cooldown-status: false -> true")
|
|
expectContains(t, changes, "transient-error-cooldown-seconds: 0 -> -1")
|
|
expectContains(t, changes, "disable-image-generation: false -> true")
|
|
expectContains(t, changes, "request-retry: 1 -> 2")
|
|
expectContains(t, changes, "max-retry-credentials: 1 -> 3")
|
|
expectContains(t, changes, "max-retry-interval: 1 -> 3")
|
|
expectContains(t, changes, "proxy-url: http://old-proxy -> http://new-proxy")
|
|
expectContains(t, changes, "ws-auth: false -> true")
|
|
expectContains(t, changes, "quota-exceeded.switch-project: false -> true")
|
|
expectContains(t, changes, "quota-exceeded.switch-preview-model: false -> true")
|
|
expectContains(t, changes, "quota-exceeded.antigravity-credits: false -> true")
|
|
expectContains(t, changes, "api-keys: values updated (count unchanged, redacted)")
|
|
expectContains(t, changes, "gemini[0].base-url: http://g-old -> http://g-new")
|
|
expectContains(t, changes, "gemini[0].proxy-url: http://gp-old -> http://gp-new")
|
|
expectContains(t, changes, "gemini[0].api-key: updated")
|
|
expectContains(t, changes, "gemini[0].headers: updated")
|
|
expectContains(t, changes, "gemini[0].excluded-models: updated (0 -> 2 entries)")
|
|
expectContains(t, changes, "claude[0].base-url: http://c-old -> http://c-new")
|
|
expectContains(t, changes, "claude[0].proxy-url: http://cp-old -> http://cp-new")
|
|
expectContains(t, changes, "claude[0].api-key: updated")
|
|
expectContains(t, changes, "claude[0].headers: updated")
|
|
expectContains(t, changes, "claude[0].excluded-models: updated (1 -> 2 entries)")
|
|
expectContains(t, changes, "codex[0].base-url: http://x-old -> http://x-new")
|
|
expectContains(t, changes, "codex[0].proxy-url: http://xp-old -> http://xp-new")
|
|
expectContains(t, changes, "codex[0].api-key: updated")
|
|
expectContains(t, changes, "codex[0].headers: updated")
|
|
expectContains(t, changes, "codex[0].excluded-models: updated (1 -> 2 entries)")
|
|
expectContains(t, changes, "vertex[0].base-url: http://v-old -> http://v-new")
|
|
expectContains(t, changes, "vertex[0].proxy-url: http://vp-old -> http://vp-new")
|
|
expectContains(t, changes, "vertex[0].api-key: updated")
|
|
expectContains(t, changes, "vertex[0].models: updated (1 -> 2 entries)")
|
|
expectContains(t, changes, "vertex[0].headers: updated")
|
|
expectContains(t, changes, "oauth-excluded-models[p1]: updated (1 -> 2 entries)")
|
|
expectContains(t, changes, "oauth-excluded-models[p2]: added (1 entries)")
|
|
expectContains(t, changes, "remote-management.allow-remote: false -> true")
|
|
expectContains(t, changes, "remote-management.disable-control-panel: false -> true")
|
|
expectContains(t, changes, "remote-management.secret-key: deleted")
|
|
expectContains(t, changes, "openai-compatibility:")
|
|
}
|
|
|
|
func TestFormatProxyURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{name: "empty", in: "", want: "<none>"},
|
|
{name: "invalid", in: "http://[::1", want: "<redacted>"},
|
|
{name: "fullURLRedactsUserinfoAndPath", in: "http://user:pass@example.com:8080/path?x=1#frag", want: "http://example.com:8080"},
|
|
{name: "socks5RedactsUserinfoAndPath", in: "socks5://user:pass@192.168.1.1:1080/path?x=1", want: "socks5://192.168.1.1:1080"},
|
|
{name: "socks5HostPort", in: "socks5://proxy.example.com:1080/", want: "socks5://proxy.example.com:1080"},
|
|
{name: "hostPortNoScheme", in: "example.com:1234/path?x=1", want: "example.com:1234"},
|
|
{name: "relativePathRedacted", in: "/just/path", want: "<redacted>"},
|
|
{name: "schemeAndHost", in: "https://example.com", want: "https://example.com"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := formatProxyURL(tt.in); got != tt.want {
|
|
t.Fatalf("expected %q, got %q", tt.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_RemoteManagementSecretUpdated(t *testing.T) {
|
|
oldCfg := &config.Config{
|
|
RemoteManagement: config.RemoteManagement{
|
|
SecretKey: "old",
|
|
},
|
|
}
|
|
newCfg := &config.Config{
|
|
RemoteManagement: config.RemoteManagement{
|
|
SecretKey: "new",
|
|
},
|
|
}
|
|
|
|
changes := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, changes, "remote-management.secret-key: updated")
|
|
}
|
|
|
|
func TestBuildConfigChangeDetails_CountBranches(t *testing.T) {
|
|
oldCfg := &config.Config{}
|
|
newCfg := &config.Config{
|
|
GeminiKey: []config.GeminiKey{{APIKey: "g"}},
|
|
ClaudeKey: []config.ClaudeKey{{APIKey: "c"}},
|
|
CodexKey: []config.CodexKey{{APIKey: "c"}},
|
|
XAIKey: []config.XAIKey{{APIKey: "x"}},
|
|
VertexCompatAPIKey: []config.VertexCompatKey{
|
|
{APIKey: "v", BaseURL: "http://v"},
|
|
},
|
|
}
|
|
|
|
changes := BuildConfigChangeDetails(oldCfg, newCfg)
|
|
expectContains(t, changes, "gemini-api-key count: 0 -> 1")
|
|
expectContains(t, changes, "claude-api-key count: 0 -> 1")
|
|
expectContains(t, changes, "codex-api-key count: 0 -> 1")
|
|
expectContains(t, changes, "xai-api-key count: 0 -> 1")
|
|
expectContains(t, changes, "vertex-api-key count: 0 -> 1")
|
|
}
|
|
|
|
func TestTrimStrings(t *testing.T) {
|
|
out := trimStrings([]string{" a ", "b", " c"})
|
|
if len(out) != 3 || out[0] != "a" || out[1] != "b" || out[2] != "c" {
|
|
t.Fatalf("unexpected trimmed strings: %v", out)
|
|
}
|
|
}
|