143 lines
5.7 KiB
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{}
|
|
}
|
|
}
|