| package test | |
| import ( | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| "testing" | |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" | |
| ) | |
| func TestLegacyConfigMigration(t *testing.T) { | |
| t.Run("onlyLegacyFields", func(t *testing.T) { | |
| path := writeConfig(t, ` | |
| port: 8080 | |
| generative-language-api-key: | |
| - "legacy-gemini-1" | |
| openai-compatibility: | |
| - name: "legacy-provider" | |
| base-url: "https://example.com" | |
| api-keys: | |
| - "legacy-openai-1" | |
| amp-upstream-url: "https://amp.example.com" | |
| amp-upstream-api-key: "amp-legacy-key" | |
| amp-restrict-management-to-localhost: false | |
| amp-model-mappings: | |
| - from: "old-model" | |
| to: "new-model" | |
| `) | |
| cfg, err := config.LoadConfig(path) | |
| if err != nil { | |
| t.Fatalf("load legacy config: %v", err) | |
| } | |
| if got := len(cfg.GeminiKey); got != 1 || cfg.GeminiKey[0].APIKey != "legacy-gemini-1" { | |
| t.Fatalf("gemini migration mismatch: %+v", cfg.GeminiKey) | |
| } | |
| if got := len(cfg.OpenAICompatibility); got != 1 { | |
| t.Fatalf("expected 1 openai-compat provider, got %d", got) | |
| } | |
| if entries := cfg.OpenAICompatibility[0].APIKeyEntries; len(entries) != 1 || entries[0].APIKey != "legacy-openai-1" { | |
| t.Fatalf("openai-compat migration mismatch: %+v", entries) | |
| } | |
| if cfg.AmpCode.UpstreamURL != "https://amp.example.com" || cfg.AmpCode.UpstreamAPIKey != "amp-legacy-key" { | |
| t.Fatalf("amp migration failed: %+v", cfg.AmpCode) | |
| } | |
| if cfg.AmpCode.RestrictManagementToLocalhost { | |
| t.Fatalf("expected amp restriction to be false after migration") | |
| } | |
| if got := len(cfg.AmpCode.ModelMappings); got != 1 || cfg.AmpCode.ModelMappings[0].From != "old-model" { | |
| t.Fatalf("amp mappings migration mismatch: %+v", cfg.AmpCode.ModelMappings) | |
| } | |
| updated := readFile(t, path) | |
| if strings.Contains(updated, "generative-language-api-key") { | |
| t.Fatalf("legacy gemini key still present:\n%s", updated) | |
| } | |
| if strings.Contains(updated, "amp-upstream-url") || strings.Contains(updated, "amp-restrict-management-to-localhost") { | |
| t.Fatalf("legacy amp keys still present:\n%s", updated) | |
| } | |
| if strings.Contains(updated, "\n api-keys:") { | |
| t.Fatalf("legacy openai compat keys still present:\n%s", updated) | |
| } | |
| }) | |
| t.Run("mixedLegacyAndNewFields", func(t *testing.T) { | |
| path := writeConfig(t, ` | |
| gemini-api-key: | |
| - api-key: "new-gemini" | |
| generative-language-api-key: | |
| - "new-gemini" | |
| - "legacy-gemini-only" | |
| openai-compatibility: | |
| - name: "mixed-provider" | |
| base-url: "https://mixed.example.com" | |
| api-key-entries: | |
| - api-key: "new-entry" | |
| api-keys: | |
| - "legacy-entry" | |
| - "new-entry" | |
| `) | |
| cfg, err := config.LoadConfig(path) | |
| if err != nil { | |
| t.Fatalf("load mixed config: %v", err) | |
| } | |
| if got := len(cfg.GeminiKey); got != 2 { | |
| t.Fatalf("expected 2 gemini entries, got %d: %+v", got, cfg.GeminiKey) | |
| } | |
| seen := make(map[string]struct{}, len(cfg.GeminiKey)) | |
| for _, entry := range cfg.GeminiKey { | |
| if _, exists := seen[entry.APIKey]; exists { | |
| t.Fatalf("duplicate gemini key %q after migration", entry.APIKey) | |
| } | |
| seen[entry.APIKey] = struct{}{} | |
| } | |
| provider := cfg.OpenAICompatibility[0] | |
| if got := len(provider.APIKeyEntries); got != 2 { | |
| t.Fatalf("expected 2 openai entries, got %d: %+v", got, provider.APIKeyEntries) | |
| } | |
| entrySeen := make(map[string]struct{}, len(provider.APIKeyEntries)) | |
| for _, entry := range provider.APIKeyEntries { | |
| if _, ok := entrySeen[entry.APIKey]; ok { | |
| t.Fatalf("duplicate openai key %q after migration", entry.APIKey) | |
| } | |
| entrySeen[entry.APIKey] = struct{}{} | |
| } | |
| }) | |
| t.Run("onlyNewFields", func(t *testing.T) { | |
| path := writeConfig(t, ` | |
| gemini-api-key: | |
| - api-key: "new-only" | |
| openai-compatibility: | |
| - name: "new-only-provider" | |
| base-url: "https://new-only.example.com" | |
| api-key-entries: | |
| - api-key: "new-only-entry" | |
| ampcode: | |
| upstream-url: "https://amp.new" | |
| upstream-api-key: "new-amp-key" | |
| restrict-management-to-localhost: true | |
| model-mappings: | |
| - from: "a" | |
| to: "b" | |
| `) | |
| cfg, err := config.LoadConfig(path) | |
| if err != nil { | |
| t.Fatalf("load new config: %v", err) | |
| } | |
| if len(cfg.GeminiKey) != 1 || cfg.GeminiKey[0].APIKey != "new-only" { | |
| t.Fatalf("unexpected gemini entries: %+v", cfg.GeminiKey) | |
| } | |
| if len(cfg.OpenAICompatibility) != 1 || len(cfg.OpenAICompatibility[0].APIKeyEntries) != 1 { | |
| t.Fatalf("unexpected openai compat entries: %+v", cfg.OpenAICompatibility) | |
| } | |
| if cfg.AmpCode.UpstreamURL != "https://amp.new" || cfg.AmpCode.UpstreamAPIKey != "new-amp-key" { | |
| t.Fatalf("unexpected amp config: %+v", cfg.AmpCode) | |
| } | |
| }) | |
| t.Run("duplicateNamesDifferentBase", func(t *testing.T) { | |
| path := writeConfig(t, ` | |
| openai-compatibility: | |
| - name: "dup-provider" | |
| base-url: "https://provider-a" | |
| api-keys: | |
| - "key-a" | |
| - name: "dup-provider" | |
| base-url: "https://provider-b" | |
| api-keys: | |
| - "key-b" | |
| `) | |
| cfg, err := config.LoadConfig(path) | |
| if err != nil { | |
| t.Fatalf("load duplicate config: %v", err) | |
| } | |
| if len(cfg.OpenAICompatibility) != 2 { | |
| t.Fatalf("expected 2 providers, got %d", len(cfg.OpenAICompatibility)) | |
| } | |
| for _, entry := range cfg.OpenAICompatibility { | |
| if len(entry.APIKeyEntries) != 1 { | |
| t.Fatalf("expected 1 key entry per provider: %+v", entry) | |
| } | |
| switch entry.BaseURL { | |
| case "https://provider-a": | |
| if entry.APIKeyEntries[0].APIKey != "key-a" { | |
| t.Fatalf("provider-a key mismatch: %+v", entry.APIKeyEntries) | |
| } | |
| case "https://provider-b": | |
| if entry.APIKeyEntries[0].APIKey != "key-b" { | |
| t.Fatalf("provider-b key mismatch: %+v", entry.APIKeyEntries) | |
| } | |
| default: | |
| t.Fatalf("unexpected provider base url: %s", entry.BaseURL) | |
| } | |
| } | |
| }) | |
| } | |
| func writeConfig(t *testing.T, content string) string { | |
| t.Helper() | |
| dir := t.TempDir() | |
| path := filepath.Join(dir, "config.yaml") | |
| if err := os.WriteFile(path, []byte(strings.TrimSpace(content)+"\n"), 0o644); err != nil { | |
| t.Fatalf("write temp config: %v", err) | |
| } | |
| return path | |
| } | |
| func readFile(t *testing.T, path string) string { | |
| t.Helper() | |
| data, err := os.ReadFile(path) | |
| if err != nil { | |
| t.Fatalf("read temp config: %v", err) | |
| } | |
| return string(data) | |
| } | |