server/internal/handlers/admin_notifications_test.go
Rama main · réplica de lectura del repositorio original.
package handlers
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"blackbox/server/internal/auth"
dbpkg "blackbox/server/internal/db"
"blackbox/server/internal/models"
"blackbox/server/internal/notify"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func TestListNotificationDests_Empty(t *testing.T) {
database := newNotificationTestDB(t)
req := adminNotificationRequest(http.MethodGet, "/api/admin/notifications", nil)
w := httptest.NewRecorder()
ListNotificationDests(database)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var result []models.NotificationDest
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
assert.Empty(t, result)
}
func TestCreateNotificationDest_Valid(t *testing.T) {
database := newNotificationTestDB(t)
body, err := json.Marshal(map[string]interface{}{
"name": "Discord #alerts",
"type": "discord",
"url": "https://discord.com/api/webhooks/1234/abcd",
"events": []string{notify.EventIncidentOpenedConfirmed, notify.EventIncidentResolved},
"enabled": true,
})
require.NoError(t, err)
req := adminNotificationRequest(http.MethodPost, "/api/admin/notifications", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
CreateNotificationDest(database)(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var dest models.NotificationDest
require.NoError(t, json.NewDecoder(w.Body).Decode(&dest))
assert.Equal(t, "Discord #alerts", dest.Name)
assert.Equal(t, "discord", dest.Type)
assert.NotEmpty(t, dest.ID)
}
func TestCreateNotificationDest_InvalidType(t *testing.T) {
database := newNotificationTestDB(t)
body, err := json.Marshal(map[string]interface{}{
"name": "Bad",
"type": "telegram",
"url": "https://example.com",
"events": []string{notify.EventIncidentResolved},
})
require.NoError(t, err)
req := adminNotificationRequest(http.MethodPost, "/api/admin/notifications", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
CreateNotificationDest(database)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestCreateNotificationDest_InvalidURL(t *testing.T) {
database := newNotificationTestDB(t)
body, err := json.Marshal(map[string]interface{}{
"name": "Bad",
"type": "discord",
"url": "not-a-url",
"events": []string{notify.EventIncidentResolved},
})
require.NoError(t, err)
req := adminNotificationRequest(http.MethodPost, "/api/admin/notifications", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
CreateNotificationDest(database)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestCreateNotificationDest_NoEvents(t *testing.T) {
database := newNotificationTestDB(t)
body, err := json.Marshal(map[string]interface{}{
"name": "Bad",
"type": "discord",
"url": "https://example.com/webhook",
"events": []string{},
})
require.NoError(t, err)
req := adminNotificationRequest(http.MethodPost, "/api/admin/notifications", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
CreateNotificationDest(database)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestUpdateNotificationDest_Valid(t *testing.T) {
database := newNotificationTestDB(t)
require.NoError(t, database.Create(&models.NotificationDest{
ID: "dest-upd",
Name: "Old",
Type: "discord",
URL: "https://discord.com/api/webhooks/1/a",
Events: `["incident_resolved"]`,
Enabled: false,
}).Error)
body, err := json.Marshal(map[string]interface{}{
"name": "New Name",
"type": "slack",
"url": "https://hooks.slack.com/services/T/B/X",
"events": []string{notify.EventIncidentResolved},
"enabled": true,
})
require.NoError(t, err)
req := adminNotificationRequest(http.MethodPut, "/api/admin/notifications/dest-upd", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withNotificationRouteParam(req, "id", "dest-upd")
w := httptest.NewRecorder()
UpdateNotificationDest(database)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var dest models.NotificationDest
require.NoError(t, json.NewDecoder(w.Body).Decode(&dest))
assert.Equal(t, "New Name", dest.Name)
assert.Equal(t, "slack", dest.Type)
assert.True(t, dest.Enabled)
}
func TestUpdateNotificationDest_NotFound(t *testing.T) {
database := newNotificationTestDB(t)
body, err := json.Marshal(map[string]interface{}{
"name": "X",
"type": "discord",
"url": "https://discord.com/api/webhooks/1/a",
"events": []string{notify.EventIncidentResolved},
})
require.NoError(t, err)
req := adminNotificationRequest(http.MethodPut, "/api/admin/notifications/nonexistent", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withNotificationRouteParam(req, "id", "nonexistent")
w := httptest.NewRecorder()
UpdateNotificationDest(database)(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestDeleteNotificationDest_Valid(t *testing.T) {
database := newNotificationTestDB(t)
require.NoError(t, database.Create(&models.NotificationDest{
ID: "dest-del",
Name: "To Delete",
Type: "ntfy",
URL: "https://ntfy.sh/mytopic",
Events: `["incident_resolved"]`,
Enabled: true,
}).Error)
req := adminNotificationRequest(http.MethodDelete, "/api/admin/notifications/dest-del", nil)
req = withNotificationRouteParam(req, "id", "dest-del")
w := httptest.NewRecorder()
DeleteNotificationDest(database)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
}
func TestDeleteNotificationDest_NotFound(t *testing.T) {
database := newNotificationTestDB(t)
req := adminNotificationRequest(http.MethodDelete, "/api/admin/notifications/nope", nil)
req = withNotificationRouteParam(req, "id", "nope")
w := httptest.NewRecorder()
DeleteNotificationDest(database)(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestTestNotificationDest_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
database := newNotificationTestDB(t)
require.NoError(t, database.Create(&models.NotificationDest{
ID: "dest-tst",
Name: "Test",
Type: "discord",
URL: srv.URL,
Events: `["incident_resolved"]`,
Enabled: true,
}).Error)
req := adminNotificationRequest(http.MethodPost, "/api/admin/notifications/dest-tst/test", nil)
req = withNotificationRouteParam(req, "id", "dest-tst")
w := httptest.NewRecorder()
TestNotificationDest(database, notify.NewDispatcher(database))(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, true, resp["ok"])
}
func TestTestNotificationDest_Failure(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer srv.Close()
database := newNotificationTestDB(t)
require.NoError(t, database.Create(&models.NotificationDest{
ID: "dest-tst2",
Name: "Failing",
Type: "discord",
URL: srv.URL,
Events: `["incident_resolved"]`,
Enabled: true,
}).Error)
req := adminNotificationRequest(http.MethodPost, "/api/admin/notifications/dest-tst2/test", nil)
req = withNotificationRouteParam(req, "id", "dest-tst2")
w := httptest.NewRecorder()
TestNotificationDest(database, notify.NewDispatcher(database))(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, false, resp["ok"])
assert.NotEmpty(t, resp["error"])
}
func TestValidatePolicyFields(t *testing.T) {
bad := []struct {
name string
p policyInput
}{
{"bad start", policyInput{QuietHoursEnabled: true, QuietHoursStart: "9am", QuietHoursEnd: "07:00", QuietHoursMode: "drop"}},
{"bad mode", policyInput{QuietHoursEnabled: true, QuietHoursStart: "22:00", QuietHoursEnd: "07:00", QuietHoursMode: "mute"}},
{"rate count zero", policyInput{RateLimitEnabled: true, RateLimitCount: 0, RateLimitUnit: "hour"}},
{"rate bad unit", policyInput{RateLimitEnabled: true, RateLimitCount: 3, RateLimitUnit: "week"}},
}
for _, c := range bad {
t.Run(c.name, func(t *testing.T) {
if err := validatePolicy(c.p); err == nil {
t.Fatalf("expected error for %s", c.name)
}
})
}
ok := policyInput{
QuietHoursEnabled: true, QuietHoursStart: "22:00", QuietHoursEnd: "07:00", QuietHoursMode: "defer",
RateLimitEnabled: true, RateLimitCount: 5, RateLimitUnit: "day",
}
if err := validatePolicy(ok); err != nil {
t.Fatalf("expected valid, got %v", err)
}
}
func TestCreateNotificationDest_WithPolicy(t *testing.T) {
database := newNotificationTestDB(t)
body, err := json.Marshal(map[string]interface{}{
"name": "Phone push",
"type": "ntfy",
"url": "https://ntfy.sh/mytopic",
"events": []string{notify.EventIncidentOpenedConfirmed},
"enabled": true,
"quiet_hours_enabled": true,
"quiet_hours_start": "22:00",
"quiet_hours_end": "07:00",
"quiet_hours_mode": "defer",
"rate_limit_enabled": true,
"rate_limit_count": 5,
"rate_limit_unit": "hour",
})
require.NoError(t, err)
req := adminNotificationRequest(http.MethodPost, "/api/admin/notifications", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
CreateNotificationDest(database)(w, req)
require.Equal(t, http.StatusCreated, w.Code)
var dest models.NotificationDest
require.NoError(t, json.NewDecoder(w.Body).Decode(&dest))
assert.True(t, dest.QuietHoursEnabled)
assert.Equal(t, "defer", dest.QuietHoursMode)
assert.Equal(t, "22:00", dest.QuietHoursStart)
assert.True(t, dest.RateLimitEnabled)
assert.Equal(t, 5, dest.RateLimitCount)
assert.Equal(t, "hour", dest.RateLimitUnit)
}
func TestCreateNotificationDest_InvalidPolicy(t *testing.T) {
database := newNotificationTestDB(t)
body, err := json.Marshal(map[string]interface{}{
"name": "Bad policy",
"type": "ntfy",
"url": "https://ntfy.sh/mytopic",
"events": []string{notify.EventIncidentOpenedConfirmed},
"enabled": true,
"quiet_hours_enabled": true,
"quiet_hours_start": "9am",
"quiet_hours_end": "07:00",
"quiet_hours_mode": "drop",
})
require.NoError(t, err)
req := adminNotificationRequest(http.MethodPost, "/api/admin/notifications", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
CreateNotificationDest(database)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestUpdateNotificationDest_WithPolicy(t *testing.T) {
database := newNotificationTestDB(t)
require.NoError(t, database.Create(&models.NotificationDest{
ID: "dest-pol",
Name: "Phone",
Type: "ntfy",
URL: "https://ntfy.sh/mytopic",
Events: `["incident_opened_confirmed"]`,
Enabled: true,
}).Error)
body, err := json.Marshal(map[string]interface{}{
"name": "Phone",
"type": "ntfy",
"url": "https://ntfy.sh/mytopic",
"events": []string{notify.EventIncidentOpenedConfirmed},
"enabled": true,
"quiet_hours_enabled": true,
"quiet_hours_start": "23:30",
"quiet_hours_end": "06:15",
"quiet_hours_mode": "defer",
"rate_limit_enabled": true,
"rate_limit_count": 3,
"rate_limit_unit": "day",
})
require.NoError(t, err)
req := adminNotificationRequest(http.MethodPut, "/api/admin/notifications/dest-pol", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withNotificationRouteParam(req, "id", "dest-pol")
w := httptest.NewRecorder()
UpdateNotificationDest(database)(w, req)
require.Equal(t, http.StatusOK, w.Code)
var dest models.NotificationDest
require.NoError(t, json.NewDecoder(w.Body).Decode(&dest))
assert.True(t, dest.QuietHoursEnabled)
assert.Equal(t, "23:30", dest.QuietHoursStart)
assert.Equal(t, "06:15", dest.QuietHoursEnd)
assert.Equal(t, "defer", dest.QuietHoursMode)
assert.True(t, dest.RateLimitEnabled)
assert.Equal(t, 3, dest.RateLimitCount)
assert.Equal(t, "day", dest.RateLimitUnit)
}
func newNotificationTestDB(t *testing.T) *gorm.DB {
t.Helper()
database, err := dbpkg.Init(":memory:")
require.NoError(t, err)
t.Cleanup(func() {
sqlDB, err := database.DB()
require.NoError(t, err)
require.NoError(t, sqlDB.Close())
})
return database
}
func adminNotificationRequest(method, target string, body *bytes.Reader) *http.Request {
var req *http.Request
if body == nil {
req = httptest.NewRequest(method, target, nil)
} else {
req = httptest.NewRequest(method, target, body)
}
return req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
}
func withNotificationRouteParam(req *http.Request, key, value string) *http.Request {
rctx := chi.NewRouteContext()
rctx.URLParams.Add(key, value)
return req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
}