mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
1748 lines
60 KiB
Go
1748 lines
60 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
|
|
)
|
|
|
|
type fakeVMwareClient struct {
|
|
testConnection func(context.Context) (*vmware.InventorySummary, error)
|
|
}
|
|
|
|
func (c *fakeVMwareClient) TestConnection(ctx context.Context) (*vmware.InventorySummary, error) {
|
|
if c == nil || c.testConnection == nil {
|
|
return &vmware.InventorySummary{}, nil
|
|
}
|
|
return c.testConnection(ctx)
|
|
}
|
|
|
|
func (c *fakeVMwareClient) Close() {}
|
|
|
|
func TestVMwareHandlers_HandleAdd_Success(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"name": "lab-vcenter",
|
|
"host": "vcsa.lab.local",
|
|
"port": 443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
"enabled": true,
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAdd(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var created config.VMwareVCenterInstance
|
|
if err := json.NewDecoder(rec.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode create response: %v", err)
|
|
}
|
|
if created.ID == "" {
|
|
t.Fatalf("expected generated ID, got empty")
|
|
}
|
|
if created.Password != "********" {
|
|
t.Fatalf("expected password redacted, got %q", created.Password)
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load saved config: %v", err)
|
|
}
|
|
if len(stored) != 1 {
|
|
t.Fatalf("expected 1 saved instance, got %d", len(stored))
|
|
}
|
|
if stored[0].Password != "super-secret" {
|
|
t.Fatalf("expected unredacted password persisted, got %q", stored[0].Password)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleAdd_ValidationAndFeatureGate(t *testing.T) {
|
|
t.Run("missing host", func(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
handler, _ := newVMwareHandlersForTest(t)
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAdd(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("feature disabled", func(t *testing.T) {
|
|
setVMwareFeatureForTest(t, false)
|
|
setMockModeForVMwareTest(t, false)
|
|
handler, _ := newVMwareHandlersForTest(t)
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"host": "vcsa.lab.local",
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAdd(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "explicitly disabled") {
|
|
t.Fatalf("expected explicit disable message, got %s", rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleAdd_BlocksProjectedNetNewSystemsAtLimit(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "host-1",
|
|
Resource: unifiedresources.Resource{
|
|
ID: "host-1",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "existing.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
Agent: &unifiedresources.AgentData{
|
|
AgentID: "agent-1",
|
|
Hostname: "existing.local",
|
|
MachineID: "machine-1",
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
MachineID: "machine-1",
|
|
Hostnames: []string{"existing.local"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
monitor := &monitoring.Monitor{}
|
|
monitor.SetResourceStore(unifiedresources.NewMonitorAdapter(registry))
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
return []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vc-1-host-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "esxi-02.lab.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: "vc-1",
|
|
ManagedObjectID: "host-1",
|
|
EntityType: "host",
|
|
HostUUID: "vmware-host-2",
|
|
},
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
DMIUUID: "vmware-host-2",
|
|
Hostnames: []string{"esxi-02.lab.local"},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"id": "vc-1",
|
|
"name": "lab-vcenter",
|
|
"host": "vcsa.lab.local",
|
|
"port": 443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
"enabled": true,
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAdd(rec, req)
|
|
|
|
if rec.Code != http.StatusPaymentRequired {
|
|
t.Fatalf("expected 402 once projected VMware inventory exceeds the cap, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
payload := decodeMonitoredSystemLimitBlockedPayload(t, rec.Body.Bytes())
|
|
if payload.Error != "license_required" {
|
|
t.Fatalf("error=%q, want license_required", payload.Error)
|
|
}
|
|
if payload.Feature != maxMonitoredSystemsLicenseGateKey {
|
|
t.Fatalf("feature=%q, want %q", payload.Feature, maxMonitoredSystemsLicenseGateKey)
|
|
}
|
|
if !payload.MonitoredSystemPreview.WouldExceedLimit {
|
|
t.Fatalf("expected monitored_system_preview.would_exceed_limit=true, got %+v", payload.MonitoredSystemPreview)
|
|
}
|
|
if payload.MonitoredSystemPreview.Effect != "creates_new" {
|
|
t.Fatalf("effect=%q, want creates_new", payload.MonitoredSystemPreview.Effect)
|
|
}
|
|
if payload.MonitoredSystemPreview.AdditionalCount != 1 {
|
|
t.Fatalf("additional_count=%d, want 1", payload.MonitoredSystemPreview.AdditionalCount)
|
|
}
|
|
if len(payload.MonitoredSystemPreview.ProjectedSystems) != 1 {
|
|
t.Fatalf("len(projected_systems)=%d, want 1", len(payload.MonitoredSystemPreview.ProjectedSystems))
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load vmware config: %v", err)
|
|
}
|
|
if len(stored) != 0 {
|
|
t.Fatalf("expected blocked VMware add not to persist, got %d connections", len(stored))
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleAdd_ReturnsUnavailableBeforePreviewingInventory(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
reason string
|
|
}{
|
|
{
|
|
name: "unsettled",
|
|
reason: monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,
|
|
},
|
|
{
|
|
name: "rebuild pending",
|
|
reason: monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryRebuildPending,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
bindUnavailableSupplementalUsageProviderForTest(t, monitor, unifiedresources.SourceVMware, tc.reason)
|
|
|
|
previewRecordsCalled := false
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
previewRecordsCalled = true
|
|
return nil, errors.New("preview records should not run while monitored-system usage is unavailable")
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"name": "lab-vcenter",
|
|
"host": "vcsa.lab.local",
|
|
"port": 443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
"enabled": true,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAdd(rec, req)
|
|
|
|
assertMonitoredSystemUsageUnavailableReason(t, rec, tc.reason)
|
|
if previewRecordsCalled {
|
|
t.Fatal("expected VMware add not to preview external inventory while monitored-system usage is unavailable")
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load vmware config: %v", err)
|
|
}
|
|
if len(stored) != 0 {
|
|
t.Fatalf("expected unavailable monitored-system usage not to persist add, got %d connections", len(stored))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleAdd_DoesNotCountDisabledConnectionAtLimit(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "host-1",
|
|
Resource: unifiedresources.Resource{
|
|
ID: "host-1",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "existing.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
Agent: &unifiedresources.AgentData{
|
|
AgentID: "agent-1",
|
|
Hostname: "existing.local",
|
|
MachineID: "machine-1",
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
MachineID: "machine-1",
|
|
Hostnames: []string{"existing.local"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
monitor := &monitoring.Monitor{}
|
|
monitor.SetResourceStore(unifiedresources.NewMonitorAdapter(registry))
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
|
|
previewRecordsCalled := false
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
previewRecordsCalled = true
|
|
return nil, errors.New("preview records should not run for disabled connections")
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"id": "vc-1",
|
|
"name": "lab-vcenter",
|
|
"host": "vcsa.lab.local",
|
|
"port": 443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
"enabled": false,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAdd(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201 for disabled connection at the limit, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if previewRecordsCalled {
|
|
t.Fatal("expected disabled VMware add not to preview inventory")
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load vmware config: %v", err)
|
|
}
|
|
if len(stored) != 1 || stored[0].Enabled {
|
|
t.Fatalf("expected disabled VMware connection to persist without counting, got %+v", stored)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleAdd_AllowsDisabledConnectionWhenUsageUnavailable(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
bindUnavailableSupplementalUsageProviderForTest(
|
|
t,
|
|
monitor,
|
|
unifiedresources.SourceVMware,
|
|
monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,
|
|
)
|
|
|
|
previewRecordsCalled := false
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
previewRecordsCalled = true
|
|
return nil, errors.New("preview records should not run for disabled connections")
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"name": "lab-vcenter",
|
|
"host": "vcsa.lab.local",
|
|
"port": 443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
"enabled": false,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAdd(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201 for disabled connection while usage is unavailable, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if previewRecordsCalled {
|
|
t.Fatal("expected disabled VMware add not to preview inventory")
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load vmware config: %v", err)
|
|
}
|
|
if len(stored) != 1 || stored[0].Enabled {
|
|
t.Fatalf("expected disabled VMware connection to persist while usage is unavailable, got %+v", stored)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleAdd_AllowsCanonicalOverlapAtLimit(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "host-1",
|
|
Resource: unifiedresources.Resource{
|
|
ID: "host-1",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "esxi-01.lab.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
Agent: &unifiedresources.AgentData{
|
|
AgentID: "agent-1",
|
|
Hostname: "esxi-01.lab.local",
|
|
MachineID: "machine-1",
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
MachineID: "machine-1",
|
|
Hostnames: []string{"esxi-01.lab.local"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
monitor := &monitoring.Monitor{}
|
|
monitor.SetResourceStore(unifiedresources.NewMonitorAdapter(registry))
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
return []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vc-1-host-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "esxi-01.lab.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: "vc-1",
|
|
ManagedObjectID: "host-1",
|
|
EntityType: "host",
|
|
HostUUID: "vmware-host-1",
|
|
},
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
DMIUUID: "vmware-host-1",
|
|
Hostnames: []string{"esxi-01.lab.local"},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"id": "vc-1",
|
|
"name": "lab-vcenter",
|
|
"host": "vcsa.lab.local",
|
|
"port": 443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
"enabled": true,
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleAdd(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("expected overlapping VMware add to be allowed at limit, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load vmware config: %v", err)
|
|
}
|
|
if len(stored) != 1 {
|
|
t.Fatalf("expected allowed VMware add to persist, got %d connections", len(stored))
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleList_RedactsSensitiveFieldsAndIncludesRuntimeSummary(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
poller := monitoring.NewVMwarePoller(nil, time.Minute)
|
|
handler.getPoller = func(context.Context) *monitoring.VMwarePoller { return poller }
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
|
|
{
|
|
ID: "vc-1",
|
|
Name: "lab-a",
|
|
Host: "vcsa-a.lab.local",
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "secret-a",
|
|
InsecureSkipVerify: false,
|
|
Enabled: true,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
recordedAt := time.Date(2026, 3, 30, 10, 11, 12, 0, time.UTC)
|
|
poller.RecordConnectionTestSuccess("default", "vc-1", &vmware.InventorySummary{
|
|
Hosts: 3,
|
|
VMs: 42,
|
|
Datastores: 6,
|
|
VIRelease: "8.0.3",
|
|
}, recordedAt)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleList(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var listed []vmwareConnectionResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&listed); err != nil {
|
|
t.Fatalf("decode list response: %v", err)
|
|
}
|
|
if len(listed) != 1 {
|
|
t.Fatalf("expected 1 listed instance, got %d", len(listed))
|
|
}
|
|
if listed[0].Password != "********" {
|
|
t.Fatalf("expected password to be redacted, got %q", listed[0].Password)
|
|
}
|
|
if listed[0].Poll == nil || listed[0].Poll.LastSuccessAt == nil {
|
|
t.Fatalf("expected saved test runtime summary, got %+v", listed[0].Poll)
|
|
}
|
|
if listed[0].Observed == nil {
|
|
t.Fatalf("expected observed summary, got nil")
|
|
}
|
|
if listed[0].Observed.Hosts != 3 || listed[0].Observed.VMs != 42 || listed[0].Observed.Datastores != 6 {
|
|
t.Fatalf("unexpected observed counts: %+v", listed[0].Observed)
|
|
}
|
|
if listed[0].Observed.VIRelease != "8.0.3" {
|
|
t.Fatalf("unexpected VI release: %+v", listed[0].Observed)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleList_ReturnsMockConnectionsInMockMode(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, true)
|
|
|
|
handler, _ := newVMwareHandlersForTest(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleList(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var listed []vmwareConnectionResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&listed); err != nil {
|
|
t.Fatalf("decode list response: %v", err)
|
|
}
|
|
if len(listed) != 1 {
|
|
t.Fatalf("expected 1 mock VMware connection, got %d", len(listed))
|
|
}
|
|
if listed[0].Host != "vcsa.lab.local" {
|
|
t.Fatalf("expected mock VMware host vcsa.lab.local, got %q", listed[0].Host)
|
|
}
|
|
if listed[0].Password != "********" {
|
|
t.Fatalf("expected redacted mock VMware password, got %q", listed[0].Password)
|
|
}
|
|
if listed[0].Poll == nil || listed[0].Poll.LastSuccessAt == nil {
|
|
t.Fatalf("expected mock VMware poll summary, got %+v", listed[0].Poll)
|
|
}
|
|
if listed[0].Observed == nil || listed[0].Observed.Hosts == 0 || listed[0].Observed.VMs == 0 {
|
|
t.Fatalf("expected populated mock VMware observed summary, got %+v", listed[0].Observed)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleDelete_RemovesAndClearsRuntimeSummary(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
|
|
{ID: "alpha", Host: "vcsa-a.lab.local", Username: "admin", Password: "a"},
|
|
{ID: "beta", Host: "vcsa-b.lab.local", Username: "admin", Password: "b"},
|
|
}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
handler.recordTestSuccess("alpha", &vmware.InventorySummary{Hosts: 1}, time.Now().UTC())
|
|
|
|
deleteReq := httptest.NewRequest(http.MethodDelete, "/api/vmware/connections/alpha", nil)
|
|
deleteRec := httptest.NewRecorder()
|
|
handler.HandleDelete(deleteRec, deleteReq)
|
|
|
|
if deleteRec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", deleteRec.Code, deleteRec.Body.String())
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load persisted config: %v", err)
|
|
}
|
|
if len(stored) != 1 || stored[0].ID != "beta" {
|
|
t.Fatalf("expected only beta to remain, got %+v", stored)
|
|
}
|
|
if status := handler.runtimeStatus("alpha"); status.Poll != nil || status.Observed != nil {
|
|
t.Fatalf("expected runtime summary to be cleared, got %+v", status)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleList_CarriesDegradedObservedSummary(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
|
|
{
|
|
ID: "vc-1",
|
|
Name: "lab-a",
|
|
Host: "vcsa-a.lab.local",
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "secret-a",
|
|
Enabled: true,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
recordedAt := time.Date(2026, 3, 31, 10, 11, 12, 0, time.UTC)
|
|
handler.statusMu.Lock()
|
|
handler.statuses = map[string]vmwareConnectionRuntimeStatus{
|
|
"vc-1": {
|
|
Poll: &monitoring.VMwareConnectionPollStatus{
|
|
IntervalSeconds: 60,
|
|
LastSuccessAt: &recordedAt,
|
|
},
|
|
Observed: &monitoring.VMwareConnectionObservedSummary{
|
|
CollectedAt: &recordedAt,
|
|
Hosts: 3,
|
|
VMs: 42,
|
|
Datastores: 6,
|
|
VIRelease: "8.0.3",
|
|
Degraded: true,
|
|
IssueCount: 2,
|
|
Issues: []monitoring.VMwareConnectionObservedIssue{
|
|
{Stage: "signals", Category: "permission", Message: "VMware permissions are insufficient for host overall status", Occurrences: 2},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
handler.statusMu.Unlock()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleList(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var listed []vmwareConnectionResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&listed); err != nil {
|
|
t.Fatalf("decode list response: %v", err)
|
|
}
|
|
if len(listed) != 1 || listed[0].Observed == nil {
|
|
t.Fatalf("expected degraded observed summary, got %+v", listed)
|
|
}
|
|
if !listed[0].Observed.Degraded || listed[0].Observed.IssueCount != 2 {
|
|
t.Fatalf("unexpected degraded observed summary: %+v", listed[0].Observed)
|
|
}
|
|
if len(listed[0].Observed.Issues) != 1 || listed[0].Observed.Issues[0].Occurrences != 2 {
|
|
t.Fatalf("unexpected observed issues: %+v", listed[0].Observed.Issues)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleUpdate_PreservesMaskedSecretsAndReplacesFields(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
|
|
{
|
|
ID: "alpha",
|
|
Name: "old-name",
|
|
Host: "old.lab.local",
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
InsecureSkipVerify: false,
|
|
Enabled: true,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"id": "ignored-id",
|
|
"name": "new-name",
|
|
"host": "new.lab.local",
|
|
"port": 8443,
|
|
"username": "operator@vsphere.local",
|
|
"password": "********",
|
|
"insecureSkipVerify": true,
|
|
"enabled": true,
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/api/vmware/connections/alpha", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleUpdate(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var updated config.VMwareVCenterInstance
|
|
if err := json.NewDecoder(rec.Body).Decode(&updated); err != nil {
|
|
t.Fatalf("decode update response: %v", err)
|
|
}
|
|
if updated.ID != "alpha" {
|
|
t.Fatalf("expected path ID to win, got %q", updated.ID)
|
|
}
|
|
if updated.Password != "********" {
|
|
t.Fatalf("expected password to remain redacted, got %q", updated.Password)
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load persisted config: %v", err)
|
|
}
|
|
if len(stored) != 1 {
|
|
t.Fatalf("expected 1 stored instance, got %d", len(stored))
|
|
}
|
|
if stored[0].Host != "new.lab.local" || stored[0].Port != 8443 {
|
|
t.Fatalf("expected updated endpoint to persist, got %+v", stored[0])
|
|
}
|
|
if stored[0].Password != "super-secret" {
|
|
t.Fatalf("expected masked password to preserve stored secret, got %q", stored[0].Password)
|
|
}
|
|
if !stored[0].InsecureSkipVerify {
|
|
t.Fatalf("expected insecureSkipVerify update to persist")
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleUpdate_BlocksProjectedNetNewSystemsAtLimit(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{{
|
|
ID: "alpha",
|
|
Name: "vc-a",
|
|
Host: "vc-a.lab.local",
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
InsecureSkipVerify: true,
|
|
Enabled: true,
|
|
}}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
monitor := &monitoring.Monitor{}
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "host-1",
|
|
Resource: unifiedresources.Resource{
|
|
ID: "host-1",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "archive.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
Agent: &unifiedresources.AgentData{
|
|
AgentID: "agent-1",
|
|
Hostname: "archive.local",
|
|
MachineID: "machine-1",
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
MachineID: "machine-1",
|
|
Hostnames: []string{"archive.local"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
registry.IngestRecords(unifiedresources.SourceVMware, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vmware:alpha:host-1",
|
|
Resource: unifiedresources.Resource{
|
|
ID: "vmware-host-1",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "archive.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: "alpha",
|
|
ConnectionName: "vc-a",
|
|
VCenterHost: "vc-a.lab.local",
|
|
ManagedObjectID: "host-1",
|
|
EntityType: "host",
|
|
HostUUID: "vmware-host-1",
|
|
},
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
MachineID: "machine-1",
|
|
Hostnames: []string{"archive.local"},
|
|
},
|
|
},
|
|
})
|
|
monitor.SetResourceStore(unifiedresources.NewMonitorAdapter(registry))
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
return []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vmware:alpha:host-2",
|
|
Resource: unifiedresources.Resource{
|
|
ID: "vmware-host-2",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "backup.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: "alpha",
|
|
ConnectionName: "vc-a",
|
|
VCenterHost: "vc-b.lab.local",
|
|
ManagedObjectID: "host-2",
|
|
EntityType: "host",
|
|
HostUUID: "vmware-host-2",
|
|
},
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
Hostnames: []string{"backup.local"},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"name": "vc-b",
|
|
"host": "vc-b.lab.local",
|
|
"port": 443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "********",
|
|
"insecureSkipVerify": true,
|
|
"enabled": true,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/vmware/connections/alpha", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleUpdate(rec, req)
|
|
|
|
if rec.Code != http.StatusPaymentRequired {
|
|
t.Fatalf("expected 402 when update would add a new monitored system, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
payload := decodeMonitoredSystemLimitBlockedPayload(t, rec.Body.Bytes())
|
|
if !payload.MonitoredSystemPreview.WouldExceedLimit {
|
|
t.Fatalf("expected monitored_system_preview.would_exceed_limit=true, got %+v", payload.MonitoredSystemPreview)
|
|
}
|
|
if payload.MonitoredSystemPreview.Effect != "splits_existing" {
|
|
t.Fatalf("effect=%q, want splits_existing", payload.MonitoredSystemPreview.Effect)
|
|
}
|
|
if payload.MonitoredSystemPreview.AdditionalCount != 1 {
|
|
t.Fatalf("additional_count=%d, want 1", payload.MonitoredSystemPreview.AdditionalCount)
|
|
}
|
|
if len(payload.MonitoredSystemPreview.CurrentSystems) != 1 {
|
|
t.Fatalf("len(current_systems)=%d, want 1", len(payload.MonitoredSystemPreview.CurrentSystems))
|
|
}
|
|
if len(payload.MonitoredSystemPreview.ProjectedSystems) != 1 {
|
|
t.Fatalf("len(projected_systems)=%d, want 1", len(payload.MonitoredSystemPreview.ProjectedSystems))
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load persisted config: %v", err)
|
|
}
|
|
if stored[0].Host != "vc-a.lab.local" {
|
|
t.Fatalf("expected blocked update to preserve original host, got %+v", stored[0])
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleUpdate_ReturnsUnavailableBeforePreviewingInventory(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
reason string
|
|
}{
|
|
{
|
|
name: "unsettled",
|
|
reason: monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,
|
|
},
|
|
{
|
|
name: "rebuild pending",
|
|
reason: monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryRebuildPending,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
bindUnavailableSupplementalUsageProviderForTest(t, monitor, unifiedresources.SourceVMware, tc.reason)
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{{
|
|
ID: "alpha",
|
|
Name: "vc-a",
|
|
Host: "vc-a.lab.local",
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
InsecureSkipVerify: true,
|
|
Enabled: true,
|
|
}}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
previewRecordsCalled := false
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
previewRecordsCalled = true
|
|
return nil, errors.New("preview records should not run while monitored-system usage is unavailable")
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"name": "vc-b",
|
|
"host": "vc-b.lab.local",
|
|
"port": 443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "********",
|
|
"insecureSkipVerify": true,
|
|
"enabled": true,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/vmware/connections/alpha", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleUpdate(rec, req)
|
|
|
|
assertMonitoredSystemUsageUnavailableReason(t, rec, tc.reason)
|
|
if previewRecordsCalled {
|
|
t.Fatal("expected VMware update not to preview external inventory while monitored-system usage is unavailable")
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load vmware config: %v", err)
|
|
}
|
|
if len(stored) != 1 || stored[0].Host != "vc-a.lab.local" {
|
|
t.Fatalf("expected unavailable monitored-system usage to preserve stored connection, got %+v", stored)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleUpdate_AllowsDisablingConnectionWhenUsageUnavailable(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMockModeForVMwareTest(t, false)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
bindUnavailableSupplementalUsageProviderForTest(
|
|
t,
|
|
monitor,
|
|
unifiedresources.SourceVMware,
|
|
monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryRebuildPending,
|
|
)
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{{
|
|
ID: "alpha",
|
|
Name: "vc-a",
|
|
Host: "vc-a.lab.local",
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
InsecureSkipVerify: true,
|
|
Enabled: true,
|
|
}}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
previewRecordsCalled := false
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
previewRecordsCalled = true
|
|
return nil, errors.New("preview records should not run for disabled connections")
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"name": "vc-a",
|
|
"host": "vc-b.lab.local",
|
|
"port": 443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "********",
|
|
"insecureSkipVerify": true,
|
|
"enabled": false,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/vmware/connections/alpha", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleUpdate(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 while disabling with unavailable usage, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if previewRecordsCalled {
|
|
t.Fatal("expected disabled VMware update not to preview inventory")
|
|
}
|
|
|
|
stored, err := persistence.LoadVMwareConfig()
|
|
if err != nil {
|
|
t.Fatalf("load vmware config: %v", err)
|
|
}
|
|
if len(stored) != 1 {
|
|
t.Fatalf("expected 1 stored connection, got %d", len(stored))
|
|
}
|
|
if stored[0].Enabled {
|
|
t.Fatalf("expected stored VMware connection to become disabled, got %+v", stored[0])
|
|
}
|
|
if stored[0].Host != "vc-b.lab.local" {
|
|
t.Fatalf("expected disabled update to persist other edits, got %+v", stored[0])
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleTestConnection_SuccessAndFailure(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, _ := newVMwareHandlersForTest(t)
|
|
var gotConfig vmware.ClientConfig
|
|
handler.newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) {
|
|
gotConfig = cfg
|
|
return &fakeVMwareClient{
|
|
testConnection: func(context.Context) (*vmware.InventorySummary, error) {
|
|
return &vmware.InventorySummary{Hosts: 1, VMs: 2, Datastores: 3, VIRelease: "8.0.3"}, nil
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
successBody := marshalVMwareRequest(t, map[string]any{
|
|
"host": "vcsa.lab.local",
|
|
"port": 8443,
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
})
|
|
successReq := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/test", bytes.NewReader(successBody))
|
|
successRec := httptest.NewRecorder()
|
|
handler.HandleTestConnection(successRec, successReq)
|
|
|
|
if successRec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", successRec.Code, successRec.Body.String())
|
|
}
|
|
if gotConfig.Host != "vcsa.lab.local" || gotConfig.Port != 8443 {
|
|
t.Fatalf("unexpected client config: %+v", gotConfig)
|
|
}
|
|
|
|
handler.newClient = nil
|
|
failureBody := marshalVMwareRequest(t, map[string]any{
|
|
"host": "http://127.0.0.1/path",
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
})
|
|
failureReq := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/test", bytes.NewReader(failureBody))
|
|
failureRec := httptest.NewRecorder()
|
|
handler.HandleTestConnection(failureRec, failureReq)
|
|
|
|
if failureRec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for bad host, got %d: %s", failureRec.Code, failureRec.Body.String())
|
|
}
|
|
|
|
handler.newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) {
|
|
return &fakeVMwareClient{
|
|
testConnection: func(context.Context) (*vmware.InventorySummary, error) {
|
|
return nil, errors.New("boom")
|
|
},
|
|
}, nil
|
|
}
|
|
errorBody := marshalVMwareRequest(t, map[string]any{
|
|
"host": "vcsa.lab.local",
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
})
|
|
errorReq := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/test", bytes.NewReader(errorBody))
|
|
errorRec := httptest.NewRecorder()
|
|
handler.HandleTestConnection(errorRec, errorReq)
|
|
|
|
if errorRec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for failing connection, got %d: %s", errorRec.Code, errorRec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleTestConnection_PreservesUnsupportedVersionCategory(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, _ := newVMwareHandlersForTest(t)
|
|
handler.newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) {
|
|
return &fakeVMwareClient{
|
|
testConnection: func(context.Context) (*vmware.InventorySummary, error) {
|
|
return nil, &vmware.ConnectionError{
|
|
Category: "unsupported_version",
|
|
Message: "VMware vCenter version is outside the implemented VI JSON probe floor; Pulse currently probes 9.0.0.0, 8.0.3, 8.0.2.0, 8.0.1.0",
|
|
}
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"host": "vcsa.lab.local",
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/test", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleTestConnection(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var payload struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Details map[string]string `json:"details"`
|
|
}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode error response: %v", err)
|
|
}
|
|
if payload.Code != "vmware_connection_failed" {
|
|
t.Fatalf("unexpected code: %+v", payload)
|
|
}
|
|
if payload.Details["category"] != "unsupported_version" {
|
|
t.Fatalf("expected unsupported_version category, got %+v", payload.Details)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandlePreviewConnection_ReturnsCanonicalMultiSystemImpact(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, _ := newVMwareHandlersForTest(t)
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "agent-host-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "esxi-01.lab.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
DMIUUID: "uuid-host-1",
|
|
Hostnames: []string{"esxi-01.lab.local"},
|
|
},
|
|
},
|
|
})
|
|
monitor := &monitoring.Monitor{}
|
|
monitor.SetResourceStore(unifiedresources.NewMonitorAdapter(registry))
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
return []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vc-1:host:host-101",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "esxi-01.lab.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: "vc-1",
|
|
ConnectionName: "Lab VC",
|
|
ManagedObjectID: "host-101",
|
|
EntityType: "host",
|
|
HostUUID: "uuid-host-1",
|
|
},
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
DMIUUID: "uuid-host-1",
|
|
Hostnames: []string{"esxi-01.lab.local"},
|
|
},
|
|
},
|
|
{
|
|
SourceID: "vc-1:host:host-102",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "esxi-02.lab.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: "vc-1",
|
|
ConnectionName: "Lab VC",
|
|
ManagedObjectID: "host-102",
|
|
EntityType: "host",
|
|
HostUUID: "uuid-host-2",
|
|
},
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
DMIUUID: "uuid-host-2",
|
|
Hostnames: []string{"esxi-02.lab.local"},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"host": "vcsa.lab.local",
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/preview", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandlePreviewConnection(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var preview MonitoredSystemLedgerPreviewResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&preview); err != nil {
|
|
t.Fatalf("decode preview response: %v", err)
|
|
}
|
|
if preview.CurrentCount != 1 || preview.ProjectedCount != 2 || preview.AdditionalCount != 1 {
|
|
t.Fatalf("unexpected preview counts: %+v", preview)
|
|
}
|
|
if !preview.WouldExceedLimit {
|
|
t.Fatalf("expected preview to report limit overrun, got %+v", preview)
|
|
}
|
|
if len(preview.CurrentSystems) != 1 {
|
|
t.Fatalf("len(CurrentSystems) = %d, want 1", len(preview.CurrentSystems))
|
|
}
|
|
if len(preview.ProjectedSystems) != 2 {
|
|
t.Fatalf("len(ProjectedSystems) = %d, want 2", len(preview.ProjectedSystems))
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandlePreviewConnection_ReturnsNoChangeForDisabledConnection(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
setMaxMonitoredSystemsLicenseForTests(t, 1)
|
|
|
|
handler, _ := newVMwareHandlersForTest(t)
|
|
monitor, state, _ := newTestMonitor(t)
|
|
state.Hosts = []models.Host{
|
|
{
|
|
ID: "host-1",
|
|
Hostname: "tower.local",
|
|
DisplayName: "Tower",
|
|
Status: "online",
|
|
LastSeen: time.Now().UTC(),
|
|
MachineID: "machine-1",
|
|
AgentVersion: "1.0.0",
|
|
},
|
|
}
|
|
syncTestResourceStore(t, monitor, state)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
|
|
previewRecordsCalled := false
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
previewRecordsCalled = true
|
|
return nil, errors.New("preview records should not run for disabled connections")
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"name": "lab-vcenter",
|
|
"host": "vcsa.lab.local",
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
"enabled": false,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/preview", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandlePreviewConnection(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if previewRecordsCalled {
|
|
t.Fatal("expected disabled VMware preview not to fetch inventory")
|
|
}
|
|
|
|
var preview MonitoredSystemLedgerPreviewResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&preview); err != nil {
|
|
t.Fatalf("decode preview response: %v", err)
|
|
}
|
|
if preview.CurrentCount != 1 || preview.ProjectedCount != 1 || preview.AdditionalCount != 0 {
|
|
t.Fatalf("unexpected preview counts: %+v", preview)
|
|
}
|
|
if preview.Effect != "no_change" {
|
|
t.Fatalf("Effect = %q, want no_change", preview.Effect)
|
|
}
|
|
if len(preview.CurrentSystems) != 0 || len(preview.ProjectedSystems) != 0 {
|
|
t.Fatalf("disabled preview should not surface affected systems, got %+v", preview)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandlePreviewConnection_ReturnsUnavailableWhenSupplementalInventoryUnsettled(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, _ := newVMwareHandlersForTest(t)
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
provider := newTestSupplementalUsageProvider(unifiedresources.SourceVMware)
|
|
bindTestSupplementalUsageProvider(monitor, unifiedresources.SourceVMware, provider)
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"host": "vcsa.lab.local",
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/preview", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandlePreviewConnection(rec, req)
|
|
|
|
assertMonitoredSystemUsageUnavailableReason(t, rec, monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled)
|
|
}
|
|
|
|
func TestVMwareHandlers_HandlePreviewConnection_ReturnsUnavailableWhenSupplementalInventoryRebuildPending(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, _ := newVMwareHandlersForTest(t)
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
provider := newTestSupplementalUsageProvider(unifiedresources.SourceVMware)
|
|
bindTestSupplementalUsageProvider(monitor, unifiedresources.SourceVMware, provider)
|
|
provider.settleAtWithRecords(time.Now().UTC().Add(time.Minute), nil)
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"host": "vcsa.lab.local",
|
|
"username": "administrator@vsphere.local",
|
|
"password": "super-secret",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/preview", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandlePreviewConnection(rec, req)
|
|
|
|
assertMonitoredSystemUsageUnavailableReason(t, rec, monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryRebuildPending)
|
|
}
|
|
|
|
func TestVMwareHandlers_HandlePreviewSavedConnection_PreservesStoredSecrets(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
|
|
{
|
|
ID: "conn-1",
|
|
Name: "lab-vcenter",
|
|
Host: "vcsa.lab.local",
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
Enabled: true,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
var previewed config.VMwareVCenterInstance
|
|
handler.previewRecords = func(_ context.Context, instance config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
previewed = instance
|
|
return []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vc-1:host:host-101",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "esxi-01.lab.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: "conn-1",
|
|
ConnectionName: "lab-vcenter",
|
|
ManagedObjectID: "host-101",
|
|
EntityType: "host",
|
|
HostUUID: "uuid-host-1",
|
|
},
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
DMIUUID: "uuid-host-1",
|
|
Hostnames: []string{"esxi-01.lab.local"},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"host": "edited.lab.local",
|
|
"username": "operator@vsphere.local",
|
|
"password": "********",
|
|
"insecureSkipVerify": true,
|
|
"enabled": true,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/conn-1/preview", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandlePreviewSavedConnection(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if previewed.Password != "super-secret" {
|
|
t.Fatalf("expected stored password to be reused, got %+v", previewed)
|
|
}
|
|
if previewed.Host != "edited.lab.local" {
|
|
t.Fatalf("expected edited host to be previewed, got %+v", previewed)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandlePreviewSavedConnection_ReturnsRemovalForDisabledConnection(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
monitor := &monitoring.Monitor{}
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
registry.IngestRecords(unifiedresources.SourceVMware, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vc-edit:host:host-101",
|
|
Resource: unifiedresources.Resource{
|
|
ID: "vmware-host-101",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "esxi-01.lab.local",
|
|
Status: unifiedresources.StatusOnline,
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: "conn-1",
|
|
ConnectionName: "lab-vcenter",
|
|
VCenterHost: "vcsa.lab.local",
|
|
ManagedObjectID: "host-101",
|
|
EntityType: "host",
|
|
HostUUID: "uuid-host-1",
|
|
},
|
|
},
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
DMIUUID: "uuid-host-1",
|
|
Hostnames: []string{"esxi-01.lab.local"},
|
|
},
|
|
},
|
|
})
|
|
monitor.SetResourceStore(unifiedresources.NewMonitorAdapter(registry))
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
|
|
{
|
|
ID: "conn-1",
|
|
Name: "lab-vcenter",
|
|
Host: "vcsa.lab.local",
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
Enabled: true,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
previewRecordsCalled := false
|
|
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
previewRecordsCalled = true
|
|
return nil, errors.New("preview records should not run for disabled connections")
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"host": "vcsa.lab.local",
|
|
"username": "administrator@vsphere.local",
|
|
"password": "********",
|
|
"enabled": false,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/conn-1/preview", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
handler.HandlePreviewSavedConnection(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if previewRecordsCalled {
|
|
t.Fatal("expected disabled VMware saved preview not to fetch inventory")
|
|
}
|
|
|
|
var preview MonitoredSystemLedgerPreviewResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&preview); err != nil {
|
|
t.Fatalf("decode preview response: %v", err)
|
|
}
|
|
if preview.CurrentCount != 1 || preview.ProjectedCount != 0 || preview.AdditionalCount != 0 {
|
|
t.Fatalf("unexpected preview counts: %+v", preview)
|
|
}
|
|
if preview.Effect != "removes_existing" {
|
|
t.Fatalf("Effect = %q, want removes_existing", preview.Effect)
|
|
}
|
|
if len(preview.CurrentSystems) != 1 {
|
|
t.Fatalf("len(CurrentSystems) = %d, want 1", len(preview.CurrentSystems))
|
|
}
|
|
if len(preview.ProjectedSystems) != 0 {
|
|
t.Fatalf("len(ProjectedSystems) = %d, want 0", len(preview.ProjectedSystems))
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandlePreviewSavedConnection_ReturnsUnavailableWhenSupplementalInventoryUnsettled(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
provider := newTestSupplementalUsageProvider(unifiedresources.SourceVMware)
|
|
bindTestSupplementalUsageProvider(monitor, unifiedresources.SourceVMware, provider)
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
|
|
{
|
|
ID: "conn-1",
|
|
Name: "lab-vcenter",
|
|
Host: "vcsa.lab.local",
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
Enabled: true,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/conn-1/preview", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandlePreviewSavedConnection(rec, req)
|
|
|
|
assertMonitoredSystemUsageUnavailableReason(t, rec, monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled)
|
|
}
|
|
|
|
func TestVMwareHandlers_HandlePreviewSavedConnection_ReturnsUnavailableWhenSupplementalInventoryRebuildPending(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
|
|
provider := newTestSupplementalUsageProvider(unifiedresources.SourceVMware)
|
|
bindTestSupplementalUsageProvider(monitor, unifiedresources.SourceVMware, provider)
|
|
provider.settleAtWithRecords(time.Now().UTC().Add(time.Minute), nil)
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
|
|
{
|
|
ID: "conn-1",
|
|
Name: "lab-vcenter",
|
|
Host: "vcsa.lab.local",
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
Enabled: true,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/conn-1/preview", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandlePreviewSavedConnection(rec, req)
|
|
|
|
assertMonitoredSystemUsageUnavailableReason(t, rec, monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryRebuildPending)
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleTestSavedConnection_UsesStoredSecretsAndUpdatesRuntimeSummary(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
connection := config.VMwareVCenterInstance{
|
|
ID: "conn-1",
|
|
Name: "lab-vcenter",
|
|
Host: "vcsa.lab.local",
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
InsecureSkipVerify: false,
|
|
Enabled: true,
|
|
}
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
poller := monitoring.NewVMwarePoller(nil, time.Minute)
|
|
handler.getPoller = func(context.Context) *monitoring.VMwarePoller { return poller }
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{connection}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
var gotConfig vmware.ClientConfig
|
|
handler.newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) {
|
|
gotConfig = cfg
|
|
return &fakeVMwareClient{
|
|
testConnection: func(context.Context) (*vmware.InventorySummary, error) {
|
|
return &vmware.InventorySummary{Hosts: 4, VMs: 25, Datastores: 5, VIRelease: "8.0.3"}, nil
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/conn-1/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleTestSavedConnection(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if gotConfig.Host != "vcsa.lab.local" || gotConfig.Password != "super-secret" {
|
|
t.Fatalf("unexpected saved client config: %+v", gotConfig)
|
|
}
|
|
|
|
listReq := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
|
|
listRec := httptest.NewRecorder()
|
|
handler.HandleList(listRec, listReq)
|
|
if listRec.Code != http.StatusOK {
|
|
t.Fatalf("list expected 200, got %d: %s", listRec.Code, listRec.Body.String())
|
|
}
|
|
|
|
var listed []vmwareConnectionResponse
|
|
if err := json.NewDecoder(listRec.Body).Decode(&listed); err != nil {
|
|
t.Fatalf("decode list response: %v", err)
|
|
}
|
|
if len(listed) != 1 || listed[0].Poll == nil || listed[0].Poll.LastSuccessAt == nil {
|
|
t.Fatalf("expected saved retest to update runtime status, got %+v", listed)
|
|
}
|
|
if listed[0].Observed == nil || listed[0].Observed.VMs != 25 {
|
|
t.Fatalf("expected saved retest to update observed summary, got %+v", listed[0].Observed)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleTestSavedConnection_UpdatesRuntimeSummaryFailure(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
connection := config.VMwareVCenterInstance{
|
|
ID: "conn-1",
|
|
Host: "vcsa.lab.local",
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
Enabled: true,
|
|
}
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
poller := monitoring.NewVMwarePoller(nil, time.Minute)
|
|
handler.getPoller = func(context.Context) *monitoring.VMwarePoller { return poller }
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{connection}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
handler.newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) {
|
|
return &fakeVMwareClient{
|
|
testConnection: func(context.Context) (*vmware.InventorySummary, error) {
|
|
return nil, &vmware.ConnectionError{Category: "auth", Message: "authentication failed"}
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/conn-1/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleTestSavedConnection(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
summary := poller.ConnectionSummaries("default", []config.VMwareVCenterInstance{connection})[connection.ID]
|
|
if summary.Poll == nil || summary.Poll.LastError == nil {
|
|
t.Fatalf("expected saved retest failure to update runtime summary, got %+v", summary.Poll)
|
|
}
|
|
if summary.Poll.LastError.Message != "authentication failed" || summary.Poll.LastError.Category != "auth" {
|
|
t.Fatalf("unexpected failure summary: %+v", summary.Poll.LastError)
|
|
}
|
|
}
|
|
|
|
func TestVMwareHandlers_HandleTestSavedConnection_MergesEditedPayloadWithoutOverwritingRuntimeSummary(t *testing.T) {
|
|
setVMwareFeatureForTest(t, true)
|
|
|
|
handler, persistence := newVMwareHandlersForTest(t)
|
|
poller := monitoring.NewVMwarePoller(nil, time.Minute)
|
|
handler.getPoller = func(context.Context) *monitoring.VMwarePoller { return poller }
|
|
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
|
|
{
|
|
ID: "conn-1",
|
|
Name: "lab-vcenter",
|
|
Host: "vcsa.lab.local",
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
InsecureSkipVerify: false,
|
|
Enabled: true,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed vmware config: %v", err)
|
|
}
|
|
|
|
previousAt := time.Date(2026, 3, 30, 9, 0, 0, 0, time.UTC)
|
|
poller.RecordConnectionTestSuccess("default", "conn-1", &vmware.InventorySummary{Hosts: 1, VMs: 2, Datastores: 3, VIRelease: "8.0.2.0"}, previousAt)
|
|
|
|
var gotConfig vmware.ClientConfig
|
|
handler.newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) {
|
|
gotConfig = cfg
|
|
return &fakeVMwareClient{
|
|
testConnection: func(context.Context) (*vmware.InventorySummary, error) {
|
|
return &vmware.InventorySummary{Hosts: 9, VMs: 99, Datastores: 12, VIRelease: "8.0.3"}, nil
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
body := marshalVMwareRequest(t, map[string]any{
|
|
"name": "edited-vcenter",
|
|
"host": "edited.lab.local",
|
|
"port": 8443,
|
|
"username": "operator@vsphere.local",
|
|
"password": "********",
|
|
"insecureSkipVerify": true,
|
|
"enabled": true,
|
|
})
|
|
req := httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/vmware/connections/conn-1/test",
|
|
bytes.NewReader(body),
|
|
)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleTestSavedConnection(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if gotConfig.Host != "edited.lab.local" || gotConfig.Port != 8443 {
|
|
t.Fatalf("expected edited endpoint, got %+v", gotConfig)
|
|
}
|
|
if gotConfig.Password != "super-secret" {
|
|
t.Fatalf("expected stored password to be reused, got %+v", gotConfig)
|
|
}
|
|
|
|
summary := poller.ConnectionSummaries("default", []config.VMwareVCenterInstance{{
|
|
ID: "conn-1",
|
|
Name: "lab-vcenter",
|
|
Host: "vcsa.lab.local",
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "super-secret",
|
|
InsecureSkipVerify: false,
|
|
Enabled: true,
|
|
}})["conn-1"]
|
|
if summary.Observed == nil || summary.Observed.VMs != 2 || summary.Observed.VIRelease != "8.0.2.0" {
|
|
t.Fatalf("expected existing runtime summary to remain unchanged, got %+v", summary.Observed)
|
|
}
|
|
if summary.Poll == nil || summary.Poll.LastSuccessAt == nil || !summary.Poll.LastSuccessAt.Equal(previousAt) {
|
|
t.Fatalf("expected existing last success timestamp to remain unchanged, got %+v", summary.Poll)
|
|
}
|
|
}
|
|
|
|
func newVMwareHandlersForTest(t *testing.T) (*VMwareHandlers, *config.ConfigPersistence) {
|
|
t.Helper()
|
|
|
|
persistence := config.NewConfigPersistence(t.TempDir())
|
|
monitor, _, _ := newTestMonitor(t)
|
|
handler := &VMwareHandlers{
|
|
getPersistence: func(context.Context) *config.ConfigPersistence { return persistence },
|
|
getMonitor: func(context.Context) *monitoring.Monitor { return monitor },
|
|
previewRecords: func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
return handler, persistence
|
|
}
|
|
|
|
func setVMwareFeatureForTest(t *testing.T, enabled bool) {
|
|
t.Helper()
|
|
previous := vmware.IsFeatureEnabled()
|
|
vmware.SetFeatureEnabled(enabled)
|
|
t.Cleanup(func() {
|
|
vmware.SetFeatureEnabled(previous)
|
|
})
|
|
}
|
|
|
|
func setMockModeForVMwareTest(t *testing.T, enabled bool) {
|
|
t.Helper()
|
|
previous := mock.IsMockEnabled()
|
|
mock.SetEnabled(enabled)
|
|
t.Cleanup(func() {
|
|
mock.SetEnabled(previous)
|
|
})
|
|
}
|
|
|
|
func marshalVMwareRequest(t *testing.T, payload map[string]any) []byte {
|
|
t.Helper()
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal request body: %v", err)
|
|
}
|
|
return body
|
|
}
|