546 lines · 21.5 KB · Go
RawDownload
package diff import (	"testing" 	"github.com/router-for-me/CLIProxyAPI/v6/internal/config"	sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/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"}},		},		AmpCode: config.AmpCode{			UpstreamURL:                   "http://old-upstream",			ModelMappings:                 []config.AmpModelMapping{{From: "from-old", To: "to-old"}},			RestrictManagementToLocalhost: false,		},		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",		GeminiKey: []config.GeminiKey{			{APIKey: "old", BaseURL: "http://old", ExcludedModels: []string{"old-model", "extra"}},		},		AmpCode: config.AmpCode{			UpstreamURL:                   "http://new-upstream",			RestrictManagementToLocalhost: true,			ModelMappings: []config.AmpModelMapping{				{From: "from-old", To: "to-old"},				{From: "from-new", To: "to-new"},			},		},		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, "ampcode.upstream-url: http://old-upstream -> http://new-upstream")	expectContains(t, details, "ampcode.model-mappings: 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, "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_GeminiVertexHeadersAndForceMappings(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"}}},		},		AmpCode: config.AmpCode{			ModelMappings:      []config.AmpModelMapping{{From: "a", To: "b"}},			ForceModelMappings: false,		},	}	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"}}},		},		AmpCode: config.AmpCode{			ModelMappings:      []config.AmpModelMapping{{From: "a", To: "c"}},			ForceModelMappings: true,		},	} 	details := BuildConfigChangeDetails(oldCfg, newCfg)	expectContains(t, details, "gemini[0].headers: updated")	expectContains(t, details, "gemini[0].excluded-models: updated (1 -> 2 entries)")	expectContains(t, details, "ampcode.model-mappings: updated (1 -> 1 entries)")	expectContains(t, details, "ampcode.force-model-mappings: false -> true")} 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_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"},		},		AmpCode: config.AmpCode{			UpstreamAPIKey: "",		},		RemoteManagement: config.RemoteManagement{			SecretKey: "",		},	}	newCfg := &config.Config{		SDKConfig: sdkconfig.SDKConfig{			APIKeys: []string{"a", "b", "c"},		},		AmpCode: config.AmpCode{			UpstreamAPIKey: "new-key",		},		RemoteManagement: config.RemoteManagement{			SecretKey: "new-secret",		},	} 	details := BuildConfigChangeDetails(oldCfg, newCfg)	expectContains(t, details, "api-keys count: 1 -> 3")	expectContains(t, details, "ampcode.upstream-api-key: added")	expectContains(t, details, "remote-management.secret-key: created")} func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) {	oldCfg := &config.Config{		Port:                   1000,		AuthDir:                "/old",		Debug:                  false,		LoggingToFile:          false,		UsageStatisticsEnabled: false,		DisableCooling:         false,		RequestRetry:           1,		MaxRetryCredentials:    1,		MaxRetryInterval:       1,		WebsocketAuth:          false,		QuotaExceeded:          config.QuotaExceeded{SwitchProject: false, SwitchPreviewModel: false},		ClaudeKey:              []config.ClaudeKey{{APIKey: "c1"}},		CodexKey:               []config.CodexKey{{APIKey: "x1"}},		AmpCode:                config.AmpCode{UpstreamAPIKey: "keep", RestrictManagementToLocalhost: false},		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,		RequestRetry:           2,		MaxRetryCredentials:    3,		MaxRetryInterval:       3,		WebsocketAuth:          true,		QuotaExceeded:          config.QuotaExceeded{SwitchProject: true, SwitchPreviewModel: 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"},		},		AmpCode: config.AmpCode{			UpstreamAPIKey:                "",			RestrictManagementToLocalhost: true,			ModelMappings:                 []config.AmpModelMapping{{From: "a", To: "b"}},		},		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,		},	} 	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, "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, "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, "ampcode.restrict-management-to-localhost: false -> true")	expectContains(t, details, "ampcode.upstream-api-key: removed")	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/repo -> new/repo")	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,		RequestRetry:           1,		MaxRetryCredentials:    1,		MaxRetryInterval:       1,		WebsocketAuth:          false,		QuotaExceeded:          config.QuotaExceeded{SwitchProject: false, SwitchPreviewModel: 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"}}},		},		AmpCode: config.AmpCode{			UpstreamURL:                   "http://amp-old",			UpstreamAPIKey:                "old-key",			RestrictManagementToLocalhost: false,			ModelMappings:                 []config.AmpModelMapping{{From: "a", To: "b"}},			ForceModelMappings:            false,		},		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{					{APIKey: "k1"},				},				Models: []config.OpenAICompatibilityModel{{Name: "m1"}},			},		},	}	newCfg := &config.Config{		Port:                   2,		AuthDir:                "/b",		Debug:                  true,		LoggingToFile:          true,		UsageStatisticsEnabled: true,		DisableCooling:         true,		RequestRetry:           2,		MaxRetryCredentials:    3,		MaxRetryInterval:       3,		WebsocketAuth:          true,		QuotaExceeded:          config.QuotaExceeded{SwitchProject: true, SwitchPreviewModel: 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"}}},		},		AmpCode: config.AmpCode{			UpstreamURL:                   "http://amp-new",			UpstreamAPIKey:                "",			RestrictManagementToLocalhost: true,			ModelMappings:                 []config.AmpModelMapping{{From: "a", To: "c"}},			ForceModelMappings:            true,		},		RemoteManagement: config.RemoteManagement{			AllowRemote:            true,			DisableControlPanel:    true,			DisableAutoUpdatePanel: true,			PanelGitHubRepository:  "new/repo",			SecretKey:              "",		},		SDKConfig: sdkconfig.SDKConfig{			RequestLog: true,			ProxyURL:   "http://new-proxy",			APIKeys:    []string{"keyB"},		},		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, "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, "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, "ampcode.upstream-url: http://amp-old -> http://amp-new")	expectContains(t, changes, "ampcode.upstream-api-key: removed")	expectContains(t, changes, "ampcode.restrict-management-to-localhost: false -> true")	expectContains(t, changes, "ampcode.model-mappings: updated (1 -> 1 entries)")	expectContains(t, changes, "ampcode.force-model-mappings: false -> true")	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.disable-auto-update-panel: false -> true")	expectContains(t, changes, "remote-management.panel-github-repository: old/repo -> new/repo")	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_SecretAndUpstreamUpdates(t *testing.T) {

Lines 1–500 of 546.