package config import ( "os" "path/filepath" "testing" ) func TestConfig_SetHost(t *testing.T) { cfg := &Config{} hostConfig := HostConfig{ Hostname: "codeberg.org", Token: "test-token", User: "testuser", } cfg.SetHost("codeberg.org", hostConfig) if cfg.Hosts == nil { t.Fatal("Hosts map should not be nil") } if len(cfg.Hosts) != 1 { t.Errorf("Expected 1 host, got %d", len(cfg.Hosts)) } host, ok := cfg.Hosts["codeberg.org"] if !ok { t.Fatal("Host codeberg.org not found") } if host.Hostname != "codeberg.org" { t.Errorf("Expected hostname 'codeberg.org', got '%s'", host.Hostname) } if host.Token != "test-token" { t.Errorf("Expected token 'test-token', got '%s'", host.Token) } } func TestConfig_GetHost(t *testing.T) { cfg := &Config{ Hosts: map[string]HostConfig{ "codeberg.org": { Hostname: "codeberg.org", Token: "test-token", }, }, } host, err := cfg.GetHost("codeberg.org", "", "") if err != nil { t.Fatalf("Unexpected error: %v", err) } if host.Hostname != "codeberg.org" { t.Errorf("Expected hostname 'codeberg.org', got '%s'", host.Hostname) } _, err = cfg.GetHost("nonexistent.org", "", "") if err == nil { t.Error("Expected error for nonexistent host") } } func TestGetConfigDir(t *testing.T) { dir, err := GetConfigDir() if err != nil { t.Fatalf("Unexpected error: %v", err) } if dir == "" { t.Error("Config directory should not be empty") } } func TestGetConfigDir_XDG(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", "/custom/config") dir, err := GetConfigDir() if err != nil { t.Fatalf("Unexpected error: %v", err) } expected := "/custom/config/fgj" if dir != expected { t.Errorf("Expected %q, got %q", expected, dir) } } func TestGetConfigPath(t *testing.T) { path, err := GetConfigPath() if err != nil { t.Fatalf("Unexpected error: %v", err) } if path == "" { t.Error("Config path should not be empty") } } func TestConfig_SaveAndLoad(t *testing.T) { // Create a temp directory for testing tempDir := t.TempDir() tempFile := filepath.Join(tempDir, "config.yaml") // Create a config with test data cfg := &Config{ Hosts: map[string]HostConfig{ "codeberg.org": { Hostname: "codeberg.org", Token: "test-token-123", User: "testuser", GitProtocol: "ssh", }, "github.com": { Hostname: "github.com", Token: "github-token-456", User: "githubuser", GitProtocol: "https", }, }, } // Test Save err := cfg.SaveToPath(tempFile) if err != nil { t.Fatalf("Failed to save config: %v", err) } // Verify file exists if _, err := os.Stat(tempFile); os.IsNotExist(err) { t.Fatal("Config file was not created") } // Test Load loadedCfg, err := LoadFromPath(tempFile) if err != nil { t.Fatalf("Failed to load config: %v", err) } // Verify loaded config matches saved config if len(loadedCfg.Hosts) != 2 { t.Errorf("Expected 2 hosts, got %d", len(loadedCfg.Hosts)) } // Check codeberg.org host codebergHost, ok := loadedCfg.Hosts["codeberg.org"] if !ok { t.Fatal("codeberg.org host not found in loaded config") } if codebergHost.Token != "test-token-123" { t.Errorf("Expected token 'test-token-123', got '%s'", codebergHost.Token) } if codebergHost.User != "testuser" { t.Errorf("Expected user 'testuser', got '%s'", codebergHost.User) } if codebergHost.GitProtocol != "ssh" { t.Errorf("Expected git_protocol 'ssh', got '%s'", codebergHost.GitProtocol) } // Check github.com host githubHost, ok := loadedCfg.Hosts["github.com"] if !ok { t.Fatal("github.com host not found in loaded config") } if githubHost.Token != "github-token-456" { t.Errorf("Expected token 'github-token-456', got '%s'", githubHost.Token) } } func TestConfig_LoadNonexistentFile(t *testing.T) { tempDir := t.TempDir() tempFile := filepath.Join(tempDir, "nonexistent.yaml") // Test Load with nonexistent file cfg, err := LoadFromPath(tempFile) if err != nil { t.Fatalf("Load should not error on nonexistent file: %v", err) } // Should return empty config if cfg == nil { t.Fatal("Config should not be nil") } if cfg.Hosts == nil { t.Fatal("Hosts map should be initialized") } if len(cfg.Hosts) != 0 { t.Errorf("Expected empty hosts map, got %d entries", len(cfg.Hosts)) } } func TestConfig_LoadInvalidYAML(t *testing.T) { tempDir := t.TempDir() tempFile := filepath.Join(tempDir, "invalid.yaml") // Write invalid YAML err := os.WriteFile(tempFile, []byte("invalid: yaml: content: [[["), 0600) if err != nil { t.Fatalf("Failed to write invalid YAML: %v", err) } // Test Load with invalid YAML _, err = LoadFromPath(tempFile) if err == nil { t.Error("Expected error when loading invalid YAML") } } func TestConfig_SaveCreatesDirectory(t *testing.T) { tempDir := t.TempDir() tempFile := filepath.Join(tempDir, "subdir", "config.yaml") cfg := &Config{ Hosts: map[string]HostConfig{ "test.org": { Hostname: "test.org", Token: "token", }, }, } // Save should create the directory err := cfg.SaveToPath(tempFile) if err != nil { t.Fatalf("Failed to save config: %v", err) } // Verify directory was created subdir := filepath.Dir(tempFile) if _, err := os.Stat(subdir); os.IsNotExist(err) { t.Error("Save() should have created the directory") } // Verify file exists if _, err := os.Stat(tempFile); os.IsNotExist(err) { t.Error("Config file was not created") } } func TestConfig_SetHost_NilMap(t *testing.T) { cfg := &Config{ Hosts: nil, } hostConfig := HostConfig{ Hostname: "test.org", Token: "test-token", } // Should not panic even with nil map cfg.SetHost("test.org", hostConfig) if cfg.Hosts == nil { t.Fatal("Hosts map should be initialized") } if len(cfg.Hosts) != 1 { t.Errorf("Expected 1 host, got %d", len(cfg.Hosts)) } } func TestConfig_GetHost_EmptyString(t *testing.T) { cfg := &Config{ Hosts: map[string]HostConfig{ "codeberg.org": { Hostname: "codeberg.org", Token: "test-token", }, }, } // Empty hostname should default to codeberg.org host, err := cfg.GetHost("", "", "") if err != nil { t.Fatalf("Unexpected error: %v", err) } if host.Hostname != "codeberg.org" { t.Errorf("Expected default hostname 'codeberg.org', got '%s'", host.Hostname) } } func TestConfig_GetHost_WhitespaceString(t *testing.T) { cfg := &Config{ Hosts: map[string]HostConfig{ "codeberg.org": { Hostname: "codeberg.org", Token: "test-token", }, }, } // Whitespace-only hostname should default to codeberg.org host, err := cfg.GetHost(" ", "", "") if err == nil { t.Logf("Got host: %+v (this may be expected behavior)", host) } else { t.Logf("Got error: %v (this may be expected behavior)", err) } } func TestConfig_SetHost_EmptyToken(t *testing.T) { cfg := &Config{} hostConfig := HostConfig{ Hostname: "codeberg.org", Token: "", // Empty token User: "testuser", } cfg.SetHost("codeberg.org", hostConfig) host, err := cfg.GetHost("codeberg.org", "", "") if err != nil { t.Fatalf("Unexpected error: %v", err) } if host.Token != "" { t.Errorf("Expected empty token, got '%s'", host.Token) } } func TestConfig_SetHost_OverwriteExisting(t *testing.T) { cfg := &Config{ Hosts: map[string]HostConfig{ "codeberg.org": { Hostname: "codeberg.org", Token: "old-token", User: "olduser", }, }, } // Overwrite with new config newConfig := HostConfig{ Hostname: "codeberg.org", Token: "new-token", User: "newuser", } cfg.SetHost("codeberg.org", newConfig) host, err := cfg.GetHost("codeberg.org", "", "") if err != nil { t.Fatalf("Unexpected error: %v", err) } if host.Token != "new-token" { t.Errorf("Expected token 'new-token', got '%s'", host.Token) } if host.User != "newuser" { t.Errorf("Expected user 'newuser', got '%s'", host.User) } } func TestConfig_MultipleHosts(t *testing.T) { cfg := &Config{} hosts := []struct { hostname string token string user string }{ {"codeberg.org", "token1", "user1"}, {"github.com", "token2", "user2"}, {"gitlab.com", "token3", "user3"}, } // Add multiple hosts for _, h := range hosts { cfg.SetHost(h.hostname, HostConfig{ Hostname: h.hostname, Token: h.token, User: h.user, }) } // Verify all hosts are stored if len(cfg.Hosts) != 3 { t.Errorf("Expected 3 hosts, got %d", len(cfg.Hosts)) } // Verify each host can be retrieved correctly for _, h := range hosts { host, err := cfg.GetHost(h.hostname, "", "") if err != nil { t.Errorf("Failed to get host %s: %v", h.hostname, err) continue } if host.Token != h.token { t.Errorf("Host %s: expected token '%s', got '%s'", h.hostname, h.token, host.Token) } if host.User != h.user { t.Errorf("Host %s: expected user '%s', got '%s'", h.hostname, h.user, host.User) } } } func TestConfig_GitProtocol(t *testing.T) { cfg := &Config{} // Test SSH protocol cfg.SetHost("test-ssh.org", HostConfig{ Hostname: "test-ssh.org", Token: "token", GitProtocol: "ssh", }) // Test HTTPS protocol cfg.SetHost("test-https.org", HostConfig{ Hostname: "test-https.org", Token: "token", GitProtocol: "https", }) // Verify protocols are stored correctly sshHost, _ := cfg.GetHost("test-ssh.org", "", "") if sshHost.GitProtocol != "ssh" { t.Errorf("Expected git_protocol 'ssh', got '%s'", sshHost.GitProtocol) } httpsHost, _ := cfg.GetHost("test-https.org", "", "") if httpsHost.GitProtocol != "https" { t.Errorf("Expected git_protocol 'https', got '%s'", httpsHost.GitProtocol) } } func TestResolveHostByPath(t *testing.T) { cfg := &Config{ Hosts: map[string]HostConfig{ "forgejo.zerova.net": { Hostname: "forgejo.zerova.net", Token: "token1", MatchDirs: []string{"/Users/sid/repos/fgj", "/Users/sid/repos/zerova"}, }, "codeberg.org": { Hostname: "codeberg.org", Token: "token2", MatchDirs: []string{"/"}, }, "gitea.example.com": { Hostname: "gitea.example.com", Token: "token3", // no match_dirs — should never be selected by path }, }, } tests := []struct { name string cwd string want string }{ {"exact dir match", "/Users/sid/repos/fgj", "forgejo.zerova.net"}, {"nested dir match", "/Users/sid/repos/fgj/cmd/root.go", "forgejo.zerova.net"}, {"second match dir", "/Users/sid/repos/zerova/pkg", "forgejo.zerova.net"}, {"longest prefix wins over /", "/Users/sid/repos/fgj/internal", "forgejo.zerova.net"}, {"/ as global catch-all", "/tmp", "codeberg.org"}, {"/ matches root itself", "/", "codeberg.org"}, {"no match_dirs host not selected", "/some/random/path", "codeberg.org"}, {"empty cwd returns empty", "", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := cfg.ResolveHostByPath(tt.cwd) if got != tt.want { t.Errorf("ResolveHostByPath(%q) = %q, want %q", tt.cwd, got, tt.want) } }) } } func TestResolveHostByPath_LongestPrefixAcrossHosts(t *testing.T) { cfg := &Config{ Hosts: map[string]HostConfig{ "broad.org": { Hostname: "broad.org", Token: "t1", MatchDirs: []string{"/Users/sid"}, }, "specific.org": { Hostname: "specific.org", Token: "t2", MatchDirs: []string{"/Users/sid/repos/myproject"}, }, }, } got := cfg.ResolveHostByPath("/Users/sid/repos/myproject/main.go") if got != "specific.org" { t.Errorf("expected specific.org, got %q", got) } got = cfg.ResolveHostByPath("/Users/sid/other") if got != "broad.org" { t.Errorf("expected broad.org, got %q", got) } } func TestGetHost_MatchDirsIntegration(t *testing.T) { cfg := &Config{ Hosts: map[string]HostConfig{ "forgejo.zerova.net": { Hostname: "forgejo.zerova.net", Token: "token1", MatchDirs: []string{"/Users/sid/repos/fgj"}, }, "codeberg.org": { Hostname: "codeberg.org", Token: "token2", }, }, } // cwd match should resolve to forgejo.zerova.net host, err := cfg.GetHost("", "", "/Users/sid/repos/fgj/cmd") if err != nil { t.Fatalf("unexpected error: %v", err) } if host.Hostname != "forgejo.zerova.net" { t.Errorf("expected forgejo.zerova.net, got %s", host.Hostname) } // no cwd match falls through to codeberg.org default host, err = cfg.GetHost("", "", "/tmp") if err != nil { t.Fatalf("unexpected error: %v", err) } if host.Hostname != "codeberg.org" { t.Errorf("expected codeberg.org, got %s", host.Hostname) } } func TestResolveHostByPath_TildeExpansion(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Skip("cannot determine home directory") } cfg := &Config{ Hosts: map[string]HostConfig{ "tilde.org": { Hostname: "tilde.org", Token: "t1", MatchDirs: []string{"~/repos"}, }, }, } got := cfg.ResolveHostByPath(filepath.Join(home, "repos", "myproject")) if got != "tilde.org" { t.Errorf("expected tilde.org, got %q", got) } got = cfg.ResolveHostByPath(filepath.Join(home, "other")) if got != "" { t.Errorf("expected empty, got %q", got) } } func TestResolveHostByPath_TieBreakByConfigOrder(t *testing.T) { cfg := &Config{ Hosts: map[string]HostConfig{ "second.org": { Hostname: "second.org", Token: "t2", MatchDirs: []string{"/shared/path"}, Order: 1, }, "first.org": { Hostname: "first.org", Token: "t1", MatchDirs: []string{"/shared/path"}, Order: 0, }, }, } got := cfg.ResolveHostByPath("/shared/path/subdir") if got != "first.org" { t.Errorf("expected first.org (earlier in config), got %q", got) } } func TestAssignHostOrder(t *testing.T) { yamlData := []byte(`hosts: alpha.org: hostname: alpha.org token: t1 beta.org: hostname: beta.org token: t2 gamma.org: hostname: gamma.org token: t3 `) cfg, err := LoadFromPath(writeTempConfig(t, yamlData)) if err != nil { t.Fatalf("unexpected error: %v", err) } if cfg.Hosts["alpha.org"].Order != 0 { t.Errorf("alpha.org order = %d, want 0", cfg.Hosts["alpha.org"].Order) } if cfg.Hosts["beta.org"].Order != 1 { t.Errorf("beta.org order = %d, want 1", cfg.Hosts["beta.org"].Order) } if cfg.Hosts["gamma.org"].Order != 2 { t.Errorf("gamma.org order = %d, want 2", cfg.Hosts["gamma.org"].Order) } } func writeTempConfig(t *testing.T, data []byte) string { t.Helper() path := filepath.Join(t.TempDir(), "config.yaml") if err := os.WriteFile(path, data, 0600); err != nil { t.Fatalf("failed to write temp config: %v", err) } return path } func TestExpandHome(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Skip("cannot determine home directory") } tests := []struct { input string want string }{ {"~/repos", filepath.Join(home, "repos")}, {"~", home}, {"/absolute/path", "/absolute/path"}, {"relative/path", "relative/path"}, {"~other", "~other"}, // only ~/... is expanded, not ~user } for _, tt := range tests { got := expandHome(tt.input) if got != tt.want { t.Errorf("expandHome(%q) = %q, want %q", tt.input, got, tt.want) } } }