Files
Charon/backend/internal/services/enhanced_security_notification_service_test.go
GitHub Actions f5db7ad0e4 chore: Enhance backend test coverage and add new functional tests for Security page
- Added tests to `proxyhost_service_validation_test.go` to validate fallback parsing and handle invalid hostname characters.
- Introduced new tests for DNS challenge validation in `proxyhost_service_validation_test.go`.
- Updated `current_spec.md` to reflect changes in testing strategy and coverage goals for PR #729.
- Enhanced `Security.functional.test.tsx` to include navigation test for Notifications button.
- Mocked `useNavigate` from `react-router-dom` to verify navigation behavior in Security page tests.
2026-02-22 17:12:17 +00:00

976 lines
28 KiB
Go

package services
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func setupEnhancedServiceDB(t *testing.T) *gorm.DB {
db := setupTestDB(t)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.NotificationConfig{}, &models.Setting{}))
return db
}
func TestNewEnhancedSecurityNotificationService(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
assert.NotNil(t, service)
assert.Equal(t, db, service.db)
}
func TestGetSettings_FeatureFlagDisabled_ReturnsLegacyConfig(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Set feature flag to false
require.NoError(t, db.Create(&models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "false",
Type: "bool",
}).Error)
// Create legacy config
legacyConfig := &models.NotificationConfig{
NotifyWAFBlocks: true,
NotifyACLDenies: false,
NotifyRateLimitHits: true,
WebhookURL: "https://example.com/webhook",
}
require.NoError(t, db.Create(legacyConfig).Error)
// Test
config, err := service.GetSettings()
require.NoError(t, err)
assert.True(t, config.NotifyWAFBlocks)
assert.False(t, config.NotifyACLDenies)
assert.True(t, config.NotifyRateLimitHits)
assert.Equal(t, "https://example.com/webhook", config.WebhookURL)
}
func TestGetSettings_FeatureFlagEnabled_ReturnsAggregatedConfig(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Set feature flag to true
require.NoError(t, db.Create(&models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "true",
Type: "bool",
}).Error)
// Create providers with different event types enabled
providers := []models.NotificationProvider{
{
ID: "p1",
Type: "discord",
Enabled: true,
NotifySecurityWAFBlocks: true,
NotifySecurityACLDenies: false,
NotifySecurityRateLimitHits: false,
URL: "https://discord.com/webhook/1",
},
{
ID: "p2",
Type: "discord",
Enabled: true,
NotifySecurityWAFBlocks: false,
NotifySecurityACLDenies: true,
NotifySecurityRateLimitHits: true,
URL: "https://discord.com/webhook/2",
},
}
for _, p := range providers {
require.NoError(t, db.Create(&p).Error)
}
// Test
config, err := service.GetSettings()
require.NoError(t, err)
// OR aggregation: at least one provider has each flag true
assert.True(t, config.NotifyWAFBlocks)
assert.True(t, config.NotifyACLDenies)
assert.True(t, config.NotifyRateLimitHits)
}
func TestGetProviderAggregatedConfig_ORSemantics(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Create providers where different providers have different flags
providers := []models.NotificationProvider{
{ID: "p1", Type: "discord", Enabled: true, NotifySecurityWAFBlocks: true, NotifySecurityACLDenies: false, NotifySecurityRateLimitHits: false},
{ID: "p2", Type: "discord", Enabled: true, NotifySecurityWAFBlocks: false, NotifySecurityACLDenies: true, NotifySecurityRateLimitHits: false},
{ID: "p3", Type: "discord", Enabled: true, NotifySecurityWAFBlocks: false, NotifySecurityACLDenies: false, NotifySecurityRateLimitHits: true},
}
for _, p := range providers {
require.NoError(t, db.Create(&p).Error)
}
// Test
config, err := service.getProviderAggregatedConfig()
require.NoError(t, err)
assert.True(t, config.NotifyWAFBlocks, "OR: p1 has WAF=true")
assert.True(t, config.NotifyACLDenies, "OR: p2 has ACL=true")
assert.True(t, config.NotifyRateLimitHits, "OR: p3 has RateLimit=true")
}
func TestGetProviderAggregatedConfig_FiltersSupportedTypes(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Create providers with both supported and unsupported types
providers := []models.NotificationProvider{
{ID: "discord", Type: "discord", Enabled: true, NotifySecurityWAFBlocks: true},
{ID: "webhook", Type: "webhook", Enabled: true, NotifySecurityWAFBlocks: true},
{ID: "slack", Type: "slack", Enabled: true, NotifySecurityACLDenies: true},
{ID: "gotify", Type: "gotify", Enabled: true, NotifySecurityRateLimitHits: true},
{ID: "unsupported", Type: "telegram", Enabled: true, NotifySecurityWAFBlocks: true}, // Should be filtered
}
for _, p := range providers {
require.NoError(t, db.Create(&p).Error)
}
// Test
config, err := service.getProviderAggregatedConfig()
require.NoError(t, err)
// Telegram is unsupported, so it shouldn't contribute to aggregation
assert.True(t, config.NotifyWAFBlocks, "Discord and webhook have WAF=true")
assert.True(t, config.NotifyACLDenies, "Slack has ACL=true")
assert.True(t, config.NotifyRateLimitHits, "Gotify has RateLimit=true")
}
func TestGetProviderAggregatedConfig_DestinationReporting_SingleManaged(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
testCases := []struct {
name string
providerType string
url string
expectedField string
}{
{
name: "webhook",
providerType: "webhook",
url: "https://example.com/webhook",
expectedField: "WebhookURL",
},
{
name: "discord",
providerType: "discord",
url: "https://discord.com/webhook/123",
expectedField: "DiscordWebhookURL",
},
{
name: "slack",
providerType: "slack",
url: "https://hooks.slack.com/services/T/B/X",
expectedField: "SlackWebhookURL",
},
{
name: "gotify",
providerType: "gotify",
url: "https://gotify.example.com",
expectedField: "GotifyURL",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Clean up
db.Exec("DELETE FROM notification_providers")
// Create single managed provider
provider := models.NotificationProvider{
ID: "managed",
Type: tc.providerType,
URL: tc.url,
Enabled: true,
ManagedLegacySecurity: true,
}
require.NoError(t, db.Create(&provider).Error)
// Test
config, err := service.getProviderAggregatedConfig()
require.NoError(t, err)
assert.False(t, config.DestinationAmbiguous, "Single managed provider = not ambiguous")
// Verify correct field is populated
switch tc.expectedField {
case "WebhookURL":
assert.Equal(t, tc.url, config.WebhookURL)
case "DiscordWebhookURL":
assert.Equal(t, tc.url, config.DiscordWebhookURL)
case "SlackWebhookURL":
assert.Equal(t, tc.url, config.SlackWebhookURL)
case "GotifyURL":
assert.Equal(t, tc.url, config.GotifyURL)
}
})
}
}
func TestGetProviderAggregatedConfig_DestinationReporting_MultipleManaged_Ambiguous(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Create multiple managed providers
providers := []models.NotificationProvider{
{ID: "m1", Type: "discord", URL: "https://discord.com/webhook/1", Enabled: true, ManagedLegacySecurity: true},
{ID: "m2", Type: "discord", URL: "https://discord.com/webhook/2", Enabled: true, ManagedLegacySecurity: true},
}
for _, p := range providers {
require.NoError(t, db.Create(&p).Error)
}
// Test
config, err := service.getProviderAggregatedConfig()
require.NoError(t, err)
assert.True(t, config.DestinationAmbiguous, "Multiple managed providers = ambiguous")
assert.Empty(t, config.WebhookURL)
assert.Empty(t, config.DiscordWebhookURL)
assert.Empty(t, config.SlackWebhookURL)
assert.Empty(t, config.GotifyURL)
}
func TestGetProviderAggregatedConfig_DestinationReporting_ZeroManaged_Ambiguous(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Create provider without managed flag
provider := models.NotificationProvider{
ID: "unmanaged",
Type: "discord",
URL: "https://discord.com/webhook/1",
Enabled: true,
ManagedLegacySecurity: false,
}
require.NoError(t, db.Create(&provider).Error)
// Test
config, err := service.getProviderAggregatedConfig()
require.NoError(t, err)
assert.True(t, config.DestinationAmbiguous, "Zero managed providers = ambiguous")
}
func TestGetSettings_LegacyConfig_NotFound_ReturnsDefaults(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Set feature flag to false to use legacy path
require.NoError(t, db.Create(&models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "false",
Type: "bool",
}).Error)
// Don't create legacy config
// Test
config, err := service.GetSettings()
require.NoError(t, err)
assert.True(t, config.NotifyWAFBlocks, "Default WAF=true")
assert.True(t, config.NotifyACLDenies, "Default ACL=true")
assert.True(t, config.NotifyRateLimitHits, "Default RateLimit=true")
}
func TestUpdateSettings_FeatureFlagDisabled_UpdatesLegacyConfig(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Set feature flag to false
require.NoError(t, db.Create(&models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "false",
Type: "bool",
}).Error)
// Update
req := &models.NotificationConfig{
NotifyWAFBlocks: false,
NotifyACLDenies: true,
NotifyRateLimitHits: false,
WebhookURL: "https://updated.com/webhook",
}
err := service.UpdateSettings(req)
require.NoError(t, err)
// Verify
var saved models.NotificationConfig
require.NoError(t, db.First(&saved).Error)
assert.False(t, saved.NotifyWAFBlocks)
assert.True(t, saved.NotifyACLDenies)
assert.False(t, saved.NotifyRateLimitHits)
assert.Equal(t, "https://updated.com/webhook", saved.WebhookURL)
}
func TestUpdateSettings_FeatureFlagEnabled_UpdatesManagedProviders(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Set feature flag to true
require.NoError(t, db.Create(&models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "true",
Type: "bool",
}).Error)
// Create managed provider
provider := models.NotificationProvider{
ID: "managed",
Type: "discord",
URL: "https://discord.com/webhook/old",
Enabled: true,
ManagedLegacySecurity: true,
NotifySecurityWAFBlocks: true,
NotifySecurityACLDenies: true,
NotifySecurityRateLimitHits: true,
}
require.NoError(t, db.Create(&provider).Error)
// Update
req := &models.NotificationConfig{
NotifyWAFBlocks: false,
NotifyACLDenies: true,
NotifyRateLimitHits: false,
DiscordWebhookURL: "https://discord.com/webhook/new",
}
err := service.UpdateSettings(req)
require.NoError(t, err)
// Verify
var updated models.NotificationProvider
require.NoError(t, db.First(&updated, "id = ?", "managed").Error)
assert.False(t, updated.NotifySecurityWAFBlocks)
assert.True(t, updated.NotifySecurityACLDenies)
assert.False(t, updated.NotifySecurityRateLimitHits)
assert.Equal(t, "https://discord.com/webhook/new", updated.URL)
assert.Equal(t, "discord", updated.Type)
}
func TestUpdateManagedProviders_CreatesProviderIfNoneExist(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// No existing providers
// Update
req := &models.NotificationConfig{
NotifyWAFBlocks: true,
NotifyACLDenies: false,
DiscordWebhookURL: "https://discord.com/webhook/1",
}
err := service.updateManagedProviders(req)
require.NoError(t, err)
// Verify
var providers []models.NotificationProvider
require.NoError(t, db.Find(&providers).Error)
require.Len(t, providers, 1)
assert.Equal(t, "discord", providers[0].Type)
assert.Equal(t, "https://discord.com/webhook/1", providers[0].URL)
assert.True(t, providers[0].ManagedLegacySecurity)
assert.True(t, providers[0].NotifySecurityWAFBlocks)
assert.False(t, providers[0].NotifySecurityACLDenies)
}
func TestUpdateManagedProviders_RejectsMultipleDestinations(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Try to set multiple destinations
req := &models.NotificationConfig{
WebhookURL: "https://example.com/webhook",
DiscordWebhookURL: "https://discord.com/webhook/1",
}
err := service.updateManagedProviders(req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "ambiguous destination")
}
func TestUpdateManagedProviders_GotifyValidation_RequiresBothURLAndToken(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
testCases := []struct {
name string
gotifyURL string
gotifyToken string
expectError bool
}{
{
name: "both_present",
gotifyURL: "https://gotify.example.com",
gotifyToken: "token123",
expectError: false,
},
{
name: "only_url",
gotifyURL: "https://gotify.example.com",
gotifyToken: "",
expectError: true,
},
{
name: "only_token",
gotifyURL: "",
gotifyToken: "token123",
expectError: true,
},
{
name: "both_empty",
gotifyURL: "",
gotifyToken: "",
expectError: false, // No gotify config = valid
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := &models.NotificationConfig{
GotifyURL: tc.gotifyURL,
GotifyToken: tc.gotifyToken,
}
err := service.updateManagedProviders(req)
if tc.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), "incomplete gotify configuration")
} else {
assert.NoError(t, err)
}
})
}
}
func TestUpdateManagedProviders_Idempotency_NoUpdateIfNoChange(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Create managed provider
initialTime := time.Now().Add(-1 * time.Hour)
provider := models.NotificationProvider{
ID: "managed",
Type: "discord",
URL: "https://discord.com/webhook/1",
Enabled: true,
ManagedLegacySecurity: true,
NotifySecurityWAFBlocks: true,
NotifySecurityACLDenies: false,
NotifySecurityRateLimitHits: true,
}
provider.CreatedAt = initialTime
provider.UpdatedAt = initialTime
require.NoError(t, db.Create(&provider).Error)
// Update with same values
req := &models.NotificationConfig{
NotifyWAFBlocks: true,
NotifyACLDenies: false,
NotifyRateLimitHits: true,
DiscordWebhookURL: "https://discord.com/webhook/1",
}
err := service.updateManagedProviders(req)
require.NoError(t, err)
// Verify UpdatedAt didn't change
var updated models.NotificationProvider
require.NoError(t, db.First(&updated, "id = ?", "managed").Error)
assert.Equal(t, initialTime.Unix(), updated.UpdatedAt.Unix(), "UpdatedAt should not change if values unchanged")
}
func TestExtractDestinationURL(t *testing.T) {
service := &EnhancedSecurityNotificationService{}
testCases := []struct {
name string
req *models.NotificationConfig
expected string
}{
{
name: "webhook",
req: &models.NotificationConfig{WebhookURL: "https://example.com/webhook"},
expected: "https://example.com/webhook",
},
{
name: "discord",
req: &models.NotificationConfig{DiscordWebhookURL: "https://discord.com/webhook/1"},
expected: "https://discord.com/webhook/1",
},
{
name: "slack",
req: &models.NotificationConfig{SlackWebhookURL: "https://hooks.slack.com/services/T/B/X"},
expected: "https://hooks.slack.com/services/T/B/X",
},
{
name: "gotify",
req: &models.NotificationConfig{GotifyURL: "https://gotify.example.com"},
expected: "https://gotify.example.com",
},
{
name: "empty",
req: &models.NotificationConfig{},
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := service.extractDestinationURL(tc.req)
assert.Equal(t, tc.expected, result)
})
}
}
func TestExtractDestinationToken(t *testing.T) {
service := &EnhancedSecurityNotificationService{}
testCases := []struct {
name string
req *models.NotificationConfig
expected string
}{
{
name: "gotify_token",
req: &models.NotificationConfig{GotifyToken: "token123"},
expected: "token123",
},
{
name: "empty",
req: &models.NotificationConfig{},
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := service.extractDestinationToken(tc.req)
assert.Equal(t, tc.expected, result)
})
}
}
func TestMigrateFromLegacyConfig_FeatureFlagDisabled_ReadOnlyMode(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Set feature flag to false
require.NoError(t, db.Create(&models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "false",
Type: "bool",
}).Error)
// Create legacy config
legacyConfig := models.NotificationConfig{
NotifyWAFBlocks: true,
WebhookURL: "https://example.com/webhook",
}
require.NoError(t, db.Create(&legacyConfig).Error)
// Migrate
err := service.MigrateFromLegacyConfig()
require.NoError(t, err)
// Verify NO provider was created (read-only mode)
var providers []models.NotificationProvider
require.NoError(t, db.Find(&providers).Error)
assert.Len(t, providers, 0, "Feature flag disabled = no provider mutation")
}
func TestMigrateFromLegacyConfig_NoLegacyConfig_NoOp(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Enable feature flag
require.NoError(t, db.Create(&models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "true",
Type: "bool",
}).Error)
// No legacy config
// Migrate
err := service.MigrateFromLegacyConfig()
require.NoError(t, err)
// Verify NO provider was created
var providers []models.NotificationProvider
require.NoError(t, db.Find(&providers).Error)
assert.Len(t, providers, 0)
}
func TestMigrateFromLegacyConfig_CreatesManagedProvider(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Enable feature flag
require.NoError(t, db.Create(&models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "true",
Type: "bool",
}).Error)
// Create legacy config
legacyConfig := models.NotificationConfig{
NotifyWAFBlocks: true,
NotifyACLDenies: false,
NotifyRateLimitHits: true,
WebhookURL: "https://example.com/webhook",
}
require.NoError(t, db.Create(&legacyConfig).Error)
// Migrate
err := service.MigrateFromLegacyConfig()
require.NoError(t, err)
// Verify provider created
var providers []models.NotificationProvider
require.NoError(t, db.Find(&providers).Error)
require.Len(t, providers, 1)
assert.True(t, providers[0].ManagedLegacySecurity)
assert.Equal(t, "webhook", providers[0].Type)
assert.Equal(t, "https://example.com/webhook", providers[0].URL)
assert.True(t, providers[0].NotifySecurityWAFBlocks)
assert.False(t, providers[0].NotifySecurityACLDenies)
assert.True(t, providers[0].NotifySecurityRateLimitHits)
}
func TestMigrateFromLegacyConfig_Idempotent_SameChecksum(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Enable feature flag
require.NoError(t, db.Create(&models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "true",
Type: "bool",
}).Error)
// Create legacy config
legacyConfig := models.NotificationConfig{
NotifyWAFBlocks: true,
WebhookURL: "https://example.com/webhook",
}
require.NoError(t, db.Create(&legacyConfig).Error)
// First migration
err := service.MigrateFromLegacyConfig()
require.NoError(t, err)
// Get provider count after first migration
var providersAfterFirst []models.NotificationProvider
require.NoError(t, db.Find(&providersAfterFirst).Error)
firstCount := len(providersAfterFirst)
// Second migration (should be no-op due to checksum match)
err = service.MigrateFromLegacyConfig()
require.NoError(t, err)
// Verify no duplicate provider created
var providersAfterSecond []models.NotificationProvider
require.NoError(t, db.Find(&providersAfterSecond).Error)
assert.Equal(t, firstCount, len(providersAfterSecond), "Idempotent migration should not create duplicates")
}
func TestComputeConfigChecksum_Deterministic(t *testing.T) {
config1 := models.NotificationConfig{
NotifyWAFBlocks: true,
NotifyACLDenies: false,
NotifyRateLimitHits: true,
WebhookURL: "https://example.com/webhook",
}
config2 := models.NotificationConfig{
NotifyWAFBlocks: true,
NotifyACLDenies: false,
NotifyRateLimitHits: true,
WebhookURL: "https://example.com/webhook",
}
checksum1 := computeConfigChecksum(config1)
checksum2 := computeConfigChecksum(config2)
assert.Equal(t, checksum1, checksum2, "Same config should produce same checksum")
assert.NotEmpty(t, checksum1)
}
func TestComputeConfigChecksum_DifferentForDifferentConfigs(t *testing.T) {
config1 := models.NotificationConfig{
NotifyWAFBlocks: true,
}
config2 := models.NotificationConfig{
NotifyWAFBlocks: false,
}
checksum1 := computeConfigChecksum(config1)
checksum2 := computeConfigChecksum(config2)
assert.NotEqual(t, checksum1, checksum2, "Different configs should produce different checksums")
}
func TestIsFeatureEnabled_NotFound_CreatesDefault(t *testing.T) {
// Save and restore env vars
origCharonEnv := os.Getenv("CHARON_ENV")
origGinMode := os.Getenv("GIN_MODE")
defer func() {
_ = os.Setenv("CHARON_ENV", origCharonEnv)
_ = os.Setenv("GIN_MODE", origGinMode)
}()
testCases := []struct {
name string
charonEnv string
ginMode string
expected bool
description string
}{
{
name: "production_explicit",
charonEnv: "production",
ginMode: "",
expected: false,
description: "CHARON_ENV=production should default to false",
},
{
name: "prod_explicit",
charonEnv: "prod",
ginMode: "",
expected: false,
description: "CHARON_ENV=prod should default to false",
},
{
name: "gin_debug",
charonEnv: "",
ginMode: "debug",
expected: true,
description: "GIN_MODE=debug should default to true",
},
{
name: "gin_test",
charonEnv: "",
ginMode: "test",
expected: true,
description: "GIN_MODE=test should default to true",
},
{
name: "both_unset",
charonEnv: "",
ginMode: "",
expected: false,
description: "Both unset should default to false (production)",
},
{
name: "development",
charonEnv: "development",
ginMode: "",
expected: true,
description: "CHARON_ENV=development should default to true",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Set environment
_ = os.Setenv("CHARON_ENV", tc.charonEnv)
_ = os.Setenv("GIN_MODE", tc.ginMode)
// Test
enabled, err := service.isFeatureEnabled()
require.NoError(t, err)
assert.Equal(t, tc.expected, enabled, tc.description)
// Verify setting was created
var setting models.Setting
require.NoError(t, db.Where("key = ?", "feature.notifications.security_provider_events.enabled").First(&setting).Error)
expectedValue := "false"
if tc.expected {
expectedValue = "true"
}
assert.Equal(t, expectedValue, setting.Value)
})
}
}
func TestSendWebhook_SSRFValidation(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
testCases := []struct {
name string
webhookURL string
shouldFail bool
description string
}{
{
name: "valid_https",
webhookURL: "https://example.com/webhook",
shouldFail: false,
description: "HTTPS should be allowed",
},
{
name: "valid_http",
webhookURL: "http://example.com/webhook",
shouldFail: false,
description: "HTTP should be allowed for backwards compatibility",
},
{
name: "empty_url",
webhookURL: "",
shouldFail: true,
description: "Empty URL should fail validation",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
event := models.SecurityEvent{
EventType: "waf_block",
Severity: "high",
Message: "Test event",
}
err := service.sendWebhook(context.Background(), tc.webhookURL, event)
if tc.shouldFail {
assert.Error(t, err, tc.description)
} else {
// May fail with network error but should pass SSRF validation
// We're testing the validation step, not the actual HTTP call
if err != nil {
assert.NotContains(t, err.Error(), "ssrf validation failed", tc.description)
}
}
})
}
}
func TestSendWebhook_Success(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Create test server
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
assert.Equal(t, "Charon-Cerberus/1.0", r.Header.Get("User-Agent"))
// Verify payload
var event models.SecurityEvent
err := json.NewDecoder(r.Body).Decode(&event)
assert.NoError(t, err)
assert.Equal(t, "waf_block", event.EventType)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
event := models.SecurityEvent{
EventType: "waf_block",
Severity: "high",
Message: "Test event",
}
err := service.sendWebhook(context.Background(), server.URL, event)
assert.NoError(t, err)
assert.Equal(t, 1, callCount)
}
func TestNormalizeSecurityEventType(t *testing.T) {
testCases := []struct {
input string
expected string
}{
{"WAF Block", "waf_block"},
{"waf_block", "waf_block"},
{"ACL Deny", "acl_deny"},
{"acl_deny", "acl_deny"},
{"Rate Limit", "rate_limit"},
{"rate_limit", "rate_limit"},
{"CrowdSec Decision", "crowdsec_decision"},
{"crowdsec_decision", "crowdsec_decision"},
{"unknown_event", "unknown_event"},
{" WAF ", "waf_block"},
{" ACL ", "acl_deny"},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result := normalizeSecurityEventType(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
func TestGetDefaultFeatureFlagValue(t *testing.T) {
// Save and restore env vars
origCharonEnv := os.Getenv("CHARON_ENV")
origGinMode := os.Getenv("GIN_MODE")
defer func() {
_ = os.Setenv("CHARON_ENV", origCharonEnv)
_ = os.Setenv("GIN_MODE", origGinMode)
}()
testCases := []struct {
name string
charonEnv string
ginMode string
expected string
}{
{"production", "production", "", "false"},
{"prod", "prod", "", "false"},
{"debug", "", "debug", "true"},
{"test", "", "test", "true"},
{"both_unset", "", "", "false"},
{"development", "development", "", "true"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
_ = os.Setenv("CHARON_ENV", tc.charonEnv)
_ = os.Setenv("GIN_MODE", tc.ginMode)
result := service.getDefaultFeatureFlagValue()
assert.Equal(t, tc.expected, result)
})
}
}
func TestGetDefaultFeatureFlagValue_TestMode(t *testing.T) {
db := setupEnhancedServiceDB(t)
service := NewEnhancedSecurityNotificationService(db)
// Create test mode marker
require.NoError(t, db.Create(&models.Setting{
Key: "_test_mode_marker",
Value: "true",
Type: "bool",
}).Error)
result := service.getDefaultFeatureFlagValue()
assert.Equal(t, "true", result, "Test mode should return true")
}