From 02864f54dd8a1de8b7d3e7617ebacc6677c47ee1 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 5 Nov 2025 18:54:18 +0000 Subject: [PATCH] Add test notification functionality for Apprise - Add support for testing Apprise notifications via /api/notifications/test endpoint - Users can now test their Apprise configuration (both CLI and HTTP modes) using method="apprise" - Added comprehensive unit tests for both CLI and HTTP modes - Tests verify correct behavior when Apprise is enabled/disabled - Tests validate that notifications are properly sent through Apprise channels Related to #584 --- internal/notifications/notifications.go | 18 +++ internal/notifications/notifications_test.go | 129 +++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/internal/notifications/notifications.go b/internal/notifications/notifications.go index e9f3a6b..dbd049f 100644 --- a/internal/notifications/notifications.go +++ b/internal/notifications/notifications.go @@ -2013,6 +2013,24 @@ func (n *NotificationManager) SendTestNotification(method string) error { } n.sendWebhook(*webhookToTest, testAlert) return nil + case "apprise": + n.mu.RLock() + appriseConfig := n.appriseConfig + n.mu.RUnlock() + + log.Info(). + Bool("enabled", appriseConfig.Enabled). + Str("mode", string(appriseConfig.Mode)). + Int("targetCount", len(appriseConfig.Targets)). + Msg("Testing Apprise notification") + + if !appriseConfig.Enabled { + return fmt.Errorf("apprise notifications are not enabled") + } + + // Use sendGroupedApprise with a single test alert + n.sendGroupedApprise(appriseConfig, []*alerts.Alert{testAlert}) + return nil default: return fmt.Errorf("unknown notification method: %s", method) } diff --git a/internal/notifications/notifications_test.go b/internal/notifications/notifications_test.go index b817439..64589b4 100644 --- a/internal/notifications/notifications_test.go +++ b/internal/notifications/notifications_test.go @@ -493,3 +493,132 @@ func TestRenderWebhookURL_InvalidTemplate(t *testing.T) { t.Fatalf("expected error for invalid URL template, got nil") } } + +func TestSendTestNotificationApprise(t *testing.T) { + nm := NewNotificationManager("") + nm.SetEmailConfig(EmailConfig{Enabled: false}) + + // Test 1: Apprise not enabled should return error + nm.SetAppriseConfig(AppriseConfig{ + Enabled: false, + Targets: []string{"discord://token"}, + }) + + err := nm.SendTestNotification("apprise") + if err == nil { + t.Fatalf("expected error when Apprise is disabled, got nil") + } + if !strings.Contains(err.Error(), "not enabled") { + t.Fatalf("expected 'not enabled' error, got: %v", err) + } + + // Test 2: Apprise enabled with CLI mode should invoke executor + done := make(chan struct{}) + var capturedArgs []string + + nm.appriseExec = func(ctx context.Context, path string, args []string) ([]byte, error) { + if path != "apprise" { + t.Fatalf("expected CLI path 'apprise', got %q", path) + } + capturedArgs = append([]string(nil), args...) + close(done) + return []byte("success"), nil + } + + nm.SetAppriseConfig(AppriseConfig{ + Enabled: true, + Targets: []string{"discord://token"}, + TimeoutSeconds: 10, + }) + + err = nm.SendTestNotification("apprise") + if err != nil { + t.Fatalf("expected no error when testing Apprise, got: %v", err) + } + + // Wait for the executor to be called + select { + case <-done: + // Success - executor was called + case <-time.After(2 * time.Second): + t.Fatalf("timeout waiting for Apprise executor to be called") + } + + // Verify the arguments contain the target + foundTarget := false + for _, arg := range capturedArgs { + if arg == "discord://token" { + foundTarget = true + break + } + } + if !foundTarget { + t.Fatalf("expected target 'discord://token' in args, got: %v", capturedArgs) + } +} + +func TestSendTestNotificationAppriseHTTP(t *testing.T) { + nm := NewNotificationManager("") + nm.SetEmailConfig(EmailConfig{Enabled: false}) + + type apprisePayload struct { + Body string `json:"body"` + Title string `json:"title"` + Type string `json:"type"` + URLs []string `json:"urls"` + } + + requests := make(chan apprisePayload, 1) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + var payload apprisePayload + if err := json.Unmarshal(body, &payload); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + requests <- payload + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"ok": true}`)) + })) + defer server.Close() + + nm.SetAppriseConfig(AppriseConfig{ + Enabled: true, + Mode: AppriseModeHTTP, + ServerURL: server.URL, + ConfigKey: "test-key", + TimeoutSeconds: 10, + }) + + err := nm.SendTestNotification("apprise") + if err != nil { + t.Fatalf("expected no error when testing Apprise HTTP, got: %v", err) + } + + // Wait for the HTTP request + select { + case payload := <-requests: + // Verify the payload contains test alert information + if payload.Title == "" { + t.Fatalf("expected non-empty title in Apprise payload") + } + if payload.Body == "" { + t.Fatalf("expected non-empty body in Apprise payload") + } + if !strings.Contains(payload.Body, "test alert") && !strings.Contains(payload.Body, "Test Resource") { + t.Fatalf("expected test alert content in body, got: %s", payload.Body) + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout waiting for Apprise HTTP request") + } +}