Pulse/internal/models/models_helpers_test.go
2026-03-18 16:06:30 +00:00

323 lines
8.7 KiB
Go

package models
import (
"testing"
"time"
)
// --- normalizeNodeIdentityPart ---
func TestNormalizeNodeIdentityPart(t *testing.T) {
tests := []struct {
input, expected string
}{
{" PVE1 ", "pve1"},
{"ONLINE", "online"},
{"", ""},
{" ", ""},
}
for _, tt := range tests {
got := normalizeNodeIdentityPart(tt.input)
if got != tt.expected {
t.Errorf("normalizeNodeIdentityPart(%q) = %q, want %q", tt.input, got, tt.expected)
}
}
}
// --- normalizeIPAddress ---
func TestNormalizeIPAddress(t *testing.T) {
tests := []struct {
input, expected string
}{
{"192.168.1.10", "192.168.1.10"},
{" 10.0.0.1 ", "10.0.0.1"},
{"192.168.1.10/24", "192.168.1.10"},
{"::1", "::1"},
{"not-an-ip", ""},
{"", ""},
}
for _, tt := range tests {
got := normalizeIPAddress(tt.input)
if got != tt.expected {
t.Errorf("normalizeIPAddress(%q) = %q, want %q", tt.input, got, tt.expected)
}
}
}
// --- extractHostEndpoint ---
func TestExtractHostEndpoint(t *testing.T) {
tests := []struct {
input, expected string
}{
{"https://pve1.homelab.lan:8006", "pve1.homelab.lan"},
{"https://192.168.1.10:8006", "192.168.1.10"},
{"http://server:9090/path", "server"},
{"192.168.1.10:8006", "192.168.1.10"},
{"pve1.local", "pve1.local"},
{"", ""},
{" ", ""},
}
for _, tt := range tests {
got := extractHostEndpoint(tt.input)
if got != tt.expected {
t.Errorf("extractHostEndpoint(%q) = %q, want %q", tt.input, got, tt.expected)
}
}
}
// --- shortHostname ---
func TestShortHostname(t *testing.T) {
tests := []struct {
input, expected string
}{
{"pve1.homelab.lan", "pve1"},
{"PVE1.local", "pve1"},
{"pve1", "pve1"},
{"", ""},
}
for _, tt := range tests {
got := shortHostname(tt.input)
if got != tt.expected {
t.Errorf("shortHostname(%q) = %q, want %q", tt.input, got, tt.expected)
}
}
}
// --- nodeLogicalKey ---
func TestNodeLogicalKey_Empty(t *testing.T) {
got := nodeLogicalKey(Node{})
if got != "" {
t.Errorf("empty node should have empty key, got %q", got)
}
}
func TestNodeLogicalKey_Cluster(t *testing.T) {
got := nodeLogicalKey(Node{Name: "pve1", ClusterName: "mycluster"})
if got != "cluster:mycluster:pve1" {
t.Errorf("expected cluster key, got %q", got)
}
}
func TestNodeLogicalKey_Instance(t *testing.T) {
got := nodeLogicalKey(Node{Name: "pve1", Instance: "inst-a"})
if got != "instance:inst-a:pve1" {
t.Errorf("expected instance key, got %q", got)
}
}
func TestNodeLogicalKey_Endpoint(t *testing.T) {
got := nodeLogicalKey(Node{Name: "pve1", Host: "https://192.168.1.10:8006"})
if got != "endpoint:https://192.168.1.10:8006:pve1" {
t.Errorf("expected endpoint key, got %q", got)
}
}
func TestNodeLogicalKey_NameOnly(t *testing.T) {
got := nodeLogicalKey(Node{Name: "pve1"})
if got != "name:pve1" {
t.Errorf("expected name-only key, got %q", got)
}
}
// --- nodeStatusRank ---
func TestNodeStatusRank(t *testing.T) {
tests := []struct {
status string
expected int
}{
{"online", 3},
{"ONLINE", 3},
{"degraded", 2},
{"unknown", 1},
{"offline", 0},
{"", 0},
}
for _, tt := range tests {
got := nodeStatusRank(tt.status)
if got != tt.expected {
t.Errorf("nodeStatusRank(%q) = %d, want %d", tt.status, got, tt.expected)
}
}
}
// --- nodeConnectionHealthRank ---
func TestNodeConnectionHealthRank(t *testing.T) {
tests := []struct {
health string
expected int
}{
{"healthy", 3},
{"degraded", 2},
{"unknown", 1},
{"offline", 0},
}
for _, tt := range tests {
got := nodeConnectionHealthRank(tt.health)
if got != tt.expected {
t.Errorf("nodeConnectionHealthRank(%q) = %d, want %d", tt.health, got, tt.expected)
}
}
}
// --- preferNodeForMerge ---
func TestPreferNodeForMerge_HigherStatus(t *testing.T) {
existing := Node{Name: "pve1", Status: "unknown"}
candidate := Node{Name: "pve1", Status: "online"}
got := preferNodeForMerge(existing, candidate)
if got.Status != "online" {
t.Error("higher status node should win")
}
}
func TestPreferNodeForMerge_LinkedAgent(t *testing.T) {
existing := Node{Name: "pve1", Status: "online", ConnectionHealth: "healthy"}
candidate := Node{Name: "pve1", Status: "online", ConnectionHealth: "healthy", LinkedAgentID: "agent-1"}
got := preferNodeForMerge(existing, candidate)
if got.LinkedAgentID != "agent-1" {
t.Error("linked agent should break tie")
}
}
func TestPreferNodeForMerge_LastSeenBreaksTie(t *testing.T) {
now := time.Now()
existing := Node{Name: "pve1", Status: "online", ConnectionHealth: "healthy", LastSeen: now.Add(-time.Hour)}
candidate := Node{Name: "pve1", Status: "online", ConnectionHealth: "healthy", LastSeen: now}
got := preferNodeForMerge(existing, candidate)
if got.LastSeen != now {
t.Error("more recent LastSeen should win tie")
}
}
func TestPreferNodeForMerge_ExistingWinsTotalTie(t *testing.T) {
now := time.Now()
existing := Node{Name: "pve1", Status: "online", LastSeen: now}
candidate := Node{Name: "pve1", Status: "online", LastSeen: now}
got := preferNodeForMerge(existing, candidate)
// On total tie, existing wins.
if got.Name != existing.Name {
t.Error("total tie should keep existing")
}
}
// --- backupKey ---
func TestBackupKey(t *testing.T) {
got := backupKey("pve1", 100)
if got != "pve1-100" {
t.Errorf("backupKey(%q, %d) = %q, want %q", "pve1", 100, got, "pve1-100")
}
}
// --- namespaceMatchesInstance ---
func TestNamespaceMatchesInstance(t *testing.T) {
tests := []struct {
namespace, instance string
expected bool
}{
{"pve1", "pve1", true}, // Exact match
{"PVE1", "pve1", true}, // Case-insensitive
{"nat", "pve-nat", true}, // Namespace is suffix of normalized instance
{"pvebackups", "pve", false}, // "pve" is a prefix not suffix of "pvebackups"
{"", "pve1", false}, // Empty namespace
{"pve1", "", false}, // Empty instance
{"completely-different", "pve1", false}, // No match
}
for _, tt := range tests {
got := namespaceMatchesInstance(tt.namespace, tt.instance)
if got != tt.expected {
t.Errorf("namespaceMatchesInstance(%q, %q) = %v, want %v", tt.namespace, tt.instance, got, tt.expected)
}
}
}
// --- nodeEndpointMergeAliases ---
func TestNodeEndpointMergeAliases_EmptyName(t *testing.T) {
aliases := nodeEndpointMergeAliases(Node{Host: "https://192.168.1.10:8006"})
if aliases != nil {
t.Error("empty name should return nil")
}
}
func TestNodeEndpointMergeAliases_EmptyHost(t *testing.T) {
aliases := nodeEndpointMergeAliases(Node{Name: "pve1"})
if aliases != nil {
t.Error("empty host should return nil")
}
}
func TestNodeEndpointMergeAliases_IPEndpoint(t *testing.T) {
aliases := nodeEndpointMergeAliases(Node{Name: "pve1", Host: "https://192.168.1.10:8006"})
if len(aliases) != 1 {
t.Fatalf("expected 1 alias, got %d: %v", len(aliases), aliases)
}
if aliases[0] != "endpoint-ip:192.168.1.10:pve1" {
t.Errorf("expected endpoint-ip alias, got %q", aliases[0])
}
}
func TestNodeEndpointMergeAliases_HostnameEndpoint(t *testing.T) {
aliases := nodeEndpointMergeAliases(Node{Name: "pve1", Host: "https://pve1.homelab.lan:8006"})
if len(aliases) < 1 {
t.Fatal("expected at least 1 alias")
}
found := false
for _, a := range aliases {
if a == "endpoint-host:pve1.homelab.lan:pve1" {
found = true
}
}
if !found {
t.Errorf("expected endpoint-host alias, got %v", aliases)
}
}
// --- registerNodeAliases ---
func TestRegisterNodeAliases_Basic(t *testing.T) {
aliasToKey := make(map[string]string)
ambiguous := make(map[string]struct{})
registerNodeAliases(aliasToKey, ambiguous, []string{"alias-1"}, "key-1")
if aliasToKey["alias-1"] != "key-1" {
t.Error("alias should be registered")
}
}
func TestRegisterNodeAliases_AmbiguousOnConflict(t *testing.T) {
aliasToKey := map[string]string{"alias-1": "key-1"}
ambiguous := make(map[string]struct{})
registerNodeAliases(aliasToKey, ambiguous, []string{"alias-1"}, "key-2")
if _, ok := ambiguous["alias-1"]; !ok {
t.Error("conflicting alias should be marked ambiguous")
}
if _, ok := aliasToKey["alias-1"]; ok {
t.Error("ambiguous alias should be removed from aliasToKey")
}
}
func TestRegisterNodeAliases_SkipsAlreadyAmbiguous(t *testing.T) {
aliasToKey := make(map[string]string)
ambiguous := map[string]struct{}{"alias-1": {}}
registerNodeAliases(aliasToKey, ambiguous, []string{"alias-1"}, "key-1")
if _, ok := aliasToKey["alias-1"]; ok {
t.Error("already ambiguous alias should not be registered")
}
}
func TestRegisterNodeAliases_SkipsEmpty(t *testing.T) {
aliasToKey := make(map[string]string)
ambiguous := make(map[string]struct{})
registerNodeAliases(aliasToKey, ambiguous, []string{""}, "key-1")
registerNodeAliases(aliasToKey, ambiguous, []string{"alias-1"}, "")
if len(aliasToKey) != 0 {
t.Error("empty values should be skipped")
}
}