chore: Add tests for enhanced security notification service and proxy host validation

This commit is contained in:
GitHub Actions
2026-02-22 22:53:11 +00:00
parent a52ba29f02
commit 9634eb65ad
4 changed files with 611 additions and 0 deletions

View File

@@ -11,10 +11,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Helper scripts are in .github/skills/scripts/
SKILLS_SCRIPTS_DIR="$(cd "${SCRIPT_DIR}/../scripts" && pwd)"
# shellcheck disable=SC1091
# shellcheck source=../scripts/_logging_helpers.sh
source "${SKILLS_SCRIPTS_DIR}/_logging_helpers.sh"
# shellcheck disable=SC1091
# shellcheck source=../scripts/_error_handling_helpers.sh
source "${SKILLS_SCRIPTS_DIR}/_error_handling_helpers.sh"
# shellcheck disable=SC1091
# shellcheck source=../scripts/_environment_helpers.sh
source "${SKILLS_SCRIPTS_DIR}/_environment_helpers.sh"

View File

@@ -2,8 +2,10 @@ package services
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
@@ -330,3 +332,429 @@ func TestEnhancedService_SendViaProviders_Non2xxResponse(t *testing.T) {
// Service logs error but doesn't fail - continues to next provider
assert.NoError(t, err)
}
func TestEnhancedService_GetProviderAggregatedConfig_CrowdSecFlag(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.Setting{}))
db.Create(&models.Setting{Key: "feature.notifications.security_provider_events.enabled", Value: "true", Type: "bool"})
db.Create(&models.NotificationProvider{
ID: "discord-crowdsec",
Name: "Discord CrowdSec",
Type: "discord",
URL: "https://discord.com/api/webhooks/123/abc",
Enabled: true,
ManagedLegacySecurity: true,
NotifySecurityCrowdSecDecisions: true,
})
service := NewEnhancedSecurityNotificationService(db)
config, err := service.GetSettings()
require.NoError(t, err)
assert.True(t, config.NotifyCrowdSecDecisions)
}
func TestEnhancedService_UpdateManagedProviders_SlackDestinationContributesToAmbiguity(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.NotificationConfig{}, &models.Setting{}))
service := NewEnhancedSecurityNotificationService(db)
err = service.updateManagedProviders(&models.NotificationConfig{
DiscordWebhookURL: "https://discord.com/api/webhooks/123/abc",
SlackWebhookURL: "https://hooks.slack.com/services/T/B/X",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "ambiguous destination")
}
func TestEnhancedService_UpdateManagedProviders_QueryManagedProvidersError(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.NotificationConfig{}, &models.Setting{}))
service := NewEnhancedSecurityNotificationService(db)
sqlDB, err := db.DB()
require.NoError(t, err)
require.NoError(t, sqlDB.Close())
err = service.updateManagedProviders(&models.NotificationConfig{DiscordWebhookURL: "https://discord.com/api/webhooks/123/abc"})
require.Error(t, err)
}
func TestEnhancedService_UpdateManagedProviders_ChangesACLTypeAndToken(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.NotificationConfig{}, &models.Setting{}))
provider := models.NotificationProvider{
ID: "managed-change",
Type: "webhook",
URL: "https://example.com/webhook",
Token: "old-token",
Enabled: true,
ManagedLegacySecurity: true,
NotifySecurityWAFBlocks: true,
NotifySecurityACLDenies: false,
NotifySecurityRateLimitHits: false,
}
require.NoError(t, db.Create(&provider).Error)
service := NewEnhancedSecurityNotificationService(db)
err = service.updateManagedProviders(&models.NotificationConfig{
NotifyWAFBlocks: true,
NotifyACLDenies: true,
NotifyRateLimitHits: false,
GotifyURL: "https://gotify.example.com",
GotifyToken: "new-token",
})
require.NoError(t, err)
var updated models.NotificationProvider
require.NoError(t, db.First(&updated, "id = ?", "managed-change").Error)
assert.True(t, updated.NotifySecurityACLDenies)
assert.Equal(t, "gotify", updated.Type)
assert.Equal(t, "new-token", updated.Token)
}
func TestEnhancedService_UpdateManagedProviders_SaveError(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "enhanced-save-error.db")
rwDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, rwDB.AutoMigrate(&models.NotificationProvider{}, &models.NotificationConfig{}, &models.Setting{}))
require.NoError(t, rwDB.Create(&models.NotificationProvider{
ID: "managed-readonly",
Type: "discord",
URL: "https://discord.com/api/webhooks/123/abc",
Enabled: true,
ManagedLegacySecurity: true,
NotifySecurityWAFBlocks: false,
}).Error)
rwSQL, err := rwDB.DB()
require.NoError(t, err)
require.NoError(t, rwSQL.Close())
roDB, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=ro", dbPath)), &gorm.Config{})
require.NoError(t, err)
service := NewEnhancedSecurityNotificationService(roDB)
err = service.updateManagedProviders(&models.NotificationConfig{
NotifyWAFBlocks: true,
DiscordWebhookURL: "https://discord.com/api/webhooks/123/abc",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "update provider")
}
func TestEnhancedService_UpdateLegacyConfig_DBErrorAndUpdatePath(t *testing.T) {
t.Run("db_error", func(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationConfig{}))
service := NewEnhancedSecurityNotificationService(db)
sqlDB, err := db.DB()
require.NoError(t, err)
require.NoError(t, sqlDB.Close())
err = service.updateLegacyConfig(&models.NotificationConfig{NotifyWAFBlocks: true})
require.Error(t, err)
assert.Contains(t, err.Error(), "fetch existing config")
})
t.Run("update_existing_preserves_id", func(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationConfig{}))
service := NewEnhancedSecurityNotificationService(db)
existing := models.NotificationConfig{ID: "legacy-id", NotifyWAFBlocks: false}
require.NoError(t, db.Create(&existing).Error)
req := &models.NotificationConfig{NotifyWAFBlocks: true}
require.NoError(t, service.updateLegacyConfig(req))
assert.Equal(t, "legacy-id", req.ID)
})
}
func TestEnhancedService_MigrateFromLegacyConfig_PreTransactionErrors(t *testing.T) {
t.Run("feature_flag_error", func(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
service := NewEnhancedSecurityNotificationService(db)
sqlDB, err := db.DB()
require.NoError(t, err)
require.NoError(t, sqlDB.Close())
err = service.MigrateFromLegacyConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "check feature flag")
})
t.Run("read_legacy_config_error", func(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.Setting{}))
require.NoError(t, db.Create(&models.Setting{Key: "feature.notifications.security_provider_events.enabled", Value: "true", Type: "bool"}).Error)
service := NewEnhancedSecurityNotificationService(db)
err = service.MigrateFromLegacyConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "read legacy config")
})
}
func TestEnhancedService_MigrateFromLegacyConfig_InvalidMarkerJSONContinues(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.NotificationConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.Setting{Key: "feature.notifications.security_provider_events.enabled", Value: "true", Type: "bool"}).Error)
require.NoError(t, db.Create(&models.NotificationConfig{ID: "legacy", NotifyWAFBlocks: true, WebhookURL: "https://example.com/webhook"}).Error)
require.NoError(t, db.Create(&models.Setting{Key: "notifications.security_provider_events.migration.v1", Value: "{invalid-json", Type: "json", Category: "notifications"}).Error)
service := NewEnhancedSecurityNotificationService(db)
require.NoError(t, service.MigrateFromLegacyConfig())
var count int64
require.NoError(t, db.Model(&models.NotificationProvider{}).Where("managed_legacy_security = ?", true).Count(&count).Error)
assert.Equal(t, int64(1), count)
}
func TestEnhancedService_MigrateFromLegacyConfig_TransactionWriteErrors(t *testing.T) {
t.Run("create_managed_provider_error", func(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "enhanced-migrate-create-error.db")
rwDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, rwDB.AutoMigrate(&models.NotificationProvider{}, &models.NotificationConfig{}, &models.Setting{}))
require.NoError(t, rwDB.Create(&models.Setting{Key: "feature.notifications.security_provider_events.enabled", Value: "true", Type: "bool"}).Error)
require.NoError(t, rwDB.Create(&models.NotificationConfig{ID: "legacy", NotifyWAFBlocks: true, WebhookURL: "https://example.com/webhook"}).Error)
rwSQL, err := rwDB.DB()
require.NoError(t, err)
require.NoError(t, rwSQL.Close())
roDB, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=ro", dbPath)), &gorm.Config{})
require.NoError(t, err)
service := NewEnhancedSecurityNotificationService(roDB)
err = service.MigrateFromLegacyConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "create managed provider")
})
t.Run("update_managed_provider_error", func(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "enhanced-migrate-update-error.db")
rwDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, rwDB.AutoMigrate(&models.NotificationProvider{}, &models.NotificationConfig{}, &models.Setting{}))
require.NoError(t, rwDB.Create(&models.Setting{Key: "feature.notifications.security_provider_events.enabled", Value: "true", Type: "bool"}).Error)
require.NoError(t, rwDB.Create(&models.NotificationConfig{ID: "legacy", NotifyWAFBlocks: true, WebhookURL: "https://example.com/webhook"}).Error)
require.NoError(t, rwDB.Create(&models.NotificationProvider{ID: "managed", Type: "webhook", URL: "https://old.example.com", Enabled: true, ManagedLegacySecurity: true}).Error)
rwSQL, err := rwDB.DB()
require.NoError(t, err)
require.NoError(t, rwSQL.Close())
roDB, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=ro", dbPath)), &gorm.Config{})
require.NoError(t, err)
service := NewEnhancedSecurityNotificationService(roDB)
err = service.MigrateFromLegacyConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "update managed provider")
})
}
func TestEnhancedService_IsFeatureEnabled_CreateAndRequeryPath(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "feature-flag-requery.db")
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.Setting{}))
raceDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
injected := false
callbackName := "test_inject_feature_flag_before_create"
_ = db.Callback().Create().Before("gorm:create").Register(callbackName, func(tx *gorm.DB) {
if tx.Statement.Schema == nil || tx.Statement.Schema.Table != "settings" || injected {
return
}
injected = true
_ = raceDB.Exec("INSERT OR IGNORE INTO settings (key, value, type, category, updated_at) VALUES (?, ?, ?, ?, ?)",
"feature.notifications.security_provider_events.enabled",
"true",
"bool",
"feature",
time.Now(),
).Error
})
defer func() {
_ = db.Callback().Create().Remove(callbackName)
}()
service := NewEnhancedSecurityNotificationService(db)
enabled, err := service.isFeatureEnabled()
require.NoError(t, err)
assert.True(t, enabled)
raceSQL, sqlErr := raceDB.DB()
if sqlErr == nil {
_ = raceSQL.Close()
}
}
func TestEnhancedService_SendViaProviders_QueryProvidersErrorAndCrowdSecRouting(t *testing.T) {
t.Run("query_providers_error", func(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}))
service := NewEnhancedSecurityNotificationService(db)
sqlDB, err := db.DB()
require.NoError(t, err)
require.NoError(t, sqlDB.Close())
err = service.SendViaProviders(context.Background(), models.SecurityEvent{EventType: "waf_block"})
require.Error(t, err)
assert.Contains(t, err.Error(), "query providers")
})
t.Run("crowdsec_decision_routes_to_subscribed_provider", func(t *testing.T) {
serverCalls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
serverCalls++
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.Setting{}))
require.NoError(t, db.Create(&models.NotificationProvider{
ID: "discord-crowdsec-route",
Type: "discord",
URL: server.URL,
Enabled: true,
NotifySecurityCrowdSecDecisions: true,
}).Error)
service := NewEnhancedSecurityNotificationService(db)
err = service.SendViaProviders(context.Background(), models.SecurityEvent{
EventType: "crowdsec_decision",
Severity: "warn",
Message: "CrowdSec decision",
Timestamp: time.Now(),
})
require.NoError(t, err)
assert.Equal(t, 1, serverCalls)
})
}
func TestEnhancedService_SendWebhook_MarshalAndExecuteErrorPaths(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
service := NewEnhancedSecurityNotificationService(db)
t.Run("marshal_error", func(t *testing.T) {
err := service.sendWebhook(context.Background(), "http://127.0.0.1:8080/webhook", models.SecurityEvent{
EventType: "waf_block",
Metadata: map[string]any{
"bad": make(chan int),
},
})
require.Error(t, err)
assert.Contains(t, err.Error(), "marshal event")
})
t.Run("execute_request_error", func(t *testing.T) {
err := service.sendWebhook(context.Background(), "http://127.0.0.1:1/webhook", models.SecurityEvent{
EventType: "waf_block",
Severity: "warn",
Message: "connect failure expected",
Timestamp: time.Now(),
})
require.Error(t, err)
assert.Contains(t, err.Error(), "execute request")
})
}
func TestEnhancedService_UpdateManagedProviders_WrapsManagedQueryError(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationConfig{}, &models.Setting{}))
// notification_providers table intentionally absent
service := NewEnhancedSecurityNotificationService(db)
err = service.updateManagedProviders(&models.NotificationConfig{DiscordWebhookURL: "https://discord.com/api/webhooks/123/abc"})
require.Error(t, err)
assert.Contains(t, err.Error(), "query managed providers")
}
func TestEnhancedService_MigrateFromLegacyConfig_WrapsManagedProviderQueryError(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.Setting{Key: "feature.notifications.security_provider_events.enabled", Value: "true", Type: "bool"}).Error)
require.NoError(t, db.Create(&models.NotificationConfig{ID: "legacy", NotifyWAFBlocks: true, WebhookURL: "https://example.com/webhook"}).Error)
// notification_providers table intentionally absent
service := NewEnhancedSecurityNotificationService(db)
err = service.MigrateFromLegacyConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "query managed provider")
}
func TestEnhancedService_IsFeatureEnabled_CreateAndRequeryErrorPath(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "feature-flag-requery-error.db")
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.Setting{}))
readonlyDB, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=ro", dbPath)), &gorm.Config{})
require.NoError(t, err)
readonlyService := NewEnhancedSecurityNotificationService(readonlyDB)
_, err = readonlyService.isFeatureEnabled()
require.Error(t, err)
assert.Contains(t, err.Error(), "create and requery feature flag")
sqlDB, sqlErr := db.DB()
if sqlErr == nil {
_ = sqlDB.Close()
}
}
func TestEnhancedService_SendViaProviders_RateLimitRoutingBranch(t *testing.T) {
serverCalls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
serverCalls++
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.Setting{}))
require.NoError(t, db.Create(&models.NotificationProvider{
ID: "discord-rate-limit-route",
Type: "discord",
URL: server.URL,
Enabled: true,
NotifySecurityRateLimitHits: true,
}).Error)
service := NewEnhancedSecurityNotificationService(db)
err = service.SendViaProviders(context.Background(), models.SecurityEvent{
EventType: "rate limit hit",
Severity: "warn",
Message: "Rate limit triggered",
Timestamp: time.Now(),
})
require.NoError(t, err)
assert.Equal(t, 1, serverCalls)
}

