mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 08:33:03 +00:00
bd6f77e093
Tighten v5-to-v6 upgrade safety, release installability, provider MSP mode handling, AI cost accounting, metrics flushing, and frontend guardrails for the v6.0.0 GA candidate.
655 lines
20 KiB
Go
655 lines
20 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestKubernetesClusterToFrontend(t *testing.T) {
|
|
now := time.Date(2024, 2, 3, 4, 5, 6, 0, time.UTC)
|
|
lastUsed := time.Date(2024, 2, 4, 5, 6, 7, 0, time.UTC)
|
|
|
|
cluster := KubernetesCluster{
|
|
ID: "cluster-1",
|
|
AgentID: "agent-1",
|
|
Name: "prod",
|
|
DisplayName: "",
|
|
Server: "https://k8s",
|
|
Context: "ctx",
|
|
Version: "v1.25.0",
|
|
Status: "healthy",
|
|
LastSeen: now,
|
|
IntervalSeconds: 30,
|
|
AgentVersion: "v1",
|
|
TokenID: "token-1",
|
|
TokenName: "token-name",
|
|
TokenHint: "hint",
|
|
TokenLastUsedAt: &lastUsed,
|
|
Hidden: true,
|
|
PendingUninstall: true,
|
|
Nodes: []KubernetesNode{
|
|
{
|
|
UID: "node-1",
|
|
Name: "node-a",
|
|
Ready: true,
|
|
Roles: []string{"master"},
|
|
},
|
|
},
|
|
Pods: []KubernetesPod{
|
|
{
|
|
UID: "pod-1",
|
|
Name: "pod-a",
|
|
Namespace: "ns",
|
|
Labels: map[string]string{"app": "svc"},
|
|
Containers: []KubernetesPodContainer{
|
|
{Name: "c1", Ready: true},
|
|
},
|
|
},
|
|
},
|
|
Deployments: []KubernetesDeployment{
|
|
{
|
|
UID: "dep-1",
|
|
Name: "deploy-a",
|
|
Namespace: "ns",
|
|
CreatedAt: now,
|
|
DesiredReplicas: 2,
|
|
ObservedGeneration: 7,
|
|
Labels: map[string]string{"tier": "web"},
|
|
},
|
|
},
|
|
}
|
|
|
|
frontend := cluster.ToFrontend()
|
|
if frontend.DisplayName != cluster.Name {
|
|
t.Fatalf("DisplayName = %q, want %q", frontend.DisplayName, cluster.Name)
|
|
}
|
|
if frontend.TokenLastUsedAt == nil || *frontend.TokenLastUsedAt != lastUsed.Unix()*1000 {
|
|
t.Fatalf("TokenLastUsedAt = %v, want %d", frontend.TokenLastUsedAt, lastUsed.Unix()*1000)
|
|
}
|
|
if len(frontend.Nodes) != 1 || frontend.Nodes[0].Name != "node-a" {
|
|
t.Fatalf("Nodes = %#v, want 1 node", frontend.Nodes)
|
|
}
|
|
if len(frontend.Pods) != 1 || frontend.Pods[0].Name != "pod-a" {
|
|
t.Fatalf("Pods = %#v, want 1 pod", frontend.Pods)
|
|
}
|
|
if len(frontend.Deployments) != 1 || frontend.Deployments[0].Name != "deploy-a" {
|
|
t.Fatalf("Deployments = %#v, want 1 deployment", frontend.Deployments)
|
|
}
|
|
if frontend.Deployments[0].CreatedAt != now.Unix()*1000 || frontend.Deployments[0].ObservedGeneration != 7 {
|
|
t.Fatalf("Deployment API metadata = %#v", frontend.Deployments[0])
|
|
}
|
|
}
|
|
|
|
func TestKubernetesClusterToFrontend_DisplayNameFallback(t *testing.T) {
|
|
cluster := KubernetesCluster{
|
|
ID: "cluster-2",
|
|
Name: "",
|
|
}
|
|
|
|
frontend := cluster.ToFrontend()
|
|
if frontend.DisplayName != cluster.ID {
|
|
t.Fatalf("DisplayName = %q, want %q", frontend.DisplayName, cluster.ID)
|
|
}
|
|
if frontend.Nodes == nil || frontend.Pods == nil || frontend.Deployments == nil {
|
|
t.Fatalf("expected empty Kubernetes frontend collections to normalize, got %#v", frontend)
|
|
}
|
|
}
|
|
|
|
func TestKubernetesClusterToFrontend_ZeroTokenLastUsedAtOmitted(t *testing.T) {
|
|
zero := time.Time{}
|
|
cluster := KubernetesCluster{
|
|
ID: "cluster-3",
|
|
Name: "cluster",
|
|
LastSeen: time.Now(),
|
|
TokenLastUsedAt: &zero,
|
|
}
|
|
|
|
frontend := cluster.ToFrontend()
|
|
if frontend.TokenLastUsedAt != nil {
|
|
t.Fatalf("TokenLastUsedAt = %v, want nil for zero timestamp", frontend.TokenLastUsedAt)
|
|
}
|
|
|
|
payload, err := json.Marshal(frontend)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal error: %v", err)
|
|
}
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal error: %v", err)
|
|
}
|
|
|
|
if _, ok := decoded["tokenLastUsedAt"]; ok {
|
|
t.Fatalf("tokenLastUsedAt should be omitted from JSON for zero timestamp")
|
|
}
|
|
}
|
|
|
|
func TestTimeToUnixMillis(t *testing.T) {
|
|
if got := timeToUnixMillis(time.Time{}); got != 0 {
|
|
t.Fatalf("timeToUnixMillis(zero) = %d, want 0", got)
|
|
}
|
|
|
|
now := time.Date(2024, 3, 4, 5, 6, 7, 0, time.UTC)
|
|
if got := timeToUnixMillis(now); got != now.Unix()*1000 {
|
|
t.Fatalf("timeToUnixMillis = %d, want %d", got, now.Unix()*1000)
|
|
}
|
|
}
|
|
|
|
func TestConvertResourceToFrontend(t *testing.T) {
|
|
total := int64(100)
|
|
used := int64(60)
|
|
free := int64(40)
|
|
identity := &ResourceIdentityInput{
|
|
Hostname: "host-a",
|
|
MachineID: "machine-1",
|
|
IPs: []string{"10.0.0.1"},
|
|
}
|
|
|
|
input := ResourceConvertInput{
|
|
ID: "res-1",
|
|
Type: "node",
|
|
Name: "node-a",
|
|
DisplayName: "Node A",
|
|
Status: "healthy",
|
|
CPU: &ResourceMetricInput{Current: 12.5, Total: &total, Used: &used, Free: &free},
|
|
Memory: &ResourceMetricInput{Current: 50.1},
|
|
Disk: &ResourceMetricInput{Current: 70.2},
|
|
NetworkRX: 1000,
|
|
NetworkTX: 2000,
|
|
HasNetwork: true,
|
|
Tags: []string{"prod"},
|
|
Labels: map[string]string{"env": "prod"},
|
|
CustomURL: "https://resource.internal",
|
|
LastSeenUnix: 12345,
|
|
Alerts: []ResourceAlertInput{
|
|
{ID: "a1", Type: "cpu", Level: "warn", Message: "high", Value: 90, Threshold: 80, StartTimeUnix: 111},
|
|
},
|
|
Identity: identity,
|
|
PlatformData: json.RawMessage(`{"key":"value"}`),
|
|
}
|
|
|
|
frontend := ConvertResourceToFrontend(input)
|
|
if frontend.ID != input.ID || frontend.Name != input.Name {
|
|
t.Fatalf("frontend = %#v, want ID %q Name %q", frontend, input.ID, input.Name)
|
|
}
|
|
if frontend.CPU == nil || frontend.CPU.Total == nil || *frontend.CPU.Total != total {
|
|
t.Fatalf("CPU = %#v, want total %d", frontend.CPU, total)
|
|
}
|
|
if frontend.Network == nil || frontend.Network.RXBytes != 1000 || frontend.Network.TXBytes != 2000 {
|
|
t.Fatalf("Network = %#v, want RX/TX set", frontend.Network)
|
|
}
|
|
if len(frontend.Alerts) != 1 || frontend.Alerts[0].ID != "a1" {
|
|
t.Fatalf("Alerts = %#v, want 1 alert", frontend.Alerts)
|
|
}
|
|
if frontend.CustomURL != input.CustomURL {
|
|
t.Fatalf("CustomURL = %q, want %q", frontend.CustomURL, input.CustomURL)
|
|
}
|
|
if frontend.Identity == nil || frontend.Identity.Hostname != identity.Hostname {
|
|
t.Fatalf("Identity = %#v, want hostname %q", frontend.Identity, identity.Hostname)
|
|
}
|
|
}
|
|
|
|
func TestConvertResourceToFrontend_UsesCanonicalEmptyCollections(t *testing.T) {
|
|
frontend := ConvertResourceToFrontend(ResourceConvertInput{
|
|
ID: "res-2",
|
|
Type: "node",
|
|
Name: "node-b",
|
|
DisplayName: "Node B",
|
|
Status: "healthy",
|
|
LastSeenUnix: 123,
|
|
Identity: &ResourceIdentityInput{},
|
|
})
|
|
|
|
if frontend.Tags == nil || frontend.Labels == nil || frontend.Alerts == nil {
|
|
t.Fatalf("expected resource collections to normalize, got %#v", frontend)
|
|
}
|
|
if frontend.Identity == nil || frontend.Identity.IPs == nil {
|
|
t.Fatalf("expected resource identity collections to normalize, got %#v", frontend.Identity)
|
|
}
|
|
}
|
|
|
|
func TestNodeToFrontend_UsesCanonicalEmptyCollections(t *testing.T) {
|
|
node := Node{
|
|
ID: "node-1",
|
|
Name: "pve1",
|
|
Instance: "default",
|
|
Status: "online",
|
|
Type: "node",
|
|
}
|
|
|
|
frontend := node.ToFrontend()
|
|
if frontend.LoadAverage == nil {
|
|
t.Fatalf("expected node loadAverage to normalize, got %#v", frontend)
|
|
}
|
|
}
|
|
|
|
func TestDockerHostToFrontend_UsesCanonicalEmptyCollections(t *testing.T) {
|
|
host := DockerHost{
|
|
ID: "docker-1",
|
|
AgentID: "agent-1",
|
|
Hostname: "tower",
|
|
LastSeen: time.Now(),
|
|
}
|
|
|
|
frontend := host.ToFrontend()
|
|
if frontend.Containers == nil || frontend.Services == nil || frontend.Tasks == nil {
|
|
t.Fatalf("expected Docker host frontend collections to normalize, got %#v", frontend)
|
|
}
|
|
}
|
|
|
|
func TestHostToFrontend_UsesCanonicalEmptyCollections(t *testing.T) {
|
|
host := Host{
|
|
ID: "host-1",
|
|
Hostname: "server1",
|
|
Status: "online",
|
|
LastSeen: time.Now(),
|
|
Sensors: HostSensorSummary{
|
|
TemperatureCelsius: map[string]float64{"cpu": 55},
|
|
},
|
|
}
|
|
|
|
frontend := host.ToFrontend()
|
|
if frontend.LoadAverage == nil || frontend.Disks == nil || frontend.DiskIO == nil || frontend.NetworkInterfaces == nil || frontend.Tags == nil {
|
|
t.Fatalf("expected host frontend collections to normalize, got %#v", frontend)
|
|
}
|
|
if frontend.Sensors == nil || frontend.Sensors.FanRPM == nil || frontend.Sensors.Additional == nil || frontend.Sensors.SMART == nil {
|
|
t.Fatalf("expected host sensor collections to normalize, got %#v", frontend.Sensors)
|
|
}
|
|
}
|
|
|
|
func TestVMAndContainerToFrontend_UsesCanonicalEmptyCollections(t *testing.T) {
|
|
vm := VM{
|
|
ID: "vm-1",
|
|
VMID: 101,
|
|
Name: "vm-a",
|
|
Node: "pve1",
|
|
Instance: "default",
|
|
Status: "running",
|
|
Type: "qemu",
|
|
}
|
|
container := Container{
|
|
ID: "ct-1",
|
|
VMID: 201,
|
|
Name: "ct-a",
|
|
Node: "pve1",
|
|
Instance: "default",
|
|
Status: "running",
|
|
Type: "lxc",
|
|
}
|
|
|
|
vmFrontend := vm.ToFrontend()
|
|
if vmFrontend.Disks == nil || vmFrontend.NetworkInterfaces == nil || vmFrontend.IPAddresses == nil {
|
|
t.Fatalf("expected VM frontend collections to normalize, got %#v", vmFrontend)
|
|
}
|
|
|
|
containerFrontend := container.ToFrontend()
|
|
if containerFrontend.Disks == nil || containerFrontend.NetworkInterfaces == nil || containerFrontend.IPAddresses == nil {
|
|
t.Fatalf("expected container frontend collections to normalize, got %#v", containerFrontend)
|
|
}
|
|
}
|
|
|
|
func TestStorageToFrontend_UsesCanonicalEmptyCollections(t *testing.T) {
|
|
frontend := Storage{
|
|
ID: "storage-1",
|
|
Name: "local",
|
|
Node: "pve1",
|
|
Instance: "default",
|
|
Type: "dir",
|
|
Status: "available",
|
|
}.ToFrontend()
|
|
|
|
if frontend.Nodes == nil || frontend.NodeIDs == nil {
|
|
t.Fatalf("expected storage frontend collections to normalize, got %#v", frontend)
|
|
}
|
|
}
|
|
|
|
func TestDockerHostToFrontend_NormalizesNestedCollections(t *testing.T) {
|
|
host := DockerHost{
|
|
ID: "docker-2",
|
|
AgentID: "agent-2",
|
|
Hostname: "tower",
|
|
LastSeen: time.Now(),
|
|
Containers: []DockerContainer{
|
|
{
|
|
ID: "container-1",
|
|
Name: "pbs",
|
|
Image: "pbs:latest",
|
|
State: "running",
|
|
Status: "Up",
|
|
CreatedAt: time.Now(),
|
|
},
|
|
},
|
|
Services: []DockerService{
|
|
{
|
|
ID: "service-1",
|
|
Name: "web",
|
|
},
|
|
},
|
|
}
|
|
|
|
frontend := host.ToFrontend()
|
|
if frontend.Containers[0].Ports == nil || frontend.Containers[0].Labels == nil || frontend.Containers[0].Networks == nil || frontend.Containers[0].Mounts == nil {
|
|
t.Fatalf("expected Docker container nested collections to normalize, got %#v", frontend.Containers[0])
|
|
}
|
|
if frontend.Services[0].Labels == nil || frontend.Services[0].EndpointPorts == nil {
|
|
t.Fatalf("expected Docker service nested collections to normalize, got %#v", frontend.Services[0])
|
|
}
|
|
}
|
|
|
|
func TestDockerContainerAndServiceFrontend_JSONUsesCanonicalEmptyCollections(t *testing.T) {
|
|
payload, err := json.Marshal(DockerHostFrontend{
|
|
ID: "docker-1",
|
|
AgentID: "agent-1",
|
|
Hostname: "tower",
|
|
Status: "healthy",
|
|
LastSeen: 1,
|
|
Containers: []DockerContainerFrontend{{ID: "container-1", Name: "pbs", Image: "pbs:latest", State: "running", Status: "Up", CreatedAt: 1}},
|
|
Services: []DockerServiceFrontend{{ID: "service-1", Name: "web"}},
|
|
}.NormalizeCollections())
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal error: %v", err)
|
|
}
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal error: %v", err)
|
|
}
|
|
|
|
containers, ok := decoded["containers"].([]any)
|
|
if !ok || len(containers) != 1 {
|
|
t.Fatalf("containers = %#v, want one container", decoded["containers"])
|
|
}
|
|
container, ok := containers[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("container = %#v, want object", containers[0])
|
|
}
|
|
for _, key := range []string{"ports", "labels", "networks", "mounts"} {
|
|
if _, ok := container[key]; !ok {
|
|
t.Fatalf("container JSON missing %q in canonical empty shape: %#v", key, container)
|
|
}
|
|
}
|
|
|
|
services, ok := decoded["services"].([]any)
|
|
if !ok || len(services) != 1 {
|
|
t.Fatalf("services = %#v, want one service", decoded["services"])
|
|
}
|
|
service, ok := services[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("service = %#v, want object", services[0])
|
|
}
|
|
for _, key := range []string{"labels", "endpointPorts"} {
|
|
if _, ok := service[key]; !ok {
|
|
t.Fatalf("service JSON missing %q in canonical empty shape: %#v", key, service)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKubernetesClusterToFrontend_NormalizesNestedCollections(t *testing.T) {
|
|
cluster := KubernetesCluster{
|
|
ID: "cluster-4",
|
|
Nodes: []KubernetesNode{
|
|
{UID: "node-1", Name: "node-a"},
|
|
},
|
|
Pods: []KubernetesPod{
|
|
{UID: "pod-1", Name: "pod-a", Namespace: "default"},
|
|
},
|
|
Deployments: []KubernetesDeployment{
|
|
{UID: "deploy-1", Name: "deploy-a", Namespace: "default"},
|
|
},
|
|
}
|
|
|
|
frontend := cluster.ToFrontend()
|
|
if frontend.Nodes[0].Roles == nil {
|
|
t.Fatalf("expected node roles to normalize, got %#v", frontend.Nodes[0])
|
|
}
|
|
if frontend.Pods[0].Labels == nil || frontend.Pods[0].Containers == nil {
|
|
t.Fatalf("expected pod nested collections to normalize, got %#v", frontend.Pods[0])
|
|
}
|
|
if frontend.Deployments[0].Labels == nil {
|
|
t.Fatalf("expected deployment labels to normalize, got %#v", frontend.Deployments[0])
|
|
}
|
|
}
|
|
|
|
func TestKubernetesClusterFrontend_JSONUsesCanonicalEmptyCollections(t *testing.T) {
|
|
payload, err := json.Marshal(KubernetesClusterFrontend{
|
|
ID: "cluster-1",
|
|
AgentID: "agent-1",
|
|
Status: "healthy",
|
|
Nodes: []KubernetesNodeFrontend{
|
|
{UID: "node-1", Name: "node-a"},
|
|
},
|
|
Pods: []KubernetesPodFrontend{
|
|
{UID: "pod-1", Name: "pod-a", Namespace: "default"},
|
|
},
|
|
Deployments: []KubernetesDeploymentFrontend{
|
|
{UID: "deploy-1", Name: "deploy-a", Namespace: "default"},
|
|
},
|
|
}.NormalizeCollections())
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal error: %v", err)
|
|
}
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal error: %v", err)
|
|
}
|
|
|
|
nodes, ok := decoded["nodes"].([]any)
|
|
if !ok || len(nodes) != 1 {
|
|
t.Fatalf("nodes = %#v, want one node", decoded["nodes"])
|
|
}
|
|
node, ok := nodes[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("node = %#v, want object", nodes[0])
|
|
}
|
|
if _, ok := node["roles"]; !ok {
|
|
t.Fatalf("node JSON missing roles in canonical empty shape: %#v", node)
|
|
}
|
|
|
|
pods, ok := decoded["pods"].([]any)
|
|
if !ok || len(pods) != 1 {
|
|
t.Fatalf("pods = %#v, want one pod", decoded["pods"])
|
|
}
|
|
pod, ok := pods[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("pod = %#v, want object", pods[0])
|
|
}
|
|
for _, key := range []string{"labels", "containers"} {
|
|
if _, ok := pod[key]; !ok {
|
|
t.Fatalf("pod JSON missing %q in canonical empty shape: %#v", key, pod)
|
|
}
|
|
}
|
|
|
|
deployments, ok := decoded["deployments"].([]any)
|
|
if !ok || len(deployments) != 1 {
|
|
t.Fatalf("deployments = %#v, want one deployment", decoded["deployments"])
|
|
}
|
|
deployment, ok := deployments[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("deployment = %#v, want object", deployments[0])
|
|
}
|
|
if _, ok := deployment["labels"]; !ok {
|
|
t.Fatalf("deployment JSON missing labels in canonical empty shape: %#v", deployment)
|
|
}
|
|
}
|
|
|
|
func TestHostResourceAndStorageFrontend_JSONUsesCanonicalEmptyCollections(t *testing.T) {
|
|
payload, err := json.Marshal(map[string]any{
|
|
"host": HostFrontend{
|
|
ID: "host-1",
|
|
Hostname: "server1",
|
|
DisplayName: "Server 1",
|
|
Status: "online",
|
|
LastSeen: 1,
|
|
Sensors: &HostSensorSummaryFrontend{TemperatureCelsius: map[string]float64{"cpu": 50}},
|
|
}.NormalizeCollections(),
|
|
"storage": StorageFrontend{
|
|
ID: "storage-1",
|
|
Storage: "local",
|
|
Name: "local",
|
|
Node: "pve1",
|
|
Instance: "default",
|
|
Type: "dir",
|
|
Status: "available",
|
|
}.NormalizeCollections(),
|
|
"resource": ResourceFrontend{
|
|
ID: "res-1",
|
|
Type: "node",
|
|
Name: "node-a",
|
|
DisplayName: "Node A",
|
|
PlatformID: "platform-1",
|
|
PlatformType: "proxmox",
|
|
SourceType: "agent",
|
|
Status: "healthy",
|
|
LastSeen: 1,
|
|
Identity: &ResourceIdentityFrontend{},
|
|
}.NormalizeCollections(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal error: %v", err)
|
|
}
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal error: %v", err)
|
|
}
|
|
|
|
host := decoded["host"].(map[string]any)
|
|
for _, key := range []string{"loadAverage", "disks", "diskIO", "networkInterfaces", "tags"} {
|
|
if _, ok := host[key]; !ok {
|
|
t.Fatalf("host JSON missing %q in canonical empty shape: %#v", key, host)
|
|
}
|
|
}
|
|
sensors := host["sensors"].(map[string]any)
|
|
for _, key := range []string{"temperatureCelsius", "fanRpm", "additional", "smart"} {
|
|
if _, ok := sensors[key]; !ok {
|
|
t.Fatalf("host sensors JSON missing %q in canonical empty shape: %#v", key, sensors)
|
|
}
|
|
}
|
|
|
|
storage := decoded["storage"].(map[string]any)
|
|
for _, key := range []string{"nodes", "nodeIds"} {
|
|
if _, ok := storage[key]; !ok {
|
|
t.Fatalf("storage JSON missing %q in canonical empty shape: %#v", key, storage)
|
|
}
|
|
}
|
|
|
|
resource := decoded["resource"].(map[string]any)
|
|
for _, key := range []string{"tags", "labels", "alerts"} {
|
|
if _, ok := resource[key]; !ok {
|
|
t.Fatalf("resource JSON missing %q in canonical empty shape: %#v", key, resource)
|
|
}
|
|
}
|
|
identity := resource["identity"].(map[string]any)
|
|
if _, ok := identity["ips"]; !ok {
|
|
t.Fatalf("resource identity JSON missing ips in canonical empty shape: %#v", identity)
|
|
}
|
|
}
|
|
|
|
func TestGuestAndDockerHostFrontend_JSONUsesCanonicalEmptyCollections(t *testing.T) {
|
|
payload, err := json.Marshal(map[string]any{
|
|
"vm": VMFrontend{
|
|
ID: "vm-1",
|
|
VMID: 101,
|
|
Name: "vm-a",
|
|
Node: "pve1",
|
|
Instance: "default",
|
|
Status: "running",
|
|
Type: "qemu",
|
|
}.NormalizeCollections(),
|
|
"container": ContainerFrontend{
|
|
ID: "ct-1",
|
|
VMID: 201,
|
|
Name: "ct-a",
|
|
Node: "pve1",
|
|
Instance: "default",
|
|
Status: "running",
|
|
Type: "lxc",
|
|
}.NormalizeCollections(),
|
|
"dockerHost": DockerHostFrontend{
|
|
ID: "docker-1",
|
|
AgentID: "agent-1",
|
|
Hostname: "tower",
|
|
DisplayName: "Tower",
|
|
Runtime: "docker",
|
|
Status: "healthy",
|
|
LastSeen: 1,
|
|
}.NormalizeCollections(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal error: %v", err)
|
|
}
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal error: %v", err)
|
|
}
|
|
|
|
vm := decoded["vm"].(map[string]any)
|
|
for _, key := range []string{"disks", "networkInterfaces", "ipAddresses"} {
|
|
if _, ok := vm[key]; !ok {
|
|
t.Fatalf("vm JSON missing %q in canonical empty shape: %#v", key, vm)
|
|
}
|
|
}
|
|
|
|
container := decoded["container"].(map[string]any)
|
|
for _, key := range []string{"disks", "networkInterfaces", "ipAddresses"} {
|
|
if _, ok := container[key]; !ok {
|
|
t.Fatalf("container JSON missing %q in canonical empty shape: %#v", key, container)
|
|
}
|
|
}
|
|
|
|
dockerHost := decoded["dockerHost"].(map[string]any)
|
|
for _, key := range []string{"loadAverage", "disks", "networkInterfaces", "containers", "services", "tasks"} {
|
|
if _, ok := dockerHost[key]; !ok {
|
|
t.Fatalf("docker host JSON missing %q in canonical empty shape: %#v", key, dockerHost)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNodeAndConnectedInfrastructureFrontend_JSONUsesCanonicalEmptyCollections(t *testing.T) {
|
|
payload, err := json.Marshal(map[string]any{
|
|
"node": NodeFrontend{
|
|
ID: "node-1",
|
|
Node: "pve1",
|
|
Name: "pve1",
|
|
DisplayName: "PVE 1",
|
|
Instance: "default",
|
|
Status: "online",
|
|
Type: "node",
|
|
CPUInfo: CPUInfo{},
|
|
}.NormalizeCollections(),
|
|
"state": StateFrontend{
|
|
ConnectedInfrastructure: []ConnectedInfrastructureItemFrontend{{
|
|
ID: "infra-1",
|
|
Name: "tower",
|
|
Status: "online",
|
|
}},
|
|
}.NormalizeCollections(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal error: %v", err)
|
|
}
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal error: %v", err)
|
|
}
|
|
|
|
node := decoded["node"].(map[string]any)
|
|
if _, ok := node["loadAverage"]; !ok {
|
|
t.Fatalf("node JSON missing loadAverage in canonical empty shape: %#v", node)
|
|
}
|
|
|
|
state := decoded["state"].(map[string]any)
|
|
connected := state["connectedInfrastructure"].([]any)
|
|
if len(connected) != 1 {
|
|
t.Fatalf("connectedInfrastructure = %#v, want one item", connected)
|
|
}
|
|
item := connected[0].(map[string]any)
|
|
if _, ok := item["surfaces"]; !ok {
|
|
t.Fatalf("connected infrastructure JSON missing surfaces in canonical empty shape: %#v", item)
|
|
}
|
|
}
|