Pulse/internal/relay/push_test.go
2026-03-18 16:06:30 +00:00

236 lines
7 KiB
Go

package relay
import (
"strings"
"testing"
)
func TestNewPatrolFindingNotification(t *testing.T) {
t.Run("warning finding", func(t *testing.T) {
n := NewPatrolFindingNotification("finding-123", "warning", "performance", "High CPU usage detected")
if n.Type != PushTypePatrolFinding {
t.Errorf("Type: got %q, want %q", n.Type, PushTypePatrolFinding)
}
if n.Priority != PushPriorityNormal {
t.Errorf("Priority: got %q, want %q", n.Priority, PushPriorityNormal)
}
if n.Title != "High CPU usage detected" {
t.Errorf("Title: got %q, want %q", n.Title, "High CPU usage detected")
}
if n.ActionType != PushActionViewFinding {
t.Errorf("ActionType: got %q, want %q", n.ActionType, PushActionViewFinding)
}
if n.ActionID != "finding-123" {
t.Errorf("ActionID: got %q, want %q", n.ActionID, "finding-123")
}
if n.Category != "performance" {
t.Errorf("Category: got %q, want %q", n.Category, "performance")
}
if n.Severity != "warning" {
t.Errorf("Severity: got %q, want %q", n.Severity, "warning")
}
})
t.Run("critical finding uses high priority", func(t *testing.T) {
n := NewPatrolFindingNotification("finding-456", "critical", "reliability", "Resource offline")
if n.Type != PushTypePatrolCritical {
t.Errorf("Type: got %q, want %q", n.Type, PushTypePatrolCritical)
}
if n.Priority != PushPriorityHigh {
t.Errorf("Priority: got %q, want %q", n.Priority, PushPriorityHigh)
}
})
}
func TestNewApprovalRequestNotification(t *testing.T) {
t.Run("with risk level", func(t *testing.T) {
n := NewApprovalRequestNotification("approval-789", "Fix disk space", "high")
if n.Type != PushTypeApprovalRequest {
t.Errorf("Type: got %q, want %q", n.Type, PushTypeApprovalRequest)
}
if n.Priority != PushPriorityHigh {
t.Errorf("Priority: got %q, want %q", n.Priority, PushPriorityHigh)
}
if n.ActionType != PushActionApproveFix {
t.Errorf("ActionType: got %q, want %q", n.ActionType, PushActionApproveFix)
}
if n.ActionID != "approval-789" {
t.Errorf("ActionID: got %q, want %q", n.ActionID, "approval-789")
}
if !strings.Contains(n.Body, "high-risk") {
t.Errorf("Body should contain risk level, got %q", n.Body)
}
})
t.Run("without risk level", func(t *testing.T) {
n := NewApprovalRequestNotification("approval-000", "Fix CPU", "")
if strings.Contains(n.Body, "-risk") {
t.Errorf("Body should not contain risk level when empty, got %q", n.Body)
}
})
}
func TestNewFixCompletedNotification(t *testing.T) {
t.Run("success", func(t *testing.T) {
n := NewFixCompletedNotification("finding-100", "Cleared disk space", true)
if n.Type != PushTypeFixCompleted {
t.Errorf("Type: got %q, want %q", n.Type, PushTypeFixCompleted)
}
if n.Priority != PushPriorityNormal {
t.Errorf("Priority: got %q, want %q", n.Priority, PushPriorityNormal)
}
if n.ActionType != PushActionViewFixResult {
t.Errorf("ActionType: got %q, want %q", n.ActionType, PushActionViewFixResult)
}
if n.ActionID != "finding-100" {
t.Errorf("ActionID: got %q, want %q", n.ActionID, "finding-100")
}
if !strings.Contains(n.Body, "successfully") {
t.Errorf("Body should indicate success, got %q", n.Body)
}
})
t.Run("failure", func(t *testing.T) {
n := NewFixCompletedNotification("finding-101", "Restart service", false)
if !strings.Contains(n.Body, "failed") {
t.Errorf("Body should indicate failure, got %q", n.Body)
}
})
}
func TestNotificationTruncation(t *testing.T) {
longTitle := strings.Repeat("A", 200)
n := NewPatrolFindingNotification("id", "warning", "capacity", longTitle)
if len(n.Title) > 100 {
t.Errorf("Title should be truncated to 100 chars, got %d", len(n.Title))
}
if !strings.HasSuffix(n.Title, "...") {
t.Errorf("Truncated title should end with '...', got %q", n.Title[len(n.Title)-5:])
}
// Body truncation: build a notification with a very long body by using long category/severity
longCategory := strings.Repeat("X", 200)
n2 := NewPatrolFindingNotification("id", "warning", longCategory, "Title")
if len(n2.Body) > 200 {
t.Errorf("Body should be truncated to 200 chars, got %d", len(n2.Body))
}
}
func TestSanitizeTitle(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "no sensitive data passes through",
input: "High CPU usage detected",
want: "High CPU usage detected",
},
{
name: "node identifier replaced",
input: "High CPU on node-1",
want: "High CPU on [resource]",
},
{
name: "pve cluster name replaced",
input: "Disk full on pve-cluster02",
want: "Disk full on [resource]",
},
{
name: "vm identifier replaced",
input: "Memory leak in vm-100",
want: "Memory leak in [resource]",
},
{
name: "ct identifier replaced",
input: "Container ct-200 unresponsive",
want: "Container [resource] unresponsive",
},
{
name: "IPv4 address replaced",
input: "Cannot reach 192.168.1.100",
want: "Cannot reach [resource]",
},
{
name: "FQDN replaced",
input: "High load on web01.prod.example.com",
want: "High load on [resource]",
},
{
name: "multiple identifiers replaced",
input: "node-1 to node-2 replication lag",
want: "[resource] to [resource] replication lag",
},
{
name: "adjacent placeholders collapsed",
input: "node-1 node-2 offline",
want: "[resource] offline",
},
{
name: "IP in parentheses",
input: "Host (192.168.1.10) unreachable",
want: "Host ([resource]) unreachable",
},
{
name: "IP in brackets (Go net format)",
input: "Connection to [10.0.0.1]:8006 failed",
want: "Connection to [[resource]]:8006 failed",
},
{
name: "IP with port (no brackets)",
input: "Connection to 10.0.0.1:8006 failed",
want: "Connection to [resource] failed",
},
{
name: "IP after equals sign",
input: "Failed host=172.16.0.5 timeout",
want: "Failed host=[resource] timeout",
},
{
name: "IP in quotes",
input: `Error from "10.20.30.40" connection reset`,
want: `Error from "[resource]" connection reset`,
},
{
name: "generic words preserved",
input: "Storage pool nearly full",
want: "Storage pool nearly full",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := sanitizeTitle(tt.input)
if got != tt.want {
t.Errorf("sanitizeTitle(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestSanitizeTitleInBuilders(t *testing.T) {
// Verify that all builder functions apply sanitization
n1 := NewPatrolFindingNotification("id", "warning", "performance", "High CPU on node-prod-1")
if strings.Contains(n1.Title, "node-prod-1") {
t.Errorf("finding title should be sanitized, got %q", n1.Title)
}
n2 := NewApprovalRequestNotification("id", "Fix disk on vm-100", "")
if strings.Contains(n2.Title, "vm-100") {
t.Errorf("approval title should be sanitized, got %q", n2.Title)
}
n3 := NewFixCompletedNotification("id", "Restarted ct-50", true)
if strings.Contains(n3.Title, "ct-50") {
t.Errorf("fix title should be sanitized, got %q", n3.Title)
}
}