- Removed redundant `gin.SetMode(gin.TestMode)` calls from individual test files. - Introduced a centralized `TestMain` function in `testmain_test.go` to set the Gin mode for all tests. - Ensured consistent test environment setup across various handler test files.
483 lines
15 KiB
Go
483 lines
15 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/Wikid82/charon/backend/internal/services"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// TestSecurityEventIntakeCompileSuccess tests that the handler is properly instantiated and route registration doesn't fail.
|
|
// Blocker 1: Fix compile error - undefined handler reference.
|
|
func TestSecurityEventIntakeCompileSuccess(t *testing.T) {
|
|
db := SetupCompatibilityTestDB(t)
|
|
|
|
// This test validates that the handler can be instantiated with all required dependencies
|
|
notificationService := services.NewNotificationService(db, nil)
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
securityService := services.NewSecurityService(db)
|
|
managementCIDRs := []string{"127.0.0.0/8"}
|
|
|
|
handler := NewSecurityNotificationHandlerWithDeps(
|
|
service,
|
|
securityService,
|
|
"/data",
|
|
notificationService,
|
|
managementCIDRs,
|
|
)
|
|
|
|
require.NotNil(t, handler, "Handler should be instantiated successfully")
|
|
require.NotNil(t, handler.notificationService, "Notification service should be set")
|
|
require.NotNil(t, handler.managementCIDRs, "Management CIDRs should be set")
|
|
assert.Equal(t, 1, len(handler.managementCIDRs), "Management CIDRs should have one entry")
|
|
}
|
|
|
|
// TestSecurityEventIntakeAuthLocalhost tests that localhost requests are accepted.
|
|
// Blocker 2: Implement real source validation - localhost should be allowed.
|
|
func TestSecurityEventIntakeAuthLocalhost(t *testing.T) {
|
|
db := SetupCompatibilityTestDB(t)
|
|
|
|
notificationService := services.NewNotificationService(db, nil)
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
managementCIDRs := []string{"10.0.0.0/8"}
|
|
|
|
handler := NewSecurityNotificationHandlerWithDeps(
|
|
service,
|
|
nil,
|
|
"",
|
|
notificationService,
|
|
managementCIDRs,
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
event := models.SecurityEvent{
|
|
EventType: "waf_block",
|
|
Severity: "error",
|
|
Message: "WAF blocked request",
|
|
ClientIP: "192.168.1.100",
|
|
Path: "/admin",
|
|
Timestamp: time.Now(),
|
|
}
|
|
body, _ := json.Marshal(event)
|
|
|
|
// Localhost IPv4 request
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
|
c.Request.RemoteAddr = "127.0.0.1:12345"
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
handler.HandleSecurityEvent(c)
|
|
|
|
assert.Equal(t, http.StatusAccepted, w.Code, "Localhost should be accepted")
|
|
}
|
|
|
|
// TestSecurityEventIntakeAuthManagementCIDR tests that management network requests are accepted.
|
|
// Blocker 2: Implement real source validation - management CIDRs should be allowed.
|
|
func TestSecurityEventIntakeAuthManagementCIDR(t *testing.T) {
|
|
db := SetupCompatibilityTestDB(t)
|
|
|
|
notificationService := services.NewNotificationService(db, nil)
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
managementCIDRs := []string{"192.168.1.0/24", "10.0.0.0/8"}
|
|
|
|
handler := NewSecurityNotificationHandlerWithDeps(
|
|
service,
|
|
nil,
|
|
"",
|
|
notificationService,
|
|
managementCIDRs,
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
event := models.SecurityEvent{
|
|
EventType: "waf_block",
|
|
Severity: "error",
|
|
Message: "WAF blocked request",
|
|
ClientIP: "8.8.8.8",
|
|
Path: "/admin",
|
|
Timestamp: time.Now(),
|
|
}
|
|
body, _ := json.Marshal(event)
|
|
|
|
// Request from management network
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
|
c.Request.RemoteAddr = "192.168.1.50:12345"
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
handler.HandleSecurityEvent(c)
|
|
|
|
assert.Equal(t, http.StatusAccepted, w.Code, "Management CIDR should be accepted")
|
|
}
|
|
|
|
// TestSecurityEventIntakeAuthUnauthorizedIP tests that external IPs are rejected.
|
|
// Blocker 2: Implement real source validation - external IPs should be rejected.
|
|
func TestSecurityEventIntakeAuthUnauthorizedIP(t *testing.T) {
|
|
db := SetupCompatibilityTestDB(t)
|
|
|
|
notificationService := services.NewNotificationService(db, nil)
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
managementCIDRs := []string{"192.168.1.0/24"}
|
|
|
|
handler := NewSecurityNotificationHandlerWithDeps(
|
|
service,
|
|
nil,
|
|
"",
|
|
notificationService,
|
|
managementCIDRs,
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
event := models.SecurityEvent{
|
|
EventType: "waf_block",
|
|
Severity: "error",
|
|
Message: "WAF blocked request",
|
|
ClientIP: "8.8.8.8",
|
|
Path: "/admin",
|
|
Timestamp: time.Now(),
|
|
}
|
|
body, _ := json.Marshal(event)
|
|
|
|
// External IP not in management network
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
|
c.Request.RemoteAddr = "8.8.8.8:12345"
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
handler.HandleSecurityEvent(c)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code, "External IP should be rejected")
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "unauthorized_source", response["error"])
|
|
}
|
|
|
|
// TestSecurityEventIntakeAuthInvalidIP tests that malformed IPs are rejected.
|
|
// Blocker 2: Implement real source validation - invalid IPs should be rejected.
|
|
func TestSecurityEventIntakeAuthInvalidIP(t *testing.T) {
|
|
db := SetupCompatibilityTestDB(t)
|
|
|
|
notificationService := services.NewNotificationService(db, nil)
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
managementCIDRs := []string{"192.168.1.0/24"}
|
|
|
|
handler := NewSecurityNotificationHandlerWithDeps(
|
|
service,
|
|
nil,
|
|
"",
|
|
notificationService,
|
|
managementCIDRs,
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
event := models.SecurityEvent{
|
|
EventType: "waf_block",
|
|
Severity: "error",
|
|
Message: "WAF blocked request",
|
|
ClientIP: "8.8.8.8",
|
|
Path: "/admin",
|
|
Timestamp: time.Now(),
|
|
}
|
|
body, _ := json.Marshal(event)
|
|
|
|
// Malformed IP
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
|
c.Request.RemoteAddr = "invalid-ip:12345"
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
handler.HandleSecurityEvent(c)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code, "Invalid IP should be rejected")
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "invalid_source", response["error"])
|
|
}
|
|
|
|
// TestSecurityEventIntakeDispatchInvoked tests that notification dispatch is invoked.
|
|
// Blocker 3: Implement actual dispatch - remove TODO/no-op behavior.
|
|
func TestSecurityEventIntakeDispatchInvoked(t *testing.T) {
|
|
db := SetupCompatibilityTestDB(t)
|
|
|
|
// Create a Discord provider with security notifications enabled
|
|
provider := &models.NotificationProvider{
|
|
Name: "Discord Security Alerts",
|
|
Type: "discord",
|
|
URL: "https://discord.com/api/webhooks/123/token",
|
|
Enabled: true,
|
|
NotifySecurityWAFBlocks: true,
|
|
NotifySecurityACLDenies: true,
|
|
NotifySecurityRateLimitHits: true,
|
|
NotifySecurityCrowdSecDecisions: true,
|
|
}
|
|
require.NoError(t, db.Create(provider).Error)
|
|
|
|
notificationService := services.NewNotificationService(db, nil)
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
managementCIDRs := []string{"127.0.0.0/8"}
|
|
|
|
handler := NewSecurityNotificationHandlerWithDeps(
|
|
service,
|
|
nil,
|
|
"",
|
|
notificationService,
|
|
managementCIDRs,
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
event := models.SecurityEvent{
|
|
EventType: "waf_block",
|
|
Severity: "error",
|
|
Message: "WAF blocked suspicious request",
|
|
ClientIP: "192.168.1.100",
|
|
Path: "/admin/login",
|
|
Timestamp: time.Now(),
|
|
Metadata: map[string]any{
|
|
"rule_id": "920100",
|
|
"matched_data": "suspicious pattern",
|
|
},
|
|
}
|
|
body, _ := json.Marshal(event)
|
|
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
|
c.Request.RemoteAddr = "127.0.0.1:12345"
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
handler.HandleSecurityEvent(c)
|
|
|
|
assert.Equal(t, http.StatusAccepted, w.Code, "Event should be accepted")
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Security event recorded", response["message"])
|
|
assert.Equal(t, "waf_block", response["event_type"])
|
|
assert.NotEmpty(t, response["timestamp"])
|
|
}
|
|
|
|
// TestSecurityEventIntakeR6Intact tests that legacy PUT endpoint returns 410 Gone.
|
|
// Constraint: Keep R6 410/no-mutation behavior intact.
|
|
func TestSecurityEventIntakeR6Intact(t *testing.T) {
|
|
// Create in-memory database with User table for this test
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(
|
|
&models.User{},
|
|
&models.NotificationProvider{},
|
|
&models.NotificationConfig{},
|
|
&models.Setting{},
|
|
))
|
|
|
|
// Enable feature flag by default in tests
|
|
featureFlag := &models.Setting{
|
|
Key: "feature.notifications.security_provider_events.enabled",
|
|
Value: "true",
|
|
Type: "bool",
|
|
Category: "feature",
|
|
}
|
|
require.NoError(t, db.Create(featureFlag).Error)
|
|
|
|
// Create an admin user for authentication
|
|
adminUser := &models.User{
|
|
Email: "admin@example.com",
|
|
Name: "Admin User",
|
|
PasswordHash: "$2a$10$abcdefghijklmnopqrstuvwxyz", // Dummy bcrypt hash
|
|
Role: models.RoleAdmin,
|
|
Enabled: true,
|
|
}
|
|
require.NoError(t, db.Create(adminUser).Error)
|
|
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
handler := NewSecurityNotificationHandler(service)
|
|
|
|
router := gin.New()
|
|
|
|
// Add auth middleware that sets user context
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("user_id", adminUser.ID)
|
|
c.Set("user", adminUser)
|
|
c.Set("role", "admin")
|
|
c.Next()
|
|
})
|
|
|
|
router.PUT("/api/v1/notifications/settings/security", handler.UpdateSettings)
|
|
|
|
reqBody := map[string]interface{}{
|
|
"enabled": true,
|
|
"security_waf_enabled": true,
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("PUT", "/api/v1/notifications/settings/security", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusGone, w.Code, "PUT should return 410 Gone")
|
|
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "legacy_security_settings_deprecated", response["error"])
|
|
assert.Equal(t, "LEGACY_SECURITY_SETTINGS_DEPRECATED", response["code"])
|
|
}
|
|
|
|
// TestSecurityEventIntakeDiscordOnly tests Discord-only dispatch enforcement.
|
|
// Constraint: Keep Discord-only dispatch enforcement for this rollout stage.
|
|
func TestSecurityEventIntakeDiscordOnly(t *testing.T) {
|
|
db := SetupCompatibilityTestDB(t)
|
|
|
|
// Create various provider types
|
|
discordProvider := &models.NotificationProvider{
|
|
Name: "Discord",
|
|
Type: "discord",
|
|
URL: "https://discord.com/api/webhooks/123/token",
|
|
Enabled: true,
|
|
NotifySecurityWAFBlocks: true,
|
|
}
|
|
require.NoError(t, db.Create(discordProvider).Error)
|
|
|
|
// Non-Discord providers should also work with supportsJSONTemplates
|
|
webhookProvider := &models.NotificationProvider{
|
|
Name: "Webhook",
|
|
Type: "webhook",
|
|
URL: "https://example.com/webhook",
|
|
Enabled: true,
|
|
NotifySecurityWAFBlocks: true,
|
|
}
|
|
require.NoError(t, db.Create(webhookProvider).Error)
|
|
|
|
notificationService := services.NewNotificationService(db, nil)
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
managementCIDRs := []string{"127.0.0.0/8"}
|
|
|
|
handler := NewSecurityNotificationHandlerWithDeps(
|
|
service,
|
|
nil,
|
|
"",
|
|
notificationService,
|
|
managementCIDRs,
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
event := models.SecurityEvent{
|
|
EventType: "waf_block",
|
|
Severity: "error",
|
|
Message: "WAF blocked request",
|
|
ClientIP: "192.168.1.100",
|
|
Path: "/admin",
|
|
Timestamp: time.Now(),
|
|
}
|
|
body, _ := json.Marshal(event)
|
|
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
|
c.Request.RemoteAddr = "127.0.0.1:12345"
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
handler.HandleSecurityEvent(c)
|
|
|
|
assert.Equal(t, http.StatusAccepted, w.Code)
|
|
|
|
// Validate both Discord and webhook providers exist (JSON-capable)
|
|
var providers []models.NotificationProvider
|
|
err := db.Where("enabled = ?", true).Find(&providers).Error
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, len(providers), "Both Discord and webhook providers should be enabled")
|
|
}
|
|
|
|
// TestSecurityEventIntakeMalformedPayload tests rejection of malformed event payloads.
|
|
func TestSecurityEventIntakeMalformedPayload(t *testing.T) {
|
|
db := SetupCompatibilityTestDB(t)
|
|
|
|
notificationService := services.NewNotificationService(db, nil)
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
managementCIDRs := []string{"127.0.0.0/8"}
|
|
|
|
handler := NewSecurityNotificationHandlerWithDeps(
|
|
service,
|
|
nil,
|
|
"",
|
|
notificationService,
|
|
managementCIDRs,
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
// Malformed JSON
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader([]byte("{invalid json")))
|
|
c.Request.RemoteAddr = "127.0.0.1:12345"
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
handler.HandleSecurityEvent(c)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code, "Malformed JSON should be rejected")
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Invalid security event payload", response["error"])
|
|
}
|
|
|
|
// TestSecurityEventIntakeIPv6Localhost tests that IPv6 localhost is accepted.
|
|
func TestSecurityEventIntakeIPv6Localhost(t *testing.T) {
|
|
db := SetupCompatibilityTestDB(t)
|
|
|
|
notificationService := services.NewNotificationService(db, nil)
|
|
service := services.NewEnhancedSecurityNotificationService(db)
|
|
managementCIDRs := []string{"10.0.0.0/8"}
|
|
|
|
handler := NewSecurityNotificationHandlerWithDeps(
|
|
service,
|
|
nil,
|
|
"",
|
|
notificationService,
|
|
managementCIDRs,
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
event := models.SecurityEvent{
|
|
EventType: "acl_deny",
|
|
Severity: "warn",
|
|
Message: "ACL denied request",
|
|
ClientIP: "::1",
|
|
Path: "/api/admin",
|
|
Timestamp: time.Now(),
|
|
}
|
|
body, _ := json.Marshal(event)
|
|
|
|
// IPv6 localhost
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
|
c.Request.RemoteAddr = "[::1]:12345"
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
handler.HandleSecurityEvent(c)
|
|
|
|
assert.Equal(t, http.StatusAccepted, w.Code, "IPv6 localhost should be accepted")
|
|
}
|