Pulse/internal/unifiedresources/resolve_test.go
2026-03-30 22:44:34 +01:00

457 lines
14 KiB
Go

package unifiedresources
import (
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
)
func TestResolveResource_NilReadState(t *testing.T) {
loc := ResolveResource(nil, "anything")
if loc.Found {
t.Fatal("expected not found for nil ReadState")
}
}
func TestResolveResource_TrimmedLookupName(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{{ID: "host1", Hostname: "myserver", Platform: "linux"}},
})
loc := ResolveResource(rr, " myserver ")
if !loc.Found || loc.ResourceType != "agent" {
t.Fatalf("expected trimmed lookup to find agent, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.Name != "myserver" {
t.Fatalf("expected canonical name myserver, got %q", loc.Name)
}
if loc.TargetID != "host1" {
t.Fatalf("expected TargetID=host1, got %q", loc.TargetID)
}
}
func TestResolveResource_WhitespaceOnlyInput(t *testing.T) {
loc := ResolveResource(nil, " ")
if loc.Found {
t.Fatal("expected whitespace-only lookup to be not found")
}
if loc.Name != "" {
t.Fatalf("expected empty normalized name, got %q", loc.Name)
}
}
func TestResolveResource_Node(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
Nodes: []models.Node{{ID: "n1", Name: "pve-node"}},
})
loc := ResolveResource(rr, "pve-node")
if !loc.Found {
t.Fatal("expected node to be found")
}
if loc.ResourceType != "node" {
t.Fatalf("expected node type, got %q", loc.ResourceType)
}
if loc.TargetHost != "pve-node" {
t.Fatalf("expected target_host pve-node, got %q", loc.TargetHost)
}
}
func TestResolveResource_VM(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
VMs: []models.VM{{ID: "vm-1", Name: "alpha", VMID: 101, Node: "node1"}},
})
loc := ResolveResource(rr, "alpha")
if !loc.Found || loc.ResourceType != "vm" {
t.Fatalf("expected vm, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.VMID != 101 || loc.Node != "node1" {
t.Fatalf("expected VMID=101 Node=node1, got VMID=%d Node=%q", loc.VMID, loc.Node)
}
}
func TestResolveResource_Container(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
Containers: []models.Container{{ID: "lxc-1", Name: "beta", VMID: 201, Node: "node1", Type: "lxc"}},
})
loc := ResolveResource(rr, "beta")
if !loc.Found || loc.ResourceType != "system-container" {
t.Fatalf("expected system-container, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.VMID != 201 {
t.Fatalf("expected VMID=201, got %d", loc.VMID)
}
}
func TestResolveResource_DockerContainer(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
Containers: []models.Container{{ID: "lxc-1", Name: "dock1", VMID: 100, Node: "node1", Type: "lxc"}},
DockerHosts: []models.DockerHost{{
ID: "dock1",
Hostname: "dock1",
Containers: []models.DockerContainer{{
ID: "cid1",
Name: "homepage",
State: "running",
}},
}},
})
loc := ResolveResource(rr, "homepage")
if !loc.Found || loc.ResourceType != "app-container" {
t.Fatalf("expected app-container, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.DockerHostName != "dock1" {
t.Fatalf("expected docker host dock1, got %q", loc.DockerHostName)
}
if loc.DockerHostType != "system-container" {
t.Fatalf("expected docker host type system-container, got %q", loc.DockerHostType)
}
if loc.DockerHostVMID != 100 {
t.Fatalf("expected docker host VMID 100, got %d", loc.DockerHostVMID)
}
// TargetHost must be rewritten to the LXC name for command routing.
if loc.TargetHost != "dock1" {
t.Fatalf("expected target_host rewritten to LXC name dock1, got %q", loc.TargetHost)
}
}
func TestResolveResource_DockerContainerTargetHostRewrite(t *testing.T) {
// Docker container lookup must rewrite TargetHost to the backing LXC name.
// Use Docker host ID matching (not hostname) to verify the rewrite path.
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
Containers: []models.Container{{ID: "lxc-1", Name: "docker-host-lxc", VMID: 100, Node: "node1", Type: "lxc"}},
DockerHosts: []models.DockerHost{{
ID: "docker-host-lxc",
Hostname: "docker-host-lxc",
Containers: []models.DockerContainer{{
ID: "cid1",
Name: "myapp",
State: "running",
}},
}},
})
loc := ResolveResource(rr, "myapp")
if !loc.Found || loc.ResourceType != "app-container" {
t.Fatalf("expected app-container, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.TargetHost != "docker-host-lxc" {
t.Fatalf("expected target_host rewritten to LXC name, got %q", loc.TargetHost)
}
if loc.DockerHostType != "system-container" {
t.Fatalf("expected system-container, got %q", loc.DockerHostType)
}
// Docker HOST lookup must NOT rewrite TargetHost.
locHost := ResolveResource(rr, "docker-host-lxc")
if !locHost.Found || locHost.ResourceType != "system-container" {
// Should match as system-container first (earlier in resolution order)
t.Fatalf("expected system-container, got found=%v type=%q", locHost.Found, locHost.ResourceType)
}
}
func TestResolveResource_DockerHost(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
DockerHosts: []models.DockerHost{{
ID: "standalone1",
Hostname: "standalone1",
}},
})
loc := ResolveResource(rr, "standalone1")
if !loc.Found || loc.ResourceType != "docker-host" {
t.Fatalf("expected docker-host, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.DockerHostType != "standalone" {
t.Fatalf("expected standalone, got %q", loc.DockerHostType)
}
}
func TestResolveResource_TrueNASAppDoesNotUseDockerRouting(t *testing.T) {
rr := newTrueNASAppRegistryFixture()
loc := ResolveResource(rr, "Nextcloud")
if !loc.Found || loc.ResourceType != "app-container" {
t.Fatalf("expected TrueNAS app-container, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.TargetHost != "truenas-main" {
t.Fatalf("expected target host truenas-main, got %q", loc.TargetHost)
}
if loc.DockerHostName != "" || loc.DockerHostType != "" {
t.Fatalf("expected TrueNAS app to avoid docker routing, got docker_host=%q type=%q", loc.DockerHostName, loc.DockerHostType)
}
}
func TestResolveResource_VMwareStorage(t *testing.T) {
rr := NewRegistry(nil)
now := time.Now().UTC()
rr.IngestRecords(SourceVMware, []IngestRecord{{
SourceID: "vc-1:datastore:datastore-11",
Resource: Resource{
Type: ResourceTypeStorage,
Name: "nvme-primary",
Status: StatusOnline,
LastSeen: now,
UpdatedAt: now,
ParentName: "Lab VC",
Storage: &StorageMeta{Type: "vmfs"},
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "datastore-11",
EntityType: "datastore",
},
},
}})
loc := ResolveResource(rr, "nvme-primary")
if !loc.Found || loc.ResourceType != "storage" {
t.Fatalf("expected storage resource, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.TargetID != "Lab VC" || loc.TargetHost != "Lab VC" {
t.Fatalf("expected VMware storage target to route through Lab VC, got %+v", loc)
}
resolved := ResolveResourceContext(rr, "nvme-primary")
if resolved.Resource == nil {
t.Fatal("expected resolved VMware storage resource")
}
if resolved.Resource.Type != ResourceTypeStorage {
t.Fatalf("expected storage resource payload, got %q", resolved.Resource.Type)
}
if resolved.Resource.VMware == nil || resolved.Resource.VMware.ManagedObjectID != "datastore-11" {
t.Fatalf("expected VMware datastore metadata, got %+v", resolved.Resource.VMware)
}
}
func TestLookupResolvedResource_TrueNASAppUsesCanonicalAppContainerSet(t *testing.T) {
rr := newTrueNASAppRegistryFixture()
loc := ResolveResource(rr, "Nextcloud")
resource := lookupResolvedResource(rr, loc)
if resource == nil {
t.Fatal("expected resolved resource for TrueNAS app")
}
if resource.Type != ResourceTypeAppContainer {
t.Fatalf("expected app-container resource, got %q", resource.Type)
}
if resource.TrueNAS == nil {
t.Fatalf("expected TrueNAS metadata on resolved resource, got %+v", resource)
}
if resource.Name != "Nextcloud" {
t.Fatalf("expected canonical app name Nextcloud, got %q", resource.Name)
}
}
func newTrueNASAppRegistryFixture() *ResourceRegistry {
rr := NewRegistry(nil)
now := time.Now().UTC()
hostID := "agent:truenas-main"
hostSourceID := "truenas-system:truenas-main"
rr.IngestRecords(SourceTrueNAS, []IngestRecord{
{
SourceID: hostSourceID,
Resource: Resource{
ID: hostID,
Type: ResourceTypeAgent,
Name: "truenas-main",
Status: StatusOnline,
LastSeen: now,
UpdatedAt: now,
Agent: &AgentData{
Hostname: "truenas-main",
Platform: "truenas",
},
TrueNAS: &TrueNASData{Hostname: "truenas-main"},
},
Identity: ResourceIdentity{Hostnames: []string{"truenas-main"}},
},
{
SourceID: "app:nextcloud",
ParentSourceID: hostSourceID,
Resource: Resource{
ID: "app-container:truenas-main:nextcloud",
Type: ResourceTypeAppContainer,
Name: "Nextcloud",
Status: StatusOnline,
LastSeen: now,
UpdatedAt: now,
ParentID: stringPtr(hostID),
ParentName: "truenas-main",
Docker: &DockerData{
ContainerID: "nextcloud",
Hostname: "truenas-main",
},
TrueNAS: &TrueNASData{Hostname: "truenas-main"},
Canonical: &CanonicalIdentity{
DisplayName: "Nextcloud",
Hostname: "truenas-main",
PrimaryID: "nextcloud",
Aliases: []string{"Nextcloud", "nextcloud"},
},
Tags: []string{"truenas", "app"},
},
Identity: ResourceIdentity{Hostnames: []string{"truenas-main"}},
},
})
return rr
}
func stringPtr(value string) *string {
return &value
}
func TestResolveResource_Host(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{{ID: "host1", Hostname: "myserver", Platform: "linux"}},
})
loc := ResolveResource(rr, "myserver")
if !loc.Found || loc.ResourceType != "agent" {
t.Fatalf("expected agent, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.Platform != "linux" {
t.Fatalf("expected linux platform, got %q", loc.Platform)
}
if loc.TargetID != "host1" {
t.Fatalf("expected TargetID=host1 (canonical target ID), got %q", loc.TargetID)
}
// Lookup by agent/source ID should also work.
loc2 := ResolveResource(rr, "host1")
if !loc2.Found || loc2.ResourceType != "agent" {
t.Fatalf("expected agent lookup by agent ID, got found=%v type=%q", loc2.Found, loc2.ResourceType)
}
}
func TestResolveResource_K8sCluster(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{{
ID: "k8s1",
Name: "prod",
AgentID: "agent-1",
}},
})
loc := ResolveResource(rr, "prod")
if !loc.Found || loc.ResourceType != "k8s-cluster" {
t.Fatalf("expected k8s-cluster, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.K8sAgentID != "agent-1" {
t.Fatalf("expected agent-1, got %q", loc.K8sAgentID)
}
}
func TestResolveResource_K8sPod(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{{
ID: "k8s1",
Name: "prod",
AgentID: "agent-1",
Pods: []models.KubernetesPod{{
Name: "nginx-abc",
Namespace: "default",
}},
}},
})
loc := ResolveResource(rr, "nginx-abc")
if !loc.Found || loc.ResourceType != "k8s-pod" {
t.Fatalf("expected k8s-pod, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.K8sNamespace != "default" {
t.Fatalf("expected namespace default, got %q", loc.K8sNamespace)
}
if loc.K8sClusterName != "prod" {
t.Fatalf("expected cluster prod, got %q", loc.K8sClusterName)
}
}
func TestResolveResource_K8sDeployment(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{{
ID: "k8s1",
Name: "prod",
AgentID: "agent-1",
Deployments: []models.KubernetesDeployment{{
Name: "web-deploy",
Namespace: "production",
}},
}},
})
loc := ResolveResource(rr, "web-deploy")
if !loc.Found || loc.ResourceType != "k8s-deployment" {
t.Fatalf("expected k8s-deployment, got found=%v type=%q", loc.Found, loc.ResourceType)
}
if loc.K8sNamespace != "production" {
t.Fatalf("expected namespace production, got %q", loc.K8sNamespace)
}
}
func TestResolveResource_NotFound(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{})
loc := ResolveResource(rr, "nonexistent")
if loc.Found {
t.Fatal("expected not found")
}
if loc.Name != "nonexistent" {
t.Fatalf("expected name preserved, got %q", loc.Name)
}
}
func TestResolveResourceContext_ReturnsGovernedResource(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
Containers: []models.Container{{
ID: "lxc-1",
Name: "customer-pg",
VMID: 201,
Node: "node1",
Type: "lxc",
Tags: []string{"customer-data"},
}},
})
resolved := ResolveResourceContext(rr, "customer-pg")
if !resolved.Location.Found || resolved.Location.ResourceType != "system-container" {
t.Fatalf("expected resolved system container, got %#v", resolved.Location)
}
if resolved.Resource == nil {
t.Fatal("expected unified resource metadata")
}
if resolved.Resource.Policy == nil {
t.Fatal("expected policy metadata on resolved resource")
}
if resolved.Resource.Policy.Sensitivity != ResourceSensitivityRestricted {
t.Fatalf("expected restricted sensitivity, got %q", resolved.Resource.Policy.Sensitivity)
}
if resolved.Resource.Policy.Routing.Scope != ResourceRoutingScopeLocalOnly {
t.Fatalf("expected local-only routing, got %q", resolved.Resource.Policy.Routing.Scope)
}
if resolved.Resource.AISafeSummary == "" {
t.Fatal("expected aiSafeSummary to be populated")
}
if resolved.Resource.AISafeSummary == resolved.Resource.Name {
t.Fatalf("expected aiSafeSummary to avoid raw name, got %q", resolved.Resource.AISafeSummary)
}
}
func TestResolveResourceContext_NotFound(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{})
resolved := ResolveResourceContext(rr, "missing")
if resolved.Location.Found {
t.Fatalf("expected missing lookup to remain not found, got %#v", resolved.Location)
}
if resolved.Resource != nil {
t.Fatalf("expected no resolved resource for missing lookup, got %#v", resolved.Resource)
}
}