package monitoring import ( "fmt" "math" "testing" "github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/models" "github.com/rcourtman/pulse-go-rewrite/pkg/proxmox" ) func TestNormalizeEndpointHost(t *testing.T) { t.Parallel() cases := []struct { input string want string }{ {"", ""}, {" node.local ", "node.local"}, {"https://node.local:8006/", "node.local"}, {"node.local:8006", "node.local"}, {"node.local/path", "node.local"}, {"https://[2001:db8::1]:8006", "2001:db8::1"}, } for _, tc := range cases { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() if got := normalizeEndpointHost(tc.input); got != tc.want { t.Fatalf("normalizeEndpointHost(%q) = %q, want %q", tc.input, got, tc.want) } }) } } func TestIsLikelyIPAddress(t *testing.T) { t.Parallel() cases := []struct { value string want bool }{ {"", false}, {"example.local", false}, {"10.0.0.1", true}, {"2001:db8::1", true}, {"fe80::1%eth0", true}, } for _, tc := range cases { tc := tc t.Run(tc.value, func(t *testing.T) { t.Parallel() if got := isLikelyIPAddress(tc.value); got != tc.want { t.Fatalf("isLikelyIPAddress(%q) = %v, want %v", tc.value, got, tc.want) } }) } } func TestGetNodeDisplayName(t *testing.T) { t.Parallel() clusterInstance := &config.PVEInstance{ IsCluster: true, Name: "cluster", ClusterEndpoints: []config.ClusterEndpoint{ {NodeName: "node1", Host: "https://node1.local:8006"}, {NodeName: "node2", Host: "", IP: "10.0.0.2"}, }, } cases := []struct { name string instance *config.PVEInstance node string want string }{ {"nil instance trims", nil, " nodeX ", "nodeX"}, {"friendly standalone", &config.PVEInstance{Name: "Friendly"}, "nodeA", "Friendly"}, {"host fallback", &config.PVEInstance{Host: "https://host.local:8006"}, "unknown-node", "host.local"}, {"cluster host label", clusterInstance, "node1", "node1.local"}, {"cluster ip fallback", clusterInstance, "node2", "node2"}, {"cluster base fallback", clusterInstance, "node3", "node3"}, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := getNodeDisplayName(tc.instance, tc.node); got != tc.want { t.Fatalf("getNodeDisplayName(%v, %q) = %q, want %q", tc.instance, tc.node, got, tc.want) } }) } } func TestMergeNVMeTempsIntoDisks(t *testing.T) { t.Parallel() original := []models.PhysicalDisk{ {Node: "nodeA", Instance: "inst", DevPath: "/dev/nvme1n1", Type: "nvme", Temperature: 55}, {Node: "nodeA", Instance: "inst", DevPath: "/dev/nvme0n1", Type: "NVME", Temperature: 60}, {Node: "nodeB", Instance: "inst", DevPath: "/dev/sda", Type: "sata", Temperature: 45}, } nodes := []models.Node{ { Name: "nodeA", Temperature: &models.Temperature{ Available: true, NVMe: []models.NVMeTemp{ {Device: "nvme0n1", Temp: 30.4}, {Device: "nvme1n1", Temp: 31.6}, }, }, }, } merged := mergeNVMeTempsIntoDisks(original, nodes) if got, want := merged[0].Temperature, 32; got != want { t.Fatalf("disk 0 temperature = %d, want %d", got, want) } if got, want := merged[1].Temperature, 30; got != want { t.Fatalf("disk 1 temperature = %d, want %d", got, want) } if got, want := merged[2].Temperature, 45; got != want { t.Fatalf("non-nvme disk temperature changed: got %d want %d", got, want) } if got := original[0].Temperature; got != 55 { t.Fatalf("expected original slice unchanged, got %d", got) } } func TestMergeNVMeTempsIntoDisksClearsMissingOrInvalid(t *testing.T) { t.Parallel() disks := []models.PhysicalDisk{ {Node: "nodeA", Instance: "inst", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 65}, {Node: "nodeC", Instance: "inst", DevPath: "/dev/nvme1n1", Type: "nvme", Temperature: 70}, } nodes := []models.Node{ { Name: "nodeA", Temperature: &models.Temperature{ Available: true, NVMe: []models.NVMeTemp{ {Device: "nvme0n1", Temp: math.NaN()}, }, }, }, } merged := mergeNVMeTempsIntoDisks(disks, nodes) if got := merged[0].Temperature; got != 0 { t.Fatalf("expected NaN temp to reset to 0, got %d", got) } if got := merged[1].Temperature; got != 0 { t.Fatalf("expected missing temps to reset to 0, got %d", got) } } func TestSafePercentage(t *testing.T) { t.Parallel() cases := []struct { used, total float64 want float64 }{ {50, 100, 50}, {0, 0, 0}, {math.NaN(), 100, 0}, {75, math.NaN(), 0}, {10, 0, 0}, {math.Inf(1), 100, 0}, } for _, tc := range cases { tc := tc t.Run(fmt.Sprintf("%v/%v", tc.used, tc.total), func(t *testing.T) { t.Parallel() if got := safePercentage(tc.used, tc.total); got != tc.want { t.Fatalf("safePercentage(%v, %v) = %v, want %v", tc.used, tc.total, got, tc.want) } }) } } func TestSafeFloat(t *testing.T) { t.Parallel() if got := safeFloat(math.NaN()); got != 0 { t.Fatalf("expected NaN to return 0, got %v", got) } if got := safeFloat(math.Inf(1)); got != 0 { t.Fatalf("expected +Inf to return 0, got %v", got) } if got := safeFloat(42.5); got != 42.5 { t.Fatalf("expected value preserved, got %v", got) } } func TestMaxInt64(t *testing.T) { t.Parallel() if got := maxInt64(5, 10); got != 10 { t.Fatalf("expected 10, got %d", got) } if got := maxInt64(-1, -5); got != -1 { t.Fatalf("expected -1, got %d", got) } } func TestConvertPoolInfoToModel(t *testing.T) { t.Parallel() info := proxmox.ZFSPoolInfo{ Name: "tank", Health: "ONLINE", State: "ONLINE", Status: "OK", Scan: "none requested", Devices: []proxmox.ZFSPoolDevice{ { Name: "mirror-0", State: "ONLINE", Leaf: 0, Children: []proxmox.ZFSPoolDevice{ { Name: "nvme0n1", State: "ONLINE", Leaf: 1, Read: 1, Write: 2, Cksum: 3, }, { Name: "nvme1n1", State: "ONLINE", Leaf: 1, Read: 4, Write: 5, Cksum: 6, }, }, }, }, } model := convertPoolInfoToModel(&info) if model == nil { t.Fatalf("expected pool model, got nil") } if model.Name != "tank" { t.Fatalf("expected pool name tank, got %s", model.Name) } if model.State != "ONLINE" { t.Fatalf("expected ONLINE state, got %s", model.State) } if len(model.Devices) != 2 { t.Fatalf("expected 2 leaf devices, got %d", len(model.Devices)) } if model.ReadErrors != 5 || model.WriteErrors != 7 || model.ChecksumErrors != 9 { t.Fatalf("unexpected error totals: read=%d write=%d checksum=%d", model.ReadErrors, model.WriteErrors, model.ChecksumErrors) } } func TestConvertPoolInfoToModelNil(t *testing.T) { t.Parallel() if model := convertPoolInfoToModel(nil); model != nil { t.Fatalf("expected nil result for nil input") } }