blob: 7027742c685d5f39b27b6e0e92003c0e16de8ba3 [file] [log] [blame]
Sean McCullough2cba6952025-04-25 20:32:10 +00001package dockerimg
2
3import (
4 "bytes"
5 "crypto/rand"
6 "crypto/rsa"
7 "fmt"
8 "io/fs"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "golang.org/x/crypto/ssh"
15)
16
17// MockFileSystem implements the FileSystem interface for testing
18type MockFileSystem struct {
19 Files map[string][]byte
20 CreatedDirs map[string]bool
21 OpenedFiles map[string]*MockFile
22 StatCalledWith []string
23 FailOn map[string]error // Map of function name to error to simulate failures
24}
25
26func NewMockFileSystem() *MockFileSystem {
27 return &MockFileSystem{
28 Files: make(map[string][]byte),
29 CreatedDirs: make(map[string]bool),
30 OpenedFiles: make(map[string]*MockFile),
31 FailOn: make(map[string]error),
32 }
33}
34
35func (m *MockFileSystem) Stat(name string) (fs.FileInfo, error) {
36 m.StatCalledWith = append(m.StatCalledWith, name)
37 if err, ok := m.FailOn["Stat"]; ok {
38 return nil, err
39 }
40
41 _, exists := m.Files[name]
42 if exists {
43 return nil, nil // File exists
44 }
45 _, exists = m.CreatedDirs[name]
46 if exists {
47 return nil, nil // Directory exists
48 }
49 return nil, os.ErrNotExist
50}
51
52func (m *MockFileSystem) Mkdir(name string, perm fs.FileMode) error {
53 if err, ok := m.FailOn["Mkdir"]; ok {
54 return err
55 }
56 m.CreatedDirs[name] = true
57 return nil
58}
59
Sean McCulloughc796e7f2025-04-30 08:44:06 -070060func (m *MockFileSystem) MkdirAll(name string, perm fs.FileMode) error {
61 if err, ok := m.FailOn["MkdirAll"]; ok {
62 return err
63 }
64 m.CreatedDirs[name] = true
65 return nil
66}
67
Sean McCullough2cba6952025-04-25 20:32:10 +000068func (m *MockFileSystem) ReadFile(name string) ([]byte, error) {
69 if err, ok := m.FailOn["ReadFile"]; ok {
70 return nil, err
71 }
72
73 data, exists := m.Files[name]
74 if !exists {
75 return nil, fmt.Errorf("file not found: %s", name)
76 }
77 return data, nil
78}
79
80func (m *MockFileSystem) WriteFile(name string, data []byte, perm fs.FileMode) error {
81 if err, ok := m.FailOn["WriteFile"]; ok {
82 return err
83 }
84 m.Files[name] = data
85 return nil
86}
87
88// MockFile implements a simple in-memory file for testing
89type MockFile struct {
90 name string
91 buffer *bytes.Buffer
92 fs *MockFileSystem
93 position int64
94}
95
96// MockFileContents represents in-memory file contents for testing
97type MockFileContents struct {
98 name string
99 contents string
100}
101
102func (m *MockFileSystem) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
103 if err, ok := m.FailOn["OpenFile"]; ok {
104 return nil, err
105 }
106
107 // Initialize the file content if it doesn't exist and we're not in read-only mode
108 if _, exists := m.Files[name]; !exists && (flag&os.O_CREATE != 0) {
109 m.Files[name] = []byte{}
110 }
111
112 data, exists := m.Files[name]
113 if !exists {
114 return nil, fmt.Errorf("file not found: %s", name)
115 }
116
117 // For OpenFile, we'll just use WriteFile to simulate file operations
118 // The actual file handle isn't used for much in the sshtheater code
119 // but we still need to return a valid file handle
120 tmpFile, err := os.CreateTemp("", "mockfile-*")
121 if err != nil {
122 return nil, err
123 }
124 if _, err := tmpFile.Write(data); err != nil {
125 tmpFile.Close()
126 return nil, err
127 }
128 if _, err := tmpFile.Seek(0, 0); err != nil {
129 tmpFile.Close()
130 return nil, err
131 }
132
133 return tmpFile, nil
134}
135
136// MockKeyGenerator implements KeyGenerator interface for testing
137type MockKeyGenerator struct {
138 privateKey *rsa.PrivateKey
139 publicKey ssh.PublicKey
140 FailOn map[string]error
141}
142
143func NewMockKeyGenerator(privateKey *rsa.PrivateKey, publicKey ssh.PublicKey) *MockKeyGenerator {
144 return &MockKeyGenerator{
145 privateKey: privateKey,
146 publicKey: publicKey,
147 FailOn: make(map[string]error),
148 }
149}
150
151func (m *MockKeyGenerator) GeneratePrivateKey(bitSize int) (*rsa.PrivateKey, error) {
152 if err, ok := m.FailOn["GeneratePrivateKey"]; ok {
153 return nil, err
154 }
155 return m.privateKey, nil
156}
157
158func (m *MockKeyGenerator) GeneratePublicKey(privateKey *rsa.PublicKey) (ssh.PublicKey, error) {
159 if err, ok := m.FailOn["GeneratePublicKey"]; ok {
160 return nil, err
161 }
162 return m.publicKey, nil
163}
164
165// setupMocks sets up common mocks for testing
166func setupMocks(t *testing.T) (*MockFileSystem, *MockKeyGenerator, *rsa.PrivateKey) {
167 // Generate a real private key using real random
168 privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
169 if err != nil {
170 t.Fatalf("Failed to generate test private key: %v", err)
171 }
172
173 // Generate a test public key
174 publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
175 if err != nil {
176 t.Fatalf("Failed to generate test public key: %v", err)
177 }
178
179 // Create mocks
180 mockFS := NewMockFileSystem()
181 mockKG := NewMockKeyGenerator(privateKey, publicKey)
182
183 return mockFS, mockKG, privateKey
184}
185
186// Helper function to setup a basic SSHTheater for testing
187func setupTestSSHTheater(t *testing.T) (*SSHTheater, *MockFileSystem, *MockKeyGenerator) {
188 mockFS, mockKG, _ := setupMocks(t)
189
190 // Setup home dir in mock filesystem
191 homePath := "/home/testuser"
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700192 sketchDir := filepath.Join(homePath, ".config/sketch")
Sean McCullough2cba6952025-04-25 20:32:10 +0000193 mockFS.CreatedDirs[sketchDir] = true
194
195 // Set HOME environment variable for the test
196 oldHome := os.Getenv("HOME")
197 os.Setenv("HOME", homePath)
198 t.Cleanup(func() { os.Setenv("HOME", oldHome) })
199
200 // Create SSH Theater with mocks
201 ssh, err := newSSHTheatherWithDeps("test-container", "localhost", "2222", mockFS, mockKG)
202 if err != nil {
203 t.Fatalf("Failed to create SSHTheater: %v", err)
204 }
205
206 return ssh, mockFS, mockKG
207}
208
209func TestNewSSHTheatherCreatesRequiredDirectories(t *testing.T) {
210 mockFS, mockKG, _ := setupMocks(t)
211
212 // Set HOME environment variable for the test
213 oldHome := os.Getenv("HOME")
214 os.Setenv("HOME", "/home/testuser")
215 defer func() { os.Setenv("HOME", oldHome) }()
216
217 // Create theater
218 _, err := newSSHTheatherWithDeps("test-container", "localhost", "2222", mockFS, mockKG)
219 if err != nil {
220 t.Fatalf("Failed to create SSHTheater: %v", err)
221 }
222
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700223 // Check if the .config/sketch directory was created
224 expectedDir := "/home/testuser/.config/sketch"
Sean McCullough2cba6952025-04-25 20:32:10 +0000225 if !mockFS.CreatedDirs[expectedDir] {
226 t.Errorf("Expected directory %s to be created", expectedDir)
227 }
228}
229
230func TestCreateKeyPairIfMissing(t *testing.T) {
231 ssh, mockFS, _ := setupTestSSHTheater(t)
232
233 // Test key pair creation
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700234 keyPath := "/home/testuser/.config/sketch/test_key"
Sean McCullough2cba6952025-04-25 20:32:10 +0000235 _, err := ssh.createKeyPairIfMissing(keyPath)
236 if err != nil {
237 t.Fatalf("Failed to create key pair: %v", err)
238 }
239
240 // Verify private key file was created
241 if _, exists := mockFS.Files[keyPath]; !exists {
242 t.Errorf("Private key file not created at %s", keyPath)
243 }
244
245 // Verify public key file was created
246 pubKeyPath := keyPath + ".pub"
247 if _, exists := mockFS.Files[pubKeyPath]; !exists {
248 t.Errorf("Public key file not created at %s", pubKeyPath)
249 }
250
251 // Verify public key content format
252 pubKeyContent, _ := mockFS.ReadFile(pubKeyPath)
253 if !bytes.HasPrefix(pubKeyContent, []byte("ssh-rsa ")) {
254 t.Errorf("Public key does not have expected format, got: %s", pubKeyContent)
255 }
256}
257
258// TestAddContainerToSSHConfig tests that the container gets added to the SSH config
259// This test uses a direct approach since the OpenFile mocking is complex
260func TestAddContainerToSSHConfig(t *testing.T) {
261 // Create a temporary directory for test files
262 tempDir, err := os.MkdirTemp("", "sshtheater-test-*")
263 if err != nil {
264 t.Fatalf("Failed to create temp dir: %v", err)
265 }
266 defer os.RemoveAll(tempDir)
267
268 // Create real files in temp directory
269 configPath := filepath.Join(tempDir, "ssh_config")
270 initialConfig := `# SSH Config
271Host existing-host
272 HostName example.com
273 User testuser
274`
Autoformatter33f71722025-04-25 23:23:22 +0000275 if err := os.WriteFile(configPath, []byte(initialConfig), 0o644); err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000276 t.Fatalf("Failed to write initial config: %v", err)
277 }
278
279 // Create a theater with the real filesystem but custom paths
280 ssh := &SSHTheater{
281 cntrName: "test-container",
282 sshHost: "localhost",
283 sshPort: "2222",
284 sshConfigPath: configPath,
285 userIdentityPath: filepath.Join(tempDir, "user_identity"),
286 fs: &RealFileSystem{},
287 kg: &RealKeyGenerator{},
288 }
289
290 // Add container to SSH config
291 err = ssh.addContainerToSSHConfig()
292 if err != nil {
293 t.Fatalf("Failed to add container to SSH config: %v", err)
294 }
295
296 // Read the updated file
297 configData, err := os.ReadFile(configPath)
298 if err != nil {
299 t.Fatalf("Failed to read updated config: %v", err)
300 }
301 configStr := string(configData)
302
303 // Check for expected values
304 if !strings.Contains(configStr, "Host test-container") {
305 t.Errorf("Container host entry not found in config")
306 }
307
308 if !strings.Contains(configStr, "HostName localhost") {
309 t.Errorf("HostName not correctly added to SSH config")
310 }
311
312 if !strings.Contains(configStr, "Port 2222") {
313 t.Errorf("Port not correctly added to SSH config")
314 }
315
316 if !strings.Contains(configStr, "User root") {
317 t.Errorf("User not correctly set to root in SSH config")
318 }
319
320 // Check if identity file path is correct
321 identityLine := "IdentityFile " + ssh.userIdentityPath
322 if !strings.Contains(configStr, identityLine) {
323 t.Errorf("Identity file path not correctly added to SSH config")
324 }
325}
326
327func TestAddContainerToKnownHosts(t *testing.T) {
328 // Skip this test as it requires more complex setup
329 // The TestSSHTheaterCleanup test covers the addContainerToKnownHosts
330 // functionality in a more integrated way
331 t.Skip("This test requires more complex setup, integrated test coverage exists in TestSSHTheaterCleanup")
332}
333
334func TestRemoveContainerFromSSHConfig(t *testing.T) {
335 // Create a temporary directory for test files
336 tempDir, err := os.MkdirTemp("", "sshtheater-test-*")
337 if err != nil {
338 t.Fatalf("Failed to create temp dir: %v", err)
339 }
340 defer os.RemoveAll(tempDir)
341
342 // Create paths for test files
343 sshConfigPath := filepath.Join(tempDir, "ssh_config")
344 userIdentityPath := filepath.Join(tempDir, "user_identity")
345 knownHostsPath := filepath.Join(tempDir, "known_hosts")
346
347 // Create initial SSH config with container entry
348 cntrName := "test-container"
349 sshHost := "localhost"
350 sshPort := "2222"
351
352 initialConfig := fmt.Sprintf(
353 `Host existing-host
354 HostName example.com
355 User testuser
356
357Host %s
358 HostName %s
359 User root
360 Port %s
361 IdentityFile %s
362 UserKnownHostsFile %s
363`,
364 cntrName, sshHost, sshPort, userIdentityPath, knownHostsPath,
365 )
366
Autoformatter33f71722025-04-25 23:23:22 +0000367 if err := os.WriteFile(sshConfigPath, []byte(initialConfig), 0o644); err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000368 t.Fatalf("Failed to write initial SSH config: %v", err)
369 }
370
371 // Create a theater with the real filesystem but custom paths
372 ssh := &SSHTheater{
373 cntrName: cntrName,
374 sshHost: sshHost,
375 sshPort: sshPort,
376 sshConfigPath: sshConfigPath,
377 userIdentityPath: userIdentityPath,
378 knownHostsPath: knownHostsPath,
379 fs: &RealFileSystem{},
380 }
381
382 // Remove container from SSH config
383 err = ssh.removeContainerFromSSHConfig()
384 if err != nil {
385 t.Fatalf("Failed to remove container from SSH config: %v", err)
386 }
387
388 // Read the updated file
389 configData, err := os.ReadFile(sshConfigPath)
390 if err != nil {
391 t.Fatalf("Failed to read updated config: %v", err)
392 }
393 configStr := string(configData)
394
395 // Check if the container host entry was removed
396 if strings.Contains(configStr, "Host "+cntrName) {
397 t.Errorf("Container host not removed from SSH config")
398 }
399
400 // Check if existing host remains
401 if !strings.Contains(configStr, "Host existing-host") {
402 t.Errorf("Existing host entry affected by container removal")
403 }
404}
405
406func TestRemoveContainerFromKnownHosts(t *testing.T) {
407 ssh, mockFS, _ := setupTestSSHTheater(t)
408
409 // Setup server public key
410 privateKey, _ := ssh.kg.GeneratePrivateKey(2048)
411 publicKey, _ := ssh.kg.GeneratePublicKey(&privateKey.PublicKey)
412 ssh.serverPublicKey = publicKey
413
414 // Create host line to be removed
415 hostLine := "[localhost]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ..."
416 otherLine := "otherhost ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ..."
417
418 // Set initial content with the line to be removed
419 initialContent := otherLine + "\n" + hostLine
420 mockFS.Files[ssh.knownHostsPath] = []byte(initialContent)
421
422 // Add the host to test remove function
423 err := ssh.addContainerToKnownHosts()
424 if err != nil {
425 t.Fatalf("Failed to add container to known_hosts for removal test: %v", err)
426 }
427
428 // Now remove it
429 err = ssh.removeContainerFromKnownHosts()
430 if err != nil {
431 t.Fatalf("Failed to remove container from known_hosts: %v", err)
432 }
433
434 // Verify content
435 updatedContent, _ := mockFS.ReadFile(ssh.knownHostsPath)
436 content := string(updatedContent)
437
438 hostPattern := ssh.sshHost + ":" + ssh.sshPort
439 if strings.Contains(content, hostPattern) {
440 t.Errorf("Container entry not removed from known_hosts")
441 }
442
443 // Verify other content remains
444 if !strings.Contains(content, otherLine) {
445 t.Errorf("Other known_hosts entries improperly removed")
446 }
447}
448
449func TestSSHTheaterCleanup(t *testing.T) {
450 // Create a temporary directory for test files
451 tempDir, err := os.MkdirTemp("", "sshtheater-test-*")
452 if err != nil {
453 t.Fatalf("Failed to create temp dir: %v", err)
454 }
455 defer os.RemoveAll(tempDir)
456
457 // Create paths for test files
458 sshConfigPath := filepath.Join(tempDir, "ssh_config")
459 userIdentityPath := filepath.Join(tempDir, "user_identity")
460 knownHostsPath := filepath.Join(tempDir, "known_hosts")
461 serverIdentityPath := filepath.Join(tempDir, "server_identity")
462
463 // Create private key for server key
464 privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
465 if err != nil {
466 t.Fatalf("Failed to generate private key: %v", err)
467 }
468 publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
469 if err != nil {
470 t.Fatalf("Failed to generate public key: %v", err)
471 }
472
473 // Initialize files
Autoformatter33f71722025-04-25 23:23:22 +0000474 os.WriteFile(sshConfigPath, []byte("initial ssh_config content"), 0o644)
475 os.WriteFile(knownHostsPath, []byte("initial known_hosts content"), 0o644)
Sean McCullough2cba6952025-04-25 20:32:10 +0000476
477 // Create a theater with the real filesystem but custom paths
478 cntrName := "test-container"
479 sshHost := "localhost"
480 sshPort := "2222"
481
482 ssh := &SSHTheater{
483 cntrName: cntrName,
484 sshHost: sshHost,
485 sshPort: sshPort,
486 sshConfigPath: sshConfigPath,
487 userIdentityPath: userIdentityPath,
488 knownHostsPath: knownHostsPath,
489 serverIdentityPath: serverIdentityPath,
490 serverPublicKey: publicKey,
491 fs: &RealFileSystem{},
492 kg: &RealKeyGenerator{},
493 }
494
495 // Add container to configs
496 err = ssh.addContainerToSSHConfig()
497 if err != nil {
498 t.Fatalf("Failed to set up SSH config for cleanup test: %v", err)
499 }
500
501 err = ssh.addContainerToKnownHosts()
502 if err != nil {
503 t.Fatalf("Failed to set up known_hosts for cleanup test: %v", err)
504 }
505
506 // Execute cleanup
507 err = ssh.Cleanup()
508 if err != nil {
509 t.Fatalf("Cleanup failed: %v", err)
510 }
511
512 // Read updated files
513 configData, err := os.ReadFile(sshConfigPath)
514 if err != nil {
515 t.Fatalf("Failed to read updated SSH config: %v", err)
516 }
517 configStr := string(configData)
518
519 // Check container was removed from SSH config
520 hostEntry := "Host " + ssh.cntrName
521 if strings.Contains(configStr, hostEntry) {
522 t.Errorf("Container not removed from SSH config during cleanup")
523 }
524
525 // Verify known hosts was updated
526 knownHostsContent, err := os.ReadFile(knownHostsPath)
527 if err != nil {
528 t.Fatalf("Failed to read updated known_hosts: %v", err)
529 }
530
531 expectedHostPattern := ssh.sshHost + ":" + ssh.sshPort
532 if strings.Contains(string(knownHostsContent), expectedHostPattern) {
533 t.Errorf("Container not removed from known_hosts during cleanup")
534 }
535}
536
537func TestCheckForInclude(t *testing.T) {
538 mockFS := NewMockFileSystem()
539
540 // Set HOME environment variable for the test
541 oldHome := os.Getenv("HOME")
542 os.Setenv("HOME", "/home/testuser")
543 defer func() { os.Setenv("HOME", oldHome) }()
544
545 // Create a mock ssh config with the expected include
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700546 includeLine := "Include /home/testuser/.config/sketch/ssh_config"
Sean McCullough2cba6952025-04-25 20:32:10 +0000547 initialConfig := fmt.Sprintf("%s\nHost example\n HostName example.com\n", includeLine)
548
549 // Add the config to the mock filesystem
550 sshConfigPath := "/home/testuser/.ssh/config"
551 mockFS.Files[sshConfigPath] = []byte(initialConfig)
552
553 // Test the function with our mock
554 err := CheckForIncludeWithFS(mockFS)
555 if err != nil {
556 t.Fatalf("CheckForInclude failed with proper include: %v", err)
557 }
558
559 // Now test with config missing the include
560 mockFS.Files[sshConfigPath] = []byte("Host example\n HostName example.com\n")
561
562 err = CheckForIncludeWithFS(mockFS)
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700563 if err != nil {
564 t.Fatalf("CheckForInclude should have created the Include line without an error")
Sean McCullough2cba6952025-04-25 20:32:10 +0000565 }
566}
567
568func TestSSHTheaterWithErrors(t *testing.T) {
569 // Test directory creation failure
570 mockFS := NewMockFileSystem()
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700571 mockFS.FailOn["MkdirAll"] = fmt.Errorf("mock mkdir error")
Sean McCullough2cba6952025-04-25 20:32:10 +0000572 mockKG := NewMockKeyGenerator(nil, nil)
573
574 // Set HOME environment variable for the test
575 oldHome := os.Getenv("HOME")
576 os.Setenv("HOME", "/home/testuser")
577 defer func() { os.Setenv("HOME", oldHome) }()
578
579 // Try to create theater with failing FS
580 _, err := newSSHTheatherWithDeps("test-container", "localhost", "2222", mockFS, mockKG)
581 if err == nil || !strings.Contains(err.Error(), "mock mkdir error") {
582 t.Errorf("Should have failed with mkdir error, got: %v", err)
583 }
584
585 // Test key generation failure
586 mockFS = NewMockFileSystem()
587 mockKG = NewMockKeyGenerator(nil, nil)
588 mockKG.FailOn["GeneratePrivateKey"] = fmt.Errorf("mock key generation error")
589
590 _, err = newSSHTheatherWithDeps("test-container", "localhost", "2222", mockFS, mockKG)
591 if err == nil || !strings.Contains(err.Error(), "key generation error") {
592 t.Errorf("Should have failed with key generation error, got: %v", err)
593 }
594}
595
596func TestRealSSHTheatherInit(t *testing.T) {
597 // This is a basic smoke test for the real NewSSHTheather method
598 // We'll mock the os.Getenv("HOME") but use real dependencies otherwise
599
600 // Create a temp dir to use as HOME
601 tempDir, err := os.MkdirTemp("", "sshtheater-test-home-*")
602 if err != nil {
603 t.Fatalf("Failed to create temp dir: %v", err)
604 }
605 defer os.RemoveAll(tempDir)
606
607 // Set HOME environment for the test
608 oldHome := os.Getenv("HOME")
609 os.Setenv("HOME", tempDir)
610 defer os.Setenv("HOME", oldHome)
611
612 // Create the theater
613 theater, err := NewSSHTheather("test-container", "localhost", "2222")
614 if err != nil {
615 t.Fatalf("Failed to create real SSHTheather: %v", err)
616 }
617
618 // Just some basic checks
619 if theater == nil {
620 t.Fatal("Theater is nil")
621 }
622
623 // Check if the sketch dir was created
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700624 sketchDir := filepath.Join(tempDir, ".config/sketch")
Sean McCullough2cba6952025-04-25 20:32:10 +0000625 if _, err := os.Stat(sketchDir); os.IsNotExist(err) {
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700626 t.Errorf(".config/sketch directory not created")
Sean McCullough2cba6952025-04-25 20:32:10 +0000627 }
628
629 // Check if key files were created
630 if _, err := os.Stat(theater.serverIdentityPath); os.IsNotExist(err) {
631 t.Errorf("Server identity file not created")
632 }
633
634 if _, err := os.Stat(theater.userIdentityPath); os.IsNotExist(err) {
635 t.Errorf("User identity file not created")
636 }
637
638 // Check if the config files were created
639 if _, err := os.Stat(theater.sshConfigPath); os.IsNotExist(err) {
640 t.Errorf("SSH config file not created")
641 }
642
643 if _, err := os.Stat(theater.knownHostsPath); os.IsNotExist(err) {
644 t.Errorf("Known hosts file not created")
645 }
646
647 // Clean up
648 err = theater.Cleanup()
649 if err != nil {
650 t.Fatalf("Failed to clean up theater: %v", err)
651 }
652}