mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-06 16:16:26 +00:00
323 lines
8.7 KiB
Go
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")
|
|
}
|
|
}
|