mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
When a resource exists in the hostname or IP index but has been removed from the main resources map, looking up and accessing .Type would cause a nil pointer dereference panic. The MachineID lookup already had this check, but hostname and IP lookups were missing it. This adds consistent nil checking across all three lookup paths.
886 lines
23 KiB
Go
886 lines
23 KiB
Go
package resources
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestStoreUpsertAndGet(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
r := Resource{
|
|
ID: "test-1",
|
|
Type: ResourceTypeNode,
|
|
Name: "node1",
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
Status: StatusOnline,
|
|
LastSeen: time.Now(),
|
|
}
|
|
|
|
id := store.Upsert(r)
|
|
if id != "test-1" {
|
|
t.Errorf("Expected ID test-1, got %s", id)
|
|
}
|
|
|
|
retrieved, ok := store.Get("test-1")
|
|
if !ok {
|
|
t.Fatal("Failed to retrieve resource")
|
|
}
|
|
if retrieved.Name != "node1" {
|
|
t.Errorf("Expected name node1, got %s", retrieved.Name)
|
|
}
|
|
}
|
|
|
|
func TestStoreGetAll(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
store.Upsert(Resource{ID: "1", Type: ResourceTypeNode, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "2", Type: ResourceTypeVM, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "3", Type: ResourceTypeContainer, LastSeen: time.Now()})
|
|
|
|
all := store.GetAll()
|
|
if len(all) != 3 {
|
|
t.Errorf("Expected 3 resources, got %d", len(all))
|
|
}
|
|
}
|
|
|
|
func TestStoreGetByType(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
store.Upsert(Resource{ID: "node1", Type: ResourceTypeNode, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "node2", Type: ResourceTypeNode, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "vm1", Type: ResourceTypeVM, LastSeen: time.Now()})
|
|
|
|
nodes := store.GetByType(ResourceTypeNode)
|
|
if len(nodes) != 2 {
|
|
t.Errorf("Expected 2 nodes, got %d", len(nodes))
|
|
}
|
|
|
|
vms := store.GetByType(ResourceTypeVM)
|
|
if len(vms) != 1 {
|
|
t.Errorf("Expected 1 VM, got %d", len(vms))
|
|
}
|
|
}
|
|
|
|
func TestStoreGetByPlatform(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
store.Upsert(Resource{ID: "1", PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "2", PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "3", PlatformType: PlatformDocker, LastSeen: time.Now()})
|
|
|
|
pve := store.GetByPlatform(PlatformProxmoxPVE)
|
|
if len(pve) != 2 {
|
|
t.Errorf("Expected 2 PVE resources, got %d", len(pve))
|
|
}
|
|
|
|
docker := store.GetByPlatform(PlatformDocker)
|
|
if len(docker) != 1 {
|
|
t.Errorf("Expected 1 Docker resource, got %d", len(docker))
|
|
}
|
|
}
|
|
|
|
func TestStoreGetInfrastructureAndWorkloads(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
store.Upsert(Resource{ID: "node1", Type: ResourceTypeNode, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "host1", Type: ResourceTypeHost, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "vm1", Type: ResourceTypeVM, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "ct1", Type: ResourceTypeContainer, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "dc1", Type: ResourceTypeDockerContainer, LastSeen: time.Now()})
|
|
|
|
infra := store.GetInfrastructure()
|
|
if len(infra) != 2 {
|
|
t.Errorf("Expected 2 infrastructure resources, got %d", len(infra))
|
|
}
|
|
|
|
workloads := store.GetWorkloads()
|
|
if len(workloads) != 3 {
|
|
t.Errorf("Expected 3 workload resources, got %d", len(workloads))
|
|
}
|
|
}
|
|
|
|
func TestStoreGetChildren(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
store.Upsert(Resource{ID: "node1", Type: ResourceTypeNode, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "vm1", Type: ResourceTypeVM, ParentID: "node1", LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "vm2", Type: ResourceTypeVM, ParentID: "node1", LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "vm3", Type: ResourceTypeVM, ParentID: "node2", LastSeen: time.Now()})
|
|
|
|
children := store.GetChildren("node1")
|
|
if len(children) != 2 {
|
|
t.Errorf("Expected 2 children of node1, got %d", len(children))
|
|
}
|
|
}
|
|
|
|
func TestStoreRemove(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
store.Upsert(Resource{ID: "test-1", LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "test-2", LastSeen: time.Now()})
|
|
|
|
if len(store.GetAll()) != 2 {
|
|
t.Fatal("Expected 2 resources before remove")
|
|
}
|
|
|
|
store.Remove("test-1")
|
|
|
|
if len(store.GetAll()) != 1 {
|
|
t.Error("Expected 1 resource after remove")
|
|
}
|
|
|
|
_, ok := store.Get("test-1")
|
|
if ok {
|
|
t.Error("Removed resource should not be retrievable")
|
|
}
|
|
}
|
|
|
|
func TestDeduplicationByHostname(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
now := time.Now()
|
|
|
|
// Add a Proxmox node (API source)
|
|
nodeResource := Resource{
|
|
ID: "pve1/node/server1",
|
|
Type: ResourceTypeNode,
|
|
Name: "server1",
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
Status: StatusOnline,
|
|
CPU: &MetricValue{Current: 50.0},
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server1",
|
|
},
|
|
}
|
|
store.Upsert(nodeResource)
|
|
|
|
// Add a host agent for the same server (agent source)
|
|
// Different types should coexist, not merge
|
|
hostResource := Resource{
|
|
ID: "host-agent/server1",
|
|
Type: ResourceTypeHost,
|
|
Name: "server1",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
Status: StatusOnline,
|
|
CPU: &MetricValue{Current: 55.0},
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server1",
|
|
},
|
|
}
|
|
store.Upsert(hostResource)
|
|
|
|
// Different types should coexist (node + host = 2 resources)
|
|
all := store.GetAll()
|
|
if len(all) != 2 {
|
|
t.Errorf("Expected 2 resources (node + host coexist), got %d", len(all))
|
|
}
|
|
|
|
// Both should be retrievable
|
|
_, ok := store.Get("pve1/node/server1")
|
|
if !ok {
|
|
t.Error("Node resource should be retrievable")
|
|
}
|
|
_, ok = store.Get("host-agent/server1")
|
|
if !ok {
|
|
t.Error("Host resource should be retrievable")
|
|
}
|
|
|
|
// Now test same-type deduplication: add another node with same hostname
|
|
nodeResource2 := Resource{
|
|
ID: "pve2/node/server1",
|
|
Type: ResourceTypeNode,
|
|
Name: "server1",
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
Status: StatusOnline,
|
|
CPU: &MetricValue{Current: 60.0},
|
|
LastSeen: now.Add(time.Second), // Newer
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server1",
|
|
},
|
|
}
|
|
store.Upsert(nodeResource2)
|
|
|
|
// Should still have 2 (newer node replaces old node, host remains)
|
|
all = store.GetAll()
|
|
if len(all) != 2 {
|
|
t.Errorf("Expected 2 resources after same-type dedup, got %d", len(all))
|
|
}
|
|
|
|
// The newer node should have replaced the old one
|
|
r, ok := store.Get("pve2/node/server1")
|
|
if !ok {
|
|
t.Error("Newer node should be present")
|
|
}
|
|
if r.CPU.Current != 60.0 {
|
|
t.Errorf("Expected CPU 60.0 from newer node, got %f", r.CPU.Current)
|
|
}
|
|
}
|
|
|
|
func TestDeduplicationByMachineID(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
now := time.Now()
|
|
machineID := "abc-123-def-456"
|
|
|
|
// Add a Docker host
|
|
dockerHost := Resource{
|
|
ID: "docker-host-1",
|
|
Type: ResourceTypeDockerHost,
|
|
Name: "server-different-name",
|
|
PlatformType: PlatformDocker,
|
|
SourceType: SourceAgent,
|
|
Status: StatusOnline,
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server-different-name",
|
|
MachineID: machineID,
|
|
},
|
|
}
|
|
store.Upsert(dockerHost)
|
|
|
|
// Add a host agent with the same machine ID but different type
|
|
// Different types should coexist
|
|
hostAgent := Resource{
|
|
ID: "host-agent-1",
|
|
Type: ResourceTypeHost,
|
|
Name: "server-production",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
Status: StatusOnline,
|
|
LastSeen: now.Add(time.Second),
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server-production",
|
|
MachineID: machineID,
|
|
},
|
|
}
|
|
store.Upsert(hostAgent)
|
|
|
|
// Different types should coexist (docker-host + host = 2 resources)
|
|
all := store.GetAll()
|
|
if len(all) != 2 {
|
|
t.Errorf("Expected 2 resources (different types coexist with same machineID), got %d", len(all))
|
|
}
|
|
|
|
// Now test same-type deduplication: add another host with same machine ID
|
|
hostAgent2 := Resource{
|
|
ID: "host-agent-2",
|
|
Type: ResourceTypeHost, // Same type as first host
|
|
Name: "server-newer",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
Status: StatusOnline,
|
|
LastSeen: now.Add(2 * time.Second), // Newer
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server-newer",
|
|
MachineID: machineID,
|
|
},
|
|
}
|
|
store.Upsert(hostAgent2)
|
|
|
|
// Should still have 2 (newer host replaces old host, docker-host remains)
|
|
all = store.GetAll()
|
|
if len(all) != 2 {
|
|
t.Errorf("Expected 2 resources after same-type dedup, got %d", len(all))
|
|
}
|
|
}
|
|
|
|
func TestDeduplicationByIP(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
now := time.Now()
|
|
sharedIP := "192.168.1.100"
|
|
|
|
// Add a Proxmox node
|
|
node := Resource{
|
|
ID: "node-1",
|
|
Type: ResourceTypeNode,
|
|
Name: "pve-node",
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
Status: StatusOnline,
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "pve-node",
|
|
IPs: []string{sharedIP},
|
|
},
|
|
}
|
|
store.Upsert(node)
|
|
|
|
// Add a host agent with the same IP but different type
|
|
// Different types should coexist
|
|
host := Resource{
|
|
ID: "host-1",
|
|
Type: ResourceTypeHost,
|
|
Name: "different-hostname",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
Status: StatusOnline,
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "different-hostname",
|
|
IPs: []string{sharedIP},
|
|
},
|
|
}
|
|
store.Upsert(host)
|
|
|
|
// Different types should coexist (node + host = 2 resources)
|
|
all := store.GetAll()
|
|
if len(all) != 2 {
|
|
t.Errorf("Expected 2 resources (different types coexist), got %d", len(all))
|
|
}
|
|
|
|
// Both should be retrievable
|
|
_, ok := store.Get("node-1")
|
|
if !ok {
|
|
t.Error("Node should be retrievable")
|
|
}
|
|
_, ok = store.Get("host-1")
|
|
if !ok {
|
|
t.Error("Host should be retrievable")
|
|
}
|
|
}
|
|
|
|
func TestNoDeduplicationForWorkloads(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
now := time.Now()
|
|
|
|
// VMs with the same hostname should NOT be deduplicated
|
|
// (they're workloads, not infrastructure)
|
|
vm1 := Resource{
|
|
ID: "pve1/vm/100",
|
|
Type: ResourceTypeVM,
|
|
Name: "webserver",
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "webserver", // Same hostname
|
|
},
|
|
}
|
|
store.Upsert(vm1)
|
|
|
|
vm2 := Resource{
|
|
ID: "pve2/vm/100",
|
|
Type: ResourceTypeVM,
|
|
Name: "webserver",
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "webserver", // Same hostname
|
|
},
|
|
}
|
|
store.Upsert(vm2)
|
|
|
|
// Both VMs should exist (workloads are not deduplicated)
|
|
all := store.GetAll()
|
|
if len(all) != 2 {
|
|
t.Errorf("Expected 2 VMs (no dedup for workloads), got %d", len(all))
|
|
}
|
|
}
|
|
|
|
// TestFindDuplicateWithOrphanedIndex verifies that findDuplicate doesn't panic
|
|
// when a resource exists in an index (byHostname, byIP) but has been removed
|
|
// from the main resources map. This is a regression test for issue #1119.
|
|
func TestFindDuplicateWithOrphanedIndex(t *testing.T) {
|
|
store := NewStore()
|
|
now := time.Now()
|
|
|
|
// Add a resource with hostname
|
|
r1 := Resource{
|
|
ID: "host-1",
|
|
Type: ResourceTypeHost,
|
|
Name: "server1",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
Status: StatusOnline,
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "test-server",
|
|
IPs: []string{"192.168.1.100"},
|
|
},
|
|
}
|
|
store.Upsert(r1)
|
|
|
|
// Verify it's in the store
|
|
_, ok := store.Get("host-1")
|
|
if !ok {
|
|
t.Fatal("Resource should exist after upsert")
|
|
}
|
|
|
|
// Now manually corrupt the store state by removing from resources but not indexes
|
|
// This simulates a race condition or bug where indexes become out of sync
|
|
store.mu.Lock()
|
|
delete(store.resources, "host-1")
|
|
// byHostname and byIP still contain "host-1"
|
|
store.mu.Unlock()
|
|
|
|
// Trying to upsert a new resource with matching hostname should NOT panic
|
|
r2 := Resource{
|
|
ID: "host-2",
|
|
Type: ResourceTypeHost,
|
|
Name: "server2",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
Status: StatusOnline,
|
|
LastSeen: now.Add(time.Second),
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "test-server", // Same hostname as orphaned index entry
|
|
},
|
|
}
|
|
|
|
// This should not panic - it should handle the nil resource gracefully
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Upsert panicked with orphaned hostname index: %v", r)
|
|
}
|
|
}()
|
|
store.Upsert(r2)
|
|
}()
|
|
|
|
// Also test with IP index
|
|
store2 := NewStore()
|
|
r3 := Resource{
|
|
ID: "host-3",
|
|
Type: ResourceTypeHost,
|
|
Name: "server3",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
Status: StatusOnline,
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "other-server",
|
|
IPs: []string{"10.0.0.50"},
|
|
},
|
|
}
|
|
store2.Upsert(r3)
|
|
|
|
// Corrupt the store state
|
|
store2.mu.Lock()
|
|
delete(store2.resources, "host-3")
|
|
store2.mu.Unlock()
|
|
|
|
// Trying to upsert with matching IP should NOT panic
|
|
r4 := Resource{
|
|
ID: "host-4",
|
|
Type: ResourceTypeHost,
|
|
Name: "server4",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
Status: StatusOnline,
|
|
LastSeen: now.Add(time.Second),
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "different-hostname",
|
|
IPs: []string{"10.0.0.50"}, // Same IP as orphaned index entry
|
|
},
|
|
}
|
|
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Upsert panicked with orphaned IP index: %v", r)
|
|
}
|
|
}()
|
|
store2.Upsert(r4)
|
|
}()
|
|
}
|
|
|
|
func TestNoDeduplicationForLocalhost(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
now := time.Now()
|
|
|
|
host1 := Resource{
|
|
ID: "host-1",
|
|
Type: ResourceTypeHost,
|
|
Name: "server1",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server1",
|
|
IPs: []string{"127.0.0.1", "192.168.1.1"},
|
|
},
|
|
}
|
|
store.Upsert(host1)
|
|
|
|
host2 := Resource{
|
|
ID: "host-2",
|
|
Type: ResourceTypeHost,
|
|
Name: "server2",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server2",
|
|
IPs: []string{"127.0.0.1", "192.168.1.2"}, // Both have localhost
|
|
},
|
|
}
|
|
store.Upsert(host2)
|
|
|
|
// Both should exist (127.0.0.1 shouldn't trigger dedup)
|
|
all := store.GetAll()
|
|
if len(all) != 2 {
|
|
t.Errorf("Expected 2 hosts (localhost shouldn't dedup), got %d", len(all))
|
|
}
|
|
}
|
|
|
|
func TestStoreStats(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
store.Upsert(Resource{ID: "1", Type: ResourceTypeNode, PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "2", Type: ResourceTypeNode, PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "3", Type: ResourceTypeVM, PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()})
|
|
store.Upsert(Resource{ID: "4", Type: ResourceTypeDockerHost, PlatformType: PlatformDocker, LastSeen: time.Now()})
|
|
|
|
stats := store.GetStats()
|
|
|
|
if stats.TotalResources != 4 {
|
|
t.Errorf("Expected 4 total resources, got %d", stats.TotalResources)
|
|
}
|
|
if stats.ByType[ResourceTypeNode] != 2 {
|
|
t.Errorf("Expected 2 nodes, got %d", stats.ByType[ResourceTypeNode])
|
|
}
|
|
if stats.ByPlatform[PlatformProxmoxPVE] != 3 {
|
|
t.Errorf("Expected 3 PVE resources, got %d", stats.ByPlatform[PlatformProxmoxPVE])
|
|
}
|
|
}
|
|
|
|
func TestStoreQuery(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
store.Upsert(Resource{
|
|
ID: "1",
|
|
Type: ResourceTypeNode,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
Status: StatusOnline,
|
|
LastSeen: time.Now(),
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "2",
|
|
Type: ResourceTypeVM,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
Status: StatusRunning,
|
|
ParentID: "1",
|
|
LastSeen: time.Now(),
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "3",
|
|
Type: ResourceTypeVM,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
Status: StatusStopped,
|
|
ParentID: "1",
|
|
LastSeen: time.Now(),
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "4",
|
|
Type: ResourceTypeDockerContainer,
|
|
PlatformType: PlatformDocker,
|
|
Status: StatusRunning,
|
|
LastSeen: time.Now(),
|
|
})
|
|
|
|
// Query by type
|
|
vms := store.Query().OfType(ResourceTypeVM).Execute()
|
|
if len(vms) != 2 {
|
|
t.Errorf("Expected 2 VMs, got %d", len(vms))
|
|
}
|
|
|
|
// Query by status
|
|
running := store.Query().WithStatus(StatusRunning).Execute()
|
|
if len(running) != 2 {
|
|
t.Errorf("Expected 2 running resources, got %d", len(running))
|
|
}
|
|
|
|
// Query by platform
|
|
pve := store.Query().FromPlatform(PlatformProxmoxPVE).Execute()
|
|
if len(pve) != 3 {
|
|
t.Errorf("Expected 3 PVE resources, got %d", len(pve))
|
|
}
|
|
|
|
// Query by parent
|
|
node1Children := store.Query().WithParent("1").Execute()
|
|
if len(node1Children) != 2 {
|
|
t.Errorf("Expected 2 children of node 1, got %d", len(node1Children))
|
|
}
|
|
|
|
// Combined query
|
|
runningVMs := store.Query().
|
|
OfType(ResourceTypeVM).
|
|
WithStatus(StatusRunning).
|
|
Execute()
|
|
if len(runningVMs) != 1 {
|
|
t.Errorf("Expected 1 running VM, got %d", len(runningVMs))
|
|
}
|
|
|
|
// Count
|
|
count := store.Query().OfType(ResourceTypeVM).Count()
|
|
if count != 2 {
|
|
t.Errorf("Expected count 2, got %d", count)
|
|
}
|
|
|
|
// Limit
|
|
limited := store.Query().Limit(2).Execute()
|
|
if len(limited) > 2 {
|
|
t.Errorf("Expected at most 2 results, got %d", len(limited))
|
|
}
|
|
}
|
|
|
|
func TestMarkStale(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
old := time.Now().Add(-2 * time.Hour)
|
|
recent := time.Now()
|
|
|
|
store.Upsert(Resource{
|
|
ID: "old-1",
|
|
Status: StatusOnline,
|
|
LastSeen: old,
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "recent-1",
|
|
Status: StatusOnline,
|
|
LastSeen: recent,
|
|
})
|
|
|
|
stale := store.MarkStale(time.Hour)
|
|
|
|
if len(stale) != 1 {
|
|
t.Errorf("Expected 1 stale resource, got %d", len(stale))
|
|
}
|
|
|
|
r, _ := store.Get("old-1")
|
|
if r.Status != StatusDegraded {
|
|
t.Errorf("Expected stale resource to be degraded, got %s", r.Status)
|
|
}
|
|
|
|
r, _ = store.Get("recent-1")
|
|
if r.Status != StatusOnline {
|
|
t.Errorf("Recent resource should still be online, got %s", r.Status)
|
|
}
|
|
}
|
|
|
|
func TestPruneStale(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
veryOld := time.Now().Add(-48 * time.Hour)
|
|
old := time.Now().Add(-2 * time.Hour)
|
|
recent := time.Now()
|
|
|
|
store.Upsert(Resource{ID: "very-old", LastSeen: veryOld})
|
|
store.Upsert(Resource{ID: "old", LastSeen: old})
|
|
store.Upsert(Resource{ID: "recent", LastSeen: recent})
|
|
|
|
removed := store.PruneStale(time.Hour, 24*time.Hour)
|
|
|
|
if len(removed) != 1 {
|
|
t.Errorf("Expected 1 removed resource, got %d", len(removed))
|
|
}
|
|
|
|
if len(store.GetAll()) != 2 {
|
|
t.Errorf("Expected 2 remaining resources, got %d", len(store.GetAll()))
|
|
}
|
|
}
|
|
|
|
func TestAPIToAgentPreference(t *testing.T) {
|
|
store := NewStore()
|
|
|
|
now := time.Now()
|
|
|
|
// First, add an API-sourced host
|
|
apiResource := Resource{
|
|
ID: "api-host",
|
|
Type: ResourceTypeHost, // Same type as agent
|
|
Name: "server",
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
CPU: &MetricValue{Current: 50.0},
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server",
|
|
},
|
|
}
|
|
store.Upsert(apiResource)
|
|
|
|
// Then, add an agent resource for the same machine (same type, different source)
|
|
agentResource := Resource{
|
|
ID: "agent-host",
|
|
Type: ResourceTypeHost, // Same type
|
|
Name: "server",
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
CPU: &MetricValue{Current: 55.0},
|
|
LastSeen: now,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: "server",
|
|
},
|
|
}
|
|
store.Upsert(agentResource)
|
|
|
|
// Only agent resource should exist (same type = dedup, agent preferred)
|
|
all := store.GetAll()
|
|
if len(all) != 1 {
|
|
t.Fatalf("Expected 1 resource (same type dedup), got %d", len(all))
|
|
}
|
|
|
|
if all[0].SourceType != SourceAgent {
|
|
t.Errorf("Expected agent source type (preferred), got %s", all[0].SourceType)
|
|
}
|
|
}
|
|
|
|
func TestGetTopByCPU(t *testing.T) {
|
|
store := NewStore()
|
|
now := time.Now()
|
|
|
|
store.Upsert(Resource{
|
|
ID: "vm1",
|
|
Type: ResourceTypeVM,
|
|
Name: "low-cpu-vm",
|
|
CPU: &MetricValue{Current: 20.0},
|
|
LastSeen: now,
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "vm2",
|
|
Type: ResourceTypeVM,
|
|
Name: "high-cpu-vm",
|
|
CPU: &MetricValue{Current: 85.0},
|
|
LastSeen: now,
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "node1",
|
|
Type: ResourceTypeNode,
|
|
Name: "busy-node",
|
|
CPU: &MetricValue{Current: 75.0},
|
|
LastSeen: now,
|
|
})
|
|
|
|
// Get top 2 by CPU
|
|
top := store.GetTopByCPU(2, nil)
|
|
if len(top) != 2 {
|
|
t.Fatalf("Expected 2 resources, got %d", len(top))
|
|
}
|
|
if top[0].Name != "high-cpu-vm" {
|
|
t.Errorf("Expected high-cpu-vm first, got %s", top[0].Name)
|
|
}
|
|
if top[1].Name != "busy-node" {
|
|
t.Errorf("Expected busy-node second, got %s", top[1].Name)
|
|
}
|
|
|
|
// Filter by type
|
|
topVMs := store.GetTopByCPU(10, []ResourceType{ResourceTypeVM})
|
|
if len(topVMs) != 2 {
|
|
t.Errorf("Expected 2 VMs, got %d", len(topVMs))
|
|
}
|
|
}
|
|
|
|
func TestGetRelated(t *testing.T) {
|
|
store := NewStore()
|
|
now := time.Now()
|
|
|
|
store.Upsert(Resource{
|
|
ID: "node1",
|
|
Type: ResourceTypeNode,
|
|
Name: "parent-node",
|
|
ClusterID: "cluster1",
|
|
LastSeen: now,
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "vm1",
|
|
Type: ResourceTypeVM,
|
|
Name: "child-vm-1",
|
|
ParentID: "node1",
|
|
ClusterID: "cluster1",
|
|
LastSeen: now,
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "vm2",
|
|
Type: ResourceTypeVM,
|
|
Name: "child-vm-2",
|
|
ParentID: "node1",
|
|
ClusterID: "cluster1",
|
|
LastSeen: now,
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "node2",
|
|
Type: ResourceTypeNode,
|
|
Name: "cluster-peer",
|
|
ClusterID: "cluster1",
|
|
LastSeen: now,
|
|
})
|
|
|
|
// Get related resources for vm1
|
|
related := store.GetRelated("vm1")
|
|
|
|
// Should have parent
|
|
if parent, ok := related["parent"]; !ok || len(parent) != 1 {
|
|
t.Error("Expected 1 parent")
|
|
}
|
|
|
|
// Should have sibling (vm2)
|
|
if siblings, ok := related["siblings"]; !ok || len(siblings) != 1 {
|
|
t.Errorf("Expected 1 sibling, got %d", len(related["siblings"]))
|
|
}
|
|
|
|
// Should have cluster members
|
|
if cluster, ok := related["cluster_members"]; !ok || len(cluster) != 3 {
|
|
t.Errorf("Expected 3 cluster members, got %d", len(related["cluster_members"]))
|
|
}
|
|
}
|
|
|
|
func TestGetResourceSummary(t *testing.T) {
|
|
store := NewStore()
|
|
now := time.Now()
|
|
|
|
store.Upsert(Resource{
|
|
ID: "node1",
|
|
Type: ResourceTypeNode,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
Status: StatusOnline,
|
|
CPU: &MetricValue{Current: 50},
|
|
Memory: &MetricValue{Current: 50}, // 50% usage
|
|
LastSeen: now,
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "vm1",
|
|
Type: ResourceTypeVM,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
Status: StatusRunning,
|
|
CPU: &MetricValue{Current: 70},
|
|
Memory: &MetricValue{Current: 50}, // 50% usage
|
|
LastSeen: now,
|
|
})
|
|
store.Upsert(Resource{
|
|
ID: "vm2",
|
|
Type: ResourceTypeVM,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
Status: StatusStopped,
|
|
LastSeen: now,
|
|
})
|
|
|
|
summary := store.GetResourceSummary()
|
|
|
|
if summary.TotalResources != 3 {
|
|
t.Errorf("Expected 3 total resources, got %d", summary.TotalResources)
|
|
}
|
|
if summary.Healthy != 2 {
|
|
t.Errorf("Expected 2 healthy, got %d", summary.Healthy)
|
|
}
|
|
if summary.Offline != 1 {
|
|
t.Errorf("Expected 1 offline, got %d", summary.Offline)
|
|
}
|
|
|
|
// Check per-type stats
|
|
vmStats := summary.ByType[ResourceTypeVM]
|
|
if vmStats.Count != 2 {
|
|
t.Errorf("Expected 2 VMs, got %d", vmStats.Count)
|
|
}
|
|
}
|