package agentapi import ( "context" "net/http" "net/http/httptest" "strings" "testing" ) // hostMetricsStub serves a GET /host/metrics payload with a populated host block (CPU temp set) // and one storage target carrying a thin-pool + SMART temp. func hostMetricsStub(cpuTempNull bool) (*httptest.Server, string) { temp := `47` if cpuTempNull { temp = `null` } mux := http.NewServeMux() mux.HandleFunc("GET /host/metrics", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"ok":true,"data":{ "vmid":8200, "host":{"node":"demo-felhom","cpu_percent":12.5,"memory_total_bytes":17179869184, "memory_used_bytes":4294967296,"memory_percent":25,"loadavg":["0.10","0.20","0.15"], "uptime_seconds":86400,"cpu_temp_c":` + temp + `}, "storage_targets":[ {"name":"local-lvm","type":"lvmthin","state":"attached","reachable":true, "total_bytes":100000000000,"used_bytes":42000000000,"used_fraction":0.42, "thin_pool":{"data_used_fraction":0.42,"metadata_used_fraction":null}, "smart":{"health":"PASSED","temperature_c":38,"percentage_used":2}}, {"name":"usb-backup","type":"usb","state":"attached","reachable":true, "total_bytes":2000000000000,"used_bytes":500000000000,"used_fraction":0.25, "smart":{"health":"PASSED","temperature_c":null,"percentage_used":null}} ]}}`)) }) s := httptest.NewTLSServer(mux) return s, strings.TrimPrefix(s.URL, "https://") } func TestHostMetrics_DecodesHostAndStorage(t *testing.T) { s, ep := hostMetricsStub(false) defer s.Close() c := clientFor(t, s, ep) resp, err := c.HostMetrics(context.Background()) if err != nil { t.Fatal(err) } if resp.Host.Node != "demo-felhom" || resp.Host.CPUPercent != 12.5 { t.Fatalf("host = %+v", resp.Host) } if resp.Host.CPUTempC == nil || *resp.Host.CPUTempC != 47 { t.Fatalf("cpu_temp_c = %v, want 47", resp.Host.CPUTempC) } if len(resp.StorageTargets) != 2 { t.Fatalf("storage targets = %d, want 2", len(resp.StorageTargets)) } lvm := resp.StorageTargets[0] if lvm.ThinPool == nil || lvm.ThinPool.DataUsedFraction != 0.42 { t.Errorf("thin_pool = %+v", lvm.ThinPool) } if lvm.Smart.TemperatureC == nil || *lvm.Smart.TemperatureC != 38 { t.Errorf("smart temp = %v, want 38", lvm.Smart.TemperatureC) } if lvm.Smart.PercentageUsed == nil || *lvm.Smart.PercentageUsed != 2 { t.Errorf("smart wear = %v, want 2", lvm.Smart.PercentageUsed) } // USB drive: SMART temp/wear are null (USB bridge exposes no SMART) → graceful null. if resp.StorageTargets[1].Smart.TemperatureC != nil { t.Errorf("usb smart temp = %v, want nil", resp.StorageTargets[1].Smart.TemperatureC) } } // A null cpu_temp_c decodes to a nil pointer (the UI renders "n/a"). func TestHostMetrics_NullCPUTemp(t *testing.T) { s, ep := hostMetricsStub(true) defer s.Close() c := clientFor(t, s, ep) resp, err := c.HostMetrics(context.Background()) if err != nil { t.Fatal(err) } if resp.Host.CPUTempC != nil { t.Fatalf("cpu_temp_c = %v, want nil (n/a)", resp.Host.CPUTempC) } }