Pulse/internal/monitoring/ceph_test.go
rcourtman 3b5c1347d1 ADA: Add unit tests for Ceph helper functions
Test coverage for isCephStorageType, countServiceDaemons,
extractCephCheckSummary, and summarizeCephHealth functions.
These parse Ceph storage types and health status JSON.
2025-11-29 19:49:43 +00:00

383 lines
8.5 KiB
Go

package monitoring
import (
"encoding/json"
"testing"
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
)
func TestIsCephStorageType(t *testing.T) {
t.Parallel()
tests := []struct {
name string
storageType string
want bool
}{
// Valid Ceph types
{"rbd lowercase", "rbd", true},
{"cephfs lowercase", "cephfs", true},
{"ceph lowercase", "ceph", true},
// Case variations
{"RBD uppercase", "RBD", true},
{"CephFS mixed case", "CephFS", true},
{"CEPH uppercase", "CEPH", true},
// Whitespace handling
{"rbd with leading space", " rbd", true},
{"rbd with trailing space", "rbd ", true},
{"rbd with surrounding spaces", " rbd ", true},
// Non-Ceph storage types
{"local storage", "local", false},
{"dir storage", "dir", false},
{"nfs storage", "nfs", false},
{"lvm storage", "lvm", false},
{"zfs storage", "zfs", false},
{"zfspool storage", "zfspool", false},
{"iscsi storage", "iscsi", false},
// Edge cases
{"empty string", "", false},
{"whitespace only", " ", false},
{"partial match rbd", "rbd-pool", false},
{"partial match ceph", "ceph-pool", false},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := isCephStorageType(tc.storageType); got != tc.want {
t.Fatalf("isCephStorageType(%q) = %v, want %v", tc.storageType, got, tc.want)
}
})
}
}
func TestCountServiceDaemons(t *testing.T) {
t.Parallel()
tests := []struct {
name string
services map[string]proxmox.CephServiceDefinition
serviceType string
want int
}{
{
name: "nil services map",
services: nil,
serviceType: "mon",
want: 0,
},
{
name: "empty services map",
services: map[string]proxmox.CephServiceDefinition{},
serviceType: "mon",
want: 0,
},
{
name: "service type not found",
services: map[string]proxmox.CephServiceDefinition{
"mon": {
Daemons: map[string]proxmox.CephServiceDaemon{
"a": {Host: "node1", Status: "running"},
},
},
},
serviceType: "osd",
want: 0,
},
{
name: "single daemon",
services: map[string]proxmox.CephServiceDefinition{
"mon": {
Daemons: map[string]proxmox.CephServiceDaemon{
"a": {Host: "node1", Status: "running"},
},
},
},
serviceType: "mon",
want: 1,
},
{
name: "multiple daemons",
services: map[string]proxmox.CephServiceDefinition{
"mon": {
Daemons: map[string]proxmox.CephServiceDaemon{
"a": {Host: "node1", Status: "running"},
"b": {Host: "node2", Status: "running"},
"c": {Host: "node3", Status: "running"},
},
},
},
serviceType: "mon",
want: 3,
},
{
name: "multiple service types",
services: map[string]proxmox.CephServiceDefinition{
"mon": {
Daemons: map[string]proxmox.CephServiceDaemon{
"a": {Host: "node1", Status: "running"},
"b": {Host: "node2", Status: "running"},
},
},
"mgr": {
Daemons: map[string]proxmox.CephServiceDaemon{
"node1": {Host: "node1", Status: "active"},
},
},
},
serviceType: "mgr",
want: 1,
},
{
name: "empty daemons map",
services: map[string]proxmox.CephServiceDefinition{
"mon": {
Daemons: map[string]proxmox.CephServiceDaemon{},
},
},
serviceType: "mon",
want: 0,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := countServiceDaemons(tc.services, tc.serviceType); got != tc.want {
t.Fatalf("countServiceDaemons(%v, %q) = %d, want %d", tc.services, tc.serviceType, got, tc.want)
}
})
}
}
func TestExtractCephCheckSummary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw json.RawMessage
want string
}{
// Empty/nil cases
{
name: "nil raw message",
raw: nil,
want: "",
},
{
name: "empty raw message",
raw: json.RawMessage{},
want: "",
},
// Object format with message field
{
name: "object with message",
raw: json.RawMessage(`{"message": "1 slow ops"}`),
want: "1 slow ops",
},
{
name: "object with summary",
raw: json.RawMessage(`{"summary": "health warning"}`),
want: "health warning",
},
{
name: "object with both message and summary prefers message",
raw: json.RawMessage(`{"message": "from message", "summary": "from summary"}`),
want: "from message",
},
{
name: "object with empty message falls back to summary",
raw: json.RawMessage(`{"message": "", "summary": "fallback summary"}`),
want: "fallback summary",
},
// Array format
{
name: "array with single item message",
raw: json.RawMessage(`[{"message": "array message"}]`),
want: "array message",
},
{
name: "array with single item summary",
raw: json.RawMessage(`[{"summary": "array summary"}]`),
want: "array summary",
},
{
name: "array with multiple items returns first",
raw: json.RawMessage(`[{"message": "first"}, {"message": "second"}]`),
want: "first",
},
{
name: "array skips empty to find message",
raw: json.RawMessage(`[{"message": ""}, {"message": "second"}]`),
want: "second",
},
{
name: "empty array",
raw: json.RawMessage(`[]`),
want: "",
},
// Plain string format
{
name: "plain string",
raw: json.RawMessage(`"simple string message"`),
want: "simple string message",
},
{
name: "empty string",
raw: json.RawMessage(`""`),
want: "",
},
// Invalid JSON
{
name: "invalid JSON",
raw: json.RawMessage(`{invalid`),
want: "",
},
{
name: "number value",
raw: json.RawMessage(`123`),
want: "",
},
{
name: "boolean value",
raw: json.RawMessage(`true`),
want: "",
},
{
name: "null value",
raw: json.RawMessage(`null`),
want: "",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := extractCephCheckSummary(tc.raw); got != tc.want {
t.Fatalf("extractCephCheckSummary(%s) = %q, want %q", tc.raw, got, tc.want)
}
})
}
}
func TestSummarizeCephHealth(t *testing.T) {
t.Parallel()
tests := []struct {
name string
status *proxmox.CephStatus
want string
}{
{
name: "nil status",
status: nil,
want: "",
},
{
name: "empty status",
status: &proxmox.CephStatus{},
want: "",
},
{
name: "single summary message",
status: &proxmox.CephStatus{
Health: proxmox.CephHealth{
Summary: []proxmox.CephHealthSummary{
{Message: "HEALTH_WARN"},
},
},
},
want: "HEALTH_WARN",
},
{
name: "summary with summary field instead of message",
status: &proxmox.CephStatus{
Health: proxmox.CephHealth{
Summary: []proxmox.CephHealthSummary{
{Summary: "1 pool(s) have no replicas configured"},
},
},
},
want: "1 pool(s) have no replicas configured",
},
{
name: "multiple summary messages",
status: &proxmox.CephStatus{
Health: proxmox.CephHealth{
Summary: []proxmox.CephHealthSummary{
{Message: "first warning"},
{Message: "second warning"},
},
},
},
want: "first warning; second warning",
},
{
name: "health check with summary object",
status: &proxmox.CephStatus{
Health: proxmox.CephHealth{
Checks: map[string]proxmox.CephHealthCheckRaw{
"SLOW_OPS": {
Summary: json.RawMessage(`{"message": "1 slow ops"}`),
},
},
},
},
want: "SLOW_OPS: 1 slow ops",
},
{
name: "health check with detail fallback",
status: &proxmox.CephStatus{
Health: proxmox.CephHealth{
Checks: map[string]proxmox.CephHealthCheckRaw{
"OSD_DOWN": {
Summary: json.RawMessage(`{}`),
Detail: []proxmox.CephCheckDetail{
{Message: "osd.0 is down"},
},
},
},
},
},
want: "OSD_DOWN: osd.0 is down",
},
{
name: "combined summary and checks",
status: &proxmox.CephStatus{
Health: proxmox.CephHealth{
Summary: []proxmox.CephHealthSummary{
{Message: "HEALTH_WARN"},
},
Checks: map[string]proxmox.CephHealthCheckRaw{
"PG_DEGRADED": {
Summary: json.RawMessage(`{"message": "Degraded data redundancy"}`),
},
},
},
},
want: "HEALTH_WARN; PG_DEGRADED: Degraded data redundancy",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := summarizeCephHealth(tc.status); got != tc.want {
t.Fatalf("summarizeCephHealth() = %q, want %q", got, tc.want)
}
})
}
}