- Refactored `SyncMonitors` method in `uptime_service.go` for better readability. - Updated unit tests for `UptimeService` to ensure proper functionality. - Introduced Playwright configuration for end-to-end testing. - Added e2e tests for WAF blocking and monitoring functionality. - Enhanced the Security page to include WAF mode and rule set selection. - Implemented tests for WAF configuration changes and validation. - Created a `.last-run.json` file to store test results.
747 lines
22 KiB
Go
747 lines
22 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"net/http/httptest"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/Wikid82/charon/backend/internal/trace"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupNotificationTestDB(t *testing.T) *gorm.DB {
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
db.AutoMigrate(&models.Notification{}, &models.NotificationProvider{})
|
|
return db
|
|
}
|
|
|
|
func TestNotificationService_Create(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
notif, err := svc.Create(models.NotificationTypeInfo, "Test", "Message")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Test", notif.Title)
|
|
assert.Equal(t, "Message", notif.Message)
|
|
assert.False(t, notif.Read)
|
|
}
|
|
|
|
func TestNotificationService_List(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
svc.Create(models.NotificationTypeInfo, "N1", "M1")
|
|
svc.Create(models.NotificationTypeInfo, "N2", "M2")
|
|
|
|
list, err := svc.List(false)
|
|
require.NoError(t, err)
|
|
assert.Len(t, list, 2)
|
|
|
|
// Mark one as read
|
|
db.Model(&models.Notification{}).Where("title = ?", "N1").Update("read", true)
|
|
|
|
listUnread, err := svc.List(true)
|
|
require.NoError(t, err)
|
|
assert.Len(t, listUnread, 1)
|
|
assert.Equal(t, "N2", listUnread[0].Title)
|
|
}
|
|
|
|
func TestNotificationService_MarkAsRead(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
notif, _ := svc.Create(models.NotificationTypeInfo, "N1", "M1")
|
|
|
|
err := svc.MarkAsRead(fmt.Sprintf("%s", notif.ID))
|
|
require.NoError(t, err)
|
|
|
|
var updated models.Notification
|
|
db.First(&updated, "id = ?", notif.ID)
|
|
assert.True(t, updated.Read)
|
|
}
|
|
|
|
func TestNotificationService_MarkAllAsRead(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
svc.Create(models.NotificationTypeInfo, "N1", "M1")
|
|
svc.Create(models.NotificationTypeInfo, "N2", "M2")
|
|
|
|
err := svc.MarkAllAsRead()
|
|
require.NoError(t, err)
|
|
|
|
var count int64
|
|
db.Model(&models.Notification{}).Where("read = ?", false).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
}
|
|
|
|
func TestNotificationService_Providers(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
// Create
|
|
provider := models.NotificationProvider{
|
|
Name: "Discord",
|
|
Type: "discord",
|
|
URL: "http://example.com",
|
|
}
|
|
err := svc.CreateProvider(&provider)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, provider.ID)
|
|
assert.Equal(t, "Discord", provider.Name)
|
|
|
|
// List
|
|
list, err := svc.ListProviders()
|
|
require.NoError(t, err)
|
|
assert.Len(t, list, 1)
|
|
|
|
// Update
|
|
provider.Name = "Discord Updated"
|
|
err = svc.UpdateProvider(&provider)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Discord Updated", provider.Name)
|
|
|
|
// Delete
|
|
err = svc.DeleteProvider(provider.ID)
|
|
require.NoError(t, err)
|
|
|
|
list, err = svc.ListProviders()
|
|
require.NoError(t, err)
|
|
assert.Len(t, list, 0)
|
|
}
|
|
|
|
func TestNotificationService_TestProvider_Webhook(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
// Start a test server
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var body map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
// Minimal template uses lowercase keys: title, message
|
|
assert.Equal(t, "Test Notification", body["title"])
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{
|
|
Name: "Test Webhook",
|
|
Type: "webhook",
|
|
URL: ts.URL,
|
|
Template: "minimal",
|
|
Config: `{"Header": "{{.Title}}"}`,
|
|
}
|
|
|
|
err := svc.TestProvider(provider)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestNotificationService_SendExternal(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
received := make(chan struct{})
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
close(received)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{
|
|
Name: "Test Webhook",
|
|
Type: "webhook",
|
|
URL: ts.URL,
|
|
Enabled: true,
|
|
NotifyProxyHosts: true,
|
|
}
|
|
svc.CreateProvider(&provider)
|
|
|
|
svc.SendExternal(context.Background(), "proxy_host", "Title", "Message", nil)
|
|
|
|
select {
|
|
case <-received:
|
|
// Success
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("Timed out waiting for webhook")
|
|
}
|
|
}
|
|
|
|
func TestNotificationService_SendExternal_MinimalVsDetailedTemplates(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
// Minimal template
|
|
rcvMinimal := make(chan map[string]interface{}, 1)
|
|
tsMin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var body map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
rcvMinimal <- body
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer tsMin.Close()
|
|
|
|
providerMin := models.NotificationProvider{
|
|
Name: "Minimal",
|
|
Type: "webhook",
|
|
URL: tsMin.URL,
|
|
Enabled: true,
|
|
NotifyUptime: true,
|
|
Template: "minimal",
|
|
}
|
|
svc.CreateProvider(&providerMin)
|
|
|
|
data := map[string]interface{}{"Title": "Min Title", "Message": "Min Message", "Time": time.Now().Format(time.RFC3339), "EventType": "uptime"}
|
|
svc.SendExternal(context.Background(), "uptime", "Min Title", "Min Message", data)
|
|
|
|
select {
|
|
case body := <-rcvMinimal:
|
|
// minimal template should contain 'title' and 'message' keys
|
|
if title, ok := body["title"].(string); ok {
|
|
assert.Equal(t, "Min Title", title)
|
|
} else {
|
|
t.Fatalf("expected title in minimal body")
|
|
}
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("Timeout waiting for minimal webhook")
|
|
}
|
|
|
|
// Detailed template
|
|
rcvDetailed := make(chan map[string]interface{}, 1)
|
|
tsDet := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var body map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
rcvDetailed <- body
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer tsDet.Close()
|
|
|
|
providerDet := models.NotificationProvider{
|
|
Name: "Detailed",
|
|
Type: "webhook",
|
|
URL: tsDet.URL,
|
|
Enabled: true,
|
|
NotifyUptime: true,
|
|
Template: "detailed",
|
|
}
|
|
svc.CreateProvider(&providerDet)
|
|
|
|
dataDet := map[string]interface{}{"Title": "Det Title", "Message": "Det Message", "Time": time.Now().Format(time.RFC3339), "EventType": "uptime", "HostName": "example-host", "HostIP": "1.2.3.4", "ServiceCount": 1, "Services": []map[string]interface{}{{"Name": "svc1"}}}
|
|
svc.SendExternal(context.Background(), "uptime", "Det Title", "Det Message", dataDet)
|
|
|
|
select {
|
|
case body := <-rcvDetailed:
|
|
// detailed template should contain 'host' and 'services'
|
|
if host, ok := body["host"].(string); ok {
|
|
assert.Equal(t, "example-host", host)
|
|
} else {
|
|
t.Fatalf("expected host in detailed body")
|
|
}
|
|
if _, ok := body["services"]; !ok {
|
|
t.Fatalf("expected services in detailed body")
|
|
}
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("Timeout waiting for detailed webhook")
|
|
}
|
|
}
|
|
|
|
func TestNotificationService_SendExternal_Filtered(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
received := make(chan struct{})
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
close(received)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{
|
|
Name: "Test Webhook",
|
|
Type: "webhook",
|
|
URL: ts.URL,
|
|
Enabled: true,
|
|
NotifyProxyHosts: false, // Disabled
|
|
}
|
|
svc.CreateProvider(&provider)
|
|
// Force update to false because GORM default tag might override zero value (false) on Create
|
|
db.Model(&provider).Update("notify_proxy_hosts", false)
|
|
|
|
svc.SendExternal(context.Background(), "proxy_host", "Title", "Message", nil)
|
|
|
|
select {
|
|
case <-received:
|
|
t.Fatal("Should not have received webhook")
|
|
case <-time.After(100 * time.Millisecond):
|
|
// Success (timeout expected)
|
|
}
|
|
}
|
|
|
|
func TestNotificationService_SendExternal_Shoutrrr(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
provider := models.NotificationProvider{
|
|
Name: "Test Discord",
|
|
Type: "discord",
|
|
URL: "discord://token@id",
|
|
Enabled: true,
|
|
NotifyProxyHosts: true,
|
|
}
|
|
svc.CreateProvider(&provider)
|
|
|
|
// This will log an error but should cover the code path
|
|
svc.SendExternal(context.Background(), "proxy_host", "Title", "Message", nil)
|
|
|
|
// Give it a moment to run goroutine
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
func TestNormalizeURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
serviceType string
|
|
rawURL string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "Discord HTTPS",
|
|
serviceType: "discord",
|
|
rawURL: "https://discord.com/api/webhooks/123456789/abcdefg",
|
|
expected: "discord://abcdefg@123456789",
|
|
},
|
|
{
|
|
name: "Discord HTTPS with app",
|
|
serviceType: "discord",
|
|
rawURL: "https://discordapp.com/api/webhooks/123456789/abcdefg",
|
|
expected: "discord://abcdefg@123456789",
|
|
},
|
|
{
|
|
name: "Discord Shoutrrr",
|
|
serviceType: "discord",
|
|
rawURL: "discord://token@id",
|
|
expected: "discord://token@id",
|
|
},
|
|
{
|
|
name: "Other Service",
|
|
serviceType: "slack",
|
|
rawURL: "https://hooks.slack.com/services/...",
|
|
expected: "https://hooks.slack.com/services/...",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := normalizeURL(tt.serviceType, tt.rawURL)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNotificationService_SendCustomWebhook_Errors(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
t.Run("invalid URL", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Type: "webhook",
|
|
URL: "://invalid-url",
|
|
}
|
|
data := map[string]interface{}{"Title": "Test", "Message": "Test Message"}
|
|
err := svc.sendCustomWebhook(context.Background(), provider, data)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("unreachable host", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Type: "webhook",
|
|
URL: "http://192.0.2.1:9999", // TEST-NET-1, unreachable
|
|
}
|
|
data := map[string]interface{}{"Title": "Test", "Message": "Test Message"}
|
|
// Set short timeout for client if possible, but here we just expect error
|
|
// Note: http.Client default timeout is 0 (no timeout), but OS might timeout
|
|
// We can't easily change client timeout here without modifying service
|
|
// So we might skip this or just check if it returns error eventually
|
|
// But for unit test speed, we should probably mock or use a closed port on localhost
|
|
// Using a closed port on localhost is faster
|
|
provider.URL = "http://127.0.0.1:54321" // Assuming this port is closed
|
|
err := svc.sendCustomWebhook(context.Background(), provider, data)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("server returns error", func(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{
|
|
Type: "webhook",
|
|
URL: ts.URL,
|
|
}
|
|
data := map[string]interface{}{"Title": "Test", "Message": "Test Message"}
|
|
err := svc.sendCustomWebhook(context.Background(), provider, data)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "500")
|
|
})
|
|
|
|
t.Run("valid custom payload template", func(t *testing.T) {
|
|
receivedBody := ""
|
|
received := make(chan struct{})
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var body map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
if custom, ok := body["custom"]; ok {
|
|
receivedBody = custom.(string)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
close(received)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{
|
|
Type: "webhook",
|
|
URL: ts.URL,
|
|
Config: `{"custom": "Test: {{.Title}}"}`,
|
|
}
|
|
data := map[string]interface{}{"Title": "My Title", "Message": "Test Message"}
|
|
svc.sendCustomWebhook(context.Background(), provider, data)
|
|
|
|
select {
|
|
case <-received:
|
|
assert.Equal(t, "Test: My Title", receivedBody)
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("Timeout waiting for webhook")
|
|
}
|
|
})
|
|
|
|
t.Run("default payload without template", func(t *testing.T) {
|
|
receivedContent := ""
|
|
received := make(chan struct{})
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var body map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
if title, ok := body["title"]; ok {
|
|
receivedContent = title.(string)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
close(received)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{
|
|
Type: "webhook",
|
|
URL: ts.URL,
|
|
// Config is empty, so default template is used: minimal
|
|
}
|
|
data := map[string]interface{}{"Title": "Default Title", "Message": "Test Message"}
|
|
svc.sendCustomWebhook(context.Background(), provider, data)
|
|
|
|
select {
|
|
case <-received:
|
|
assert.Equal(t, "Default Title", receivedContent)
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("Timeout waiting for webhook")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNotificationService_SendCustomWebhook_PropagatesRequestID(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
received := make(chan string, 1)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
received <- r.Header.Get("X-Request-ID")
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{Type: "webhook", URL: ts.URL}
|
|
data := map[string]interface{}{"Title": "Test", "Message": "Test"}
|
|
// Build context with requestID value
|
|
ctx := context.WithValue(context.Background(), trace.RequestIDKey, "my-rid")
|
|
err := svc.sendCustomWebhook(ctx, provider, data)
|
|
require.NoError(t, err)
|
|
|
|
select {
|
|
case rid := <-received:
|
|
assert.Equal(t, "my-rid", rid)
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("Timed out waiting for webhook request")
|
|
}
|
|
}
|
|
|
|
func TestNotificationService_TestProvider_Errors(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
t.Run("unsupported provider type", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Type: "unsupported",
|
|
URL: "http://example.com",
|
|
}
|
|
err := svc.TestProvider(provider)
|
|
assert.Error(t, err)
|
|
// Shoutrrr returns "unknown service" for unsupported schemes
|
|
assert.Contains(t, err.Error(), "unknown service")
|
|
})
|
|
|
|
t.Run("webhook with invalid URL", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Type: "webhook",
|
|
URL: "://invalid",
|
|
}
|
|
err := svc.TestProvider(provider)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("discord with invalid URL format", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Type: "discord",
|
|
URL: "invalid-discord-url",
|
|
}
|
|
err := svc.TestProvider(provider)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("slack with unreachable webhook", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Type: "slack",
|
|
URL: "https://hooks.slack.com/services/INVALID/WEBHOOK/URL",
|
|
}
|
|
err := svc.TestProvider(provider)
|
|
// Shoutrrr will return error for unreachable/invalid webhook
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("webhook success", func(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{
|
|
Type: "webhook",
|
|
URL: ts.URL,
|
|
}
|
|
err := svc.TestProvider(provider)
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestValidateWebhookURL_PrivateIP(t *testing.T) {
|
|
// Direct IP literal within RFC1918 block should be rejected
|
|
_, err := validateWebhookURL("http://10.0.0.1")
|
|
assert.Error(t, err)
|
|
|
|
// Loopback allowed
|
|
u, err := validateWebhookURL("http://127.0.0.1:8080")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "127.0.0.1", u.Hostname())
|
|
}
|
|
|
|
func TestNotificationService_SendExternal_EdgeCases(t *testing.T) {
|
|
t.Run("no enabled providers", func(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
provider := models.NotificationProvider{
|
|
Name: "Disabled",
|
|
Type: "webhook",
|
|
URL: "http://example.com",
|
|
Enabled: false,
|
|
}
|
|
svc.CreateProvider(&provider)
|
|
|
|
// Should complete without error
|
|
svc.SendExternal(context.Background(), "proxy_host", "Title", "Message", nil)
|
|
time.Sleep(50 * time.Millisecond)
|
|
})
|
|
|
|
t.Run("provider filtered by category", func(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Fatal("Should not call webhook")
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{
|
|
Name: "Filtered",
|
|
Type: "webhook",
|
|
URL: ts.URL,
|
|
Enabled: true,
|
|
NotifyProxyHosts: false,
|
|
NotifyUptime: false,
|
|
NotifyCerts: false,
|
|
}
|
|
// Create provider first (might get defaults)
|
|
err := db.Create(&provider).Error
|
|
require.NoError(t, err)
|
|
|
|
// Force update to false using map (to bypass zero value check)
|
|
err = db.Model(&provider).Updates(map[string]interface{}{
|
|
"notify_proxy_hosts": false,
|
|
"notify_uptime": false,
|
|
"notify_certs": false,
|
|
"notify_remote_servers": false,
|
|
"notify_domains": false,
|
|
}).Error
|
|
require.NoError(t, err)
|
|
|
|
// Verify DB state
|
|
var saved models.NotificationProvider
|
|
db.First(&saved, "id = ?", provider.ID)
|
|
require.False(t, saved.NotifyProxyHosts, "NotifyProxyHosts should be false")
|
|
require.False(t, saved.NotifyUptime, "NotifyUptime should be false")
|
|
require.False(t, saved.NotifyCerts, "NotifyCerts should be false")
|
|
|
|
svc.SendExternal(context.Background(), "proxy_host", "Title", "Message", nil)
|
|
svc.SendExternal(context.Background(), "uptime", "Title", "Message", nil)
|
|
svc.SendExternal(context.Background(), "cert", "Title", "Message", nil)
|
|
time.Sleep(50 * time.Millisecond)
|
|
})
|
|
|
|
t.Run("custom data passed to webhook", func(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
var receivedCustom atomic.Value
|
|
receivedCustom.Store("")
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var body map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
if custom, ok := body["custom"]; ok {
|
|
receivedCustom.Store(custom.(string))
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
provider := models.NotificationProvider{
|
|
Name: "Custom Data",
|
|
Type: "webhook",
|
|
URL: ts.URL,
|
|
Enabled: true,
|
|
NotifyProxyHosts: true,
|
|
Config: `{"custom": "{{.CustomField}}"}`,
|
|
}
|
|
svc.CreateProvider(&provider)
|
|
|
|
customData := map[string]interface{}{
|
|
"CustomField": "test-value",
|
|
}
|
|
svc.SendExternal(context.Background(), "proxy_host", "Title", "Message", customData)
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
assert.Equal(t, "test-value", receivedCustom.Load().(string))
|
|
})
|
|
}
|
|
|
|
func TestNotificationService_RenderTemplate(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
// Minimal template
|
|
provider := models.NotificationProvider{Type: "webhook", Template: "minimal"}
|
|
data := map[string]interface{}{"Title": "T1", "Message": "M1", "Time": time.Now().Format(time.RFC3339), "EventType": "preview"}
|
|
rendered, parsed, err := svc.RenderTemplate(provider, data)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, rendered, "T1")
|
|
if parsedMap, ok := parsed.(map[string]interface{}); ok {
|
|
assert.Equal(t, "T1", parsedMap["title"])
|
|
}
|
|
|
|
// Invalid custom template returns error
|
|
provider = models.NotificationProvider{Type: "webhook", Template: "custom", Config: `{"bad": "{{.Title"}`}
|
|
_, _, err = svc.RenderTemplate(provider, data)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestNotificationService_CreateProvider_Validation(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
t.Run("creates provider with defaults", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Name: "Test",
|
|
Type: "webhook",
|
|
URL: "http://example.com",
|
|
}
|
|
err := svc.CreateProvider(&provider)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, provider.ID)
|
|
assert.False(t, provider.Enabled) // Default
|
|
})
|
|
|
|
t.Run("updates existing provider", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Name: "Original",
|
|
Type: "webhook",
|
|
URL: "http://example.com",
|
|
Enabled: true,
|
|
}
|
|
err := svc.CreateProvider(&provider)
|
|
assert.NoError(t, err)
|
|
|
|
provider.Name = "Updated"
|
|
err = svc.UpdateProvider(&provider)
|
|
assert.NoError(t, err)
|
|
|
|
var updated models.NotificationProvider
|
|
db.First(&updated, "id = ?", provider.ID)
|
|
assert.Equal(t, "Updated", updated.Name)
|
|
})
|
|
|
|
t.Run("deletes non-existent provider", func(t *testing.T) {
|
|
err := svc.DeleteProvider("non-existent-id")
|
|
// Should not error on missing provider
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestNotificationService_CreateProvider_InvalidCustomTemplate(t *testing.T) {
|
|
db := setupNotificationTestDB(t)
|
|
svc := NewNotificationService(db)
|
|
|
|
t.Run("invalid custom template on create", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Name: "Bad Custom",
|
|
Type: "webhook",
|
|
URL: "http://example.com",
|
|
Template: "custom",
|
|
Config: `{"bad": "{{.Title"}`,
|
|
}
|
|
err := svc.CreateProvider(&provider)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("invalid custom template on update", func(t *testing.T) {
|
|
provider := models.NotificationProvider{
|
|
Name: "Valid",
|
|
Type: "webhook",
|
|
URL: "http://example.com",
|
|
Template: "minimal",
|
|
}
|
|
err := svc.CreateProvider(&provider)
|
|
require.NoError(t, err)
|
|
|
|
provider.Template = "custom"
|
|
provider.Config = `{"bad": "{{.Title"}`
|
|
err = svc.UpdateProvider(&provider)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|