package service import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestShouldSendWebhookByMinSeverity(t *testing.T) { tests := []struct { name string minSeverity string ruleSeverity string want bool }{ {"critical >= critical", "critical", "critical", true}, {"warning >= critical", "critical", "warning", false}, {"critical >= warning", "warning", "critical", true}, {"warning >= warning", "warning", "warning", true}, {"info >= warning", "warning", "info", false}, {"info >= info", "info", "info", true}, {"empty min sends all", "", "info", true}, {"unknown severity sends by default", "unknown", "info", true}, {"case insensitive", "CRITICAL", "Warning", false}, {"case insensitive 2", "critical", "WARNING", false}, {"spaces trimmed", " critical ", " warning ", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := shouldSendWebhookByMinSeverity(tt.minSeverity, tt.ruleSeverity) assert.Equal(t, tt.want, got) }) } } func TestWebhookService_BuildAlertPayload(t *testing.T) { svc := &WebhookService{} rule := &OpsAlertRule{ ID: 1, Name: "Test Rule", Severity: "critical", } event := &OpsAlertEvent{ ID: 1, RuleID: 1, Title: "Test Alert", Description: "Test alert description", Severity: "critical", Status: "firing", } t.Run("alert payload", func(t *testing.T) { payload := svc.buildAlertPayload(rule, event, false) assert.Equal(t, "alert", payload.Type) assert.NotEmpty(t, payload.Timestamp) assert.Equal(t, rule, payload.Data.Rule) assert.Equal(t, event, payload.Data.Event) assert.Empty(t, payload.Data.ResolvedAt) }) t.Run("resolved payload", func(t *testing.T) { payload := svc.buildAlertPayload(rule, event, true) assert.Equal(t, "alert_resolved", payload.Type) assert.NotEmpty(t, payload.Data.ResolvedAt) }) } func TestWebhookService_SignPayload(t *testing.T) { svc := &WebhookService{} t.Run("empty secret does not sign", func(t *testing.T) { payload := &OpsWebhookPayload{ Type: "alert", Timestamp: "2024-01-01T00:00:00Z", } err := svc.signPayload(payload, "") assert.NoError(t, err) assert.Empty(t, payload.Signature) }) t.Run("nil payload returns nil", func(t *testing.T) { err := svc.signPayload(nil, "secret") assert.NoError(t, err) }) t.Run("signs payload with HMAC-SHA256", func(t *testing.T) { payload := &OpsWebhookPayload{ Type: "alert", Timestamp: "2024-01-01T00:00:00Z", Data: OpsWebhookData{ Rule: &OpsAlertRule{ID: 1}, }, } secret := "test-secret" err := svc.signPayload(payload, secret) require.NoError(t, err) assert.NotEmpty(t, payload.Signature) assert.Len(t, payload.Signature, 64) // SHA256 hex encoding = 64 chars // Verify signature signPayload := &OpsWebhookPayload{ Type: payload.Type, Timestamp: payload.Timestamp, Data: payload.Data, } data, _ := json.Marshal(signPayload) mac := hmac.New(sha256.New, []byte(secret)) mac.Write(data) expectedSig := hex.EncodeToString(mac.Sum(nil)) assert.Equal(t, expectedSig, payload.Signature) }) } func TestWebhookService_SendWebhook(t *testing.T) { svc := &WebhookService{ httpClient: &http.Client{Timeout: 30 * time.Second}, } t.Run("successful webhook", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) assert.Contains(t, r.Header.Get("User-Agent"), "Sub2API-Webhook") body, _ := io.ReadAll(r.Body) assert.Contains(t, string(body), "test") w.WriteHeader(http.StatusOK) })) defer server.Close() payload := []byte(`{"type":"alert","test":true}`) err := svc.sendWebhook(context.Background(), server.URL, payload, 5, "") assert.NoError(t, err) }) t.Run("webhook with signature header", func(t *testing.T) { secret := "test-secret" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sigHeader := r.Header.Get("X-Webhook-Signature") assert.NotEmpty(t, sigHeader) assert.True(t, strings.HasPrefix(sigHeader, "sha256=")) w.WriteHeader(http.StatusOK) })) defer server.Close() payload := []byte(`{"type":"alert"}`) err := svc.sendWebhook(context.Background(), server.URL, payload, 5, secret) assert.NoError(t, err) }) t.Run("webhook returns error status", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("internal error")) })) defer server.Close() payload := []byte(`{"type":"alert"}`) err := svc.sendWebhook(context.Background(), server.URL, payload, 5, "") assert.Error(t, err) assert.Contains(t, err.Error(), "500") }) t.Run("webhook timeout", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(2 * time.Second) w.WriteHeader(http.StatusOK) })) defer server.Close() payload := []byte(`{"type":"alert"}`) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() err := svc.sendWebhook(ctx, server.URL, payload, 1, "") assert.Error(t, err) }) t.Run("invalid URL", func(t *testing.T) { payload := []byte(`{"type":"alert"}`) err := svc.sendWebhook(context.Background(), "://invalid-url", payload, 5, "") assert.Error(t, err) }) t.Run("nil http client returns error", func(t *testing.T) { svc := &WebhookService{httpClient: nil} payload := []byte(`{"type":"alert"}`) err := svc.sendWebhook(context.Background(), "http://example.com", payload, 5, "") assert.Error(t, err) assert.Contains(t, err.Error(), "not initialized") }) } func TestWebhookService_SendAlertWebhook_NilChecks(t *testing.T) { t.Run("nil service returns nil", func(t *testing.T) { var svc *WebhookService err := svc.SendAlertWebhook(context.Background(), &OpsAlertRule{}, &OpsAlertEvent{}, false) assert.NoError(t, err) }) t.Run("nil ops service returns nil", func(t *testing.T) { svc := &WebhookService{opsService: nil} err := svc.SendAlertWebhook(context.Background(), &OpsAlertRule{}, &OpsAlertEvent{}, false) assert.NoError(t, err) }) }