Pulse/pkg/pbs/client_full_coverage_test.go

1773 lines
50 KiB
Go

package pbs
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
// Helper to create a client connected to a test server
func newTestClient(t *testing.T, handler http.HandlerFunc) (*Client, *httptest.Server) {
server := httptest.NewServer(handler)
client, err := NewClient(ClientConfig{
Host: server.URL,
TokenName: "root@pam!pulse-token",
TokenValue: "secret",
Timeout: 1 * time.Second,
})
if err != nil {
server.Close()
t.Fatalf("Failed to create client: %v", err)
}
return client, server
}
func TestClient_CreateUser(t *testing.T) {
tests := []struct {
name string
userID string
comment string
handler http.HandlerFunc
expectError bool
}{
{
name: "success",
userID: "user1@pbs",
comment: "test user",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/api2/json/access/users" {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if r.FormValue("userid") != "user1@pbs" || r.FormValue("comment") != "test user" {
http.Error(w, "bad params", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
},
expectError: false,
},
{
name: "already exists",
userID: "user1@pbs",
handler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "user already exists", http.StatusInternalServerError) // PBS returns 500 or 400 with message
},
expectError: false, // Should be ignored
},
{
name: "other error",
userID: "user1@pbs",
handler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "something went wrong", http.StatusInternalServerError)
},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client, server := newTestClient(t, tc.handler)
defer server.Close()
err := client.CreateUser(context.Background(), tc.userID, tc.comment)
if tc.expectError && err == nil {
t.Error("Expected error, got nil")
}
if !tc.expectError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
}
}
func TestClient_SetUserACL(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/api2/json/access/acl" {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if r.FormValue("auth-id") != "user1@pbs" || r.FormValue("path") != "/" || r.FormValue("role") != "Audit" {
http.Error(w, "bad params", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
})
defer server.Close()
err := client.SetUserACL(context.Background(), "user1@pbs", "/", "Audit")
if err != nil {
t.Errorf("SetUserACL failed: %v", err)
}
}
func TestClient_SetUserACL_Error(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "fail", http.StatusInternalServerError)
})
defer server.Close()
err := client.SetUserACL(context.Background(), "user1@pbs", "/", "Audit")
if err == nil {
t.Error("Expected error, got nil")
}
}
func TestClient_CreateUserToken(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
expectError bool
}{
{
name: "success",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/api2/json/access/users/user1@pbs/token/token1" {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{
"tokenid": "user1@pbs!token1",
"value": "secret123",
},
})
},
expectError: false,
},
{
name: "error response",
handler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "fail", http.StatusInternalServerError)
},
expectError: true,
},
{
name: "bad json",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("invalid json"))
},
expectError: true,
},
{
name: "empty value",
handler: func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{
"tokenid": "user1@pbs!token1",
"value": "",
},
})
},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client, server := newTestClient(t, tc.handler)
defer server.Close()
resp, err := client.CreateUserToken(context.Background(), "user1@pbs", "token1")
if tc.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if resp != nil && resp.Value != "secret123" {
t.Errorf("Expected token value 'secret123', got %q", resp.Value)
}
}
})
}
}
func TestClient_SetupMonitoringAccess(t *testing.T) {
// This tests the flow: CreateUser -> SetUserACL -> CreateUserToken -> SetUserACL (token)
steps := make(map[string]bool)
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api2/json/access/users":
steps["create_user"] = true
if r.FormValue("userid") != "pulse-monitor@pbs" {
http.Error(w, "bad user", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
case "/api2/json/access/acl":
if r.FormValue("auth-id") == "pulse-monitor@pbs" {
steps["set_acl_user"] = true
} else if r.FormValue("auth-id") == "pulse-monitor@pbs!monitor1" {
steps["set_acl_token"] = true
}
w.WriteHeader(http.StatusOK)
case "/api2/json/access/users/pulse-monitor@pbs/token/monitor1":
steps["create_token"] = true
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{
"tokenid": "pulse-monitor@pbs!monitor1",
"value": "newsecret",
},
})
default:
http.Error(w, "unknown path "+r.URL.Path, http.StatusNotFound)
}
})
defer server.Close()
id, val, err := client.SetupMonitoringAccess(context.Background(), "monitor1")
if err != nil {
t.Fatalf("SetupMonitoringAccess failed: %v", err)
}
if id != "pulse-monitor@pbs!monitor1" {
t.Errorf("Expected token ID 'pulse-monitor@pbs!monitor1', got %q", id)
}
if val != "newsecret" {
t.Errorf("Expected token value 'newsecret', got %q", val)
}
if !steps["create_user"] {
t.Error("CreateUser step not executed")
}
if !steps["set_acl_user"] {
t.Error("SetUserACL (user) step not executed")
}
if !steps["create_token"] {
t.Error("CreateUserToken step not executed")
}
// The last step is optional/warns on failure, but here it succeeds
if !steps["set_acl_token"] {
t.Error("SetUserACL (token) step not executed")
}
}
func TestClient_SetupMonitoringAccess_RotatesExistingToken(t *testing.T) {
createAttempts := 0
deleteCalled := false
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api2/json/access/users":
w.WriteHeader(http.StatusOK)
case "/api2/json/access/acl":
w.WriteHeader(http.StatusOK)
case "/api2/json/access/users/pulse-monitor@pbs/token/monitor1":
switch r.Method {
case http.MethodPost:
createAttempts++
if createAttempts == 1 {
http.Error(w, "token already exists", http.StatusBadRequest)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{
"tokenid": "pulse-monitor@pbs!monitor1",
"value": "rotated-secret",
},
})
case http.MethodDelete:
deleteCalled = true
w.WriteHeader(http.StatusOK)
default:
http.Error(w, "bad method", http.StatusMethodNotAllowed)
}
default:
http.Error(w, "unknown path "+r.URL.Path, http.StatusNotFound)
}
})
defer server.Close()
id, val, err := client.SetupMonitoringAccess(context.Background(), "monitor1")
if err != nil {
t.Fatalf("SetupMonitoringAccess failed: %v", err)
}
if id != "pulse-monitor@pbs!monitor1" {
t.Fatalf("token id = %q", id)
}
if val != "rotated-secret" {
t.Fatalf("token value = %q", val)
}
if !deleteCalled {
t.Fatal("expected existing token to be deleted before recreate")
}
if createAttempts != 2 {
t.Fatalf("expected 2 token creation attempts, got %d", createAttempts)
}
}
func TestClient_SetupMonitoringAccess_Errors(t *testing.T) {
// 1. Fail at SetUserACL (user)
client1, server1 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && strings.HasSuffix(r.URL.Path, "/acl") && r.FormValue("auth-id") == "pulse-monitor@pbs" {
http.Error(w, "acl error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
defer server1.Close()
_, _, err1 := client1.SetupMonitoringAccess(context.Background(), "t1")
if err1 == nil || !strings.Contains(err1.Error(), "set user ACL") {
t.Errorf("Expected set user ACL error, got: %v", err1)
}
// 2. Fail at CreateToken
client2, server2 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/token/") {
http.Error(w, "token error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
defer server2.Close()
_, _, err2 := client2.SetupMonitoringAccess(context.Background(), "t2")
if err2 == nil || !strings.Contains(err2.Error(), "create token") {
t.Errorf("Expected create token error, got: %v", err2)
}
}
func TestClient_GetVersion_Error(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "fail", http.StatusInternalServerError)
})
defer server.Close()
_, err := client.GetVersion(context.Background())
if err == nil {
t.Error("Expected error, got nil")
}
}
func TestClient_GetVersion_BadJSON(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("invalid json"))
})
defer server.Close()
_, err := client.GetVersion(context.Background())
if err == nil {
t.Error("Expected error on bad JSON, got nil")
}
}
func TestClient_GetNodeStatus_Error(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "fail", http.StatusInternalServerError)
})
defer server.Close()
_, err := client.GetNodeStatus(context.Background())
if err == nil {
t.Error("Expected error, got nil")
}
}
func TestClient_GetNodeStatus_JsonError(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("invalid json"))
})
defer server.Close()
_, err := client.GetNodeStatus(context.Background())
if err == nil {
t.Error("Expected error, got nil")
}
}
func TestClient_GetNodeName(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api2/json/nodes" {
http.Error(w, "bad path", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]string{
{"node": "pve1"},
},
})
})
defer server.Close()
name, err := client.GetNodeName(context.Background())
if err != nil {
t.Fatalf("GetNodeName failed: %v", err)
}
if name != "pve1" {
t.Errorf("Expected 'pve1', got %q", name)
}
}
func TestClient_GetNodeName_Empty(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]string{},
})
})
defer server.Close()
_, err := client.GetNodeName(context.Background())
if err == nil {
t.Error("Expected error for empty node list, got nil")
}
}
func TestClient_GetNodeStatus_Success(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api2/json/nodes/localhost/status" {
http.Error(w, "bad path", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(map[string]any{
"data": NodeStatus{
CPU: 0.1,
Memory: Memory{
Total: 1000,
Used: 500,
},
},
})
})
defer server.Close()
status, err := client.GetNodeStatus(context.Background())
if err != nil {
t.Fatalf("GetNodeStatus failed: %v", err)
}
if status.CPU != 0.1 {
t.Errorf("Expected CPU 0.1, got %f", status.CPU)
}
}
func TestClient_GetDatastores_Full(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api2/json/admin/datastore" {
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]string{
{"store": "store1"},
{"store": "store2"},
},
})
return
}
if r.URL.Path == "/api2/json/admin/datastore/store1/rrd" {
// Returns RRD data for dedup factor
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]any{
{"time": 1, "dedup_factor": 2.5},
},
})
return
}
if r.URL.Path == "/api2/json/admin/datastore/store1/status" {
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"total": 1000.0,
"used": 200.0,
"avail": 800.0,
},
})
return
}
if r.URL.Path == "/api2/json/admin/datastore/store2/rrd" {
http.Error(w, "no rrd", http.StatusNotFound)
return
}
if r.URL.Path == "/api2/json/admin/datastore/store2/status" {
// Missing dedup in status
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"total": 2000.0,
},
})
return
}
if r.URL.Path == "/api2/json/admin/datastore/store2/gc" {
// Fallback to GC for dedup
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"index-data-bytes": 300.0,
"disk-bytes": 100.0,
},
})
return
}
http.NotFound(w, r)
})
defer server.Close()
dss, err := client.GetDatastores(context.Background())
if err != nil {
t.Fatalf("GetDatastores failed: %v", err)
}
if len(dss) != 2 {
t.Fatalf("Expected 2 datastores, got %d", len(dss))
}
// Check store1 (RRD dedup)
if dss[0].Store != "store1" {
t.Errorf("Expected store1, got %s", dss[0].Store)
}
if dss[0].DeduplicationFactor != 2.5 {
t.Errorf("Expected store1 dedup 2.5, got %f", dss[0].DeduplicationFactor)
}
// Check store2 (GC dedup: 300/100 = 3.0)
if dss[1].Store != "store2" {
t.Errorf("Expected store2, got %s", dss[1].Store)
}
if dss[1].DeduplicationFactor != 3.0 {
t.Errorf("Expected store2 dedup 3.0, got %f", dss[1].DeduplicationFactor)
}
}
func TestClient_ListNamespaces(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api2/json/admin/datastore/store1/namespace" {
http.Error(w, "bad path", http.StatusNotFound)
return
}
if r.FormValue("ns") == "parent" && r.FormValue("max-depth") == "2" {
json.NewEncoder(w).Encode(map[string]any{
"data": []Namespace{
{Name: "child"},
},
})
return
}
// Default
json.NewEncoder(w).Encode(map[string]any{
"data": []Namespace{
{Name: "ns1"},
},
})
})
defer server.Close()
// Test with params
nss, err := client.ListNamespaces(context.Background(), "store1", "parent", 2)
if err != nil {
t.Fatalf("ListNamespaces failed: %v", err)
}
if len(nss) != 1 || nss[0].Name != "child" {
t.Errorf("Unexpected namespaces: %v", nss)
}
// Test 404
client2, server2 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not found", http.StatusNotFound) // 404
})
defer server2.Close()
nss2, err := client2.ListNamespaces(context.Background(), "store1", "", 0)
if err != nil {
t.Fatalf("Expected no error on 404, got %v", err)
}
if len(nss2) != 0 {
t.Errorf("Expected empty list on 404, got %d", len(nss2))
}
}
func TestClient_ListBackupGroups(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api2/json/admin/datastore/store1/groups" {
http.NotFound(w, r)
return
}
json.NewEncoder(w).Encode(map[string]any{
"data": []BackupGroup{
{BackupID: "vm/100"},
},
})
})
defer server.Close()
groups, err := client.ListBackupGroups(context.Background(), "store1", "ns1")
if err != nil {
t.Fatalf("ListBackupGroups failed: %v", err)
}
if len(groups) != 1 || groups[0].BackupID != "vm/100" {
t.Errorf("Unexpected groups: %v", groups)
}
}
func TestClient_ListBackupSnapshots(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api2/json/admin/datastore/store1/snapshots" {
http.NotFound(w, r)
return
}
if r.FormValue("backup-type") != "vm" || r.FormValue("backup-id") != "100" {
http.Error(w, "bad params", http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(map[string]any{
"data": []BackupSnapshot{
{BackupTime: 12345},
},
})
})
defer server.Close()
snaps, err := client.ListBackupSnapshots(context.Background(), "store1", "ns1", "vm", "100")
if err != nil {
t.Fatalf("ListBackupSnapshots failed: %v", err)
}
if len(snaps) != 1 || snaps[0].BackupTime != 12345 {
t.Errorf("Unexpected snapshots: %v", snaps)
}
}
func TestClient_ListAllBackups(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
// Mock groups list for ns1 and ns2
if strings.HasSuffix(r.URL.Path, "/groups") {
json.NewEncoder(w).Encode(map[string]any{
"data": []BackupGroup{
{BackupType: "vm", BackupID: "100"},
},
})
return
}
// Mock snapshots
if strings.HasSuffix(r.URL.Path, "/snapshots") {
json.NewEncoder(w).Encode(map[string]any{
"data": []BackupSnapshot{
{BackupTime: 12345},
},
})
return
}
http.NotFound(w, r)
})
defer server.Close()
results, err := client.ListAllBackups(context.Background(), "store1", []string{"ns1", "ns2"})
if err != nil {
t.Fatalf("ListAllBackups failed: %v", err)
}
if len(results) != 2 {
t.Errorf("Expected 2 namespaces, got %d", len(results))
}
if len(results["ns1"]) != 1 {
t.Errorf("Expected 1 snapshot in ns1, got %d", len(results["ns1"]))
}
}
func TestClient_ListAllBackups_Error(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "fail", http.StatusInternalServerError)
})
defer server.Close()
_, err := client.ListAllBackups(context.Background(), "store1", []string{"ns1"})
if err == nil {
t.Error("Expected error, got nil")
}
}
func TestNewClient_TokenParsing(t *testing.T) {
// Test user@realm!tokenname format
cfg1 := ClientConfig{
Host: "http://example.com",
TokenName: "user@realm!token1",
TokenValue: "secret",
}
client1, err := NewClient(cfg1)
if err != nil {
t.Fatalf("Failed to create client with complex token name: %v", err)
}
if client1.auth.user != "user" || client1.auth.realm != "realm" || client1.auth.tokenName != "token1" {
t.Errorf("Incorrect parsing: user=%q realm=%q token=%q", client1.auth.user, client1.auth.realm, client1.auth.tokenName)
}
// Test user provided separately
cfg2 := ClientConfig{
Host: "http://example.com",
User: "u2@r2",
TokenName: "token2",
TokenValue: "secret",
}
client2, err := NewClient(cfg2)
if err != nil {
t.Fatalf("Failed to create client with user+token: %v", err)
}
if client2.auth.user != "u2" || client2.auth.realm != "r2" || client2.auth.tokenName != "token2" {
t.Errorf("Incorrect parsing: user=%q realm=%q token=%q", client2.auth.user, client2.auth.realm, client2.auth.tokenName)
}
// Test user no realm (default pbs)
cfg3 := ClientConfig{
Host: "http://example.com",
User: "u3",
TokenName: "token3",
TokenValue: "secret",
}
client3, err := NewClient(cfg3)
if err != nil {
t.Fatalf("Failed to create client with user no realm: %v", err)
}
if client3.auth.user != "u3" || client3.auth.realm != "pbs" {
t.Errorf("Incorrect parsing: user=%q realm=%q", client3.auth.user, client3.auth.realm)
}
}
func TestNewClient_AuthFailure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "auth failed", http.StatusUnauthorized)
}))
defer server.Close()
cfg := ClientConfig{
Host: server.URL,
User: "u@r",
Password: "p",
Timeout: 1 * time.Second,
}
_, err := NewClient(cfg)
if err == nil {
t.Error("Expected auth error, got nil")
}
}
func TestClient_GetDatastores_PartialErrors(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api2/json/admin/datastore" {
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]string{
{"store": "store1"}, // Bad status JSON
{"store": "store2"}, // Status HTTP error
},
})
return
}
if strings.Contains(r.URL.Path, "rrd") {
http.Error(w, "no rrd", http.StatusNotFound)
return
}
if r.URL.Path == "/api2/json/admin/datastore/store1/status" {
w.Write([]byte("invalid json"))
return
}
if r.URL.Path == "/api2/json/admin/datastore/store2/status" {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
http.NotFound(w, r)
})
defer server.Close()
dss, err := client.GetDatastores(context.Background())
if err != nil {
t.Fatalf("GetDatastores failed: %v", err)
}
if len(dss) != 2 {
t.Fatalf("Expected 2 datastores (even with errors), got %d", len(dss))
}
if !strings.Contains(dss[0].Error, "parse status") {
t.Errorf("Expected parse error for store1, got field: %v", dss[0].Error)
}
if !strings.Contains(dss[1].Error, "get status") {
t.Errorf("Expected get status error for store2, got field: %v", dss[1].Error)
}
}
func TestClient_Request_Reauthentication(t *testing.T) {
var authCalls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api2/json/access/ticket" {
authCalls++
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{
"ticket": "newticket",
"CSRFPreventionToken": "newcsrf",
},
})
return
}
if r.URL.Path == "/api2/json/test" {
if r.Header.Get("Cookie") != "PBSAuthCookie=newticket" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
return
}
}))
defer server.Close()
client, err := NewClient(ClientConfig{
Host: server.URL,
User: "u@r",
Password: "p", // Password auth enables re-auth
})
if err != nil {
t.Fatalf("NewClient: %v", err)
}
// Manually set expired auth
client.auth.ticket = "oldticket"
client.auth.expiresAt = time.Now().Add(-1 * time.Hour)
// Make request
_, err = client.request(context.Background(), "GET", "/test", nil)
if err != nil {
t.Fatalf("request failed: %v", err)
}
if authCalls < 1 {
t.Error("Expected re-authentication to occur")
}
if client.auth.ticket != "newticket" {
t.Error("Expected ticket to be updated")
}
}
func TestClient_Request_Reauthentication_Failure(t *testing.T) {
var calls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api2/json/access/ticket" {
calls++
if calls == 1 {
// Initial auth success
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{
"ticket": "ticket1",
"CSRFPreventionToken": "csrf1",
},
})
return
}
// Re-auth fail
http.Error(w, "auth fail", http.StatusUnauthorized)
return
}
}))
defer server.Close()
client, err := NewClient(ClientConfig{
Host: server.URL,
User: "u@r",
Password: "p",
})
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
// Force expiration
client.auth.expiresAt = time.Now().Add(-1 * time.Hour)
_, err = client.request(context.Background(), "GET", "/test", nil)
if err == nil {
t.Error("Expected error on re-auth failure, got nil")
}
}
func TestClient_Authenticate_AuthResponseError(t *testing.T) {
var calls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if calls == 1 {
// Success for NewClient
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{
"ticket": "t",
"CSRFPreventionToken": "c",
},
})
return
}
// Failure for manual call
w.Write([]byte("invalid json"))
}))
defer server.Close()
client, err := NewClient(ClientConfig{
Host: server.URL,
User: "u@r",
Password: "p",
})
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
// manually call authenticateJSON which writes to client
err = client.authenticateJSON(context.Background(), "u", "p")
if err == nil {
t.Error("Expected JSON error on bad auth response, got nil")
}
}
func TestClient_SetupMonitoringAccess_ErrorLastStep(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "token/") {
// Token creation success
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{
"tokenid": "t1",
"value": "secret",
},
})
return
}
if r.URL.Path == "/api2/json/access/acl" && r.FormValue("auth-id") != "pulse-monitor@pbs" {
// Second ACL call (for token) -> fail
http.Error(w, "acl error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
defer server.Close()
// Should succeed but log warning
_, _, err := client.SetupMonitoringAccess(context.Background(), "t1")
if err != nil {
t.Errorf("Expected success (with warning), got error: %v", err)
}
}
// Custom transport to simulate network errors
type errorTransport struct{}
func (t *errorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("network error")
}
func TestClient_Authenticate_NetworkError(t *testing.T) {
// Use token auth to avoid NewClient trying to authenticate immediately
client, err := NewClient(ClientConfig{
Host: "http://example.com",
TokenName: "u@r!t",
TokenValue: "v",
})
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
// Inject error client
client.httpClient = &http.Client{Transport: &errorTransport{}}
// Manually call authenticateJSON
err = client.authenticateJSON(context.Background(), "u", "p")
if err == nil {
t.Error("Expected network error, got nil")
}
}
func TestClient_AuthenticateForm_NetworkError(t *testing.T) {
// Use token auth to avoid NewClient trying to authenticate immediately
client, err := NewClient(ClientConfig{
Host: "http://example.com",
TokenName: "u@r!t",
TokenValue: "v",
})
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
// Inject error client
client.httpClient = &http.Client{Transport: &errorTransport{}}
err = client.authenticateForm(context.Background(), "u", "p")
if err == nil {
t.Error("Expected network error, got nil")
}
}
func TestClient_GetDatastores_HTMLResponseOnHTTPS(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("<html>Error</html>"))
})
defer server.Close()
// Ensure config is HTTPS (newTestClient uses httptest URL which is http, but we can override host string for this check logic)
// Actually NewClient sets baseURL from config.Host.
// The html check looks at c.config.Host.
client.config.Host = "https://pbs.example.com"
_, err := client.GetDatastores(context.Background())
if err == nil {
t.Error("Expected error on HTML response, got nil")
}
if !strings.Contains(err.Error(), "PBS returned HTML instead of JSON") {
t.Errorf("Unexpected error message: %v", err)
}
if strings.Contains(err.Error(), "Try changing your URL") {
t.Error("Should not suggest changing URL when already HTTPS")
}
}
func TestClient_Request_NewRequestError(t *testing.T) {
// Use token auth to skip initial auth
client, err := NewClient(ClientConfig{
Host: "http://example.com",
TokenName: "u@r!t",
TokenValue: "v",
})
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
_, err = client.request(context.Background(), "INVALID\nMETHOD", "/", nil)
if err == nil {
t.Error("Expected error on invalid method, got nil")
}
}
func TestClient_GetDatastores_AdvancedDedup(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "rrd") {
if strings.Contains(r.URL.Path, "store1") {
// Bad JSON for RRD
w.Write([]byte("invalid json"))
return
}
http.NotFound(w, r)
return
}
if strings.HasSuffix(r.URL.Path, "gc") {
if strings.Contains(r.URL.Path, "store2") {
// Good GC data
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"index-data-bytes": 300.0,
"disk-bytes": 100.0,
},
})
return
}
if strings.Contains(r.URL.Path, "store3") {
// Bad GC JSON
w.Write([]byte("invalid json"))
return
}
http.NotFound(w, r)
return
}
if strings.HasSuffix(r.URL.Path, "status") {
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"total": 100.0,
},
})
return
}
// List
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]string{
{"store": "store1"}, // Bad RRD
{"store": "store2"}, // Good GC
{"store": "store3"}, // Bad GC
},
})
})
defer server.Close()
dss, err := client.GetDatastores(context.Background())
if err != nil {
t.Fatalf("GetDatastores failed: %v", err)
}
if len(dss) != 3 {
t.Errorf("Expected 3 datastores, got %d", len(dss))
}
// Store 2 should have dedup 3.0
if dss[1].Store == "store2" && dss[1].DeduplicationFactor != 3.0 {
t.Errorf("Expected store2 dedup 3.0, got %f", dss[1].DeduplicationFactor)
}
}
func TestListAllBackups_PartialFailure(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("ns") == "ns1" {
http.Error(w, "ns1 fail", http.StatusInternalServerError)
return
}
// ns2 success
if strings.HasSuffix(r.URL.Path, "/groups") {
json.NewEncoder(w).Encode(map[string]any{
"data": []BackupGroup{},
})
return
}
})
defer server.Close()
_, err := client.ListAllBackups(context.Background(), "store1", []string{"ns1", "ns2"})
if err == nil {
t.Error("Expected error on partial failure, got nil")
}
}
func TestClient_ListMethods_NetworkError(t *testing.T) {
// Use token auth to allow creation
client, err := NewClient(ClientConfig{Host: "http://example.com", TokenName: "u@r!t", TokenValue: "v"})
if err != nil {
t.Fatal(err)
}
client.httpClient = &http.Client{Transport: &errorTransport{}}
if _, err := client.ListNamespaces(context.Background(), "s", "", 0); err == nil {
t.Error("ListNamespaces: expected network error")
}
if _, err := client.ListBackupGroups(context.Background(), "s", ""); err == nil {
t.Error("ListBackupGroups: expected network error")
}
if _, err := client.ListBackupSnapshots(context.Background(), "s", "", "vm", "1"); err == nil {
t.Error("ListBackupSnapshots: expected network error")
}
if _, err := client.GetNodeName(context.Background()); err == nil {
t.Error("GetNodeName: expected network error")
}
}
func TestNewClient_MalformedToken(t *testing.T) {
// Malformed token with "!" but not valid parts
_, err := NewClient(ClientConfig{Host: "http://e.com", TokenName: "invalid!token!", TokenValue: "v"})
if err == nil {
t.Error("Expected error on malformed token name")
}
}
func TestClient_GetDatastores_BadJSONList(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("invalid json"))
})
defer server.Close()
_, err := client.GetDatastores(context.Background())
if err == nil {
t.Error("Expected error on bad datastore list JSON, got nil")
}
}
func TestClient_SetupMonitoringAccess_UserExists(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/users") && r.Method == "POST" {
// User exists
http.Error(w, "user already exists", http.StatusBadRequest)
return
}
// Steps continue... set acl, create token...
if strings.Contains(r.URL.Path, "token") {
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{"value": "secret"},
})
return
}
w.WriteHeader(http.StatusOK)
})
defer server.Close()
_, _, err := client.SetupMonitoringAccess(context.Background(), "t1")
if err != nil {
t.Errorf("Expected success when user exists, got error: %v", err)
}
}
func TestClient_Authenticate_InvalidURL(t *testing.T) {
// Bypass NewClient validation/auth by using token and manual struct
// Control character in host
client := &Client{
baseURL: "http://ex\nample.com",
httpClient: &http.Client{},
}
err := client.authenticateJSON(context.Background(), "u", "p")
if err == nil {
t.Error("Expected error on invalid URL, got nil")
}
err = client.authenticateForm(context.Background(), "u", "p")
if err == nil {
t.Error("Expected error on invalid URL, got nil")
}
}
// Mock failing reader
type failReader struct{}
func (f *failReader) Read(p []byte) (n int, err error) { return 0, fmt.Errorf("read error") }
func (f *failReader) Close() error { return nil }
type bodyErrorTransport struct{}
func (t *bodyErrorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: &failReader{},
Header: make(http.Header),
}, nil
}
type bodyErrorTransportBadStatus struct{}
func (t *bodyErrorTransportBadStatus) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusBadRequest, // trigger error paths reading body
Body: &failReader{},
Header: make(http.Header),
}, nil
}
func TestClient_ReadBodyErrors(t *testing.T) {
client, err := NewClient(ClientConfig{Host: "http://e.com", TokenName: "u@r!t", TokenValue: "v"})
if err != nil {
t.Fatal(err)
}
// 1. Test methods reading body on success (JSON decode usually fails on read error too)
client.httpClient = &http.Client{Transport: &bodyErrorTransport{}}
if _, err := client.GetDatastores(context.Background()); err == nil {
t.Error("GetDatastores: expected read error")
}
// Note: GetNodeStatus, List* use json.Decode. json.Decode fails if Read fails.
// But GetDatastores calls io.ReadAll explicitly.
// 2. Test methods reading body on error status
client.httpClient = &http.Client{Transport: &bodyErrorTransportBadStatus{}}
// CreateUserToken reads body on error
if _, err := client.CreateUserToken(context.Background(), "u", "t"); err == nil {
t.Error("CreateUserToken: expected read error handling")
}
// authenticateJSON reads body on error
if err := client.authenticateJSON(context.Background(), "u", "p"); err == nil {
t.Error("authenticateJSON: expected error")
}
if _, err := client.ListBackupGroups(context.Background(), "s", "n"); err == nil {
t.Error("ListBackupGroups: expected error")
}
}
type sequentialFailTransport struct {
mu sync.Mutex
requestCount int
failOn int // 0-based index of request to fail reading
}
func (t *sequentialFailTransport) RoundTrip(req *http.Request) (*http.Response, error) {
t.mu.Lock()
count := t.requestCount
t.requestCount++
t.mu.Unlock()
// If this is the list request (first), succeed with valid JSON
if strings.HasSuffix(req.URL.Path, "datastore") && !strings.Contains(req.URL.Path, "status") && !strings.Contains(req.URL.Path, "rrd") && !strings.Contains(req.URL.Path, "gc") {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`{"data": [{"store": "store1"}]}`)),
Header: make(http.Header),
}, nil
}
// For inner requests (rrd, status, gc)
// Check if we should fail this one
if count == t.failOn {
return &http.Response{
StatusCode: http.StatusOK,
Body: &failReader{},
Header: make(http.Header),
}, nil
}
// Otherwise succeed (empty JSON valid enough for decode to not error drastically or just fail decode gracefully)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`{"data": {}}`)),
Header: make(http.Header),
}, nil
}
func TestClient_GetDatastores_InnerReadErrors(t *testing.T) {
client, err := NewClient(ClientConfig{Host: "http://example.com", TokenName: "u@r!t", TokenValue: "v"})
if err != nil {
t.Fatal(err)
}
// Fail RRD read (request index 1: list is 0, rrd is 1)
client.httpClient = &http.Client{Transport: &sequentialFailTransport{failOn: 1}}
dss, err := client.GetDatastores(context.Background())
if err != nil {
t.Fatalf("GetDatastores failed on RRD error: %v", err)
}
if len(dss) != 1 {
t.Error("Expected 1 datastore even with RRD error")
}
// Fail Status read (request index 2: list, rrd, status)
// Note: Status read error logs error and appends datastore with Error field.
// So function returns success, but datastore list has item with Error.
client.httpClient = &http.Client{Transport: &sequentialFailTransport{failOn: 2}}
client.httpClient.Transport.(*sequentialFailTransport).requestCount = 0 // Reset
dss, err = client.GetDatastores(context.Background())
if err != nil {
t.Fatalf("GetDatastores failed on Status read error (unexpected): %v", err)
}
if len(dss) != 1 {
t.Errorf("Expected 1 datastore (with error), got %d", len(dss))
}
if dss[0].Error == "" {
t.Error("Expected Error field to be set on Status read failure")
}
// Fail GC read (request index 3: list, rrd, status, gc)
// Code: if err := json.Decode(...); err != nil { log.Warn; continue }
// So this should SUCCEED.
client.httpClient = &http.Client{Transport: &sequentialFailTransport{failOn: 3}}
client.httpClient.Transport.(*sequentialFailTransport).requestCount = 0 // Reset
dss, err = client.GetDatastores(context.Background())
if err != nil {
t.Fatalf("GetDatastores failed on GC error: %v", err)
}
if len(dss) != 1 {
t.Error("Expected 1 datastore even with GC error")
}
}
func TestClient_GetDatastores_DedupFromStatus(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "rrd") {
http.NotFound(w, r)
return
}
if strings.HasSuffix(r.URL.Path, "gc") {
http.NotFound(w, r)
return
}
if strings.HasSuffix(r.URL.Path, "status") {
if strings.Contains(r.URL.Path, "store1") {
// Hyphenated key
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"deduplication-factor": 5.5,
},
})
return
}
if strings.Contains(r.URL.Path, "store2") {
// Underscore key
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"deduplication_factor": 6.6,
},
})
return
}
}
// List
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]string{
{"store": "store1"},
{"store": "store2"},
},
})
})
defer server.Close()
dss, err := client.GetDatastores(context.Background())
if err != nil {
t.Fatalf("GetDatastores failed: %v", err)
}
if len(dss) != 2 {
t.Fatalf("Expected 2 datastores, got %d", len(dss))
}
// Check store1 (hyphenated)
if dss[0].Store == "store1" {
if dss[0].DeduplicationFactor != 5.5 {
t.Errorf("Store1: expected dedup 5.5, got %f", dss[0].DeduplicationFactor)
}
}
// Check store2 (underscore)
if dss[1].Store == "store2" {
if dss[1].DeduplicationFactor != 6.6 {
t.Errorf("Store2: expected dedup 6.6, got %f", dss[1].DeduplicationFactor)
}
}
}
func TestClient_SetupMonitoringAccess_SetUserACLError(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
// CreateUser succeeds (ignored if fails anyway)
if strings.HasSuffix(r.URL.Path, "users") {
w.WriteHeader(http.StatusOK)
return
}
// SetUserACL fails
if strings.HasSuffix(r.URL.Path, "acl") {
http.Error(w, "acl failed", http.StatusInternalServerError)
return
}
})
defer server.Close()
_, _, err := client.SetupMonitoringAccess(context.Background(), "t1")
if err == nil {
t.Error("Expected error on SetUserACL failure, got nil")
}
if !strings.Contains(err.Error(), "set user ACL") {
t.Errorf("Expected 'set user ACL' error, got: %v", err)
}
}
func TestListMethods_Errors(t *testing.T) {
// ListNamespaces errors
client1, server1 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "namespace") {
w.Write([]byte("invalid json"))
return
}
})
defer server1.Close()
_, err := client1.ListNamespaces(context.Background(), "s1", "", 0)
if err == nil {
t.Error("Expected JSON error for ListNamespaces, got nil")
}
client1b, server1b := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
// Non-404 error
http.Error(w, "server error", http.StatusInternalServerError)
})
defer server1b.Close()
_, err = client1b.ListNamespaces(context.Background(), "s1", "", 0)
if err == nil {
t.Error("Expected server error for ListNamespaces, got nil")
}
// ListBackupGroups errors
client2, server2 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad status", http.StatusBadRequest)
})
defer server2.Close()
_, err = client2.ListBackupGroups(context.Background(), "s1", "ns1")
if err == nil {
t.Error("Expected error for ListBackupGroups, got nil")
}
client3, server3 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("invalid json"))
})
defer server3.Close()
_, err = client3.ListBackupGroups(context.Background(), "s1", "ns1")
if err == nil {
t.Error("Expected JSON error for ListBackupGroups, got nil")
}
// ListBackupSnapshots errors
client4, server4 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad status", http.StatusBadRequest)
})
defer server4.Close()
_, err = client4.ListBackupSnapshots(context.Background(), "s1", "ns1", "vm", "100")
if err == nil {
t.Error("Expected error for ListBackupSnapshots, got nil")
}
client5, server5 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("invalid json"))
})
defer server5.Close()
_, err = client5.ListBackupSnapshots(context.Background(), "s1", "ns1", "vm", "100")
if err == nil {
t.Error("Expected JSON error for ListBackupSnapshots, got nil")
}
}
func TestGetNodeName_Errors(t *testing.T) {
client1, server1 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "server error", http.StatusInternalServerError)
})
defer server1.Close()
_, err := client1.GetNodeName(context.Background())
if err == nil {
t.Error("Expected server error for GetNodeName, got nil")
}
client2, server2 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("invalid json"))
})
defer server2.Close()
_, err = client2.GetNodeName(context.Background())
if err == nil {
t.Error("Expected JSON error for GetNodeName, got nil")
}
}
func TestListAllBackups_SnapshotError(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/groups") {
json.NewEncoder(w).Encode(map[string]any{
"data": []BackupGroup{
{BackupType: "vm", BackupID: "100"},
},
})
return
}
if strings.HasSuffix(r.URL.Path, "/snapshots") {
http.Error(w, "fail", http.StatusInternalServerError)
return
}
http.NotFound(w, r)
})
defer server.Close()
results, err := client.ListAllBackups(context.Background(), "store1", []string{"ns1"})
if err != nil {
t.Fatalf("ListAllBackups failed (snapshot error should be swallowed): %v", err)
}
// Snapshots fail, so list should be empty (but existing)
if len(results["ns1"]) != 0 {
t.Errorf("Expected 0 snapshots, got %d", len(results["ns1"]))
}
}
func TestListAllBackups_ContextCancel(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
// Delay response to allow cancellation
time.Sleep(200 * time.Millisecond)
w.WriteHeader(http.StatusOK)
})
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
_, err := client.ListAllBackups(ctx, "store1", []string{"ns1"})
if err == nil {
t.Error("Expected cancellation error, got nil")
}
}
func TestSetupMonitoringAccess_Error(t *testing.T) {
// Test CreateUser fails
client1, server1 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api2/json/access/users" {
http.Error(w, "fail", http.StatusInternalServerError)
return
}
})
defer server1.Close()
if _, _, err := client1.SetupMonitoringAccess(context.Background(), "test-token"); err == nil {
t.Error("expected error when CreateUser fails")
}
// Test SetUserACL fails
client2, server2 := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api2/json/access/users" {
w.WriteHeader(200)
return
}
if r.URL.Path == "/api2/json/access/acl" {
http.Error(w, "fail acl", http.StatusInternalServerError)
return
}
})
defer server2.Close()
if _, _, err := client2.SetupMonitoringAccess(context.Background(), "test-token"); err == nil {
t.Error("expected error when SetUserACL fails")
}
}
func TestListAllBackups_JSONDecodeError(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("invalid json"))
})
defer server.Close()
// ListBackupGroups will fail to decode
_, err := client.ListBackupGroups(context.Background(), "store1", "ns1")
if err == nil {
t.Error("expected error for invalid json in ListBackupGroups")
}
}
func TestListBackupSnapshots_JSONDecodeError(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("invalid json"))
})
defer server.Close()
_, err := client.ListBackupSnapshots(context.Background(), "store1", "ns1", "vm", "100")
if err == nil {
t.Error("expected error for invalid json in ListBackupSnapshots")
}
}
func TestListBackupGroups_JSONDecodeError(t *testing.T) {
// Covered by TestListAllBackups_JSONDecodeError effectively, but explicit test:
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("invalid json"))
})
defer server.Close()
_, err := client.ListBackupGroups(context.Background(), "store1", "ns1")
if err == nil {
t.Error("expected error for invalid json")
}
}
func TestListAllBackups_ContextCancellation_Inner(t *testing.T) {
// We want to trigger ctx.Done() inside the group processing loop
ctx, cancel := context.WithCancel(context.Background())
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "groups") {
// Return many groups to ensure loop runs
json.NewEncoder(w).Encode(map[string]interface{}{
"data": []map[string]interface{}{
{"backup-type": "vm", "backup-id": "100"},
{"backup-type": "vm", "backup-id": "101"},
{"backup-type": "vm", "backup-id": "102"},
},
})
// Cancel context after getting groups but before processing all snapshots
go func() {
time.Sleep(10 * time.Millisecond)
cancel()
}()
return
}
// Slow down snapshot listing to ensure cancellation is hit
time.Sleep(50 * time.Millisecond)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"data":[]}`))
})
defer server.Close()
defer cancel() // ensure cancel called
_, err := client.ListAllBackups(ctx, "store1", []string{"ns1"})
// We expect an error, likely context canceled
if err == nil {
t.Error("expected error due to context cancellation")
}
}
// Helper to test read errors
type bodyReadErrorReader struct{}
func (e *bodyReadErrorReader) Read(p []byte) (n int, err error) {
return 0, fmt.Errorf("read error")
}
func (e *bodyReadErrorReader) Close() error { return nil }
func TestCreateUserToken_ReadBodyError_JSON(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
// We can't strictly force io.ReadAll to fail easily with httptest unless we do something hacky.
// Instead we can use a small buffer and panic or something, but io.ReadAll usually works.
// However, we can mock the client.httpClient or use a transport that returns a bad body.
// Since we cannot mock httpClient easily via NewClient, we can set it.
w.WriteHeader(200)
})
defer server.Close()
// Replace httpClient with one that returns an errorReader
client.httpClient.Transport = &readErrorTransport{
transport: http.DefaultTransport,
}
_, err := client.CreateUserToken(context.Background(), "user1@pbs", "token")
if err == nil {
t.Error("expected error reading body")
}
}
func TestListBackupSnapshots_HTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
client, err := NewClient(ClientConfig{
Host: server.URL,
TokenName: "root@pam!token",
TokenValue: "token",
})
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
_, err = client.ListBackupSnapshots(context.Background(), "store", "", "vm", "100")
if err == nil {
t.Error("Expected error for HTTP 500")
}
}
func TestListBackupGroups_HTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
client, err := NewClient(ClientConfig{
Host: server.URL,
TokenName: "root@pam!token",
TokenValue: "token",
})
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
_, err = client.ListBackupGroups(context.Background(), "store", "")
if err == nil {
t.Error("Expected error for HTTP 500")
}
}
type readErrorTransport struct {
transport http.RoundTripper
}
func (et *readErrorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := et.transport.RoundTrip(req)
if err != nil {
return nil, err
}
resp.Body = &bodyReadErrorReader{}
return resp, nil
}
func TestGetNodeStatus_ReadBodyError(t *testing.T) {
client, server := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
defer server.Close()
client.httpClient.Transport = &readErrorTransport{
transport: http.DefaultTransport,
}
_, err := client.GetNodeStatus(context.Background())
if err == nil {
t.Error("expected error reading body in GetNodeStatus")
}
}