Ctrl K
ComunidadGuías…

server/internal/handlers/admin_notifications_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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))
}