diff --git a/src/config/config.go b/src/config.go similarity index 99% rename from src/config/config.go rename to src/config.go index 6ddd8bb..c9086f9 100644 --- a/src/config/config.go +++ b/src/config.go @@ -1,4 +1,4 @@ -package config +package main import ( "fmt" diff --git a/src/config_test.go b/src/config_test.go new file mode 100644 index 0000000..61129ff --- /dev/null +++ b/src/config_test.go @@ -0,0 +1,319 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestConfigRead(t *testing.T) { + tempDir := t.TempDir() + + // Create a single config file with types and devices + configPath := filepath.Join(tempDir, "test-config.yaml") + configContent := `types: + test-type: + commands: + - show version + - show status + +devices: + test-device: + user: testuser + type: test-type + direct-device: + user: directuser + commands: + - direct command +` + + err := os.WriteFile(configPath, []byte(configContent), 0644) + if err != nil { + t.Fatalf("Failed to create test config file: %v", err) + } + + cfg, err := ConfigRead([]string{configPath}) + if err != nil { + t.Fatalf("Failed to load config: %v", err) + } + + // Test types section + if len(cfg.Types) != 1 { + t.Errorf("Expected 1 type, got %d", len(cfg.Types)) + } + + testType, exists := cfg.Types["test-type"] + if !exists { + t.Error("Expected 'test-type' to exist in types") + } + + if len(testType.Commands) != 2 { + t.Errorf("Expected 2 commands in test-type, got %d", len(testType.Commands)) + } + + // Test devices section + if len(cfg.Devices) != 2 { + t.Errorf("Expected 2 devices, got %d", len(cfg.Devices)) + } + + testDevice, exists := cfg.Devices["test-device"] + if !exists { + t.Error("Expected 'test-device' to exist in devices") + } + + if testDevice.User != "testuser" { + t.Errorf("Expected user 'testuser', got '%s'", testDevice.User) + } + + if testDevice.Type != "test-type" { + t.Errorf("Expected type 'test-type', got '%s'", testDevice.Type) + } +} + +func TestConfigReadMerging(t *testing.T) { + tempDir := t.TempDir() + + // Create first config file with device types + typesPath := filepath.Join(tempDir, "types.yaml") + typesContent := `types: + test-type: + commands: + - show version + - show status` + + err := os.WriteFile(typesPath, []byte(typesContent), 0644) + if err != nil { + t.Fatalf("Failed to create types file: %v", err) + } + + // Create second config file with devices + devicesPath := filepath.Join(tempDir, "devices.yaml") + devicesContent := `devices: + test-device: + user: testuser + type: test-type` + + err = os.WriteFile(devicesPath, []byte(devicesContent), 0644) + if err != nil { + t.Fatalf("Failed to create devices file: %v", err) + } + + // Load and merge configs + cfg, err := ConfigRead([]string{typesPath, devicesPath}) + if err != nil { + t.Fatalf("Failed to merge configs: %v", err) + } + + // Check that merging worked + if len(cfg.Types) != 1 { + t.Errorf("Expected 1 type, got %d", len(cfg.Types)) + } + + testType, exists := cfg.Types["test-type"] + if !exists { + t.Error("Expected 'test-type' to exist in merged config") + } + + if len(testType.Commands) != 2 { + t.Errorf("Expected 2 commands in test-type, got %d", len(testType.Commands)) + } + + if len(cfg.Devices) != 1 { + t.Errorf("Expected 1 device, got %d", len(cfg.Devices)) + } + + testDevice, exists := cfg.Devices["test-device"] + if !exists { + t.Error("Expected 'test-device' to exist in merged config") + } + + if testDevice.Type != "test-type" { + t.Errorf("Expected device type 'test-type', got '%s'", testDevice.Type) + } +} + +func TestConfigReadOverrides(t *testing.T) { + tempDir := t.TempDir() + + // Create base config + basePath := filepath.Join(tempDir, "base.yaml") + baseContent := `devices: + test-device: + user: baseuser + type: base-type` + + err := os.WriteFile(basePath, []byte(baseContent), 0644) + if err != nil { + t.Fatalf("Failed to create base file: %v", err) + } + + // Create override config + overridePath := filepath.Join(tempDir, "override.yaml") + overrideContent := `devices: + test-device: + user: overrideuser` + + err = os.WriteFile(overridePath, []byte(overrideContent), 0644) + if err != nil { + t.Fatalf("Failed to create override file: %v", err) + } + + // Load with override (later file should override earlier file) + cfg, err := ConfigRead([]string{basePath, overridePath}) + if err != nil { + t.Fatalf("Failed to merge configs: %v", err) + } + + testDevice := cfg.Devices["test-device"] + if testDevice.User != "overrideuser" { + t.Errorf("Expected overridden user 'overrideuser', got '%s'", testDevice.User) + } + + // Type should be preserved from base config + if testDevice.Type != "base-type" { + t.Errorf("Expected type 'base-type' to be preserved, got '%s'", testDevice.Type) + } +} + +func TestConfigReadInvalidFile(t *testing.T) { + _, err := ConfigRead([]string{"/nonexistent/config.yaml"}) + if err == nil { + t.Error("Expected error when loading nonexistent config file") + } +} + +func TestConfigReadInvalidYAML(t *testing.T) { + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "invalid-config.yaml") + + // Create a file with invalid YAML syntax + invalidContent := `types: + test-type: + commands + - invalid yaml` + + err := os.WriteFile(configPath, []byte(invalidContent), 0644) + if err != nil { + t.Fatalf("Failed to create invalid config file: %v", err) + } + + _, err = ConfigRead([]string{configPath}) + if err == nil { + t.Error("Expected error when loading invalid YAML") + } +} + +func TestConfigReadEmptyFile(t *testing.T) { + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "empty-config.yaml") + + // Create empty file + err := os.WriteFile(configPath, []byte(""), 0644) + if err != nil { + t.Fatalf("Failed to create empty config file: %v", err) + } + + cfg, err := ConfigRead([]string{configPath}) + if err != nil { + t.Fatalf("Failed to load empty config: %v", err) + } + + // Should have empty maps + if len(cfg.Types) != 0 { + t.Errorf("Expected 0 types in empty config, got %d", len(cfg.Types)) + } + + if len(cfg.Devices) != 0 { + t.Errorf("Expected 0 devices in empty config, got %d", len(cfg.Devices)) + } +} + +func TestConfigReadComplexMerge(t *testing.T) { + tempDir := t.TempDir() + + // Create device types file + typesPath := filepath.Join(tempDir, "types.yaml") + typesContent := `types: + srlinux: + commands: + - show version + - show platform linecard + eos: + commands: + - show version + - show inventory` + + err := os.WriteFile(typesPath, []byte(typesContent), 0644) + if err != nil { + t.Fatalf("Failed to create types file: %v", err) + } + + // Create production devices file + prodPath := filepath.Join(tempDir, "production.yaml") + prodContent := `devices: + prod-asw100: + user: netops + type: srlinux + prod-core-01: + user: netops + type: eos` + + err = os.WriteFile(prodPath, []byte(prodContent), 0644) + if err != nil { + t.Fatalf("Failed to create production file: %v", err) + } + + // Create lab devices file + labPath := filepath.Join(tempDir, "lab.yaml") + labContent := `devices: + lab-switch: + user: admin + type: srlinux + commands: + - show version only` + + err = os.WriteFile(labPath, []byte(labContent), 0644) + if err != nil { + t.Fatalf("Failed to create lab file: %v", err) + } + + // Load merged configuration + cfg, err := ConfigRead([]string{typesPath, prodPath, labPath}) + if err != nil { + t.Fatalf("Failed to load merged config: %v", err) + } + + // Verify types were loaded correctly + if len(cfg.Types) != 2 { + t.Errorf("Expected 2 types, got %d", len(cfg.Types)) + } + + srlinuxType, exists := cfg.Types["srlinux"] + if !exists { + t.Error("Expected 'srlinux' type to exist") + } + if len(srlinuxType.Commands) != 2 { + t.Errorf("Expected 2 commands for srlinux type, got %d", len(srlinuxType.Commands)) + } + + // Verify devices reference the correct types + if len(cfg.Devices) != 3 { + t.Errorf("Expected 3 devices, got %d", len(cfg.Devices)) + } + + prodDevice, exists := cfg.Devices["prod-asw100"] + if !exists { + t.Error("Expected 'prod-asw100' device to exist") + } + if prodDevice.Type != "srlinux" { + t.Errorf("Expected prod-asw100 type 'srlinux', got '%s'", prodDevice.Type) + } + + labDevice, exists := cfg.Devices["lab-switch"] + if !exists { + t.Error("Expected 'lab-switch' device to exist") + } + if len(labDevice.Commands) != 1 { + t.Errorf("Expected 1 custom command for lab-switch, got %d", len(labDevice.Commands)) + } +} diff --git a/src/main.go b/src/main.go index 0b2414c..9e0f64f 100644 --- a/src/main.go +++ b/src/main.go @@ -6,7 +6,6 @@ import ( "fmt" "log" "os" - "router_backup/config" "github.com/spf13/cobra" ) @@ -38,7 +37,7 @@ func main() { fmt.Printf("IPng Networks Router Backup v%s\n", Version) // Load configuration - cfg, err := config.ConfigRead(yamlFiles) + cfg, err := ConfigRead(yamlFiles) if err != nil { log.Fatalf("Failed to load config: %v", err) } @@ -63,7 +62,7 @@ func main() { // Filter devices if --host flags are provided devicesToProcess := cfg.Devices if len(hostFilter) > 0 { - devicesToProcess = make(map[string]config.Device) + devicesToProcess = make(map[string]Device) for _, hostname := range hostFilter { if deviceConfig, exists := cfg.Devices[hostname]; exists { devicesToProcess[hostname] = deviceConfig diff --git a/src/main_test.go b/src/main_test.go deleted file mode 100644 index dc15707..0000000 --- a/src/main_test.go +++ /dev/null @@ -1,474 +0,0 @@ -// Copyright 2025, IPng Networks GmbH, Pim van Pelt - -package main - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func TestNewRouterBackup(t *testing.T) { - rb := NewRouterBackup("test-host", "test-user", "test-pass", "/test/key", 2222) - - if rb.hostname != "test-host" { - t.Errorf("Expected hostname 'test-host', got '%s'", rb.hostname) - } - if rb.username != "test-user" { - t.Errorf("Expected username 'test-user', got '%s'", rb.username) - } - if rb.password != "test-pass" { - t.Errorf("Expected password 'test-pass', got '%s'", rb.password) - } - if rb.keyFile != "/test/key" { - t.Errorf("Expected keyFile '/test/key', got '%s'", rb.keyFile) - } - if rb.port != 2222 { - t.Errorf("Expected port 2222, got %d", rb.port) - } -} - -func TestFindDefaultSSHKey(t *testing.T) { - // Create a temporary directory to simulate home directory - tempDir := t.TempDir() - sshDir := filepath.Join(tempDir, ".ssh") - err := os.MkdirAll(sshDir, 0755) - if err != nil { - t.Fatalf("Failed to create .ssh directory: %v", err) - } - - // Create a fake SSH key - keyPath := filepath.Join(sshDir, "id_rsa") - err = os.WriteFile(keyPath, []byte("fake-key"), 0600) - if err != nil { - t.Fatalf("Failed to create fake SSH key: %v", err) - } - - // Temporarily change HOME environment variable - originalHome := os.Getenv("HOME") - defer os.Setenv("HOME", originalHome) - os.Setenv("HOME", tempDir) - - result := findDefaultSSHKey() - if result != keyPath { - t.Errorf("Expected SSH key path '%s', got '%s'", keyPath, result) - } -} - -func TestFindDefaultSSHKeyNotFound(t *testing.T) { - // Create a temporary directory with no SSH keys - tempDir := t.TempDir() - - // Temporarily change HOME environment variable - originalHome := os.Getenv("HOME") - defer os.Setenv("HOME", originalHome) - os.Setenv("HOME", tempDir) - - result := findDefaultSSHKey() - if result != "" { - t.Errorf("Expected empty string when no SSH key found, got '%s'", result) - } -} - -func TestLoadConfig(t *testing.T) { - // Create a temporary directory and files - tempDir := t.TempDir() - - // Create device-types.yaml file - deviceTypesPath := filepath.Join(tempDir, "device-types.yaml") - deviceTypesContent := `test-type: - commands: - - show version - - show status` - - err := os.WriteFile(deviceTypesPath, []byte(deviceTypesContent), 0644) - if err != nil { - t.Fatalf("Failed to create device-types file: %v", err) - } - - // Create main config file with !include - configPath := filepath.Join(tempDir, "test-config.yaml") - configContent := `types: - !include device-types.yaml - -devices: - test-device: - user: testuser - type: test-type - direct-device: - user: directuser - commands: - - direct command -` - - err = os.WriteFile(configPath, []byte(configContent), 0644) - if err != nil { - t.Fatalf("Failed to create test config file: %v", err) - } - - config, err := loadConfig(configPath) - if err != nil { - t.Fatalf("Failed to load config: %v", err) - } - - // Test types section - if len(config.Types) != 1 { - t.Errorf("Expected 1 type, got %d", len(config.Types)) - } - - testType, exists := config.Types["test-type"] - if !exists { - t.Error("Expected 'test-type' to exist in types") - } - - if len(testType.Commands) != 2 { - t.Errorf("Expected 2 commands in test-type, got %d", len(testType.Commands)) - } - - // Test devices section - if len(config.Devices) != 2 { - t.Errorf("Expected 2 devices, got %d", len(config.Devices)) - } - - testDevice, exists := config.Devices["test-device"] - if !exists { - t.Error("Expected 'test-device' to exist in devices") - } - - if testDevice.User != "testuser" { - t.Errorf("Expected user 'testuser', got '%s'", testDevice.User) - } - - if testDevice.Type != "test-type" { - t.Errorf("Expected type 'test-type', got '%s'", testDevice.Type) - } -} - -func TestLoadConfigInvalidFile(t *testing.T) { - _, err := loadConfig("/nonexistent/config.yaml") - if err == nil { - t.Error("Expected error when loading nonexistent config file") - } -} - -func TestLoadConfigInvalidYAML(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "invalid-config.yaml") - - // Create invalid YAML content - invalidYAML := `types: - test-type: - commands: - - show version - invalid: [unclosed -` - - err := os.WriteFile(configPath, []byte(invalidYAML), 0644) - if err != nil { - t.Fatalf("Failed to create invalid config file: %v", err) - } - - _, err = loadConfig(configPath) - if err == nil { - t.Error("Expected error when loading invalid YAML") - } -} - -func TestBackupCommandsDirectoryCreation(t *testing.T) { - rb := NewRouterBackup("test-host", "test-user", "", "", 22) - - tempDir := t.TempDir() - outputDir := filepath.Join(tempDir, "new-directory") - - // Test with empty commands to avoid SSH connection - err := rb.BackupCommands([]string{}, outputDir) - if err != nil { - t.Fatalf("BackupCommands failed: %v", err) - } - - // Check if directory was created - if _, err := os.Stat(outputDir); os.IsNotExist(err) { - t.Error("Expected output directory to be created") - } -} - -func TestBackupCommandsFileCreation(t *testing.T) { - rb := NewRouterBackup("test-host", "test-user", "", "", 22) - - tempDir := t.TempDir() - expectedFilePath := filepath.Join(tempDir, "test-host") - - // Test with empty commands to avoid SSH connection - err := rb.BackupCommands([]string{}, tempDir) - if err != nil { - t.Fatalf("BackupCommands failed: %v", err) - } - - // Check if file was created - if _, err := os.Stat(expectedFilePath); os.IsNotExist(err) { - t.Error("Expected output file to be created") - } -} - -// Benchmark tests -func BenchmarkNewRouterBackup(b *testing.B) { - for i := 0; i < b.N; i++ { - NewRouterBackup("bench-host", "bench-user", "bench-pass", "/bench/key", 22) - } -} - -func BenchmarkLoadConfig(b *testing.B) { - // Create a temporary config file - tempDir := b.TempDir() - configPath := filepath.Join(tempDir, "bench-config.yaml") - - configContent := `types: - srlinux: - commands: - - show version - - show platform linecard - -devices: - device1: - user: user1 - type: srlinux - device2: - user: user2 - type: srlinux -` - - err := os.WriteFile(configPath, []byte(configContent), 0644) - if err != nil { - b.Fatalf("Failed to create benchmark config file: %v", err) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := loadConfig(configPath) - if err != nil { - b.Fatalf("Failed to load config: %v", err) - } - } -} - -// Example test to demonstrate usage -func ExampleNewRouterBackup() { - rb := NewRouterBackup("example-host", "admin", "", "/home/user/.ssh/id_rsa", 22) - _ = rb // Use the router backup instance -} - -// Table-driven test for multiple scenarios -func TestRouterBackupCreation(t *testing.T) { - tests := []struct { - name string - hostname string - username string - password string - keyFile string - port int - }{ - {"Basic", "host1", "user1", "pass1", "/key1", 22}, - {"Custom Port", "host2", "user2", "pass2", "/key2", 2222}, - {"No Password", "host3", "user3", "", "/key3", 22}, - {"No Key", "host4", "user4", "pass4", "", 22}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - rb := NewRouterBackup(tt.hostname, tt.username, tt.password, tt.keyFile, tt.port) - - if rb.hostname != tt.hostname { - t.Errorf("Expected hostname '%s', got '%s'", tt.hostname, rb.hostname) - } - if rb.username != tt.username { - t.Errorf("Expected username '%s', got '%s'", tt.username, rb.username) - } - if rb.password != tt.password { - t.Errorf("Expected password '%s', got '%s'", tt.password, rb.password) - } - if rb.keyFile != tt.keyFile { - t.Errorf("Expected keyFile '%s', got '%s'", tt.keyFile, rb.keyFile) - } - if rb.port != tt.port { - t.Errorf("Expected port %d, got %d", tt.port, rb.port) - } - }) - } -} - -// Test !include functionality -func TestProcessIncludes(t *testing.T) { - tempDir := t.TempDir() - - // Create included file - includedPath := filepath.Join(tempDir, "included.yaml") - includedContent := `test-type: - commands: - - show version - - show status` - - err := os.WriteFile(includedPath, []byte(includedContent), 0644) - if err != nil { - t.Fatalf("Failed to create included file: %v", err) - } - - // Create main file with !include - mainPath := filepath.Join(tempDir, "main.yaml") - mainContent := `types: - !include included.yaml -devices: - test-device: - user: testuser - type: test-type` - - err = os.WriteFile(mainPath, []byte(mainContent), 0644) - if err != nil { - t.Fatalf("Failed to create main file: %v", err) - } - - // Process includes - result, err := processIncludes(mainPath) - if err != nil { - t.Fatalf("Failed to process includes: %v", err) - } - - // Check that include was processed - if !strings.Contains(result, "show version") { - t.Error("Expected included content to be present in result") - } - if !strings.Contains(result, "show status") { - t.Error("Expected included content to be present in result") - } - if strings.Contains(result, "!include") { - t.Error("Expected !include directive to be replaced") - } -} - -func TestProcessIncludesWithQuotes(t *testing.T) { - tempDir := t.TempDir() - - // Create included file with spaces in name - includedPath := filepath.Join(tempDir, "file with spaces.yaml") - includedContent := `production-srlinux: - commands: - - show version` - - err := os.WriteFile(includedPath, []byte(includedContent), 0644) - if err != nil { - t.Fatalf("Failed to create included file: %v", err) - } - - // Create main file with quoted !include - mainPath := filepath.Join(tempDir, "main.yaml") - mainContent := `types: - !include "file with spaces.yaml"` - - err = os.WriteFile(mainPath, []byte(mainContent), 0644) - if err != nil { - t.Fatalf("Failed to create main file: %v", err) - } - - // Process includes - result, err := processIncludes(mainPath) - if err != nil { - t.Fatalf("Failed to process includes: %v", err) - } - - // Check that include was processed - if !strings.Contains(result, "production-srlinux") { - t.Error("Expected included content to be present in result") - } -} - -func TestProcessIncludesNonexistentFile(t *testing.T) { - tempDir := t.TempDir() - - // Create main file with include to nonexistent file - mainPath := filepath.Join(tempDir, "main.yaml") - mainContent := `types: - !include nonexistent.yaml` - - err := os.WriteFile(mainPath, []byte(mainContent), 0644) - if err != nil { - t.Fatalf("Failed to create main file: %v", err) - } - - // Process includes should fail - _, err = processIncludes(mainPath) - if err == nil { - t.Error("Expected error for nonexistent include file") - } -} - -func TestLoadConfigWithIncludes(t *testing.T) { - tempDir := t.TempDir() - - // Create device types file - typesPath := filepath.Join(tempDir, "types.yaml") - typesContent := `srlinux: - commands: - - show version - - show platform linecard -eos: - commands: - - show version - - show inventory` - - err := os.WriteFile(typesPath, []byte(typesContent), 0644) - if err != nil { - t.Fatalf("Failed to create types file: %v", err) - } - - // Create main config file with includes - mainPath := filepath.Join(tempDir, "config.yaml") - mainContent := `types: - !include types.yaml -devices: - asw100: - user: admin - type: srlinux - edge-01: - user: operator - type: eos` - - err = os.WriteFile(mainPath, []byte(mainContent), 0644) - if err != nil { - t.Fatalf("Failed to create main config file: %v", err) - } - - // Load configuration - config, err := loadConfig(mainPath) - if err != nil { - t.Fatalf("Failed to load config with includes: %v", err) - } - - // Verify types were loaded correctly - if len(config.Types) != 2 { - t.Errorf("Expected 2 types, got %d", len(config.Types)) - } - - srlinuxType, exists := config.Types["srlinux"] - if !exists { - t.Error("Expected 'srlinux' type to exist") - } - if len(srlinuxType.Commands) != 2 { - t.Errorf("Expected 2 commands for srlinux type, got %d", len(srlinuxType.Commands)) - } - - // Verify devices were loaded correctly - if len(config.Devices) != 2 { - t.Errorf("Expected 2 devices, got %d", len(config.Devices)) - } - - asw100, exists := config.Devices["asw100"] - if !exists { - t.Error("Expected 'asw100' device to exist") - } - if asw100.User != "admin" { - t.Errorf("Expected user 'admin', got '%s'", asw100.User) - } - if asw100.Type != "srlinux" { - t.Errorf("Expected type 'srlinux', got '%s'", asw100.Type) - } -} diff --git a/src/ssh_test.go b/src/ssh_test.go new file mode 100644 index 0000000..7ed06be --- /dev/null +++ b/src/ssh_test.go @@ -0,0 +1,190 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestNewRouterBackup(t *testing.T) { + rb := NewRouterBackup("testhost", "testuser", "testpass", "/path/to/key", 2222) + + if rb.hostname != "testhost" { + t.Errorf("Expected hostname 'testhost', got '%s'", rb.hostname) + } + + if rb.username != "testuser" { + t.Errorf("Expected username 'testuser', got '%s'", rb.username) + } + + if rb.password != "testpass" { + t.Errorf("Expected password 'testpass', got '%s'", rb.password) + } + + if rb.keyFile != "/path/to/key" { + t.Errorf("Expected keyFile '/path/to/key', got '%s'", rb.keyFile) + } + + if rb.port != 2222 { + t.Errorf("Expected port 2222, got %d", rb.port) + } + + if rb.client != nil { + t.Error("Expected client to be nil initially") + } +} + +func TestRunCommandWithoutConnection(t *testing.T) { + rb := NewRouterBackup("testhost", "testuser", "testpass", "", 22) + + _, err := rb.RunCommand("show version") + if err == nil { + t.Error("Expected error when running command without connection") + } + + if err.Error() != "no active connection" { + t.Errorf("Expected 'no active connection' error, got '%s'", err.Error()) + } +} + +func TestBackupCommandsDirectoryCreation(t *testing.T) { + tempDir := t.TempDir() + outputDir := filepath.Join(tempDir, "nonexistent", "backup") + + rb := NewRouterBackup("testhost", "testuser", "testpass", "", 22) + + // This should create the directory even without a connection + // and fail gracefully when trying to run commands + _ = rb.BackupCommands([]string{"show version"}, outputDir) + + // Should not error on directory creation + if _, statErr := os.Stat(outputDir); os.IsNotExist(statErr) { + t.Error("Expected output directory to be created") + } + + // Should create the output file even if commands fail + expectedFile := filepath.Join(outputDir, "testhost") + if _, statErr := os.Stat(expectedFile); os.IsNotExist(statErr) { + t.Error("Expected output file to be created") + } +} + +func TestBackupCommandsEmptyCommands(t *testing.T) { + tempDir := t.TempDir() + + rb := NewRouterBackup("testhost", "testuser", "testpass", "", 22) + + err := rb.BackupCommands([]string{}, tempDir) + if err != nil { + t.Errorf("Expected no error for empty commands list, got %v", err) + } + + // Should still create the output file + expectedFile := filepath.Join(tempDir, "testhost") + if _, statErr := os.Stat(expectedFile); os.IsNotExist(statErr) { + t.Error("Expected output file to be created even for empty commands") + } +} + +func TestDisconnectWithoutConnection(t *testing.T) { + rb := NewRouterBackup("testhost", "testuser", "testpass", "", 22) + + // Should not panic or error when disconnecting without connection + rb.Disconnect() +} + +func TestFindDefaultSSHKey(t *testing.T) { + // Test when no SSH keys exist + originalHome := os.Getenv("HOME") + tempDir := t.TempDir() + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", originalHome) + + keyPath := findDefaultSSHKey() + if keyPath != "" { + t.Errorf("Expected empty string when no SSH keys exist, got '%s'", keyPath) + } + + // Create .ssh directory and a test key + sshDir := filepath.Join(tempDir, ".ssh") + err := os.MkdirAll(sshDir, 0700) + if err != nil { + t.Fatalf("Failed to create .ssh directory: %v", err) + } + + // Create id_rsa key (should be found first) + rsaKeyPath := filepath.Join(sshDir, "id_rsa") + err = os.WriteFile(rsaKeyPath, []byte("fake rsa key"), 0600) + if err != nil { + t.Fatalf("Failed to create RSA key: %v", err) + } + + keyPath = findDefaultSSHKey() + if keyPath != rsaKeyPath { + t.Errorf("Expected to find RSA key at '%s', got '%s'", rsaKeyPath, keyPath) + } + + // Remove RSA key and create ed25519 key + os.Remove(rsaKeyPath) + ed25519KeyPath := filepath.Join(sshDir, "id_ed25519") + err = os.WriteFile(ed25519KeyPath, []byte("fake ed25519 key"), 0600) + if err != nil { + t.Fatalf("Failed to create ed25519 key: %v", err) + } + + keyPath = findDefaultSSHKey() + if keyPath != ed25519KeyPath { + t.Errorf("Expected to find ed25519 key at '%s', got '%s'", ed25519KeyPath, keyPath) + } +} + +func TestFindDefaultSSHKeyHomeError(t *testing.T) { + // Test behavior when HOME environment is invalid + originalHome := os.Getenv("HOME") + os.Unsetenv("HOME") + defer os.Setenv("HOME", originalHome) + + keyPath := findDefaultSSHKey() + if keyPath != "" { + t.Errorf("Expected empty string when HOME is not set, got '%s'", keyPath) + } +} + +func TestBackupCommandsFileOperations(t *testing.T) { + tempDir := t.TempDir() + + rb := NewRouterBackup("testhost", "testuser", "testpass", "", 22) + + // Create some fake commands (they will fail but we can test file operations) + commands := []string{"show version", "show interfaces"} + + err := rb.BackupCommands(commands, tempDir) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Check that output file was created + outputFile := filepath.Join(tempDir, "testhost") + _, err = os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read output file: %v", err) + } + + // File should be created (it will be empty if all commands fail) + // This test just verifies the file creation works +} + +func TestRouterBackupConnectionState(t *testing.T) { + rb := NewRouterBackup("testhost", "testuser", "testpass", "", 22) + + // Initially no client + if rb.client != nil { + t.Error("Expected client to be nil initially") + } + + // After disconnect, should still be nil (safe to call multiple times) + rb.Disconnect() + if rb.client != nil { + t.Error("Expected client to remain nil after disconnect") + } +} \ No newline at end of file