mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 03:33:53 +00:00
Add report engine and cluster client coverage
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClusterClient_GetCephDF(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/cluster/ceph/df" {
|
||||
fmt.Fprint(w, `{"data":{"data":{"stats":{"total_bytes":100,"total_used_bytes":20}}}}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
df, err := cc.GetCephDF(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetCephDF failed: %v", err)
|
||||
}
|
||||
if df == nil || df.Data.Stats.TotalBytes != 100 {
|
||||
t.Fatalf("unexpected ceph df: %+v", df)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetReplicationStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/api2/json/nodes":
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
case "/api2/json/cluster/replication":
|
||||
fmt.Fprint(w, `{"data":[{"id":"job1","guest":"vm/100","source":"node1"}]}`)
|
||||
case "/api2/json/nodes/node1/replication/job1/status":
|
||||
fmt.Fprint(w, `{"data":{"last_sync":1700000000,"duration":10,"fail_count":0,"state":"ok"}}`)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
jobs, err := cc.GetReplicationStatus(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetReplicationStatus failed: %v", err)
|
||||
}
|
||||
if len(jobs) != 1 || jobs[0].ID != "job1" {
|
||||
t.Fatalf("unexpected jobs: %+v", jobs)
|
||||
}
|
||||
if jobs[0].LastSyncUnix == 0 || jobs[0].State == "" {
|
||||
t.Fatalf("expected enriched status fields, got %+v", jobs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_IsClusterMemberFalseOnError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/cluster/status" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, `{"data":null}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
member, err := cc.IsClusterMember(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("IsClusterMember returned error: %v", err)
|
||||
}
|
||||
if member {
|
||||
t.Fatal("expected IsClusterMember to return false when status fetch fails")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClusterClient_GetContainerInterfaces_ErrorStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/lxc/101/interfaces" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, "boom")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
_, err := cc.GetContainerInterfaces(context.Background(), "node1", 101)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-200 response")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestClusterClient_GetZFSPoolsWithDetails(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/api2/json/nodes":
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
case "/api2/json/nodes/node1/disks/zfs":
|
||||
fmt.Fprint(w, `{"data":[{"name":"rpool","health":"ONLINE"}]}`)
|
||||
case "/api2/json/nodes/node1/disks/zfs/rpool":
|
||||
fmt.Fprint(w, `{"data":{"name":"rpool","state":"ONLINE","status":"ok","scan":"none","errors":"0"}}`)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
pools, err := cc.GetZFSPoolsWithDetails(context.Background(), "node1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetZFSPoolsWithDetails failed: %v", err)
|
||||
}
|
||||
if len(pools) != 1 || pools[0].Name != "rpool" {
|
||||
t.Fatalf("unexpected pools: %+v", pools)
|
||||
}
|
||||
if pools[0].State != "ONLINE" {
|
||||
t.Fatalf("expected state ONLINE, got %q", pools[0].State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_IsQuorate(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/cluster/status" {
|
||||
fmt.Fprint(w, `{"data":[{"type":"cluster","name":"pve","quorate":1}]}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
quorate, err := cc.IsQuorate(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("IsQuorate failed: %v", err)
|
||||
}
|
||||
if !quorate {
|
||||
t.Fatal("expected quorate true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_IsQuorateStandalone(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/cluster/status" {
|
||||
fmt.Fprint(w, `{"data":[{"type":"node","name":"node1"}]}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
quorate, err := cc.IsQuorate(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("IsQuorate failed: %v", err)
|
||||
}
|
||||
if !quorate {
|
||||
t.Fatal("expected quorate true for standalone node")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetDisks_NoHealthyNodes(t *testing.T) {
|
||||
cc := &ClusterClient{
|
||||
name: "test",
|
||||
endpoints: []string{"node1", "node2"},
|
||||
clients: make(map[string]*Client),
|
||||
nodeHealth: map[string]bool{"node1": false, "node2": false},
|
||||
lastError: make(map[string]string),
|
||||
lastHealthCheck: map[string]time.Time{"node1": time.Now(), "node2": time.Now()},
|
||||
rateLimitUntil: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
disks, err := cc.GetDisks(context.Background(), "node1")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if len(disks) != 0 {
|
||||
t.Fatalf("expected empty disks on no healthy nodes, got %+v", disks)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClusterClient_GetNodeStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/status" {
|
||||
fmt.Fprint(w, `{"data":{"cpu":0.25,"uptime":123}}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
status, err := cc.GetNodeStatus(context.Background(), "node1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetNodeStatus failed: %v", err)
|
||||
}
|
||||
if status == nil || status.Uptime != 123 {
|
||||
t.Fatalf("unexpected status: %+v", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetAllStorage(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/storage" {
|
||||
fmt.Fprint(w, `{"data":[{"storage":"local"}]}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
storage, err := cc.GetAllStorage(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllStorage failed: %v", err)
|
||||
}
|
||||
if len(storage) != 1 || storage[0].Storage != "local" {
|
||||
t.Fatalf("unexpected storage result: %+v", storage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetVMStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/qemu/100/status/current" {
|
||||
fmt.Fprint(w, `{"data":{"status":"running","cpu":0.5,"uptime":10,"agent":1}}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
status, err := cc.GetVMStatus(context.Background(), "node1", 100)
|
||||
if err != nil {
|
||||
t.Fatalf("GetVMStatus failed: %v", err)
|
||||
}
|
||||
if status == nil || status.Status != "running" {
|
||||
t.Fatalf("unexpected VM status: %+v", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetContainerStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/lxc/101/status/current" {
|
||||
fmt.Fprint(w, `{"data":{"status":"running"}}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
status, err := cc.GetContainerStatus(context.Background(), "node1", 101)
|
||||
if err != nil {
|
||||
t.Fatalf("GetContainerStatus failed: %v", err)
|
||||
}
|
||||
if status == nil || status.Status != "running" {
|
||||
t.Fatalf("unexpected container status: %+v", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetVMConfig(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/qemu/100/config" {
|
||||
fmt.Fprint(w, `{"data":{"name":"vm-100"}}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
config, err := cc.GetVMConfig(context.Background(), "node1", 100)
|
||||
if err != nil {
|
||||
t.Fatalf("GetVMConfig failed: %v", err)
|
||||
}
|
||||
if config["name"] != "vm-100" {
|
||||
t.Fatalf("unexpected VM config: %+v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetVMAgentInfo(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/qemu/100/agent/get-osinfo" {
|
||||
fmt.Fprint(w, `{"data":{"name":"debian"}}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
info, err := cc.GetVMAgentInfo(context.Background(), "node1", 100)
|
||||
if err != nil {
|
||||
t.Fatalf("GetVMAgentInfo failed: %v", err)
|
||||
}
|
||||
if info["name"] != "debian" {
|
||||
t.Fatalf("unexpected agent info: %+v", info)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetVMAgentVersion(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/qemu/100/agent/info" {
|
||||
fmt.Fprint(w, `{"data":{"result":{"version":"1.2.3"}}}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
version, err := cc.GetVMAgentVersion(context.Background(), "node1", 100)
|
||||
if err != nil {
|
||||
t.Fatalf("GetVMAgentVersion failed: %v", err)
|
||||
}
|
||||
if version != "1.2.3" {
|
||||
t.Fatalf("unexpected version: %s", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetVMNetworkInterfaces(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/qemu/100/agent/network-get-interfaces" {
|
||||
fmt.Fprint(w, `{"data":{"result":[{"name":"eth0"}]}}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
ifaces, err := cc.GetVMNetworkInterfaces(context.Background(), "node1", 100)
|
||||
if err != nil {
|
||||
t.Fatalf("GetVMNetworkInterfaces failed: %v", err)
|
||||
}
|
||||
if len(ifaces) != 1 || ifaces[0].Name != "eth0" {
|
||||
t.Fatalf("unexpected interfaces: %+v", ifaces)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetContainerInterfaces(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/lxc/101/interfaces" {
|
||||
fmt.Fprint(w, `{"data":[{"name":"eth0","ip":"10.0.0.2"}]}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
ifaces, err := cc.GetContainerInterfaces(context.Background(), "node1", 101)
|
||||
if err != nil {
|
||||
t.Fatalf("GetContainerInterfaces failed: %v", err)
|
||||
}
|
||||
if len(ifaces) != 1 || ifaces[0].Name != "eth0" {
|
||||
t.Fatalf("unexpected interfaces: %+v", ifaces)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetClusterResources(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/api2/json/cluster/resources") {
|
||||
fmt.Fprint(w, `{"data":[{"id":"qemu/100","type":"qemu","node":"node1"}]}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
resources, err := cc.GetClusterResources(context.Background(), "qemu")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClusterResources failed: %v", err)
|
||||
}
|
||||
if len(resources) != 1 || resources[0].ID != "qemu/100" {
|
||||
t.Fatalf("unexpected resources: %+v", resources)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetClusterStatusAndIsClusterMember(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/cluster/status" {
|
||||
fmt.Fprint(w, `{"data":[{"type":"cluster","name":"pve"},{"type":"node","name":"node1"}]}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
status, err := cc.GetClusterStatus(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetClusterStatus failed: %v", err)
|
||||
}
|
||||
if len(status) != 2 {
|
||||
t.Fatalf("unexpected cluster status: %+v", status)
|
||||
}
|
||||
|
||||
member, err := cc.IsClusterMember(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("IsClusterMember failed: %v", err)
|
||||
}
|
||||
if !member {
|
||||
t.Fatal("expected cluster membership to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetZFSPoolStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/disks/zfs" {
|
||||
fmt.Fprint(w, `{"data":[{"name":"rpool","health":"ONLINE"}]}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
pools, err := cc.GetZFSPoolStatus(context.Background(), "node1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetZFSPoolStatus failed: %v", err)
|
||||
}
|
||||
if len(pools) != 1 || pools[0].Name != "rpool" {
|
||||
t.Fatalf("unexpected pools: %+v", pools)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetDisks(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/disks/list" {
|
||||
fmt.Fprint(w, `{"data":[{"devpath":"/dev/sda","model":"Samsung","wearout":"N/A"}]}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
disks, err := cc.GetDisks(context.Background(), "node1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetDisks failed: %v", err)
|
||||
}
|
||||
if len(disks) != 1 || disks[0].DevPath != "/dev/sda" {
|
||||
t.Fatalf("unexpected disks: %+v", disks)
|
||||
}
|
||||
if disks[0].Wearout != wearoutUnknown {
|
||||
t.Fatalf("expected wearout unknown, got %d", disks[0].Wearout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClient_GetNodePendingUpdates(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api2/json/nodes/node1/apt/update" {
|
||||
fmt.Fprint(w, `{"data":[{"Package":"vim","Version":"2"}]}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := ClientConfig{Host: server.URL, TokenName: "u@p!t", TokenValue: "v"}
|
||||
cc := NewClusterClient("test", cfg, []string{server.URL}, nil)
|
||||
|
||||
updates, err := cc.GetNodePendingUpdates(context.Background(), "node1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetNodePendingUpdates failed: %v", err)
|
||||
}
|
||||
if len(updates) != 1 || updates[0].Package != "vim" {
|
||||
t.Fatalf("unexpected updates: %+v", updates)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetHealthyClientSingleEndpointFallback(t *testing.T) {
|
||||
endpoint := "http://example.invalid"
|
||||
cc := &ClusterClient{
|
||||
name: "single",
|
||||
endpoints: []string{endpoint},
|
||||
clients: map[string]*Client{endpoint: {}},
|
||||
nodeHealth: map[string]bool{endpoint: false},
|
||||
lastError: make(map[string]string),
|
||||
lastHealthCheck: make(map[string]time.Time),
|
||||
rateLimitUntil: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
client, err := cc.getHealthyClient(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("getHealthyClient failed: %v", err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatal("expected client, got nil")
|
||||
}
|
||||
if !cc.nodeHealth[endpoint] {
|
||||
t.Fatal("expected endpoint to be marked healthy after fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteWithFailoverUsesRecoveredEndpoint(t *testing.T) {
|
||||
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node1","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, `{"data":{}}`)
|
||||
}))
|
||||
defer server1.Close()
|
||||
|
||||
server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api2/json/nodes" {
|
||||
fmt.Fprint(w, `{"data":[{"node":"node2","status":"online"}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, `{"data":{}}`)
|
||||
}))
|
||||
defer server2.Close()
|
||||
|
||||
cfg := ClientConfig{
|
||||
Host: server1.URL,
|
||||
TokenName: "u@p!t",
|
||||
TokenValue: "v",
|
||||
VerifySSL: false,
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
cc := NewClusterClient("test", cfg, []string{server1.URL, server2.URL}, nil)
|
||||
|
||||
// Force deterministic selection: only server1 healthy, server2 unhealthy.
|
||||
cc.mu.Lock()
|
||||
cc.nodeHealth[server1.URL] = true
|
||||
cc.nodeHealth[server2.URL] = false
|
||||
cc.lastHealthCheck[server2.URL] = time.Now().Add(-11 * time.Second)
|
||||
cc.mu.Unlock()
|
||||
|
||||
usedEndpoints := make([]string, 0, 2)
|
||||
err := cc.executeWithFailover(context.Background(), func(c *Client) error {
|
||||
endpoint := ""
|
||||
cc.mu.RLock()
|
||||
for ep, client := range cc.clients {
|
||||
if client == c {
|
||||
endpoint = ep
|
||||
break
|
||||
}
|
||||
}
|
||||
cc.mu.RUnlock()
|
||||
if endpoint == "" {
|
||||
return fmt.Errorf("failed to resolve endpoint for client")
|
||||
}
|
||||
usedEndpoints = append(usedEndpoints, endpoint)
|
||||
if endpoint == server1.URL {
|
||||
return fmt.Errorf("boom")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("executeWithFailover failed: %v", err)
|
||||
}
|
||||
if len(usedEndpoints) < 2 {
|
||||
t.Fatalf("expected failover to second endpoint, used: %v", usedEndpoints)
|
||||
}
|
||||
|
||||
health := cc.GetHealthStatus()
|
||||
if health[server1.URL] {
|
||||
t.Fatal("expected server1 to be unhealthy after failure")
|
||||
}
|
||||
if !health[server2.URL] {
|
||||
t.Fatal("expected server2 to be healthy after recovery")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
|
||||
)
|
||||
|
||||
func TestReportEngineGenerateMissingStore(t *testing.T) {
|
||||
engine := NewReportEngine(EngineConfig{})
|
||||
|
||||
_, _, err := engine.Generate(MetricReportRequest{
|
||||
ResourceType: "node",
|
||||
ResourceID: "node-1",
|
||||
Start: time.Now().Add(-1 * time.Hour),
|
||||
End: time.Now(),
|
||||
Format: FormatCSV,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when metrics store is nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "metrics store not initialized") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportEngineGenerateUnsupportedFormat(t *testing.T) {
|
||||
store := newTestMetricsStore(t)
|
||||
defer store.Close()
|
||||
|
||||
engine := NewReportEngine(EngineConfig{MetricsStore: store})
|
||||
|
||||
_, _, err := engine.Generate(MetricReportRequest{
|
||||
ResourceType: "node",
|
||||
ResourceID: "node-1",
|
||||
Start: time.Now().Add(-1 * time.Hour),
|
||||
End: time.Now(),
|
||||
Format: ReportFormat("xls"),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected unsupported format error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsupported format") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportEngineQueryMetricsSpecificMetric(t *testing.T) {
|
||||
store := newTestMetricsStore(t)
|
||||
defer store.Close()
|
||||
|
||||
start := time.Now().Add(-30 * time.Minute)
|
||||
points := []metrics.WriteMetric{
|
||||
{ResourceType: "node", ResourceID: "node-1", MetricType: "cpu", Value: 10, Timestamp: start, Tier: metrics.TierRaw},
|
||||
{ResourceType: "node", ResourceID: "node-1", MetricType: "cpu", Value: 30, Timestamp: start.Add(5 * time.Minute), Tier: metrics.TierRaw},
|
||||
{ResourceType: "node", ResourceID: "node-1", MetricType: "memory", Value: 50, Timestamp: start, Tier: metrics.TierRaw},
|
||||
}
|
||||
store.WriteBatchSync(points)
|
||||
|
||||
engine := NewReportEngine(EngineConfig{MetricsStore: store})
|
||||
|
||||
data, err := engine.queryMetrics(MetricReportRequest{
|
||||
ResourceType: "node",
|
||||
ResourceID: "node-1",
|
||||
MetricType: "cpu",
|
||||
Start: start.Add(-1 * time.Minute),
|
||||
End: start.Add(10 * time.Minute),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("queryMetrics failed: %v", err)
|
||||
}
|
||||
|
||||
if data.Title != "node Report: node-1" {
|
||||
t.Fatalf("expected default title, got %q", data.Title)
|
||||
}
|
||||
|
||||
if len(data.Metrics) != 1 {
|
||||
t.Fatalf("expected 1 metric, got %d", len(data.Metrics))
|
||||
}
|
||||
if _, ok := data.Metrics["cpu"]; !ok {
|
||||
t.Fatal("expected cpu metric data to be present")
|
||||
}
|
||||
if _, ok := data.Metrics["memory"]; ok {
|
||||
t.Fatal("expected memory metric to be filtered out")
|
||||
}
|
||||
|
||||
stats, ok := data.Summary.ByMetric["cpu"]
|
||||
if !ok {
|
||||
t.Fatal("expected cpu summary stats")
|
||||
}
|
||||
if stats.Count != 2 {
|
||||
t.Fatalf("expected cpu count 2, got %d", stats.Count)
|
||||
}
|
||||
if stats.Min != 10 || stats.Max != 30 {
|
||||
t.Fatalf("unexpected cpu min/max: %+v", stats)
|
||||
}
|
||||
if stats.Avg != 20 {
|
||||
t.Fatalf("expected cpu avg 20, got %v", stats.Avg)
|
||||
}
|
||||
if stats.Current != 30 {
|
||||
t.Fatalf("expected cpu current 30, got %v", stats.Current)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportEngineQueryMetricsNoData(t *testing.T) {
|
||||
store := newTestMetricsStore(t)
|
||||
defer store.Close()
|
||||
|
||||
engine := NewReportEngine(EngineConfig{MetricsStore: store})
|
||||
|
||||
start := time.Now().Add(-1 * time.Hour)
|
||||
data, err := engine.queryMetrics(MetricReportRequest{
|
||||
ResourceType: "node",
|
||||
ResourceID: "missing-node",
|
||||
Start: start,
|
||||
End: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("queryMetrics failed: %v", err)
|
||||
}
|
||||
if data.TotalPoints != 0 {
|
||||
t.Fatalf("expected zero points, got %d", data.TotalPoints)
|
||||
}
|
||||
if len(data.Metrics) != 0 {
|
||||
t.Fatalf("expected no metrics, got %d", len(data.Metrics))
|
||||
}
|
||||
if data.Title != "node Report: missing-node" {
|
||||
t.Fatalf("expected default title, got %q", data.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportEngineGenerateMultiAggregatesData(t *testing.T) {
|
||||
store := newTestMetricsStore(t)
|
||||
defer store.Close()
|
||||
|
||||
base := time.Now().Add(-15 * time.Minute)
|
||||
store.WriteBatchSync([]metrics.WriteMetric{
|
||||
{ResourceType: "node", ResourceID: "node-1", MetricType: "cpu", Value: 10, Timestamp: base, Tier: metrics.TierRaw},
|
||||
{ResourceType: "node", ResourceID: "node-2", MetricType: "cpu", Value: 20, Timestamp: base.Add(2 * time.Minute), Tier: metrics.TierRaw},
|
||||
})
|
||||
|
||||
engine := NewReportEngine(EngineConfig{MetricsStore: store})
|
||||
|
||||
data, contentType, err := engine.GenerateMulti(MultiReportRequest{
|
||||
Resources: []MetricReportRequest{
|
||||
{ResourceType: "node", ResourceID: "node-1"},
|
||||
{ResourceType: "node", ResourceID: "node-2"},
|
||||
},
|
||||
MetricType: "cpu",
|
||||
Start: base.Add(-1 * time.Minute),
|
||||
End: base.Add(5 * time.Minute),
|
||||
Format: FormatCSV,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateMulti failed: %v", err)
|
||||
}
|
||||
if contentType != "text/csv" {
|
||||
t.Fatalf("expected text/csv, got %s", contentType)
|
||||
}
|
||||
|
||||
csv := string(data)
|
||||
if !strings.Contains(csv, "# Pulse Multi-Resource Metrics Report") {
|
||||
t.Fatal("missing multi-resource header")
|
||||
}
|
||||
if !strings.Contains(csv, "node-1") || !strings.Contains(csv, "node-2") {
|
||||
t.Fatal("expected both resource IDs in output")
|
||||
}
|
||||
}
|
||||
|
||||
func newTestMetricsStore(t *testing.T) *metrics.Store {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
cfg := metrics.DefaultConfig(dir)
|
||||
cfg.DBPath = filepath.Join(dir, "metrics.db")
|
||||
cfg.WriteBufferSize = 1
|
||||
cfg.FlushInterval = 50 * time.Millisecond
|
||||
|
||||
store, err := metrics.NewStore(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create metrics store: %v", err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
Reference in New Issue
Block a user