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) }