Files
rmm-openwrt/server/internal/httpapi/notifications_test.go

143 lines
5.7 KiB
Go

package httpapi_test
import (
"context"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"rmm-openwrt/server/internal/httpapi"
"rmm-openwrt/server/internal/model"
"rmm-openwrt/server/internal/store"
)
type capturedNotification struct {
destination string
title string
body string
}
type captureNotificationSender struct {
messages chan capturedNotification
}
func (s *captureNotificationSender) SendNotification(_ context.Context, destination, title, body string) error {
s.messages <- capturedNotification{destination: destination, title: title, body: body}
return nil
}
func TestNotificationSettingsTestAndAlertLifecycle(t *testing.T) {
st, err := store.OpenSQLite(context.Background(), filepath.Join(t.TempDir(), "notifications.db"))
if err != nil {
t.Fatal(err)
}
defer st.Close()
sender := &captureNotificationSender{messages: make(chan capturedNotification, 8)}
srv := httptest.NewServer(httpapi.NewHandler(st, httpapi.Config{
OperatorUsername: "admin",
OperatorPassword: "notification-password-123",
PublicURL: "https://rmm.example.test",
AlertEmailSender: sender,
}))
defer srv.Close()
client := authenticatedClient(t, srv.URL, "admin", "notification-password-123")
authRequestJSON(t, client, http.MethodPatch, srv.URL+"/api/auth/profile", map[string]any{
"display_name": "Owner", "email": "owner@example.test",
}, http.StatusOK, nil)
var defaults struct {
Settings model.NotificationSettings `json:"settings"`
}
authRequestJSON(t, client, http.MethodGet, srv.URL+"/api/notifications/settings", nil, http.StatusOK, &defaults)
if defaults.Settings.Configured || defaults.Settings.MemoryThresholdPercent != 85 {
t.Fatalf("unexpected defaults: %#v", defaults.Settings)
}
var saved struct {
Settings model.NotificationSettings `json:"settings"`
}
authRequestJSON(t, client, http.MethodPut, srv.URL+"/api/notifications/settings", map[string]any{
"email_enabled": true, "telegram_enabled": false, "notify_warning": true,
"notify_critical": true, "notify_resolved": true, "memory_threshold_percent": 80,
"disk_threshold_percent": 90, "packet_loss_percent": 25, "latency_threshold_ms": 250,
"repeat_minutes": 0,
}, http.StatusOK, &saved)
if !saved.Settings.Configured || !saved.Settings.EmailEnabled || saved.Settings.MemoryThresholdPercent != 80 {
t.Fatalf("settings were not saved: %#v", saved.Settings)
}
authRequestJSON(t, client, http.MethodPost, srv.URL+"/api/notifications/test", nil, http.StatusOK, nil)
testMessage := waitNotification(t, sender.messages)
if testMessage.destination != "owner@example.test" || testMessage.title == "" || testMessage.body == "" {
t.Fatalf("unexpected test notification: %#v", testMessage)
}
enrolled := enrollForClient(t, srv.URL, client, "notify-router", "notify-router")
requestJSON(t, http.MethodPost, srv.URL+"/api/agent/heartbeat", enrolled.DeviceToken, map[string]any{
"device_id": enrolled.DeviceID,
"inventory": map[string]any{"hostname": "notify-router"},
"metrics": map[string]any{"memory": map[string]any{"used_kb": 900, "total_kb": 1000}},
}, http.StatusOK, nil)
activeMessage := waitNotification(t, sender.messages)
if activeMessage.title == "" {
t.Fatal("active alert notification was empty")
}
requestJSON(t, http.MethodPost, srv.URL+"/api/agent/heartbeat", enrolled.DeviceToken, map[string]any{
"device_id": enrolled.DeviceID,
"inventory": map[string]any{"hostname": "notify-router"},
"metrics": map[string]any{"memory": map[string]any{"used_kb": 100, "total_kb": 1000}},
}, http.StatusOK, nil)
resolvedMessage := waitNotification(t, sender.messages)
if resolvedMessage.title == activeMessage.title {
t.Fatalf("resolved notification did not change state: active=%q resolved=%q", activeMessage.title, resolvedMessage.title)
}
var history struct {
Notifications []model.NotificationDelivery `json:"notifications"`
}
authRequestJSON(t, client, http.MethodGet, srv.URL+"/api/notifications", nil, http.StatusOK, &history)
if len(history.Notifications) < 3 {
t.Fatalf("expected test, active and resolved deliveries, got %#v", history.Notifications)
}
for _, delivery := range history.Notifications {
if delivery.Destination != "" {
t.Fatal("raw notification destination leaked through API")
}
}
}
func TestNotificationSettingsValidation(t *testing.T) {
st, err := store.OpenSQLite(context.Background(), filepath.Join(t.TempDir(), "notification-validation.db"))
if err != nil {
t.Fatal(err)
}
defer st.Close()
srv := httptest.NewServer(httpapi.NewHandler(st, httpapi.Config{OperatorUsername: "admin", OperatorPassword: "notification-password-123"}))
defer srv.Close()
client := authenticatedClient(t, srv.URL, "admin", "notification-password-123")
authRequestJSON(t, client, http.MethodPut, srv.URL+"/api/notifications/settings", map[string]any{
"email_enabled": true, "notify_warning": true, "notify_critical": true, "notify_resolved": true,
"memory_threshold_percent": 85, "disk_threshold_percent": 85, "packet_loss_percent": 20,
"latency_threshold_ms": 200, "repeat_minutes": 0,
}, http.StatusBadRequest, nil)
authRequestJSON(t, client, http.MethodPut, srv.URL+"/api/notifications/settings", map[string]any{
"telegram_enabled": true, "telegram_chat_id": "not-a-chat", "notify_warning": true,
"notify_critical": true, "notify_resolved": true, "memory_threshold_percent": 85,
"disk_threshold_percent": 85, "packet_loss_percent": 20, "latency_threshold_ms": 200,
"repeat_minutes": 0,
}, http.StatusBadRequest, nil)
}
func waitNotification(t *testing.T, messages <-chan capturedNotification) capturedNotification {
t.Helper()
select {
case message := <-messages:
return message
case <-time.After(2 * time.Second):
t.Fatal("notification was not delivered")
return capturedNotification{}
}
}