Files
pulse/internal/models/deepcopy_test.go
T
2026-08-31 15:53:50 +01:00

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")
}
}