Rework tests, and move all source files into main package
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
package config
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
319
src/config_test.go
Normal file
319
src/config_test.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"router_backup/config"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@ -38,7 +37,7 @@ func main() {
|
|||||||
fmt.Printf("IPng Networks Router Backup v%s\n", Version)
|
fmt.Printf("IPng Networks Router Backup v%s\n", Version)
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
cfg, err := config.ConfigRead(yamlFiles)
|
cfg, err := ConfigRead(yamlFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@ -63,7 +62,7 @@ func main() {
|
|||||||
// Filter devices if --host flags are provided
|
// Filter devices if --host flags are provided
|
||||||
devicesToProcess := cfg.Devices
|
devicesToProcess := cfg.Devices
|
||||||
if len(hostFilter) > 0 {
|
if len(hostFilter) > 0 {
|
||||||
devicesToProcess = make(map[string]config.Device)
|
devicesToProcess = make(map[string]Device)
|
||||||
for _, hostname := range hostFilter {
|
for _, hostname := range hostFilter {
|
||||||
if deviceConfig, exists := cfg.Devices[hostname]; exists {
|
if deviceConfig, exists := cfg.Devices[hostname]; exists {
|
||||||
devicesToProcess[hostname] = deviceConfig
|
devicesToProcess[hostname] = deviceConfig
|
||||||
|
474
src/main_test.go
474
src/main_test.go
@ -1,474 +0,0 @@
|
|||||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
190
src/ssh_test.go
Normal file
190
src/ssh_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user