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
This commit is contained in:
rcourtman 2025-11-05 18:54:18 +00:00
parent 6404b6a5fc
commit 02864f54dd
2 changed files with 147 additions and 0 deletions

View file

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

View file

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