Files
Charon/backend/internal/api/handlers/security_event_intake_test.go
GitHub Actions e6c4e46dd8 chore: Refactor test setup for Gin framework
- 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.
2026-03-25 22:00:07 +00:00

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")
}