mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
393 lines
13 KiB
Go
393 lines
13 KiB
Go
package relay
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
func TestCanonicalActionOutcomeNotificationMatrix(t *testing.T) {
|
|
executions := []unifiedresources.ActionExecutionStatus{unifiedresources.ActionExecutionNotRun, unifiedresources.ActionExecutionSucceeded, unifiedresources.ActionExecutionFailed, unifiedresources.ActionExecutionInconclusive}
|
|
verificationClasses := map[unifiedresources.ActionVerificationStatus][]unifiedresources.ActionEvidenceClass{
|
|
unifiedresources.ActionVerificationNotAttempted: {unifiedresources.ActionEvidenceNone},
|
|
unifiedresources.ActionVerificationConfirmed: {unifiedresources.ActionEvidenceAgentAttested, unifiedresources.ActionEvidenceIndependent},
|
|
unifiedresources.ActionVerificationContradicted: {unifiedresources.ActionEvidenceAgentAttested, unifiedresources.ActionEvidenceIndependent},
|
|
unifiedresources.ActionVerificationInconclusive: {unifiedresources.ActionEvidenceNone, unifiedresources.ActionEvidenceAgentAttested, unifiedresources.ActionEvidenceIndependent},
|
|
}
|
|
cases := 0
|
|
for _, execution := range executions {
|
|
for verification, classes := range verificationClasses {
|
|
for _, class := range classes {
|
|
cases++
|
|
t.Run(fmt.Sprintf("%s/%s/%s", execution, verification, class), func(t *testing.T) {
|
|
result := unifiedresources.ActionResultV2{Execution: unifiedresources.ActionExecutionTruth{Status: execution}, Verification: unifiedresources.ActionVerificationTruth{Status: verification, EvidenceClass: class}}
|
|
got := NewCanonicalActionOutcomeNotification("finding-1", "Action", result)
|
|
if !strings.Contains(got.Body, "Execution ") || !strings.Contains(got.Body, "verification ") {
|
|
t.Fatalf("notification collapsed an axis: %#v", got)
|
|
}
|
|
if class == unifiedresources.ActionEvidenceAgentAttested && !strings.Contains(got.Body, "agent-attested") {
|
|
t.Fatalf("agent evidence source omitted: %#v", got)
|
|
}
|
|
if class == unifiedresources.ActionEvidenceIndependent && !strings.Contains(got.Body, "independent") {
|
|
t.Fatalf("independent evidence source omitted: %#v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
if cases != 32 {
|
|
t.Fatalf("legal notification cases=%d, want 32", cases)
|
|
}
|
|
}
|
|
|
|
func TestMobileCompatibilityPushActionVocabulary(t *testing.T) {
|
|
want := []string{
|
|
"view_finding",
|
|
"view_alert",
|
|
"approve_fix",
|
|
"decide_action",
|
|
"view_fix_result",
|
|
}
|
|
got := []string{
|
|
PushActionViewFinding,
|
|
PushActionViewAlert,
|
|
PushActionApproveFix,
|
|
PushActionDecideAction,
|
|
PushActionViewFixResult,
|
|
}
|
|
if fmt.Sprint(got) != fmt.Sprint(want) {
|
|
t.Fatalf("mobile push action vocabulary = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
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 TestNewExternalProbeUnavailableNotification(t *testing.T) {
|
|
n := NewExternalProbeUnavailableNotification(" alert-123 ")
|
|
if n.Type != PushTypeExternalProbeOffline || n.Priority != PushPriorityHigh {
|
|
t.Fatalf("notification posture = %#v", n)
|
|
}
|
|
if n.Title != "External Probe Offline" {
|
|
t.Fatalf("notification presentation = %#v", n)
|
|
}
|
|
if n.ActionType != PushActionViewAlert || n.ActionID != "alert-123" {
|
|
t.Fatalf("notification must route the canonical alert into mobile attention: %#v", n)
|
|
}
|
|
if n.Category != "availability" || n.Severity != "warning" {
|
|
t.Fatalf("notification routing = %#v", n)
|
|
}
|
|
if strings.Contains(n.Body, "agent-") || strings.Contains(n.Body, ".local") {
|
|
t.Fatalf("notification leaked infrastructure context: %#v", n)
|
|
}
|
|
}
|
|
|
|
func TestNewAlertFiredNotification(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
severity string
|
|
priority string
|
|
title string
|
|
}{
|
|
{name: "warning", severity: "warning", priority: PushPriorityNormal, title: "Pulse warning"},
|
|
{name: "info", severity: "info", priority: PushPriorityNormal, title: "Pulse information"},
|
|
{name: "critical", severity: "critical", priority: PushPriorityHigh, title: "Critical Pulse alert"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
n := NewAlertFiredNotification(" alert-123 ", tc.severity)
|
|
if n.Type != PushTypeAlertFired || n.Priority != tc.priority || n.Title != tc.title {
|
|
t.Fatalf("notification posture = %#v", n)
|
|
}
|
|
if n.ActionType != PushActionViewAlert || n.ActionID != "alert-123" {
|
|
t.Fatalf("notification must open the canonical alert: %#v", n)
|
|
}
|
|
if n.Category != "alert" || n.Severity != tc.severity {
|
|
t.Fatalf("notification routing metadata = %#v", n)
|
|
}
|
|
if strings.Contains(n.Title+n.Body, "alert-123") {
|
|
t.Fatalf("notification exposed alert identity in lock-screen copy: %#v", n)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAlertMinimumSeverityRouting(t *testing.T) {
|
|
if !AlertMeetsMinimumSeverity("info", AlertMinimumSeverityAll) {
|
|
t.Fatal("all-alert policy excluded an informational alert")
|
|
}
|
|
if !AlertMeetsMinimumSeverity("warning", AlertMinimumSeverityAll) {
|
|
t.Fatal("all-alert policy excluded a warning")
|
|
}
|
|
if AlertMeetsMinimumSeverity("warning", AlertMinimumSeverityCritical) {
|
|
t.Fatal("critical-only policy included a warning")
|
|
}
|
|
if !AlertMeetsMinimumSeverity(" CRITICAL ", AlertMinimumSeverityCritical) {
|
|
t.Fatal("critical-only policy excluded a normalized critical alert")
|
|
}
|
|
if NormalizeAlertMinimumSeverity("unexpected") != AlertMinimumSeverityAll {
|
|
t.Fatal("invalid persisted policy did not fail open to backwards-compatible all-alert delivery")
|
|
}
|
|
}
|
|
|
|
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 TestNewActionDecisionNotificationUsesCanonicalActionIdentity(t *testing.T) {
|
|
n := NewActionDecisionNotification("act-123", "Restart unhealthy workload")
|
|
if n.Type != PushTypeApprovalRequest || n.Priority != PushPriorityHigh {
|
|
t.Fatalf("notification posture = %#v", n)
|
|
}
|
|
if n.ActionType != PushActionDecideAction {
|
|
t.Fatalf("ActionType = %q, want %q", n.ActionType, PushActionDecideAction)
|
|
}
|
|
if n.ActionID != "act-123" {
|
|
t.Fatalf("ActionID = %q, want canonical action id", n.ActionID)
|
|
}
|
|
if strings.Contains(n.Body, "fix") {
|
|
t.Fatalf("typed action notification must not claim a command-shaped fix: %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 TestNewActionOutcomeNotificationIsHonestAboutVerification(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
status string
|
|
body string
|
|
}{
|
|
{status: "verified", body: "Action completed and verified"},
|
|
{status: "unverified", body: "Action completed; verification was inconclusive"},
|
|
{status: "failed", body: "Action completed, but verification failed"},
|
|
{status: "execution_failed", body: "Action failed before verification"},
|
|
} {
|
|
n := NewActionOutcomeNotification("finding-1", "Workload action", tc.status)
|
|
if n.Body != tc.body || n.ActionType != PushActionViewFixResult || n.ActionID != "finding-1" {
|
|
t.Fatalf("status %q notification = %#v", tc.status, n)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|