Files
pulse/pkg/proxmox/client_api_more_test.go
T
pulse-triage[bot] 9dcd5697cc Keep incomplete Proxmox guest inventories out of publication
Require whole bounded, unambiguous enumerations on the efficient and node fallback routes. Preserve scoped empty inventories, LXC integer compatibility and ordinary counter/receipt semantics. Validate retained readings, History, breaches and template policy through actual fallback and recovery.

Change-source: pulse-maintainer
2026-10-14 11:35:55 +01:00

379 lines
11 KiB
Go

package proxmox
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func newTestClient(t *testing.T, handler http.HandlerFunc) *Client {
t.Helper()
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
cfg := ClientConfig{
Host: server.URL,
TokenName: "user@pve!token",
TokenValue: "secret",
VerifySSL: false,
Timeout: 2 * time.Second,
}
client, err := NewClient(cfg)
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
return client
}
func writeJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
t.Helper()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(payload); err != nil {
t.Fatalf("encode json: %v", err)
}
}
func TestClientStorageAndTasks(t *testing.T) {
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api2/json/storage":
writeJSON(t, w, map[string]interface{}{
"data": []Storage{{Storage: "local", Type: "dir"}},
})
case "/api2/json/nodes":
writeJSON(t, w, map[string]interface{}{
"data": []Node{{Node: "node1", Status: "online"}, {Node: "node2", Status: "offline"}},
})
case "/api2/json/nodes/node1/tasks":
writeJSON(t, w, map[string]interface{}{
"data": []Task{
{UPID: "1", Type: "vzdump"},
{UPID: "2", Type: "other"},
},
})
default:
http.NotFound(w, r)
}
})
ctx := context.Background()
storage, err := client.GetAllStorage(ctx)
if err != nil {
t.Fatalf("GetAllStorage error: %v", err)
}
if len(storage) != 1 || storage[0].Storage != "local" {
t.Fatalf("unexpected storage: %+v", storage)
}
tasks, err := client.GetBackupTasks(ctx)
if err != nil {
t.Fatalf("GetBackupTasks error: %v", err)
}
if len(tasks) != 1 || tasks[0].Type != "vzdump" {
t.Fatalf("unexpected tasks: %+v", tasks)
}
}
func TestClientSnapshotsAndContent(t *testing.T) {
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api2/json/nodes/node1/storage/local/content":
writeJSON(t, w, map[string]interface{}{
"data": []StorageContent{
{Volid: "backup1", Content: "backup"},
{Volid: "iso1", Content: "iso"},
{Volid: "tmpl1", Content: "vztmpl"},
},
})
case "/api2/json/nodes/node1/qemu/100/snapshot":
writeJSON(t, w, map[string]interface{}{
"data": []Snapshot{{Name: "current"}, {Name: "snap1"}},
})
case "/api2/json/nodes/node1/lxc/101/snapshot":
writeJSON(t, w, map[string]interface{}{
"data": []Snapshot{{Name: "current"}, {Name: "snap2"}},
})
default:
http.NotFound(w, r)
}
})
ctx := context.Background()
content, err := client.GetStorageContent(ctx, "node1", "local")
if err != nil {
t.Fatalf("GetStorageContent error: %v", err)
}
if len(content) != 2 {
t.Fatalf("expected 2 backup-related items, got %d", len(content))
}
snaps, err := client.GetVMSnapshots(ctx, "node1", 100)
if err != nil {
t.Fatalf("GetVMSnapshots error: %v", err)
}
if len(snaps) != 1 || snaps[0].Name != "snap1" || snaps[0].VMID != 100 {
t.Fatalf("unexpected VM snapshots: %+v", snaps)
}
ctSnaps, err := client.GetContainerSnapshots(ctx, "node1", 101)
if err != nil {
t.Fatalf("GetContainerSnapshots error: %v", err)
}
if len(ctSnaps) != 1 || ctSnaps[0].Name != "snap2" || ctSnaps[0].VMID != 101 {
t.Fatalf("unexpected container snapshots: %+v", ctSnaps)
}
}
func TestClientClusterAndAgentInfo(t *testing.T) {
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api2/json/cluster/status":
writeJSON(t, w, map[string]interface{}{
"data": []ClusterStatus{
{Type: "cluster", Name: "prod"},
{Type: "node", Name: "node1"},
},
})
case "/api2/json/nodes/node1/qemu/100/config":
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{"name": "vm1"},
})
case "/api2/json/nodes/node1/qemu/100/agent/get-osinfo":
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{"id": "linux"},
})
case "/api2/json/nodes/node1/qemu/100/agent/info":
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{
"result": map[string]interface{}{
"version": "1.2.3",
},
},
})
case "/api2/json/nodes/node1/qemu/100/agent/network-get-interfaces":
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{
"result": []VMNetworkInterface{{Name: "eth0"}},
},
})
default:
http.NotFound(w, r)
}
})
ctx := context.Background()
status, err := client.GetClusterStatus(ctx)
if err != nil {
t.Fatalf("GetClusterStatus error: %v", err)
}
if len(status) != 2 {
t.Fatalf("unexpected cluster status: %+v", status)
}
member, err := client.IsClusterMember(ctx)
if err != nil {
t.Fatalf("IsClusterMember error: %v", err)
}
if !member {
t.Fatal("expected cluster membership")
}
config, err := client.GetVMConfig(ctx, "node1", 100)
if err != nil {
t.Fatalf("GetVMConfig error: %v", err)
}
if config["name"] != "vm1" {
t.Fatalf("unexpected vm config: %+v", config)
}
osInfo, err := client.GetVMAgentInfo(ctx, "node1", 100)
if err != nil {
t.Fatalf("GetVMAgentInfo error: %v", err)
}
if osInfo["id"] != "linux" {
t.Fatalf("unexpected agent info: %+v", osInfo)
}
version, err := client.GetVMAgentVersion(ctx, "node1", 100)
if err != nil {
t.Fatalf("GetVMAgentVersion error: %v", err)
}
if version != "1.2.3" {
t.Fatalf("unexpected agent version: %q", version)
}
ifaces, err := client.GetVMNetworkInterfaces(ctx, "node1", 100)
if err != nil {
t.Fatalf("GetVMNetworkInterfaces error: %v", err)
}
if len(ifaces) != 1 || ifaces[0].Name != "eth0" {
t.Fatalf("unexpected interfaces: %+v", ifaces)
}
}
func TestClientGetVMNetworkInterfaces_ObjectAndPartialPayloads(t *testing.T) {
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
// Guest reads require fresh operation-lock evidence, independently of their payload fixture.
if strings.HasSuffix(r.URL.Path, "/config") {
fmt.Fprint(w, `{"data":{}}`)
return
}
switch r.URL.Path {
case "/api2/json/nodes/node1/qemu/100/agent/network-get-interfaces":
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{
"result": []interface{}{
map[string]interface{}{
"name": "eth0",
"hardware-address": "00:11:22:33:44:55",
"ip-addresses": []interface{}{
map[string]interface{}{"ip-address": "192.168.1.10", "prefix": 24},
map[string]interface{}{"ip-address": "fe80::1", "prefix": "64"},
},
},
map[string]interface{}{
"name": 123,
"ip-addresses": map[string]interface{}{"ip-address": "10.0.0.5", "prefix": "16"},
},
"malformed",
},
},
})
case "/api2/json/nodes/node1/qemu/101/agent/network-get-interfaces":
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{
"result": map[string]interface{}{
"name": "eth1",
"hardware-address": "aa:bb:cc:dd:ee:ff",
"ip-addresses": map[string]interface{}{"ip-address": "10.10.0.8", "prefix": 24},
},
},
})
default:
http.NotFound(w, r)
}
})
ctx := context.Background()
ifaces, err := client.GetVMNetworkInterfaces(ctx, "node1", 100)
if err != nil {
t.Fatalf("GetVMNetworkInterfaces array payload error: %v", err)
}
if len(ifaces) != 2 {
t.Fatalf("expected 2 useful interfaces from partial payload, got %+v", ifaces)
}
if ifaces[0].Name != "eth0" || len(ifaces[0].IPAddresses) != 2 {
t.Fatalf("expected eth0 addresses to be preserved, got %+v", ifaces[0])
}
if ifaces[1].Name != "123" || len(ifaces[1].IPAddresses) != 1 || ifaces[1].IPAddresses[0].Address != "10.0.0.5" {
t.Fatalf("expected object-style address to be coerced, got %+v", ifaces[1])
}
objectIfaces, err := client.GetVMNetworkInterfaces(ctx, "node1", 101)
if err != nil {
t.Fatalf("GetVMNetworkInterfaces object payload error: %v", err)
}
if len(objectIfaces) != 1 || objectIfaces[0].Name != "eth1" || objectIfaces[0].IPAddresses[0].Address != "10.10.0.8" {
t.Fatalf("unexpected object-style interfaces: %+v", objectIfaces)
}
}
func TestClientStatusAndResources(t *testing.T) {
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api2/json/nodes/node1/qemu/100/status/current":
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{"status": "running"},
})
case "/api2/json/nodes/node1/lxc/101/status/current":
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{"status": "running", "vmid": 101},
})
case "/api2/json/cluster/resources":
writeJSON(t, w, map[string]interface{}{
"data": []ClusterResource{{ID: "qemu/100", Type: "qemu", Node: "node1", VMID: 100, Pool: "prod-vms"}},
})
case "/api2/json/nodes/node1/lxc/101/interfaces":
writeJSON(t, w, map[string]interface{}{
"data": []ContainerInterface{{Name: "eth0", HWAddr: "aa:bb"}},
})
default:
http.NotFound(w, r)
}
})
ctx := context.Background()
vmStatus, err := client.GetVMStatus(ctx, "node1", 100)
if err != nil {
t.Fatalf("GetVMStatus error: %v", err)
}
if vmStatus.Status != "running" {
t.Fatalf("unexpected VM status: %+v", vmStatus)
}
ctStatus, err := client.GetContainerStatus(ctx, "node1", 101)
if err != nil {
t.Fatalf("GetContainerStatus error: %v", err)
}
if ctStatus.Status != "running" || ctStatus.VMID != 101 {
t.Fatalf("unexpected container status: %+v", ctStatus)
}
resources, err := client.GetClusterResources(ctx, "vm")
if err != nil {
t.Fatalf("GetClusterResources error: %v", err)
}
if len(resources) != 1 || resources[0].VMID != 100 {
t.Fatalf("unexpected resources: %+v", resources)
}
if resources[0].Pool != "prod-vms" {
t.Fatalf("expected cluster resource pool to decode, got %+v", resources[0])
}
ifaces, err := client.GetContainerInterfaces(ctx, "node1", 101)
if err != nil {
t.Fatalf("GetContainerInterfaces error: %v", err)
}
if len(ifaces) != 1 || ifaces[0].Name != "eth0" {
t.Fatalf("unexpected container interfaces: %+v", ifaces)
}
}
func TestClientGetVMMemAvailableFromAgent(t *testing.T) {
client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
// Guest reads require fresh operation-lock evidence, independently of their payload fixture.
if strings.HasSuffix(r.URL.Path, "/config") {
fmt.Fprint(w, `{"data":{}}`)
return
}
switch r.URL.Path {
case "/api2/json/nodes/node1/qemu/100/agent/file-read":
if got := r.URL.Query().Get("file"); got != "/proc/meminfo" {
t.Fatalf("unexpected file query = %q", got)
}
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{
"content": "MemTotal: 8163488 kB\nMemAvailable: 5242880 kB\n",
},
})
default:
http.NotFound(w, r)
}
})
ctx := context.Background()
available, err := client.GetVMMemAvailableFromAgent(ctx, "node1", 100)
if err != nil {
t.Fatalf("GetVMMemAvailableFromAgent error: %v", err)
}
if available != 5242880*1024 {
t.Fatalf("available = %d, want %d", available, uint64(5242880*1024))
}
}