Files
2026-08-28 00:45:20 +01:00

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)
}
}