mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 05:57:27 +00:00
da3fcf69f7
The Machines/Infrastructure drawer and the Docker host drawer built their "Needs attention" list from the alerts embedded in each resource. No backend code ever filled that list, so every resource carried `alerts: []` and both drawers stayed silent while an alert was open: a mock stack had 0 of 142 resources with embedded alerts while 14 alerts were active, and a real dev Pulse showed `alerts: []` on a host whose temperature alert was open. Both drawers now read the websocket activeAlerts map, the source the Proxmox node, guest and Mail Gateway drawers already use, through one matcher, getAlertsForUnifiedResource. It keys a resource on the ids the backend writes for it (unified id, canonical primary and superseded ids, Proxmox source id, metrics target) and, for agent and Docker hosts, agent:<id> and docker:<host source id> plus the disk, RAID, container and service alerts nested under them. Rows show the affected resource and metric like the other drawers, most severe first. The dead embedded path is gone: the frontend ResourceAlert type and its unused hasAlerts filter, the Proxmox summary alertCount nobody rendered, and ResourceFrontend.alerts / ResourceAlertInput on the backend, which also drops "alerts":[] from every resource in each broadcast. Now that incident alerts reach the infrastructure drawer, a health reason counts as stated when it is a whole sentence of an alert's text, so a storage incident alert that joins its summary and impact does not repeat beside either reason.
654 lines
20 KiB
Go
654 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,
|
|
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 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 {
|
|
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"} {
|
|
if _, ok := resource[key]; !ok {
|
|
t.Fatalf("resource JSON missing %q in canonical empty shape: %#v", key, resource)
|
|
}
|
|
}
|
|
// Open alerts reach the frontend once, through the activeAlerts map;
|
|
// resources do not embed a copy.
|
|
if _, ok := resource["alerts"]; ok {
|
|
t.Fatalf("resource JSON must not embed alerts: %#v", 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)
|
|
}
|
|
}
|