package daemon import ( "os" "path/filepath" "strings" "testing" "banger/internal/paths" ) // Under the opt-in contract the daemon writes its own ssh_config file // and never touches ~/.ssh/config on its own. func TestSyncVMSSHClientConfigWritesBangerFileOnly(t *testing.T) { homeDir := t.TempDir() t.Setenv("HOME", homeDir) knownHostsPath := filepath.Join(homeDir, ".local", "state", "banger", "ssh", "known_hosts") layout := paths.Layout{ ConfigDir: filepath.Join(homeDir, ".config", "banger"), KnownHostsPath: knownHostsPath, } keyPath := filepath.Join(homeDir, ".config", "banger", "ssh", "id_ed25519") if err := SyncVMSSHClientConfig(layout, keyPath); err != nil { t.Fatalf("SyncVMSSHClientConfig: %v", err) } // Banger's own ssh_config file has the `Host *.vm` stanza. bangerConfig, err := os.ReadFile(BangerSSHConfigPath(layout)) if err != nil { t.Fatalf("ReadFile(banger ssh_config): %v", err) } for _, want := range []string{ "Host *.vm", "IdentityFile " + keyPath, "UserKnownHostsFile " + knownHostsPath, "StrictHostKeyChecking accept-new", } { if !strings.Contains(string(bangerConfig), want) { t.Fatalf("banger ssh_config missing %q:\n%s", want, bangerConfig) } } // ~/.ssh/config must NOT have been created or modified. if _, err := os.Stat(filepath.Join(homeDir, ".ssh", "config")); !os.IsNotExist(err) { t.Fatalf("~/.ssh/config should be untouched; stat err = %v", err) } } func TestInstallUserSSHIncludeAddsIncludeBlock(t *testing.T) { homeDir := t.TempDir() t.Setenv("HOME", homeDir) layout := paths.Layout{ConfigDir: filepath.Join(homeDir, ".config", "banger")} if err := os.MkdirAll(layout.ConfigDir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } // Write a fake banger ssh_config so Install has something to include. if err := os.WriteFile(BangerSSHConfigPath(layout), []byte("Host *.vm\n"), 0o644); err != nil { t.Fatalf("WriteFile(banger ssh_config): %v", err) } if err := InstallUserSSHInclude(layout); err != nil { t.Fatalf("InstallUserSSHInclude: %v", err) } got, err := os.ReadFile(filepath.Join(homeDir, ".ssh", "config")) if err != nil { t.Fatalf("ReadFile(~/.ssh/config): %v", err) } want := "Include " + BangerSSHConfigPath(layout) if !strings.Contains(string(got), want) { t.Fatalf("user config missing %q:\n%s", want, got) } if !strings.Contains(string(got), bangerSSHIncludeBegin) { t.Fatalf("user config missing begin marker:\n%s", got) } } func TestInstallUserSSHIncludeIsIdempotent(t *testing.T) { homeDir := t.TempDir() t.Setenv("HOME", homeDir) layout := paths.Layout{ConfigDir: filepath.Join(homeDir, ".config", "banger")} if err := os.MkdirAll(layout.ConfigDir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } if err := os.WriteFile(BangerSSHConfigPath(layout), []byte("Host *.vm\n"), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } for i := 0; i < 3; i++ { if err := InstallUserSSHInclude(layout); err != nil { t.Fatalf("InstallUserSSHInclude (%d): %v", i, err) } } got, err := os.ReadFile(filepath.Join(homeDir, ".ssh", "config")) if err != nil { t.Fatalf("ReadFile: %v", err) } if n := strings.Count(string(got), bangerSSHIncludeBegin); n != 1 { t.Fatalf("begin markers = %d, want 1:\n%s", n, got) } } func TestUninstallUserSSHIncludeRemovesIncludeBlock(t *testing.T) { homeDir := t.TempDir() t.Setenv("HOME", homeDir) sshDir := filepath.Join(homeDir, ".ssh") if err := os.MkdirAll(sshDir, 0o700); err != nil { t.Fatalf("MkdirAll: %v", err) } seed := strings.Join([]string{ "Host keep", " HostName 198.51.100.1", "", bangerSSHIncludeBegin, "Include /tmp/banger-ssh-config", bangerSSHIncludeEnd, "", }, "\n") if err := os.WriteFile(filepath.Join(sshDir, "config"), []byte(seed), 0o600); err != nil { t.Fatalf("seed: %v", err) } if err := UninstallUserSSHInclude(); err != nil { t.Fatalf("UninstallUserSSHInclude: %v", err) } got, err := os.ReadFile(filepath.Join(sshDir, "config")) if err != nil { t.Fatalf("ReadFile: %v", err) } gotStr := string(got) if strings.Contains(gotStr, bangerSSHIncludeBegin) { t.Fatalf("begin marker survived uninstall:\n%s", gotStr) } if !strings.Contains(gotStr, "Host keep") { t.Fatalf("lost unrelated entry:\n%s", gotStr) } } func TestUninstallUserSSHIncludeIsNoOpWhenMissing(t *testing.T) { homeDir := t.TempDir() t.Setenv("HOME", homeDir) if err := UninstallUserSSHInclude(); err != nil { t.Fatalf("UninstallUserSSHInclude on missing file: %v", err) } // Still no ~/.ssh/config. if _, err := os.Stat(filepath.Join(homeDir, ".ssh", "config")); !os.IsNotExist(err) { t.Fatalf("~/.ssh/config unexpectedly created; stat err = %v", err) } } func TestUserSSHIncludeInstalledDetectsMarker(t *testing.T) { for _, tc := range []struct { name string seed string wantIn bool }{ {"missing file", "", false}, {"unrelated only", "Host other\n HostName 1.2.3.4\n", false}, {"installed", bangerSSHIncludeBegin + "\nInclude /tmp/banger\n" + bangerSSHIncludeEnd + "\n", true}, } { t.Run(tc.name, func(t *testing.T) { homeDir := t.TempDir() t.Setenv("HOME", homeDir) if tc.seed != "" { if err := os.MkdirAll(filepath.Join(homeDir, ".ssh"), 0o700); err != nil { t.Fatalf("MkdirAll: %v", err) } if err := os.WriteFile(filepath.Join(homeDir, ".ssh", "config"), []byte(tc.seed), 0o600); err != nil { t.Fatalf("WriteFile: %v", err) } } got, err := UserSSHIncludeInstalled() if err != nil { t.Fatalf("UserSSHIncludeInstalled: %v", err) } if got != tc.wantIn { t.Fatalf("got %v, want %v", got, tc.wantIn) } }) } }