blob: 3ca80a7e7bd398a0e34e1b948997f14c133eb12a [file] [log] [blame]
Sean McCullough2cba6952025-04-25 20:32:10 +00001package dockerimg
2
3import (
Sean McCullough15c95282025-05-08 16:48:38 -07004 "bufio"
Sean McCullough2cba6952025-04-25 20:32:10 +00005 "bytes"
Sean McCullough3e9d80c2025-05-13 23:35:23 +00006 "crypto/ed25519"
Sean McCullough2cba6952025-04-25 20:32:10 +00007 "crypto/rand"
Sean McCullough2cba6952025-04-25 20:32:10 +00008 "fmt"
9 "io/fs"
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14
15 "golang.org/x/crypto/ssh"
16)
17
18// MockFileSystem implements the FileSystem interface for testing
19type MockFileSystem struct {
20 Files map[string][]byte
21 CreatedDirs map[string]bool
22 OpenedFiles map[string]*MockFile
23 StatCalledWith []string
Sean McCullough0d95d3a2025-04-30 16:22:28 +000024 TempFiles []string
Sean McCullough2cba6952025-04-25 20:32:10 +000025 FailOn map[string]error // Map of function name to error to simulate failures
26}
27
28func NewMockFileSystem() *MockFileSystem {
29 return &MockFileSystem{
30 Files: make(map[string][]byte),
31 CreatedDirs: make(map[string]bool),
32 OpenedFiles: make(map[string]*MockFile),
Sean McCullough0d95d3a2025-04-30 16:22:28 +000033 TempFiles: []string{},
Sean McCullough2cba6952025-04-25 20:32:10 +000034 FailOn: make(map[string]error),
35 }
36}
37
38func (m *MockFileSystem) Stat(name string) (fs.FileInfo, error) {
39 m.StatCalledWith = append(m.StatCalledWith, name)
40 if err, ok := m.FailOn["Stat"]; ok {
41 return nil, err
42 }
43
44 _, exists := m.Files[name]
45 if exists {
46 return nil, nil // File exists
47 }
48 _, exists = m.CreatedDirs[name]
49 if exists {
50 return nil, nil // Directory exists
51 }
52 return nil, os.ErrNotExist
53}
54
55func (m *MockFileSystem) Mkdir(name string, perm fs.FileMode) error {
56 if err, ok := m.FailOn["Mkdir"]; ok {
57 return err
58 }
59 m.CreatedDirs[name] = true
60 return nil
61}
62
Sean McCulloughc796e7f2025-04-30 08:44:06 -070063func (m *MockFileSystem) MkdirAll(name string, perm fs.FileMode) error {
64 if err, ok := m.FailOn["MkdirAll"]; ok {
65 return err
66 }
67 m.CreatedDirs[name] = true
68 return nil
69}
70
Sean McCullough2cba6952025-04-25 20:32:10 +000071func (m *MockFileSystem) ReadFile(name string) ([]byte, error) {
72 if err, ok := m.FailOn["ReadFile"]; ok {
73 return nil, err
74 }
75
76 data, exists := m.Files[name]
77 if !exists {
78 return nil, fmt.Errorf("file not found: %s", name)
79 }
80 return data, nil
81}
82
83func (m *MockFileSystem) WriteFile(name string, data []byte, perm fs.FileMode) error {
84 if err, ok := m.FailOn["WriteFile"]; ok {
85 return err
86 }
87 m.Files[name] = data
88 return nil
89}
90
91// MockFile implements a simple in-memory file for testing
92type MockFile struct {
93 name string
94 buffer *bytes.Buffer
95 fs *MockFileSystem
96 position int64
97}
98
99// MockFileContents represents in-memory file contents for testing
100type MockFileContents struct {
101 name string
102 contents string
103}
104
105func (m *MockFileSystem) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
106 if err, ok := m.FailOn["OpenFile"]; ok {
107 return nil, err
108 }
109
110 // Initialize the file content if it doesn't exist and we're not in read-only mode
111 if _, exists := m.Files[name]; !exists && (flag&os.O_CREATE != 0) {
112 m.Files[name] = []byte{}
113 }
114
115 data, exists := m.Files[name]
116 if !exists {
117 return nil, fmt.Errorf("file not found: %s", name)
118 }
119
120 // For OpenFile, we'll just use WriteFile to simulate file operations
121 // The actual file handle isn't used for much in the sshtheater code
122 // but we still need to return a valid file handle
123 tmpFile, err := os.CreateTemp("", "mockfile-*")
124 if err != nil {
125 return nil, err
126 }
127 if _, err := tmpFile.Write(data); err != nil {
128 tmpFile.Close()
129 return nil, err
130 }
131 if _, err := tmpFile.Seek(0, 0); err != nil {
132 tmpFile.Close()
133 return nil, err
134 }
135
136 return tmpFile, nil
137}
138
Sean McCullough0d95d3a2025-04-30 16:22:28 +0000139func (m *MockFileSystem) TempFile(dir, pattern string) (*os.File, error) {
140 if err, ok := m.FailOn["TempFile"]; ok {
141 return nil, err
142 }
143
144 // Create an actual temporary file for testing purposes
145 tmpFile, err := os.CreateTemp(dir, pattern)
146 if err != nil {
147 return nil, err
148 }
149
150 // Record the temp file path
151 m.TempFiles = append(m.TempFiles, tmpFile.Name())
152
153 return tmpFile, nil
154}
155
156func (m *MockFileSystem) Rename(oldpath, newpath string) error {
157 if err, ok := m.FailOn["Rename"]; ok {
158 return err
159 }
160
161 // If the old path exists in our mock file system, move its contents
162 if data, exists := m.Files[oldpath]; exists {
163 m.Files[newpath] = data
164 delete(m.Files, oldpath)
165 }
166
167 return nil
168}
169
170func (m *MockFileSystem) SafeWriteFile(name string, data []byte, perm fs.FileMode) error {
171 if err, ok := m.FailOn["SafeWriteFile"]; ok {
172 return err
173 }
174
175 // For the mock, we'll create a backup if the file exists
176 if existingData, exists := m.Files[name]; exists {
177 backupName := name + ".bak"
178 m.Files[backupName] = existingData
179 }
180
181 // Write the new data
182 m.Files[name] = data
183
184 return nil
185}
186
Sean McCullough2cba6952025-04-25 20:32:10 +0000187// MockKeyGenerator implements KeyGenerator interface for testing
188type MockKeyGenerator struct {
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000189 privateKey ed25519.PrivateKey
190 publicKey ed25519.PublicKey
191 sshPublicKey ssh.PublicKey
192 FailOn map[string]error
Sean McCullough2cba6952025-04-25 20:32:10 +0000193}
194
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000195func NewMockKeyGenerator(privateKey ed25519.PrivateKey, publicKey ed25519.PublicKey, sshPublicKey ssh.PublicKey) *MockKeyGenerator {
Sean McCullough2cba6952025-04-25 20:32:10 +0000196 return &MockKeyGenerator{
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000197 privateKey: privateKey,
198 publicKey: publicKey,
199 sshPublicKey: sshPublicKey,
200 FailOn: make(map[string]error),
Sean McCullough2cba6952025-04-25 20:32:10 +0000201 }
202}
203
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000204func (m *MockKeyGenerator) GenerateKeyPair() (ed25519.PrivateKey, ed25519.PublicKey, error) {
205 if err, ok := m.FailOn["GenerateKeyPair"]; ok {
206 return nil, nil, err
Sean McCullough2cba6952025-04-25 20:32:10 +0000207 }
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000208 return m.privateKey, m.publicKey, nil
Sean McCullough2cba6952025-04-25 20:32:10 +0000209}
210
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000211func (m *MockKeyGenerator) ConvertToSSHPublicKey(publicKey ed25519.PublicKey) (ssh.PublicKey, error) {
212 if err, ok := m.FailOn["ConvertToSSHPublicKey"]; ok {
Sean McCullough2cba6952025-04-25 20:32:10 +0000213 return nil, err
214 }
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000215 return m.sshPublicKey, nil
Sean McCullough2cba6952025-04-25 20:32:10 +0000216}
217
218// setupMocks sets up common mocks for testing
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000219func setupMocks(t *testing.T) (*MockFileSystem, *MockKeyGenerator, ed25519.PrivateKey) {
220 // Generate a real Ed25519 key pair
221 publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
Sean McCullough2cba6952025-04-25 20:32:10 +0000222 if err != nil {
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000223 t.Fatalf("Failed to generate test key pair: %v", err)
Sean McCullough2cba6952025-04-25 20:32:10 +0000224 }
225
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000226 // Generate a test SSH public key
227 sshPublicKey, err := ssh.NewPublicKey(publicKey)
Sean McCullough2cba6952025-04-25 20:32:10 +0000228 if err != nil {
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000229 t.Fatalf("Failed to generate test SSH public key: %v", err)
Sean McCullough2cba6952025-04-25 20:32:10 +0000230 }
231
232 // Create mocks
233 mockFS := NewMockFileSystem()
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000234 mockKG := NewMockKeyGenerator(privateKey, publicKey, sshPublicKey)
Sean McCullough2cba6952025-04-25 20:32:10 +0000235
236 return mockFS, mockKG, privateKey
237}
238
239// Helper function to setup a basic SSHTheater for testing
240func setupTestSSHTheater(t *testing.T) (*SSHTheater, *MockFileSystem, *MockKeyGenerator) {
241 mockFS, mockKG, _ := setupMocks(t)
242
243 // Setup home dir in mock filesystem
244 homePath := "/home/testuser"
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700245 sketchDir := filepath.Join(homePath, ".config/sketch")
Sean McCullough2cba6952025-04-25 20:32:10 +0000246 mockFS.CreatedDirs[sketchDir] = true
247
Sean McCullough0d95d3a2025-04-30 16:22:28 +0000248 // Create empty files so the tests don't fail
249 sketchConfigPath := filepath.Join(sketchDir, "ssh_config")
250 mockFS.Files[sketchConfigPath] = []byte("")
251 knownHostsPath := filepath.Join(sketchDir, "known_hosts")
252 mockFS.Files[knownHostsPath] = []byte("")
253
Sean McCullough2cba6952025-04-25 20:32:10 +0000254 // Set HOME environment variable for the test
255 oldHome := os.Getenv("HOME")
256 os.Setenv("HOME", homePath)
257 t.Cleanup(func() { os.Setenv("HOME", oldHome) })
258
259 // Create SSH Theater with mocks
260 ssh, err := newSSHTheatherWithDeps("test-container", "localhost", "2222", mockFS, mockKG)
261 if err != nil {
262 t.Fatalf("Failed to create SSHTheater: %v", err)
263 }
264
265 return ssh, mockFS, mockKG
266}
267
268func TestNewSSHTheatherCreatesRequiredDirectories(t *testing.T) {
269 mockFS, mockKG, _ := setupMocks(t)
270
271 // Set HOME environment variable for the test
272 oldHome := os.Getenv("HOME")
273 os.Setenv("HOME", "/home/testuser")
274 defer func() { os.Setenv("HOME", oldHome) }()
275
Sean McCullough0d95d3a2025-04-30 16:22:28 +0000276 // Create empty files so the test doesn't fail
277 sketchDir := "/home/testuser/.config/sketch"
278 sketchConfigPath := filepath.Join(sketchDir, "ssh_config")
279 mockFS.Files[sketchConfigPath] = []byte("")
280 knownHostsPath := filepath.Join(sketchDir, "known_hosts")
281 mockFS.Files[knownHostsPath] = []byte("")
282
Sean McCullough2cba6952025-04-25 20:32:10 +0000283 // Create theater
284 _, err := newSSHTheatherWithDeps("test-container", "localhost", "2222", mockFS, mockKG)
285 if err != nil {
286 t.Fatalf("Failed to create SSHTheater: %v", err)
287 }
288
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700289 // Check if the .config/sketch directory was created
290 expectedDir := "/home/testuser/.config/sketch"
Sean McCullough2cba6952025-04-25 20:32:10 +0000291 if !mockFS.CreatedDirs[expectedDir] {
292 t.Errorf("Expected directory %s to be created", expectedDir)
293 }
294}
295
296func TestCreateKeyPairIfMissing(t *testing.T) {
297 ssh, mockFS, _ := setupTestSSHTheater(t)
298
299 // Test key pair creation
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700300 keyPath := "/home/testuser/.config/sketch/test_key"
Sean McCullough2cba6952025-04-25 20:32:10 +0000301 _, err := ssh.createKeyPairIfMissing(keyPath)
302 if err != nil {
303 t.Fatalf("Failed to create key pair: %v", err)
304 }
305
306 // Verify private key file was created
307 if _, exists := mockFS.Files[keyPath]; !exists {
308 t.Errorf("Private key file not created at %s", keyPath)
309 }
310
311 // Verify public key file was created
312 pubKeyPath := keyPath + ".pub"
313 if _, exists := mockFS.Files[pubKeyPath]; !exists {
314 t.Errorf("Public key file not created at %s", pubKeyPath)
315 }
316
317 // Verify public key content format
318 pubKeyContent, _ := mockFS.ReadFile(pubKeyPath)
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000319 if !bytes.HasPrefix(pubKeyContent, []byte("ssh-ed25519 ")) {
Sean McCullough2cba6952025-04-25 20:32:10 +0000320 t.Errorf("Public key does not have expected format, got: %s", pubKeyContent)
321 }
322}
323
324// TestAddContainerToSSHConfig tests that the container gets added to the SSH config
325// This test uses a direct approach since the OpenFile mocking is complex
326func TestAddContainerToSSHConfig(t *testing.T) {
327 // Create a temporary directory for test files
328 tempDir, err := os.MkdirTemp("", "sshtheater-test-*")
329 if err != nil {
330 t.Fatalf("Failed to create temp dir: %v", err)
331 }
332 defer os.RemoveAll(tempDir)
333
334 // Create real files in temp directory
335 configPath := filepath.Join(tempDir, "ssh_config")
336 initialConfig := `# SSH Config
337Host existing-host
338 HostName example.com
339 User testuser
340`
Autoformatter33f71722025-04-25 23:23:22 +0000341 if err := os.WriteFile(configPath, []byte(initialConfig), 0o644); err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000342 t.Fatalf("Failed to write initial config: %v", err)
343 }
344
345 // Create a theater with the real filesystem but custom paths
346 ssh := &SSHTheater{
347 cntrName: "test-container",
348 sshHost: "localhost",
349 sshPort: "2222",
350 sshConfigPath: configPath,
351 userIdentityPath: filepath.Join(tempDir, "user_identity"),
352 fs: &RealFileSystem{},
353 kg: &RealKeyGenerator{},
354 }
355
356 // Add container to SSH config
357 err = ssh.addContainerToSSHConfig()
358 if err != nil {
359 t.Fatalf("Failed to add container to SSH config: %v", err)
360 }
361
362 // Read the updated file
363 configData, err := os.ReadFile(configPath)
364 if err != nil {
365 t.Fatalf("Failed to read updated config: %v", err)
366 }
367 configStr := string(configData)
368
369 // Check for expected values
370 if !strings.Contains(configStr, "Host test-container") {
371 t.Errorf("Container host entry not found in config")
372 }
373
374 if !strings.Contains(configStr, "HostName localhost") {
375 t.Errorf("HostName not correctly added to SSH config")
376 }
377
378 if !strings.Contains(configStr, "Port 2222") {
379 t.Errorf("Port not correctly added to SSH config")
380 }
381
382 if !strings.Contains(configStr, "User root") {
383 t.Errorf("User not correctly set to root in SSH config")
384 }
385
386 // Check if identity file path is correct
387 identityLine := "IdentityFile " + ssh.userIdentityPath
388 if !strings.Contains(configStr, identityLine) {
389 t.Errorf("Identity file path not correctly added to SSH config")
390 }
391}
392
393func TestAddContainerToKnownHosts(t *testing.T) {
394 // Skip this test as it requires more complex setup
395 // The TestSSHTheaterCleanup test covers the addContainerToKnownHosts
396 // functionality in a more integrated way
397 t.Skip("This test requires more complex setup, integrated test coverage exists in TestSSHTheaterCleanup")
398}
399
400func TestRemoveContainerFromSSHConfig(t *testing.T) {
401 // Create a temporary directory for test files
402 tempDir, err := os.MkdirTemp("", "sshtheater-test-*")
403 if err != nil {
404 t.Fatalf("Failed to create temp dir: %v", err)
405 }
406 defer os.RemoveAll(tempDir)
407
408 // Create paths for test files
409 sshConfigPath := filepath.Join(tempDir, "ssh_config")
410 userIdentityPath := filepath.Join(tempDir, "user_identity")
411 knownHostsPath := filepath.Join(tempDir, "known_hosts")
412
413 // Create initial SSH config with container entry
414 cntrName := "test-container"
415 sshHost := "localhost"
416 sshPort := "2222"
417
418 initialConfig := fmt.Sprintf(
419 `Host existing-host
420 HostName example.com
421 User testuser
422
423Host %s
424 HostName %s
425 User root
426 Port %s
427 IdentityFile %s
428 UserKnownHostsFile %s
429`,
430 cntrName, sshHost, sshPort, userIdentityPath, knownHostsPath,
431 )
432
Autoformatter33f71722025-04-25 23:23:22 +0000433 if err := os.WriteFile(sshConfigPath, []byte(initialConfig), 0o644); err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000434 t.Fatalf("Failed to write initial SSH config: %v", err)
435 }
436
437 // Create a theater with the real filesystem but custom paths
438 ssh := &SSHTheater{
439 cntrName: cntrName,
440 sshHost: sshHost,
441 sshPort: sshPort,
442 sshConfigPath: sshConfigPath,
443 userIdentityPath: userIdentityPath,
444 knownHostsPath: knownHostsPath,
445 fs: &RealFileSystem{},
446 }
447
448 // Remove container from SSH config
449 err = ssh.removeContainerFromSSHConfig()
450 if err != nil {
451 t.Fatalf("Failed to remove container from SSH config: %v", err)
452 }
453
454 // Read the updated file
455 configData, err := os.ReadFile(sshConfigPath)
456 if err != nil {
457 t.Fatalf("Failed to read updated config: %v", err)
458 }
459 configStr := string(configData)
460
461 // Check if the container host entry was removed
462 if strings.Contains(configStr, "Host "+cntrName) {
463 t.Errorf("Container host not removed from SSH config")
464 }
465
466 // Check if existing host remains
467 if !strings.Contains(configStr, "Host existing-host") {
468 t.Errorf("Existing host entry affected by container removal")
469 }
470}
471
472func TestRemoveContainerFromKnownHosts(t *testing.T) {
473 ssh, mockFS, _ := setupTestSSHTheater(t)
474
475 // Setup server public key
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000476 _, publicKey, _ := ssh.kg.GenerateKeyPair()
477 sshPublicKey, _ := ssh.kg.ConvertToSSHPublicKey(publicKey)
478 ssh.serverPublicKey = sshPublicKey
Sean McCullough2cba6952025-04-25 20:32:10 +0000479
480 // Create host line to be removed
481 hostLine := "[localhost]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ..."
482 otherLine := "otherhost ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ..."
483
484 // Set initial content with the line to be removed
485 initialContent := otherLine + "\n" + hostLine
486 mockFS.Files[ssh.knownHostsPath] = []byte(initialContent)
487
488 // Add the host to test remove function
489 err := ssh.addContainerToKnownHosts()
490 if err != nil {
491 t.Fatalf("Failed to add container to known_hosts for removal test: %v", err)
492 }
493
494 // Now remove it
495 err = ssh.removeContainerFromKnownHosts()
496 if err != nil {
497 t.Fatalf("Failed to remove container from known_hosts: %v", err)
498 }
499
500 // Verify content
501 updatedContent, _ := mockFS.ReadFile(ssh.knownHostsPath)
502 content := string(updatedContent)
503
504 hostPattern := ssh.sshHost + ":" + ssh.sshPort
505 if strings.Contains(content, hostPattern) {
506 t.Errorf("Container entry not removed from known_hosts")
507 }
508
509 // Verify other content remains
510 if !strings.Contains(content, otherLine) {
511 t.Errorf("Other known_hosts entries improperly removed")
512 }
513}
514
515func TestSSHTheaterCleanup(t *testing.T) {
516 // Create a temporary directory for test files
517 tempDir, err := os.MkdirTemp("", "sshtheater-test-*")
518 if err != nil {
519 t.Fatalf("Failed to create temp dir: %v", err)
520 }
521 defer os.RemoveAll(tempDir)
522
523 // Create paths for test files
524 sshConfigPath := filepath.Join(tempDir, "ssh_config")
525 userIdentityPath := filepath.Join(tempDir, "user_identity")
526 knownHostsPath := filepath.Join(tempDir, "known_hosts")
527 serverIdentityPath := filepath.Join(tempDir, "server_identity")
528
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000529 // Create keys for server key
530 publicKey, _, err := ed25519.GenerateKey(rand.Reader)
Sean McCullough2cba6952025-04-25 20:32:10 +0000531 if err != nil {
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000532 t.Fatalf("Failed to generate key pair: %v", err)
Sean McCullough2cba6952025-04-25 20:32:10 +0000533 }
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000534 sshPublicKey, err := ssh.NewPublicKey(publicKey)
Sean McCullough2cba6952025-04-25 20:32:10 +0000535 if err != nil {
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000536 t.Fatalf("Failed to generate SSH public key: %v", err)
Sean McCullough2cba6952025-04-25 20:32:10 +0000537 }
538
539 // Initialize files
Autoformatter33f71722025-04-25 23:23:22 +0000540 os.WriteFile(sshConfigPath, []byte("initial ssh_config content"), 0o644)
541 os.WriteFile(knownHostsPath, []byte("initial known_hosts content"), 0o644)
Sean McCullough2cba6952025-04-25 20:32:10 +0000542
543 // Create a theater with the real filesystem but custom paths
544 cntrName := "test-container"
545 sshHost := "localhost"
546 sshPort := "2222"
547
548 ssh := &SSHTheater{
549 cntrName: cntrName,
550 sshHost: sshHost,
551 sshPort: sshPort,
552 sshConfigPath: sshConfigPath,
553 userIdentityPath: userIdentityPath,
554 knownHostsPath: knownHostsPath,
555 serverIdentityPath: serverIdentityPath,
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000556 serverPublicKey: sshPublicKey,
Sean McCullough2cba6952025-04-25 20:32:10 +0000557 fs: &RealFileSystem{},
558 kg: &RealKeyGenerator{},
559 }
560
561 // Add container to configs
562 err = ssh.addContainerToSSHConfig()
563 if err != nil {
564 t.Fatalf("Failed to set up SSH config for cleanup test: %v", err)
565 }
566
567 err = ssh.addContainerToKnownHosts()
568 if err != nil {
569 t.Fatalf("Failed to set up known_hosts for cleanup test: %v", err)
570 }
571
572 // Execute cleanup
573 err = ssh.Cleanup()
574 if err != nil {
575 t.Fatalf("Cleanup failed: %v", err)
576 }
577
578 // Read updated files
579 configData, err := os.ReadFile(sshConfigPath)
580 if err != nil {
581 t.Fatalf("Failed to read updated SSH config: %v", err)
582 }
583 configStr := string(configData)
584
585 // Check container was removed from SSH config
586 hostEntry := "Host " + ssh.cntrName
587 if strings.Contains(configStr, hostEntry) {
588 t.Errorf("Container not removed from SSH config during cleanup")
589 }
590
591 // Verify known hosts was updated
592 knownHostsContent, err := os.ReadFile(knownHostsPath)
593 if err != nil {
594 t.Fatalf("Failed to read updated known_hosts: %v", err)
595 }
596
597 expectedHostPattern := ssh.sshHost + ":" + ssh.sshPort
598 if strings.Contains(string(knownHostsContent), expectedHostPattern) {
599 t.Errorf("Container not removed from known_hosts during cleanup")
600 }
601}
602
Sean McCullough15c95282025-05-08 16:48:38 -0700603func TestCheckForInclude_userAccepts(t *testing.T) {
Sean McCullough2cba6952025-04-25 20:32:10 +0000604 mockFS := NewMockFileSystem()
605
606 // Set HOME environment variable for the test
607 oldHome := os.Getenv("HOME")
608 os.Setenv("HOME", "/home/testuser")
609 defer func() { os.Setenv("HOME", oldHome) }()
610
611 // Create a mock ssh config with the expected include
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700612 includeLine := "Include /home/testuser/.config/sketch/ssh_config"
Sean McCullough2cba6952025-04-25 20:32:10 +0000613 initialConfig := fmt.Sprintf("%s\nHost example\n HostName example.com\n", includeLine)
614
615 // Add the config to the mock filesystem
616 sshConfigPath := "/home/testuser/.ssh/config"
617 mockFS.Files[sshConfigPath] = []byte(initialConfig)
Sean McCullough15c95282025-05-08 16:48:38 -0700618 stdinReader := bufio.NewReader(strings.NewReader("y\n"))
Sean McCullough2cba6952025-04-25 20:32:10 +0000619 // Test the function with our mock
Sean McCullough15c95282025-05-08 16:48:38 -0700620 err := CheckForIncludeWithFS(mockFS, *stdinReader)
Sean McCullough2cba6952025-04-25 20:32:10 +0000621 if err != nil {
622 t.Fatalf("CheckForInclude failed with proper include: %v", err)
623 }
624
625 // Now test with config missing the include
626 mockFS.Files[sshConfigPath] = []byte("Host example\n HostName example.com\n")
627
Sean McCullough15c95282025-05-08 16:48:38 -0700628 stdinReader = bufio.NewReader(strings.NewReader("y\n"))
629 err = CheckForIncludeWithFS(mockFS, *stdinReader)
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700630 if err != nil {
631 t.Fatalf("CheckForInclude should have created the Include line without an error")
Sean McCullough2cba6952025-04-25 20:32:10 +0000632 }
633}
634
Sean McCullough15c95282025-05-08 16:48:38 -0700635func TestCheckForInclude_userDeclines(t *testing.T) {
636 mockFS := NewMockFileSystem()
637
638 // Set HOME environment variable for the test
639 oldHome := os.Getenv("HOME")
640 os.Setenv("HOME", "/home/testuser")
641 defer func() { os.Setenv("HOME", oldHome) }()
642
643 // Create a mock ssh config with the expected include
644 includeLine := "Include /home/testuser/.config/sketch/ssh_config"
645 initialConfig := fmt.Sprintf("%s\nHost example\n HostName example.com\n", includeLine)
646
647 // Add the config to the mock filesystem
648 sshConfigPath := "/home/testuser/.ssh/config"
649 mockFS.Files[sshConfigPath] = []byte(initialConfig)
650 stdinReader := bufio.NewReader(strings.NewReader("n\n"))
651 // Test the function with our mock
652 err := CheckForIncludeWithFS(mockFS, *stdinReader)
653 if err != nil {
654 t.Fatalf("CheckForInclude failed with proper include: %v", err)
655 }
656
657 // Now test with config missing the include
658 missingInclude := []byte("Host example\n HostName example.com\n")
659 mockFS.Files[sshConfigPath] = missingInclude
660
661 stdinReader = bufio.NewReader(strings.NewReader("n\n"))
662 err = CheckForIncludeWithFS(mockFS, *stdinReader)
663 if err == nil {
664 t.Errorf("CheckForInclude should have returned an error")
665 }
666 if !bytes.Equal(mockFS.Files[sshConfigPath], missingInclude) {
667 t.Errorf("ssh config should not have been edited")
668 }
669}
670
Sean McCullough2cba6952025-04-25 20:32:10 +0000671func TestSSHTheaterWithErrors(t *testing.T) {
672 // Test directory creation failure
673 mockFS := NewMockFileSystem()
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700674 mockFS.FailOn["MkdirAll"] = fmt.Errorf("mock mkdir error")
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000675 mockKG := NewMockKeyGenerator(nil, nil, nil)
Sean McCullough2cba6952025-04-25 20:32:10 +0000676
677 // Set HOME environment variable for the test
678 oldHome := os.Getenv("HOME")
679 os.Setenv("HOME", "/home/testuser")
680 defer func() { os.Setenv("HOME", oldHome) }()
681
682 // Try to create theater with failing FS
683 _, err := newSSHTheatherWithDeps("test-container", "localhost", "2222", mockFS, mockKG)
684 if err == nil || !strings.Contains(err.Error(), "mock mkdir error") {
685 t.Errorf("Should have failed with mkdir error, got: %v", err)
686 }
687
688 // Test key generation failure
689 mockFS = NewMockFileSystem()
Sean McCullough3e9d80c2025-05-13 23:35:23 +0000690 mockKG = NewMockKeyGenerator(nil, nil, nil)
691 mockKG.FailOn["GenerateKeyPair"] = fmt.Errorf("mock key generation error")
Sean McCullough2cba6952025-04-25 20:32:10 +0000692
693 _, err = newSSHTheatherWithDeps("test-container", "localhost", "2222", mockFS, mockKG)
694 if err == nil || !strings.Contains(err.Error(), "key generation error") {
695 t.Errorf("Should have failed with key generation error, got: %v", err)
696 }
697}
698
699func TestRealSSHTheatherInit(t *testing.T) {
700 // This is a basic smoke test for the real NewSSHTheather method
701 // We'll mock the os.Getenv("HOME") but use real dependencies otherwise
702
703 // Create a temp dir to use as HOME
704 tempDir, err := os.MkdirTemp("", "sshtheater-test-home-*")
705 if err != nil {
706 t.Fatalf("Failed to create temp dir: %v", err)
707 }
708 defer os.RemoveAll(tempDir)
709
710 // Set HOME environment for the test
711 oldHome := os.Getenv("HOME")
712 os.Setenv("HOME", tempDir)
713 defer os.Setenv("HOME", oldHome)
714
715 // Create the theater
Josh Bleecher Snyder50608b12025-05-03 22:55:49 +0000716 theater, err := NewSSHTheater("test-container", "localhost", "2222")
Sean McCullough2cba6952025-04-25 20:32:10 +0000717 if err != nil {
718 t.Fatalf("Failed to create real SSHTheather: %v", err)
719 }
720
721 // Just some basic checks
722 if theater == nil {
723 t.Fatal("Theater is nil")
724 }
725
726 // Check if the sketch dir was created
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700727 sketchDir := filepath.Join(tempDir, ".config/sketch")
Sean McCullough2cba6952025-04-25 20:32:10 +0000728 if _, err := os.Stat(sketchDir); os.IsNotExist(err) {
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700729 t.Errorf(".config/sketch directory not created")
Sean McCullough2cba6952025-04-25 20:32:10 +0000730 }
731
732 // Check if key files were created
733 if _, err := os.Stat(theater.serverIdentityPath); os.IsNotExist(err) {
734 t.Errorf("Server identity file not created")
735 }
736
737 if _, err := os.Stat(theater.userIdentityPath); os.IsNotExist(err) {
738 t.Errorf("User identity file not created")
739 }
740
741 // Check if the config files were created
742 if _, err := os.Stat(theater.sshConfigPath); os.IsNotExist(err) {
743 t.Errorf("SSH config file not created")
744 }
745
746 if _, err := os.Stat(theater.knownHostsPath); os.IsNotExist(err) {
747 t.Errorf("Known hosts file not created")
748 }
749
750 // Clean up
751 err = theater.Cleanup()
752 if err != nil {
753 t.Fatalf("Failed to clean up theater: %v", err)
754 }
755}