View File

@@ -8,6 +8,8 @@ import (
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
@@ -1802,6 +1804,160 @@ func TestLegacyFallbackInvocationError(t *testing.T) {
assert.Contains(t, err.Error(), "only discord provider type is supported")
}
func TestLegacyFallbackInvocationError_DirectHelperAndHook(t *testing.T) {
err := legacyFallbackInvocationError("telegram")
require.Error(t, err)
assert.Contains(t, err.Error(), "legacy fallback is retired and disabled")
assert.Contains(t, err.Error(), "provider type \"telegram\"")
hookErr := legacySendFunc("ignored", "ignored")
require.Error(t, hookErr)
assert.ErrorIs(t, hookErr, ErrLegacyFallbackDisabled)
}
func TestNotificationService_SendExternal_SecurityEventRouting(t *testing.T) {
eventCases := []struct {
name string
eventType string
apply func(p *models.NotificationProvider)
}{
{
name: "security_waf",
eventType: "security_waf",
apply: func(p *models.NotificationProvider) {
p.NotifySecurityWAFBlocks = true
},
},
{
name: "security_acl",
eventType: "security_acl",
apply: func(p *models.NotificationProvider) {
p.NotifySecurityACLDenies = true
},
},
{
name: "security_rate_limit",
eventType: "security_rate_limit",
apply: func(p *models.NotificationProvider) {
p.NotifySecurityRateLimitHits = true
},
},
{
name: "security_crowdsec",
eventType: "security_crowdsec",
apply: func(p *models.NotificationProvider) {
p.NotifySecurityCrowdSecDecisions = true
},
},
}
for _, tc := range eventCases {
t.Run(tc.name, func(t *testing.T) {
db := setupNotificationTestDB(t)
svc := NewNotificationService(db)
origValidate := validateDiscordProviderURLFunc
defer func() { validateDiscordProviderURLFunc = origValidate }()
validateDiscordProviderURLFunc = func(providerType, rawURL string) error { return nil }
received := make(chan struct{}, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received <- struct{}{}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
provider := models.NotificationProvider{
Name: "discord-security",
Type: "discord",
URL: server.URL,
Enabled: true,
Template: "minimal",
}
tc.apply(&provider)
require.NoError(t, db.Create(&provider).Error)
svc.SendExternal(context.Background(), tc.eventType, "Security Title", "Security Message", nil)
select {
case <-received:
case <-time.After(1 * time.Second):
t.Fatalf("expected dispatch for event type %s", tc.eventType)
}
})
}
}
func TestNotificationService_UpdateProvider_ReturnsErrorWhenProviderMissing(t *testing.T) {
db := setupNotificationTestDB(t)
svc := NewNotificationService(db)
err := svc.UpdateProvider(&models.NotificationProvider{
ID: "missing-id",
Type: "discord",
URL: "https://discord.com/api/webhooks/123/token",
})
require.Error(t, err)
}
func TestNotificationService_EnsureNotifyOnlyProviderMigration_QueryProvidersError(t *testing.T) {
db := setupNotificationTestDB(t)
svc := NewNotificationService(db)
sqlDB, err := db.DB()
require.NoError(t, err)
require.NoError(t, sqlDB.Close())
err = svc.EnsureNotifyOnlyProviderMigration(context.Background())
require.Error(t, err)
}
func TestNotificationService_EnsureNotifyOnlyProviderMigration_UpdateError(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "migration_update_error.db")
rwDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, rwDB.AutoMigrate(&models.NotificationProvider{}))
require.NoError(t, rwDB.Create(&models.NotificationProvider{
ID: "provider-to-update",
Name: "Legacy Webhook",
Type: "webhook",
URL: "https://example.com/webhook",
Enabled: true,
MigrationState: "pending",
}).Error)
rwSQLDB, err := rwDB.DB()
require.NoError(t, err)
require.NoError(t, rwSQLDB.Close())
roDSN := fmt.Sprintf("file:%s?mode=ro", dbPath)
roDB, err := gorm.Open(sqlite.Open(roDSN), &gorm.Config{})
require.NoError(t, err)
svc := NewNotificationService(roDB)
err = svc.EnsureNotifyOnlyProviderMigration(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to migrate notification provider")
roSQLDB, sqlErr := roDB.DB()
if sqlErr == nil {
_ = roSQLDB.Close()
}
_ = os.Remove(dbPath)
}
func TestNotificationService_EnsureNotifyOnlyProviderMigration_WrapsFindError(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
// Intentionally do not migrate notification_providers table.
svc := NewNotificationService(db)
err = svc.EnsureNotifyOnlyProviderMigration(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to fetch notification providers for migration")
}
func TestTestProvider_NotifyOnlyRejectsUnsupportedProvider(t *testing.T) {
db := setupNotificationTestDB(t)
svc := NewNotificationService(db)

View File

@@ -342,3 +342,27 @@ func TestProxyHostService_ValidateProxyHost_DNSChallenge(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "dns provider is required")
}
func TestProxyHostService_ValidateHostname_StripsPath(t *testing.T) {
db := setupProxyHostTestDB(t)
service := NewProxyHostService(db)
err := service.ValidateHostname("backend.internal/api/v1")
assert.NoError(t, err)
}
func TestProxyHostService_ValidateProxyHost_ParseFallbackAndPathTrim(t *testing.T) {
db := setupProxyHostTestDB(t)
service := NewProxyHostService(db)
host := &models.ProxyHost{
UUID: uuid.New().String(),
DomainNames: "fallback-path.example.com",
ForwardHost: "https://bad host/path",
ForwardPort: 8080,
}
err := service.Create(host)
assert.Error(t, err)
assert.Contains(t, err.Error(), "forward host must be a valid IP address or hostname")
}