From dfc0059bd9dc6e8a0127bdea2b0e4955af106ca7 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 1 Dec 2025 19:04:23 +0000 Subject: [PATCH] test: Add tests for convertDockerSwarmInfo, namespacePathsForDatastore, preserveFailedStorageBackups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - convertDockerSwarmInfo: 66.7%→100% (4 cases for nil, empty, populated structs) - namespacePathsForDatastore: 92.3%→100% (removed unreachable dead code) - preserveFailedStorageBackups: 91.3%→100% (6 cases for filtering, deduplication) --- internal/monitoring/monitor.go | 3 - internal/monitoring/monitor_test.go | 108 +++++++++++++ .../storage_backup_preserve_test.go | 146 ++++++++++++++++++ 3 files changed, 254 insertions(+), 3 deletions(-) diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index ae4515c7b..9cf138358 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -8134,9 +8134,6 @@ func namespacePathsForDatastore(ds models.PBSDatastore) []string { seen[path] = struct{}{} paths = append(paths, path) } - if len(paths) == 0 { - paths = append(paths, "") - } return paths } diff --git a/internal/monitoring/monitor_test.go b/internal/monitoring/monitor_test.go index 85613a4cf..719d3b380 100644 --- a/internal/monitoring/monitor_test.go +++ b/internal/monitoring/monitor_test.go @@ -7,6 +7,7 @@ import ( "github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/models" + agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker" ) func TestParseDurationEnv(t *testing.T) { @@ -1677,3 +1678,110 @@ func TestSchedulerHealth(t *testing.T) { } }) } + +func TestConvertDockerSwarmInfo(t *testing.T) { + tests := []struct { + name string + input *agentsdocker.SwarmInfo + expected *models.DockerSwarmInfo + }{ + { + name: "nil input returns nil", + input: nil, + expected: nil, + }, + { + name: "empty struct returns empty struct", + input: &agentsdocker.SwarmInfo{}, + expected: &models.DockerSwarmInfo{}, + }, + { + name: "all fields populated", + input: &agentsdocker.SwarmInfo{ + NodeID: "node-abc123", + NodeRole: "manager", + LocalState: "active", + ControlAvailable: true, + ClusterID: "cluster-xyz789", + ClusterName: "my-swarm", + Scope: "swarm", + Error: "", + }, + expected: &models.DockerSwarmInfo{ + NodeID: "node-abc123", + NodeRole: "manager", + LocalState: "active", + ControlAvailable: true, + ClusterID: "cluster-xyz789", + ClusterName: "my-swarm", + Scope: "swarm", + Error: "", + }, + }, + { + name: "worker node with error", + input: &agentsdocker.SwarmInfo{ + NodeID: "node-worker1", + NodeRole: "worker", + LocalState: "pending", + ControlAvailable: false, + ClusterID: "", + ClusterName: "", + Scope: "local", + Error: "connection refused", + }, + expected: &models.DockerSwarmInfo{ + NodeID: "node-worker1", + NodeRole: "worker", + LocalState: "pending", + ControlAvailable: false, + ClusterID: "", + ClusterName: "", + Scope: "local", + Error: "connection refused", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertDockerSwarmInfo(tt.input) + + if tt.expected == nil { + if result != nil { + t.Errorf("expected nil, got %+v", result) + } + return + } + + if result == nil { + t.Fatal("expected non-nil result") + } + + if result.NodeID != tt.expected.NodeID { + t.Errorf("NodeID: expected %q, got %q", tt.expected.NodeID, result.NodeID) + } + if result.NodeRole != tt.expected.NodeRole { + t.Errorf("NodeRole: expected %q, got %q", tt.expected.NodeRole, result.NodeRole) + } + if result.LocalState != tt.expected.LocalState { + t.Errorf("LocalState: expected %q, got %q", tt.expected.LocalState, result.LocalState) + } + if result.ControlAvailable != tt.expected.ControlAvailable { + t.Errorf("ControlAvailable: expected %v, got %v", tt.expected.ControlAvailable, result.ControlAvailable) + } + if result.ClusterID != tt.expected.ClusterID { + t.Errorf("ClusterID: expected %q, got %q", tt.expected.ClusterID, result.ClusterID) + } + if result.ClusterName != tt.expected.ClusterName { + t.Errorf("ClusterName: expected %q, got %q", tt.expected.ClusterName, result.ClusterName) + } + if result.Scope != tt.expected.Scope { + t.Errorf("Scope: expected %q, got %q", tt.expected.Scope, result.Scope) + } + if result.Error != tt.expected.Error { + t.Errorf("Error: expected %q, got %q", tt.expected.Error, result.Error) + } + }) + } +} diff --git a/internal/monitoring/storage_backup_preserve_test.go b/internal/monitoring/storage_backup_preserve_test.go index 24fccb492..e56e70d7e 100644 --- a/internal/monitoring/storage_backup_preserve_test.go +++ b/internal/monitoring/storage_backup_preserve_test.go @@ -92,6 +92,152 @@ func TestPreserveFailedStorageBackupsSkipsDuplicates(t *testing.T) { } } +func TestPreserveFailedStorageBackupsEmptyPreserveMap(t *testing.T) { + instance := "pve01" + current := []models.StorageBackup{ + {ID: "backup1", Instance: instance, Storage: "local"}, + } + snapshot := models.StateSnapshot{ + PVEBackups: models.PVEBackups{ + StorageBackups: []models.StorageBackup{ + {ID: "backup2", Instance: instance, Storage: "nas-share"}, + }, + }, + } + + merged, storages := preserveFailedStorageBackups(instance, snapshot, nil, current) + + if len(merged) != 1 { + t.Fatalf("expected current unchanged with 1 backup, got %d", len(merged)) + } + if storages != nil { + t.Fatalf("expected nil storages list, got %v", storages) + } + + // Also test empty map (not nil) + merged2, storages2 := preserveFailedStorageBackups(instance, snapshot, map[string]struct{}{}, current) + if len(merged2) != 1 { + t.Fatalf("expected current unchanged with 1 backup, got %d", len(merged2)) + } + if storages2 != nil { + t.Fatalf("expected nil storages list for empty map, got %v", storages2) + } +} + +func TestPreserveFailedStorageBackupsNoMatchingBackups(t *testing.T) { + instance := "pve01" + current := []models.StorageBackup{ + {ID: "backup1", Instance: instance, Storage: "local"}, + } + snapshot := models.StateSnapshot{ + PVEBackups: models.PVEBackups{ + StorageBackups: []models.StorageBackup{ + {ID: "backup2", Instance: instance, Storage: "other-storage"}, + }, + }, + } + toPreserve := map[string]struct{}{ + "nas-share": {}, // Storage not in snapshot + } + + merged, storages := preserveFailedStorageBackups(instance, snapshot, toPreserve, current) + + if len(merged) != 1 { + t.Fatalf("expected current unchanged with 1 backup, got %d", len(merged)) + } + if storages != nil { + t.Fatalf("expected nil storages list when no matches, got %v", storages) + } +} + +func TestPreserveFailedStorageBackupsWrongInstance(t *testing.T) { + instance := "pve01" + current := []models.StorageBackup{ + {ID: "backup1", Instance: instance, Storage: "local"}, + } + snapshot := models.StateSnapshot{ + PVEBackups: models.PVEBackups{ + StorageBackups: []models.StorageBackup{ + {ID: "backup2", Instance: "pve02", Storage: "nas-share"}, // Wrong instance + }, + }, + } + toPreserve := map[string]struct{}{ + "nas-share": {}, + } + + merged, storages := preserveFailedStorageBackups(instance, snapshot, toPreserve, current) + + if len(merged) != 1 { + t.Fatalf("expected current unchanged (wrong instance skipped), got %d backups", len(merged)) + } + if storages != nil { + t.Fatalf("expected nil storages list, got %v", storages) + } +} + +func TestPreserveFailedStorageBackupsStorageNotInPreserveMap(t *testing.T) { + instance := "pve01" + current := []models.StorageBackup{ + {ID: "backup1", Instance: instance, Storage: "local"}, + } + snapshot := models.StateSnapshot{ + PVEBackups: models.PVEBackups{ + StorageBackups: []models.StorageBackup{ + {ID: "backup2", Instance: instance, Storage: "nas-share"}, + {ID: "backup3", Instance: instance, Storage: "other-storage"}, + }, + }, + } + toPreserve := map[string]struct{}{ + "nas-share": {}, // Only nas-share should be preserved + } + + merged, storages := preserveFailedStorageBackups(instance, snapshot, toPreserve, current) + + if len(merged) != 2 { + t.Fatalf("expected 2 backups (original + nas-share), got %d", len(merged)) + } + if len(storages) != 1 || storages[0] != "nas-share" { + t.Fatalf("expected [nas-share], got %v", storages) + } + // Verify other-storage was not added + for _, b := range merged { + if b.Storage == "other-storage" { + t.Fatal("other-storage should not have been preserved") + } + } +} + +func TestPreserveFailedStorageBackupsSortedStorageNames(t *testing.T) { + instance := "pve01" + current := []models.StorageBackup{} + snapshot := models.StateSnapshot{ + PVEBackups: models.PVEBackups{ + StorageBackups: []models.StorageBackup{ + {ID: "backup1", Instance: instance, Storage: "zebra-storage"}, + {ID: "backup2", Instance: instance, Storage: "alpha-storage"}, + {ID: "backup3", Instance: instance, Storage: "middle-storage"}, + }, + }, + } + toPreserve := map[string]struct{}{ + "zebra-storage": {}, + "alpha-storage": {}, + "middle-storage": {}, + } + + merged, storages := preserveFailedStorageBackups(instance, snapshot, toPreserve, current) + + if len(merged) != 3 { + t.Fatalf("expected 3 backups, got %d", len(merged)) + } + expected := []string{"alpha-storage", "middle-storage", "zebra-storage"} + if !slices.Equal(storages, expected) { + t.Fatalf("expected sorted storages %v, got %v", expected, storages) + } +} + func TestStorageNamesForNode(t *testing.T) { tests := []struct { name string