|
6 | 6 | "net"
|
7 | 7 | "os"
|
8 | 8 | "os/exec"
|
| 9 | + "path/filepath" |
9 | 10 | "strconv"
|
10 | 11 | "strings"
|
11 | 12 | "testing"
|
@@ -138,3 +139,292 @@ func TestConfigSSH(t *testing.T) {
|
138 | 139 | require.NoError(t, err)
|
139 | 140 | require.Equal(t, "test", strings.TrimSpace(string(data)))
|
140 | 141 | }
|
| 142 | + |
| 143 | +func sshConfigFileNames(t *testing.T) (sshConfig string, coderConfig string) { |
| 144 | + t.Helper() |
| 145 | + tmpdir := t.TempDir() |
| 146 | + n1 := filepath.Join(tmpdir, "config") |
| 147 | + n2 := filepath.Join(tmpdir, "coder") |
| 148 | + return n1, n2 |
| 149 | +} |
| 150 | + |
| 151 | +func sshConfigFileCreate(t *testing.T, name string, data io.Reader) { |
| 152 | + t.Helper() |
| 153 | + t.Logf("Writing %s", name) |
| 154 | + f, err := os.Create(name) |
| 155 | + require.NoError(t, err) |
| 156 | + n, err := io.Copy(f, data) |
| 157 | + t.Logf("Wrote %d", n) |
| 158 | + require.NoError(t, err) |
| 159 | + err = f.Close() |
| 160 | + require.NoError(t, err) |
| 161 | +} |
| 162 | + |
| 163 | +func sshConfigFileRead(t *testing.T, name string) string { |
| 164 | + b, err := os.ReadFile(name) |
| 165 | + require.NoError(t, err) |
| 166 | + return string(b) |
| 167 | +} |
| 168 | + |
| 169 | +func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { |
| 170 | + t.Parallel() |
| 171 | + |
| 172 | + type writeConfig struct { |
| 173 | + ssh string |
| 174 | + coder string |
| 175 | + } |
| 176 | + type wantConfig struct { |
| 177 | + ssh string |
| 178 | + coder string |
| 179 | + coderPartial bool |
| 180 | + } |
| 181 | + type match struct { |
| 182 | + match, write string |
| 183 | + } |
| 184 | + tests := []struct { |
| 185 | + name string |
| 186 | + args []string |
| 187 | + matches []match |
| 188 | + writeConfig writeConfig |
| 189 | + wantConfig wantConfig |
| 190 | + wantErr bool |
| 191 | + }{ |
| 192 | + { |
| 193 | + name: "Config files are created", |
| 194 | + matches: []match{ |
| 195 | + {match: "Continue?", write: "yes"}, |
| 196 | + }, |
| 197 | + wantConfig: wantConfig{ |
| 198 | + ssh: strings.Join([]string{ |
| 199 | + "Include coder", |
| 200 | + "", |
| 201 | + }, "\n"), |
| 202 | + coder: "# This file is managed by coder. DO NOT EDIT.", |
| 203 | + coderPartial: true, |
| 204 | + }, |
| 205 | + }, |
| 206 | + { |
| 207 | + name: "Include is written to top of ssh config", |
| 208 | + writeConfig: writeConfig{ |
| 209 | + ssh: strings.Join([]string{ |
| 210 | + "# This is a host", |
| 211 | + "Host test", |
| 212 | + " HostName test", |
| 213 | + }, "\n"), |
| 214 | + }, |
| 215 | + wantConfig: wantConfig{ |
| 216 | + ssh: strings.Join([]string{ |
| 217 | + "Include coder", |
| 218 | + "", |
| 219 | + "# This is a host", |
| 220 | + "Host test", |
| 221 | + " HostName test", |
| 222 | + }, "\n"), |
| 223 | + }, |
| 224 | + matches: []match{ |
| 225 | + {match: "Continue?", write: "yes"}, |
| 226 | + }, |
| 227 | + }, |
| 228 | + { |
| 229 | + name: "Include below Host is invalid, move it to the top", |
| 230 | + writeConfig: writeConfig{ |
| 231 | + ssh: strings.Join([]string{ |
| 232 | + "Host test", |
| 233 | + " HostName test", |
| 234 | + "", |
| 235 | + "Include coder", |
| 236 | + "", |
| 237 | + "", |
| 238 | + }, "\n"), |
| 239 | + }, |
| 240 | + wantConfig: wantConfig{ |
| 241 | + ssh: strings.Join([]string{ |
| 242 | + "Include coder", |
| 243 | + "", |
| 244 | + "Host test", |
| 245 | + " HostName test", |
| 246 | + "", |
| 247 | + // Only "Include coder" with accompanying |
| 248 | + // newline is removed. |
| 249 | + "", |
| 250 | + "", |
| 251 | + }, "\n"), |
| 252 | + }, |
| 253 | + matches: []match{ |
| 254 | + {match: "Continue?", write: "yes"}, |
| 255 | + }, |
| 256 | + }, |
| 257 | + { |
| 258 | + name: "SSH Config does not need modification", |
| 259 | + writeConfig: writeConfig{ |
| 260 | + ssh: strings.Join([]string{ |
| 261 | + "Include something/other", |
| 262 | + "Include coder", |
| 263 | + "", |
| 264 | + "# This is a host", |
| 265 | + "Host test", |
| 266 | + " HostName test", |
| 267 | + }, "\n"), |
| 268 | + }, |
| 269 | + wantConfig: wantConfig{ |
| 270 | + ssh: strings.Join([]string{ |
| 271 | + "Include something/other", |
| 272 | + "Include coder", |
| 273 | + "", |
| 274 | + "# This is a host", |
| 275 | + "Host test", |
| 276 | + " HostName test", |
| 277 | + }, "\n"), |
| 278 | + }, |
| 279 | + matches: []match{ |
| 280 | + {match: "Continue?", write: "yes"}, |
| 281 | + }, |
| 282 | + }, |
| 283 | + { |
| 284 | + name: "When options differ, selecting yes overwrites previous options", |
| 285 | + writeConfig: writeConfig{ |
| 286 | + coder: strings.Join([]string{ |
| 287 | + "# This file is managed by coder. DO NOT EDIT.", |
| 288 | + "#", |
| 289 | + "# You should not hand-edit this file, all changes will be lost upon workspace", |
| 290 | + "# creation, deletion or when running \"coder config-ssh\".", |
| 291 | + "#", |
| 292 | + "# Last config-ssh options:", |
| 293 | + "# :ssh-option=ForwardAgent=yes", |
| 294 | + "#", |
| 295 | + }, "\n"), |
| 296 | + }, |
| 297 | + wantConfig: wantConfig{ |
| 298 | + coder: strings.Join([]string{ |
| 299 | + "# This file is managed by coder. DO NOT EDIT.", |
| 300 | + "#", |
| 301 | + "# You should not hand-edit this file, all changes will be lost upon workspace", |
| 302 | + "# creation, deletion or when running \"coder config-ssh\".", |
| 303 | + "#", |
| 304 | + "# Last config-ssh options:", |
| 305 | + "#", |
| 306 | + }, "\n"), |
| 307 | + coderPartial: true, |
| 308 | + }, |
| 309 | + matches: []match{ |
| 310 | + {match: "Use new options?", write: "yes"}, |
| 311 | + {match: "Continue?", write: "yes"}, |
| 312 | + }, |
| 313 | + }, |
| 314 | + { |
| 315 | + name: "When options differ, selecting no preserves previous options", |
| 316 | + writeConfig: writeConfig{ |
| 317 | + coder: strings.Join([]string{ |
| 318 | + "# This file is managed by coder. DO NOT EDIT.", |
| 319 | + "#", |
| 320 | + "# You should not hand-edit this file, all changes will be lost upon workspace", |
| 321 | + "# creation, deletion or when running \"coder config-ssh\".", |
| 322 | + "#", |
| 323 | + "# Last config-ssh options:", |
| 324 | + "# :ssh-option=ForwardAgent=yes", |
| 325 | + "#", |
| 326 | + }, "\n"), |
| 327 | + }, |
| 328 | + wantConfig: wantConfig{ |
| 329 | + coder: strings.Join([]string{ |
| 330 | + "# This file is managed by coder. DO NOT EDIT.", |
| 331 | + "#", |
| 332 | + "# You should not hand-edit this file, all changes will be lost upon workspace", |
| 333 | + "# creation, deletion or when running \"coder config-ssh\".", |
| 334 | + "#", |
| 335 | + "# Last config-ssh options:", |
| 336 | + "# :ssh-option=ForwardAgent=yes", |
| 337 | + "#", |
| 338 | + }, "\n"), |
| 339 | + coderPartial: true, |
| 340 | + }, |
| 341 | + matches: []match{ |
| 342 | + {match: "Use new options?", write: "no"}, |
| 343 | + {match: "Continue?", write: "yes"}, |
| 344 | + }, |
| 345 | + }, |
| 346 | + { |
| 347 | + name: "Do not overwrite unknown coder config", |
| 348 | + writeConfig: writeConfig{ |
| 349 | + coder: strings.Join([]string{ |
| 350 | + "We're no strangers to love", |
| 351 | + "You know the rules and so do I (do I)", |
| 352 | + }, "\n"), |
| 353 | + }, |
| 354 | + wantConfig: wantConfig{ |
| 355 | + coder: strings.Join([]string{ |
| 356 | + "We're no strangers to love", |
| 357 | + "You know the rules and so do I (do I)", |
| 358 | + }, "\n"), |
| 359 | + }, |
| 360 | + wantErr: true, |
| 361 | + }, |
| 362 | + } |
| 363 | + for _, tt := range tests { |
| 364 | + tt := tt |
| 365 | + t.Run(tt.name, func(t *testing.T) { |
| 366 | + t.Parallel() |
| 367 | + |
| 368 | + var ( |
| 369 | + client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) |
| 370 | + user = coderdtest.CreateFirstUser(t, client) |
| 371 | + version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) |
| 372 | + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) |
| 373 | + project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) |
| 374 | + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) |
| 375 | + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) |
| 376 | + ) |
| 377 | + |
| 378 | + // Prepare ssh config files. |
| 379 | + sshConfigName, coderConfigName := sshConfigFileNames(t) |
| 380 | + if tt.writeConfig.ssh != "" { |
| 381 | + sshConfigFileCreate(t, sshConfigName, strings.NewReader(tt.writeConfig.ssh)) |
| 382 | + } |
| 383 | + if tt.writeConfig.coder != "" { |
| 384 | + sshConfigFileCreate(t, coderConfigName, strings.NewReader(tt.writeConfig.coder)) |
| 385 | + } |
| 386 | + |
| 387 | + args := []string{ |
| 388 | + "config-ssh", |
| 389 | + "--ssh-config-file", sshConfigName, |
| 390 | + "--test.default-ssh-config-file", sshConfigName, |
| 391 | + "--test.ssh-coder-config-file", coderConfigName, |
| 392 | + } |
| 393 | + args = append(args, tt.args...) |
| 394 | + cmd, root := clitest.New(t, args...) |
| 395 | + clitest.SetupConfig(t, client, root) |
| 396 | + |
| 397 | + pty := ptytest.New(t) |
| 398 | + cmd.SetIn(pty.Input()) |
| 399 | + cmd.SetOut(pty.Output()) |
| 400 | + done := tGo(t, func() { |
| 401 | + err := cmd.Execute() |
| 402 | + if !tt.wantErr { |
| 403 | + assert.NoError(t, err) |
| 404 | + } else { |
| 405 | + assert.Error(t, err) |
| 406 | + } |
| 407 | + }) |
| 408 | + |
| 409 | + for _, m := range tt.matches { |
| 410 | + pty.ExpectMatch(m.match) |
| 411 | + pty.WriteLine(m.write) |
| 412 | + } |
| 413 | + |
| 414 | + <-done |
| 415 | + |
| 416 | + if tt.wantConfig.ssh != "" { |
| 417 | + got := sshConfigFileRead(t, sshConfigName) |
| 418 | + assert.Equal(t, tt.wantConfig.ssh, got) |
| 419 | + } |
| 420 | + if tt.wantConfig.coder != "" { |
| 421 | + got := sshConfigFileRead(t, coderConfigName) |
| 422 | + if tt.wantConfig.coderPartial { |
| 423 | + assert.Contains(t, got, tt.wantConfig.coder) |
| 424 | + } else { |
| 425 | + assert.Equal(t, tt.wantConfig.coder, got) |
| 426 | + } |
| 427 | + } |
| 428 | + }) |
| 429 | + } |
| 430 | +} |
0 commit comments