Files
pulse/internal/api/alerting/notification_queue_error_test.go
T
pulse-triage[bot] 439c1a6195 fix(notifications): refresh recovery queue owner after monitor reload
Recovery handlers retained the startup monitor after router reload, unlike normal notification handlers. Stopping that monitor cleared its queue and left Retry/Dismiss returning 503 even when the replacement notifier was available. Refresh the stable handler in both replacement paths and synchronise its owner reads. Add stopped-owner regression, concurrent replacement and scope proofs; keep queue persistence semantics unchanged.

Change-source: pulse-maintainer
2026-09-08 11:37:20 +01:00

157 lines
5.1 KiB
Go

package alerting
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
)
func TestNotificationQueueHandlers_GetDLQ_MissingScope(t *testing.T) {
handler := &NotificationQueueHandlers{}
req := httptest.NewRequest(http.MethodGet, "/api/notifications/dlq", nil)
record := &config.APITokenRecord{Scopes: []string{config.ScopeMonitoringWrite}}
req = req.WithContext(internalauth.WithAPIToken(req.Context(), record))
rec := httptest.NewRecorder()
handler.GetDLQ(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("expected 403, got %d", rec.Code)
}
}
func TestNotificationQueueHandlers_GetDLQ_QueueNil(t *testing.T) {
monitor := &monitoring.Monitor{}
setUnexportedField(t, monitor, "notificationMgr", &notifications.NotificationManager{})
handler := NewNotificationQueueHandlers(monitor)
req := httptest.NewRequest(http.MethodGet, "/api/notifications/dlq", nil)
rec := httptest.NewRecorder()
handler.GetDLQ(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d", rec.Code)
}
}
func TestNotificationQueueHandlers_GetQueueStats_QueueNil(t *testing.T) {
monitor := &monitoring.Monitor{}
setUnexportedField(t, monitor, "notificationMgr", &notifications.NotificationManager{})
handler := NewNotificationQueueHandlers(monitor)
req := httptest.NewRequest(http.MethodGet, "/api/notifications/queue/stats", nil)
rec := httptest.NewRecorder()
handler.GetQueueStats(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d", rec.Code)
}
}
func TestNotificationQueueHandlers_RetryDLQItem_Errors(t *testing.T) {
handler := &NotificationQueueHandlers{}
req := httptest.NewRequest(http.MethodPost, "/api/notifications/dlq/retry", bytes.NewReader([]byte("{bad")))
rec := httptest.NewRecorder()
handler.RetryDLQItem(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
req = httptest.NewRequest(http.MethodPost, "/api/notifications/dlq/retry", bytes.NewReader([]byte(`{"id":""}`)))
rec = httptest.NewRecorder()
handler.RetryDLQItem(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
}
func TestNotificationQueueHandlers_RetryDLQItem_QueueNil(t *testing.T) {
monitor := &monitoring.Monitor{}
setUnexportedField(t, monitor, "notificationMgr", &notifications.NotificationManager{})
handler := NewNotificationQueueHandlers(monitor)
req := httptest.NewRequest(http.MethodPost, "/api/notifications/dlq/retry", bytes.NewReader([]byte(`{"id":"missing"}`)))
rec := httptest.NewRecorder()
handler.RetryDLQItem(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d", rec.Code)
}
}
func TestNotificationQueueHandlers_DeleteDLQItem_Errors(t *testing.T) {
handler := &NotificationQueueHandlers{}
req := httptest.NewRequest(http.MethodPost, "/api/notifications/dlq/delete", bytes.NewReader([]byte("{bad")))
rec := httptest.NewRecorder()
handler.DeleteDLQItem(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
req = httptest.NewRequest(http.MethodPost, "/api/notifications/dlq/delete", bytes.NewReader([]byte(`{"id":""}`)))
rec = httptest.NewRecorder()
handler.DeleteDLQItem(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
}
func TestNotificationQueueHandlers_DeleteDLQItem_QueueNil(t *testing.T) {
monitor := &monitoring.Monitor{}
setUnexportedField(t, monitor, "notificationMgr", &notifications.NotificationManager{})
handler := NewNotificationQueueHandlers(monitor)
req := httptest.NewRequest(http.MethodPost, "/api/notifications/dlq/delete", bytes.NewReader([]byte(`{"id":"missing"}`)))
rec := httptest.NewRecorder()
handler.DeleteDLQItem(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d", rec.Code)
}
}
func TestNotificationQueueHandlers_GetDLQ_InvalidLimit(t *testing.T) {
handler, _ := newNotificationQueueHandlers(t)
req := httptest.NewRequest(http.MethodGet, "/api/notifications/dlq?limit=invalid", nil)
rec := httptest.NewRecorder()
handler.GetDLQ(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
var dlq []notifications.QueuedNotification
if err := json.Unmarshal(rec.Body.Bytes(), &dlq); err != nil {
t.Fatalf("decode dlq: %v", err)
}
}
func TestNotificationQueueHandlers_MonitorReplacementConcurrent(t *testing.T) {
h := NewNotificationQueueHandlers(nil)
m := &monitoring.Monitor{}
done := make(chan struct{})
go func() {
defer close(done)
for i := 0; i < 100; i++ {
h.SetMonitor(m)
h.SetMonitor(nil)
}
}()
for i := 0; i < 100; i++ {
rec := httptest.NewRecorder()
h.GetQueueStats(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("unavailable monitor: got %d", rec.Code)
}
}
<-done
}