mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
515 lines
14 KiB
Go
515 lines
14 KiB
Go
package models
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// clonePBSInstance must carry scalar identity fields, including the
|
|
// PBS-reported node hostname that connected-system grouping relies on, and
|
|
// its collection copies must stay independent of the source.
|
|
func TestClonePBSInstance_PreservesReportedNodeName(t *testing.T) {
|
|
src := PBSInstance{
|
|
ID: "pbs-backup",
|
|
Name: "backup",
|
|
Host: "https://192.0.2.40:8007",
|
|
NodeName: "pbs01",
|
|
Datastores: []PBSDatastore{{Name: "internal"}},
|
|
}
|
|
|
|
dest := clonePBSInstance(src)
|
|
if dest.NodeName != "pbs01" {
|
|
t.Fatalf("NodeName = %q, want %q", dest.NodeName, "pbs01")
|
|
}
|
|
|
|
dest.Datastores[0].Name = "mutated"
|
|
if src.Datastores[0].Name != "internal" {
|
|
t.Fatalf("clone datastores share backing array with source")
|
|
}
|
|
}
|
|
|
|
// --- Pointer clone helpers ---
|
|
|
|
func TestCloneBoolPtr_Nil(t *testing.T) {
|
|
if cloneBoolPtr(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneBoolPtr_Value(t *testing.T) {
|
|
v := true
|
|
got := cloneBoolPtr(&v)
|
|
if got == nil || *got != true {
|
|
t.Error("value should be preserved")
|
|
}
|
|
v = false
|
|
if *got != true {
|
|
t.Error("clone should be independent of source")
|
|
}
|
|
}
|
|
|
|
func TestCloneHostAndZFSPoolIsolateZFSDatasets(t *testing.T) {
|
|
host := Host{ZFSPools: []HostZFSPool{{
|
|
Name: "tank",
|
|
Datasets: []ZFSDataset{{Name: "tank/apps", UsedBytes: 100}},
|
|
}}}
|
|
hostClone := cloneHost(host)
|
|
hostClone.ZFSPools[0].Datasets[0].Name = "mutated"
|
|
if host.ZFSPools[0].Datasets[0].Name != "tank/apps" {
|
|
t.Fatal("host clone aliased zfs datasets")
|
|
}
|
|
|
|
pool := &ZFSPool{Name: "tank", Datasets: []ZFSDataset{{Name: "tank/apps"}}}
|
|
poolClone := cloneZFSPool(pool)
|
|
poolClone.Datasets[0].Name = "mutated"
|
|
if pool.Datasets[0].Name != "tank/apps" {
|
|
t.Fatal("pool clone aliased zfs datasets")
|
|
}
|
|
}
|
|
|
|
func TestCloneHostIsolatesAgentPrivilege(t *testing.T) {
|
|
host := Host{AgentPrivilege: &AgentPrivilegeStatus{
|
|
RunningAsRoot: false,
|
|
ServiceUser: "pulse-agent",
|
|
}}
|
|
hostClone := cloneHost(host)
|
|
if hostClone.AgentPrivilege == nil {
|
|
t.Fatal("host clone dropped agent privilege")
|
|
}
|
|
hostClone.AgentPrivilege.ServiceUser = "mutated"
|
|
if host.AgentPrivilege.ServiceUser != "pulse-agent" {
|
|
t.Fatal("host clone aliased agent privilege")
|
|
}
|
|
}
|
|
|
|
func TestCloneDockerHostIsolatesAgentModuleStatus(t *testing.T) {
|
|
src := DockerHost{AgentModules: []AgentModuleStatus{{
|
|
Name: "typed-privilege-helper", State: "degraded", LastError: "container.inventory: helper provider unavailable",
|
|
}}}
|
|
clone := cloneDockerHost(src)
|
|
if len(clone.AgentModules) != 1 || clone.AgentModules[0].LastError != src.AgentModules[0].LastError {
|
|
t.Fatalf("clone dropped Docker agent module status: %+v", clone.AgentModules)
|
|
}
|
|
clone.AgentModules[0].LastError = "mutated"
|
|
if src.AgentModules[0].LastError != "container.inventory: helper provider unavailable" {
|
|
t.Fatalf("Docker host clone aliased agent module status: %+v", src.AgentModules)
|
|
}
|
|
}
|
|
|
|
func TestCloneDockerContainer_PreservesIndependentOOMEvidence(t *testing.T) {
|
|
oomKilled := false
|
|
src := DockerContainer{
|
|
ID: "container-1",
|
|
HealthcheckTargets: []string{"dependency-service"},
|
|
OOMKilled: &oomKilled,
|
|
}
|
|
|
|
got := cloneDockerContainer(src)
|
|
if got.OOMKilled == nil || *got.OOMKilled {
|
|
t.Fatalf("OOMKilled = %v, want explicit false", got.OOMKilled)
|
|
}
|
|
oomKilled = true
|
|
if *got.OOMKilled {
|
|
t.Fatal("cloned Docker container must own its OOM evidence value")
|
|
}
|
|
got.HealthcheckTargets[0] = "mutated"
|
|
if src.HealthcheckTargets[0] != "dependency-service" {
|
|
t.Fatal("cloned Docker container must own its health-check targets")
|
|
}
|
|
}
|
|
|
|
func TestCloneDockerHost_PreservesIndependentIdentityConflict(t *testing.T) {
|
|
src := DockerHost{
|
|
ID: "docker-host-1",
|
|
IdentityConflict: &DockerHostIdentityConflict{
|
|
Hostnames: []string{"clone-a", "clone-b"},
|
|
MachineIDs: []string{"machine-a", "machine-b"},
|
|
},
|
|
}
|
|
|
|
got := cloneDockerHost(src)
|
|
if got.IdentityConflict == nil {
|
|
t.Fatal("clone dropped identity conflict evidence")
|
|
}
|
|
if len(got.IdentityConflict.Hostnames) != 2 || got.IdentityConflict.Hostnames[0] != "clone-a" {
|
|
t.Fatalf("hostnames = %v, want [clone-a clone-b]", got.IdentityConflict.Hostnames)
|
|
}
|
|
src.IdentityConflict.Hostnames[0] = "mutated"
|
|
src.IdentityConflict.MachineIDs[0] = "mutated"
|
|
if got.IdentityConflict.Hostnames[0] != "clone-a" || got.IdentityConflict.MachineIDs[0] != "machine-a" {
|
|
t.Fatal("cloned Docker host must own its identity conflict slices")
|
|
}
|
|
|
|
if clean := cloneDockerHost(DockerHost{ID: "healthy"}); clean.IdentityConflict != nil {
|
|
t.Fatalf("healthy host should clone with nil conflict, got %+v", clean.IdentityConflict)
|
|
}
|
|
}
|
|
|
|
func TestCloneHost_PreservesIndependentIdentityConflict(t *testing.T) {
|
|
src := Host{
|
|
ID: "host-1",
|
|
IdentityConflict: &HostIdentityConflict{
|
|
Hostnames: []string{"clone-a", "clone-b"},
|
|
ReportIPs: []string{"192.168.1.10", "10.0.0.10"},
|
|
},
|
|
}
|
|
|
|
got := cloneHost(src)
|
|
if got.IdentityConflict == nil {
|
|
t.Fatal("clone dropped identity conflict evidence")
|
|
}
|
|
if len(got.IdentityConflict.Hostnames) != 2 || got.IdentityConflict.Hostnames[0] != "clone-a" {
|
|
t.Fatalf("hostnames = %v, want [clone-a clone-b]", got.IdentityConflict.Hostnames)
|
|
}
|
|
src.IdentityConflict.Hostnames[0] = "mutated"
|
|
src.IdentityConflict.ReportIPs[0] = "mutated"
|
|
if got.IdentityConflict.Hostnames[0] != "clone-a" || got.IdentityConflict.ReportIPs[0] != "192.168.1.10" {
|
|
t.Fatal("cloned host must own its identity conflict slices")
|
|
}
|
|
|
|
if clean := cloneHost(Host{ID: "healthy"}); clean.IdentityConflict != nil {
|
|
t.Fatalf("healthy host should clone with nil conflict, got %+v", clean.IdentityConflict)
|
|
}
|
|
}
|
|
|
|
func TestCloneFloat64Ptr_Nil(t *testing.T) {
|
|
if cloneFloat64Ptr(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneFloat64Ptr_Value(t *testing.T) {
|
|
v := 3.14
|
|
got := cloneFloat64Ptr(&v)
|
|
if got == nil || *got != 3.14 {
|
|
t.Error("value should be preserved")
|
|
}
|
|
v = 0
|
|
if *got != 3.14 {
|
|
t.Error("clone should be independent of source")
|
|
}
|
|
}
|
|
|
|
func TestCloneIntPtr_Nil(t *testing.T) {
|
|
if cloneIntPtr(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneIntPtr_Value(t *testing.T) {
|
|
v := 42
|
|
got := cloneIntPtr(&v)
|
|
if got == nil || *got != 42 {
|
|
t.Error("value should be preserved")
|
|
}
|
|
v = 0
|
|
if *got != 42 {
|
|
t.Error("clone should be independent")
|
|
}
|
|
}
|
|
|
|
func TestCloneInt64Ptr_Nil(t *testing.T) {
|
|
if cloneInt64Ptr(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneInt64Ptr_Value(t *testing.T) {
|
|
v := int64(999)
|
|
got := cloneInt64Ptr(&v)
|
|
if got == nil || *got != 999 {
|
|
t.Error("value should be preserved")
|
|
}
|
|
v = 0
|
|
if *got != 999 {
|
|
t.Error("clone should be independent")
|
|
}
|
|
}
|
|
|
|
// --- Map clone helpers ---
|
|
|
|
func TestCloneStringMap_Nil(t *testing.T) {
|
|
if cloneStringMap(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneStringMap_Isolation(t *testing.T) {
|
|
src := map[string]string{"a": "1", "b": "2"}
|
|
dst := cloneStringMap(src)
|
|
dst["c"] = "3"
|
|
if _, ok := src["c"]; ok {
|
|
t.Error("mutating clone should not affect source")
|
|
}
|
|
if len(dst) != 3 {
|
|
t.Error("clone should have the added entry")
|
|
}
|
|
}
|
|
|
|
func TestCloneStringFloat64Map_Nil(t *testing.T) {
|
|
if cloneStringFloat64Map(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneStringFloat64Map_Isolation(t *testing.T) {
|
|
src := map[string]float64{"temp": 42.5}
|
|
dst := cloneStringFloat64Map(src)
|
|
dst["fan"] = 1200.0
|
|
if _, ok := src["fan"]; ok {
|
|
t.Error("mutating clone should not affect source")
|
|
}
|
|
}
|
|
|
|
// --- Slice clone helpers ---
|
|
|
|
func TestCloneCoreTemps_Nil(t *testing.T) {
|
|
if cloneCoreTemps(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneCoreTemps_Isolation(t *testing.T) {
|
|
src := []CoreTemp{{Core: 0, Temp: 45.0}}
|
|
dst := cloneCoreTemps(src)
|
|
dst[0].Temp = 99.0
|
|
if src[0].Temp != 45.0 {
|
|
t.Error("mutating clone should not affect source")
|
|
}
|
|
}
|
|
|
|
func TestCloneGPUTemps_Nil(t *testing.T) {
|
|
if cloneGPUTemps(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneNVMeTemps_Nil(t *testing.T) {
|
|
if cloneNVMeTemps(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneDiskTemps_Nil(t *testing.T) {
|
|
if cloneDiskTemps(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
// --- Temperature clone ---
|
|
|
|
func TestCloneTemperature_Nil(t *testing.T) {
|
|
if cloneTemperature(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneTemperature_Isolation(t *testing.T) {
|
|
src := &Temperature{
|
|
Cores: []CoreTemp{{Core: 0, Temp: 45.0}},
|
|
NVMe: []NVMeTemp{{Device: "nvme0", Temp: 35.0}},
|
|
}
|
|
dst := cloneTemperature(src)
|
|
dst.Cores[0].Temp = 99.0
|
|
if src.Cores[0].Temp != 45.0 {
|
|
t.Error("mutating cloned temperature should not affect source")
|
|
}
|
|
}
|
|
|
|
// --- Guest network interface clone ---
|
|
|
|
func TestCloneGuestNetworkInterfaces_Nil(t *testing.T) {
|
|
if cloneGuestNetworkInterfaces(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneGuestNetworkInterfaces_Isolation(t *testing.T) {
|
|
src := []GuestNetworkInterface{
|
|
{Name: "eth0", Addresses: []string{"192.168.1.10"}},
|
|
}
|
|
dst := cloneGuestNetworkInterfaces(src)
|
|
dst[0].Addresses = append(dst[0].Addresses, "10.0.0.1")
|
|
if len(src[0].Addresses) != 1 {
|
|
t.Error("mutating clone addresses should not affect source")
|
|
}
|
|
}
|
|
|
|
// --- Host network interface clone ---
|
|
|
|
func TestCloneHostNetworkInterfaces_Nil(t *testing.T) {
|
|
if cloneHostNetworkInterfaces(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneHostNetworkInterfaces_Isolation(t *testing.T) {
|
|
src := []HostNetworkInterface{
|
|
{Name: "eth0", Addresses: []string{"192.168.1.10"}, MAC: "00:11:22:33:44:55"},
|
|
}
|
|
dst := cloneHostNetworkInterfaces(src)
|
|
dst[0].Addresses = append(dst[0].Addresses, "10.0.0.1")
|
|
if len(src[0].Addresses) != 1 {
|
|
t.Error("mutating clone addresses should not affect source")
|
|
}
|
|
}
|
|
|
|
// --- SMART attributes clone ---
|
|
|
|
func TestCloneSMARTAttributes_Nil(t *testing.T) {
|
|
if cloneSMARTAttributes(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneSMARTAttributes_Isolation(t *testing.T) {
|
|
pending := int64(3)
|
|
media := int64(1)
|
|
src := &SMARTAttributes{
|
|
PendingSectors: &pending,
|
|
MediaErrors: &media,
|
|
}
|
|
dst := cloneSMARTAttributes(src)
|
|
newVal := int64(99)
|
|
dst.PendingSectors = &newVal
|
|
if *src.PendingSectors != 3 {
|
|
t.Error("mutating cloned SMART attributes should not affect source")
|
|
}
|
|
}
|
|
|
|
// --- Node clone ---
|
|
|
|
func TestCloneNode_Isolation(t *testing.T) {
|
|
src := Node{
|
|
ID: "node-1",
|
|
Name: "pve1",
|
|
Status: "online",
|
|
Temperature: &Temperature{
|
|
Cores: []CoreTemp{{Core: 0, Temp: 45.0}},
|
|
},
|
|
}
|
|
dst := cloneNode(src)
|
|
dst.Name = "changed"
|
|
dst.Temperature.Cores[0].Temp = 99.0
|
|
if src.Name != "pve1" {
|
|
t.Error("clone should not affect source name")
|
|
}
|
|
if src.Temperature.Cores[0].Temp != 45.0 {
|
|
t.Error("clone temperature should be independent")
|
|
}
|
|
}
|
|
|
|
func TestCloneNodes_Nil(t *testing.T) {
|
|
if cloneNodes(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneNodes_Empty(t *testing.T) {
|
|
got := cloneNodes([]Node{})
|
|
if got != nil {
|
|
t.Error("empty slice should clone to nil (consistent with nil input)")
|
|
}
|
|
}
|
|
|
|
// --- VM clone ---
|
|
|
|
func TestCloneVM_NetworkIsolation(t *testing.T) {
|
|
src := VM{
|
|
ID: "vm-1",
|
|
Name: "web",
|
|
NetworkInterfaces: []GuestNetworkInterface{
|
|
{Name: "eth0", Addresses: []string{"192.168.1.10"}},
|
|
},
|
|
}
|
|
dst := cloneVM(src)
|
|
dst.NetworkInterfaces[0].Addresses = append(dst.NetworkInterfaces[0].Addresses, "10.0.0.1")
|
|
if len(src.NetworkInterfaces[0].Addresses) != 1 {
|
|
t.Error("clone should not affect source network interfaces")
|
|
}
|
|
}
|
|
|
|
// --- Host clone ---
|
|
|
|
func TestCloneHost_MapIsolation(t *testing.T) {
|
|
thermalWarningLevel := 0
|
|
src := Host{
|
|
ID: "host-1",
|
|
Hostname: "server1",
|
|
Tags: []string{"env:prod"},
|
|
Sensors: HostSensorSummary{
|
|
TemperatureCelsius: map[string]float64{"cpu": 42.0},
|
|
ThermalState: &HostThermalState{
|
|
Source: "pmset",
|
|
Pressure: "nominal",
|
|
ThermalWarningLevel: &thermalWarningLevel,
|
|
LimitsPercent: map[string]int{"cpu_speed_limit": 100},
|
|
},
|
|
},
|
|
PackageUpdates: &HostPackageUpdateStatus{
|
|
Supported: true,
|
|
Packages: []HostPackageUpdate{{Name: "openssl"}},
|
|
},
|
|
StorageCleanup: &HostStorageCleanupStatus{Provider: "apt-package-cache", ReclaimableBytes: 512},
|
|
}
|
|
dst := cloneHost(src)
|
|
dst.Tags = append(dst.Tags, "new:val")
|
|
dst.Sensors.TemperatureCelsius["gpu"] = 65.0
|
|
dst.Sensors.ThermalState.LimitsPercent["cpu_speed_limit"] = 80
|
|
*dst.Sensors.ThermalState.ThermalWarningLevel = 1
|
|
dst.PackageUpdates.Packages[0].Name = "mutated"
|
|
dst.StorageCleanup.Provider = "mutated"
|
|
if len(src.Tags) != 1 {
|
|
t.Error("clone tags should be independent")
|
|
}
|
|
if _, ok := src.Sensors.TemperatureCelsius["gpu"]; ok {
|
|
t.Error("clone sensor map should be independent")
|
|
}
|
|
if src.Sensors.ThermalState.LimitsPercent["cpu_speed_limit"] != 100 {
|
|
t.Error("clone thermal limit map should be independent")
|
|
}
|
|
if *src.Sensors.ThermalState.ThermalWarningLevel != 0 {
|
|
t.Error("clone thermal warning pointer should be independent")
|
|
}
|
|
if src.PackageUpdates.Packages[0].Name != "openssl" {
|
|
t.Error("clone package update inventory should be independent")
|
|
}
|
|
if src.StorageCleanup.Provider != "apt-package-cache" {
|
|
t.Error("clone storage cleanup posture should be independent")
|
|
}
|
|
}
|
|
|
|
func TestCloneHosts_Nil(t *testing.T) {
|
|
if cloneHosts(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneHostLibvirtInventoryIsolation(t *testing.T) {
|
|
src := Host{Libvirt: &HostLibvirtInventory{
|
|
Domains: []HostLibvirtDomain{{ID: "domain-a", Name: "app"}},
|
|
}}
|
|
dst := cloneHost(src)
|
|
if dst.Libvirt == nil || len(dst.Libvirt.Domains) != 1 {
|
|
t.Fatalf("cloned libvirt inventory = %+v", dst.Libvirt)
|
|
}
|
|
dst.Libvirt.Domains[0].Name = "mutated"
|
|
if src.Libvirt.Domains[0].Name != "app" {
|
|
t.Fatal("clone libvirt inventory aliases source domains")
|
|
}
|
|
}
|
|
|
|
func TestCloneHostXCPNGInventoryIsolation(t *testing.T) {
|
|
src := Host{XCPNG: &HostXCPNGInventory{
|
|
PoolUUID: "11111111-1111-1111-1111-111111111111",
|
|
VMs: []HostXCPNGVM{{UUID: "vm-a", Name: "app"}},
|
|
}}
|
|
dst := cloneHost(src)
|
|
if dst.XCPNG == nil || len(dst.XCPNG.VMs) != 1 {
|
|
t.Fatalf("cloned XCP-ng inventory = %+v", dst.XCPNG)
|
|
}
|
|
dst.XCPNG.VMs[0].Name = "mutated"
|
|
if src.XCPNG.VMs[0].Name != "app" {
|
|
t.Fatal("cloned XCP-ng inventory aliases source VMs")
|
|
}
|
|
}
|