677 lines · 27.8 KB · Go
RawDownload
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,			DisableAutoUpdatePanel: false,			PanelGitHubRepository:  "repo-old",		},		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,			DisableAutoUpdatePanel: true,			PanelGitHubRepository:  "repo-new",		},		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.disable-auto-update-panel: 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_CodexOrphanDelegationCompatibility(t *testing.T) {	oldCfg := &config.Config{Codex: config.CodexConfig{OrphanDelegationCompatibility: false}}	newCfg := &config.Config{Codex: config.CodexConfig{OrphanDelegationCompatibility: true}} 	changes := BuildConfigChangeDetails(oldCfg, newCfg)	expectContains(t, changes, "codex.orphan-delegation-compatibility: 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"}},		RemoteManagement: config.RemoteManagement{			PanelGitHubRepository: "https://old-user:old-pass@old-panel.example/private?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"}},		RemoteManagement: config.RemoteManagement{			PanelGitHubRepository: "https://new-user:new-pass@new-panel.example/private?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")	expectContains(t, details, "remote-management.panel-github-repository: https://old-panel.example -> https://new-panel.example")	joined := strings.Join(details, "\n")	for _, sensitive := range []string{"old-user", "new-user", "old-pass", "new-pass", "old-token", "new-token", "/private", "/v1"} {		if strings.Contains(joined, sensitive) {			t.Fatalf("config change details leaked %q: %s", sensitive, joined)		}	}} func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) {	oldPoolEnabled := false	newPoolEnabled := true	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"},			ConnectionPool: config.AntigravityConnectionPoolConfig{				Enabled:         &oldPoolEnabled,				IdleConnTimeout: "30s",			},		},		ClaudeKey:        []config.ClaudeKey{{APIKey: "c1"}},		CodexKey:         []config.CodexKey{{APIKey: "x1"}},		RemoteManagement: config.RemoteManagement{DisableControlPanel: false, PanelGitHubRepository: "old/repo", 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"},			ConnectionPool: config.AntigravityConnectionPoolConfig{				Enabled:         &newPoolEnabled,				IdleConnTimeout: "10s",			},		},		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,			DisableAutoUpdatePanel: true,			PanelGitHubRepository:  "new/repo",			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, "antigravity.connection-pool.enabled: false -> true")	expectContains(t, details, `antigravity.connection-pool.idle-conn-timeout: "30s" -> "10s"`)	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.disable-auto-update-panel: false -> true")	expectContains(t, details, "remote-management.panel-github-repository: old -> new")	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,			DisableAutoUpdatePanel: false,			PanelGitHubRepository:  "old/repo",			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{

Lines 1–500 of 677.