package daemon import ( "os" "path/filepath" "strings" "testing" "banger/internal/paths" ) func TestSyncVMSSHClientConfigCreatesManagedBlock(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(layout.ConfigDir, "ssh", "id_ed25519") if err := syncVMSSHClientConfig(layout, keyPath); err != nil { t.Fatalf("syncVMSSHClientConfig: %v", err) } userConfigPath := filepath.Join(homeDir, ".ssh", "config") userConfig, err := os.ReadFile(userConfigPath) if err != nil { t.Fatalf("ReadFile(user config): %v", err) } userContent := string(userConfig) if !strings.Contains(userContent, vmSSHConfigIncludeBegin) { t.Fatalf("user config = %q, want begin marker", userContent) } for _, want := range []string{ "Host *.vm", "User root", "IdentityFile " + keyPath, "IdentitiesOnly yes", "BatchMode yes", "PasswordAuthentication no", "UserKnownHostsFile " + knownHostsPath, "StrictHostKeyChecking accept-new", } { if !strings.Contains(userContent, want) { t.Fatalf("user config = %q, want %q", userContent, want) } } // Regression: the legacy posture (StrictHostKeyChecking no + // UserKnownHostsFile /dev/null) must never reappear. for _, must := range []string{ "StrictHostKeyChecking no", "UserKnownHostsFile /dev/null", } { if strings.Contains(userContent, must) { t.Fatalf("user config leaked legacy posture %q:\n%s", must, userContent) } } } func TestSyncVMSSHClientConfigReplacesManagedIncludeBlock(t *testing.T) { homeDir := t.TempDir() t.Setenv("HOME", homeDir) layout := paths.Layout{ ConfigDir: filepath.Join(homeDir, ".config", "banger"), } keyPath := filepath.Join(layout.ConfigDir, "ssh", "id_ed25519") sshDir := filepath.Join(homeDir, ".ssh") if err := os.MkdirAll(sshDir, 0o700); err != nil { t.Fatalf("MkdirAll(.ssh): %v", err) } initial := strings.Join([]string{ "ServerAliveInterval 120", "", vmSSHConfigIncludeBegin, "Include /tmp/old-banger-config", vmSSHConfigIncludeEnd, "", "Host other", " HostName 192.0.2.5", "", }, "\n") if err := os.WriteFile(filepath.Join(sshDir, "config"), []byte(initial), 0o644); err != nil { t.Fatalf("WriteFile(user config): %v", err) } if err := syncVMSSHClientConfig(layout, keyPath); err != nil { t.Fatalf("syncVMSSHClientConfig: %v", err) } userConfig, err := os.ReadFile(filepath.Join(sshDir, "config")) if err != nil { t.Fatalf("ReadFile(user config): %v", err) } userContent := string(userConfig) if strings.Count(userContent, vmSSHConfigIncludeBegin) != 1 { t.Fatalf("user config = %q, want one managed block", userContent) } if !strings.Contains(userContent, "ServerAliveInterval 120") || !strings.Contains(userContent, "Host other") { t.Fatalf("user config = %q, want existing entries preserved", userContent) } if !strings.Contains(userContent, "Host *.vm") || !strings.Contains(userContent, "IdentityFile "+keyPath) { t.Fatalf("user config = %q, want refreshed managed vm block", userContent) } }