chore: git cache cleanup
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// mockSecurityNotificationService implements the service interface for controlled testing.
|
||||
type mockSecurityNotificationService struct {
|
||||
getSettingsFunc func() (*models.NotificationConfig, error)
|
||||
updateSettingsFunc func(*models.NotificationConfig) error
|
||||
}
|
||||
|
||||
func (m *mockSecurityNotificationService) GetSettings() (*models.NotificationConfig, error) {
|
||||
if m.getSettingsFunc != nil {
|
||||
return m.getSettingsFunc()
|
||||
}
|
||||
return &models.NotificationConfig{}, nil
|
||||
}
|
||||
|
||||
func (m *mockSecurityNotificationService) UpdateSettings(c *models.NotificationConfig) error {
|
||||
if m.updateSettingsFunc != nil {
|
||||
return m.updateSettingsFunc(c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupSecNotifTestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.NotificationConfig{}))
|
||||
return db
|
||||
}
|
||||
|
||||
// TestNewSecurityNotificationHandler verifies constructor returns non-nil handler.
|
||||
func TestNewSecurityNotificationHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := setupSecNotifTestDB(t)
|
||||
svc := services.NewSecurityNotificationService(db)
|
||||
handler := NewSecurityNotificationHandler(svc)
|
||||
|
||||
assert.NotNil(t, handler, "Handler should not be nil")
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_GetSettings_Success tests successful settings retrieval.
|
||||
func TestSecurityNotificationHandler_GetSettings_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expectedConfig := &models.NotificationConfig{
|
||||
ID: "test-id",
|
||||
Enabled: true,
|
||||
MinLogLevel: "warn",
|
||||
WebhookURL: "https://example.com/webhook",
|
||||
NotifyWAFBlocks: true,
|
||||
NotifyACLDenies: false,
|
||||
}
|
||||
|
||||
mockService := &mockSecurityNotificationService{
|
||||
getSettingsFunc: func() (*models.NotificationConfig, error) {
|
||||
return expectedConfig, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/security/notifications/settings", http.NoBody)
|
||||
|
||||
handler.GetSettings(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var config models.NotificationConfig
|
||||
err := json.Unmarshal(w.Body.Bytes(), &config)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedConfig.ID, config.ID)
|
||||
assert.Equal(t, expectedConfig.Enabled, config.Enabled)
|
||||
assert.Equal(t, expectedConfig.MinLogLevel, config.MinLogLevel)
|
||||
assert.Equal(t, expectedConfig.WebhookURL, config.WebhookURL)
|
||||
assert.Equal(t, expectedConfig.NotifyWAFBlocks, config.NotifyWAFBlocks)
|
||||
assert.Equal(t, expectedConfig.NotifyACLDenies, config.NotifyACLDenies)
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_GetSettings_ServiceError tests service error handling.
|
||||
func TestSecurityNotificationHandler_GetSettings_ServiceError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockService := &mockSecurityNotificationService{
|
||||
getSettingsFunc: func() (*models.NotificationConfig, error) {
|
||||
return nil, errors.New("database connection failed")
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/security/notifications/settings", http.NoBody)
|
||||
|
||||
handler.GetSettings(c)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, response["error"], "Failed to retrieve settings")
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_UpdateSettings_InvalidJSON tests malformed JSON handling.
|
||||
func TestSecurityNotificationHandler_UpdateSettings_InvalidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockService := &mockSecurityNotificationService{}
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
malformedJSON := []byte(`{enabled: true, "min_log_level": "error"`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
setAdminContext(c)
|
||||
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(malformedJSON))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.UpdateSettings(c)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, response["error"], "Invalid request body")
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_UpdateSettings_InvalidMinLogLevel tests invalid log level rejection.
|
||||
func TestSecurityNotificationHandler_UpdateSettings_InvalidMinLogLevel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
invalidLevels := []struct {
|
||||
name string
|
||||
level string
|
||||
}{
|
||||
{"trace", "trace"},
|
||||
{"critical", "critical"},
|
||||
{"fatal", "fatal"},
|
||||
{"unknown", "unknown"},
|
||||
}
|
||||
|
||||
for _, tc := range invalidLevels {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mockService := &mockSecurityNotificationService{}
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
config := models.NotificationConfig{
|
||||
Enabled: true,
|
||||
MinLogLevel: tc.level,
|
||||
NotifyWAFBlocks: true,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
setAdminContext(c)
|
||||
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.UpdateSettings(c)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
var response map[string]string
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, response["error"], "Invalid min_log_level")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_UpdateSettings_InvalidWebhookURL_SSRF tests SSRF protection.
|
||||
func TestSecurityNotificationHandler_UpdateSettings_InvalidWebhookURL_SSRF(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ssrfURLs := []struct {
|
||||
name string
|
||||
url string
|
||||
}{
|
||||
{"AWS Metadata", "http://169.254.169.254/latest/meta-data/"},
|
||||
{"GCP Metadata", "http://metadata.google.internal/computeMetadata/v1/"},
|
||||
{"Azure Metadata", "http://169.254.169.254/metadata/instance"},
|
||||
{"Private IP 10.x", "http://10.0.0.1/admin"},
|
||||
{"Private IP 172.16.x", "http://172.16.0.1/config"},
|
||||
{"Private IP 192.168.x", "http://192.168.1.1/api"},
|
||||
{"Link-local", "http://169.254.1.1/"},
|
||||
}
|
||||
|
||||
for _, tc := range ssrfURLs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mockService := &mockSecurityNotificationService{}
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
config := models.NotificationConfig{
|
||||
Enabled: true,
|
||||
MinLogLevel: "error",
|
||||
WebhookURL: tc.url,
|
||||
NotifyWAFBlocks: true,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
setAdminContext(c)
|
||||
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.UpdateSettings(c)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, response["error"], "Invalid webhook URL")
|
||||
if help, ok := response["help"]; ok {
|
||||
assert.Contains(t, help, "private networks")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_UpdateSettings_PrivateIPWebhook tests private IP handling.
|
||||
func TestSecurityNotificationHandler_UpdateSettings_PrivateIPWebhook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Note: localhost is allowed by WithAllowLocalhost() option
|
||||
localhostURLs := []string{
|
||||
"http://127.0.0.1/hook",
|
||||
"http://localhost/webhook",
|
||||
"http://[::1]/api",
|
||||
}
|
||||
|
||||
for _, url := range localhostURLs {
|
||||
t.Run(url, func(t *testing.T) {
|
||||
mockService := &mockSecurityNotificationService{
|
||||
updateSettingsFunc: func(c *models.NotificationConfig) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
config := models.NotificationConfig{
|
||||
Enabled: true,
|
||||
MinLogLevel: "warn",
|
||||
WebhookURL: url,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
setAdminContext(c)
|
||||
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.UpdateSettings(c)
|
||||
|
||||
// Localhost should be allowed with AllowLocalhost option
|
||||
assert.Equal(t, http.StatusOK, w.Code, "Localhost should be allowed: %s", url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_UpdateSettings_ServiceError tests database error handling.
|
||||
func TestSecurityNotificationHandler_UpdateSettings_ServiceError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockService := &mockSecurityNotificationService{
|
||||
updateSettingsFunc: func(c *models.NotificationConfig) error {
|
||||
return errors.New("database write failed")
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
config := models.NotificationConfig{
|
||||
Enabled: true,
|
||||
MinLogLevel: "error",
|
||||
WebhookURL: "http://localhost:9090/webhook", // Use localhost
|
||||
NotifyWAFBlocks: true,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
setAdminContext(c)
|
||||
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.UpdateSettings(c)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
|
||||
var response map[string]string
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, response["error"], "Failed to update settings")
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_UpdateSettings_Success tests successful settings update.
|
||||
func TestSecurityNotificationHandler_UpdateSettings_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var capturedConfig *models.NotificationConfig
|
||||
|
||||
mockService := &mockSecurityNotificationService{
|
||||
updateSettingsFunc: func(c *models.NotificationConfig) error {
|
||||
capturedConfig = c
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
config := models.NotificationConfig{
|
||||
Enabled: true,
|
||||
MinLogLevel: "warn",
|
||||
WebhookURL: "http://localhost:8080/security", // Use localhost which is allowed
|
||||
NotifyWAFBlocks: true,
|
||||
NotifyACLDenies: false,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
setAdminContext(c)
|
||||
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.UpdateSettings(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]string
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Settings updated successfully", response["message"])
|
||||
|
||||
// Verify the service was called with the correct config
|
||||
require.NotNil(t, capturedConfig)
|
||||
assert.Equal(t, config.Enabled, capturedConfig.Enabled)
|
||||
assert.Equal(t, config.MinLogLevel, capturedConfig.MinLogLevel)
|
||||
assert.Equal(t, config.WebhookURL, capturedConfig.WebhookURL)
|
||||
assert.Equal(t, config.NotifyWAFBlocks, capturedConfig.NotifyWAFBlocks)
|
||||
assert.Equal(t, config.NotifyACLDenies, capturedConfig.NotifyACLDenies)
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_UpdateSettings_EmptyWebhookURL tests empty webhook is valid.
|
||||
func TestSecurityNotificationHandler_UpdateSettings_EmptyWebhookURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockService := &mockSecurityNotificationService{
|
||||
updateSettingsFunc: func(c *models.NotificationConfig) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
config := models.NotificationConfig{
|
||||
Enabled: true,
|
||||
MinLogLevel: "info",
|
||||
WebhookURL: "",
|
||||
NotifyWAFBlocks: true,
|
||||
NotifyACLDenies: true,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
setAdminContext(c)
|
||||
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.UpdateSettings(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]string
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Settings updated successfully", response["message"])
|
||||
}
|
||||
|
||||
func TestSecurityNotificationHandler_RouteAliasGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expectedConfig := &models.NotificationConfig{
|
||||
ID: "alias-test-id",
|
||||
Enabled: true,
|
||||
MinLogLevel: "info",
|
||||
WebhookURL: "https://example.com/webhook",
|
||||
NotifyWAFBlocks: true,
|
||||
NotifyACLDenies: true,
|
||||
}
|
||||
|
||||
mockService := &mockSecurityNotificationService{
|
||||
getSettingsFunc: func() (*models.NotificationConfig, error) {
|
||||
return expectedConfig, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/api/v1/security/notifications/settings", handler.GetSettings)
|
||||
router.GET("/api/v1/notifications/settings/security", handler.GetSettings)
|
||||
|
||||
originalWriter := httptest.NewRecorder()
|
||||
originalRequest := httptest.NewRequest(http.MethodGet, "/api/v1/security/notifications/settings", http.NoBody)
|
||||
router.ServeHTTP(originalWriter, originalRequest)
|
||||
|
||||
aliasWriter := httptest.NewRecorder()
|
||||
aliasRequest := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/settings/security", http.NoBody)
|
||||
router.ServeHTTP(aliasWriter, aliasRequest)
|
||||
|
||||
assert.Equal(t, http.StatusOK, originalWriter.Code)
|
||||
assert.Equal(t, originalWriter.Code, aliasWriter.Code)
|
||||
assert.Equal(t, originalWriter.Body.String(), aliasWriter.Body.String())
|
||||
}
|
||||
|
||||
func TestSecurityNotificationHandler_RouteAliasUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
legacyUpdates := 0
|
||||
canonicalUpdates := 0
|
||||
mockService := &mockSecurityNotificationService{
|
||||
updateSettingsFunc: func(c *models.NotificationConfig) error {
|
||||
if c.WebhookURL == "http://localhost:8080/security" {
|
||||
canonicalUpdates++
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
config := models.NotificationConfig{
|
||||
Enabled: true,
|
||||
MinLogLevel: "warn",
|
||||
WebhookURL: "http://localhost:8080/security",
|
||||
NotifyWAFBlocks: true,
|
||||
NotifyACLDenies: false,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
setAdminContext(c)
|
||||
c.Next()
|
||||
})
|
||||
router.PUT("/api/v1/security/notifications/settings", handler.DeprecatedUpdateSettings)
|
||||
router.PUT("/api/v1/notifications/settings/security", handler.UpdateSettings)
|
||||
|
||||
originalWriter := httptest.NewRecorder()
|
||||
originalRequest := httptest.NewRequest(http.MethodPut, "/api/v1/security/notifications/settings", bytes.NewBuffer(body))
|
||||
originalRequest.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(originalWriter, originalRequest)
|
||||
|
||||
aliasWriter := httptest.NewRecorder()
|
||||
aliasRequest := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/settings/security", bytes.NewBuffer(body))
|
||||
aliasRequest.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(aliasWriter, aliasRequest)
|
||||
|
||||
assert.Equal(t, http.StatusGone, originalWriter.Code)
|
||||
assert.Equal(t, "true", originalWriter.Header().Get("X-Charon-Deprecated"))
|
||||
assert.Equal(t, "/api/v1/notifications/settings/security", originalWriter.Header().Get("X-Charon-Canonical-Endpoint"))
|
||||
|
||||
assert.Equal(t, http.StatusOK, aliasWriter.Code)
|
||||
assert.Equal(t, 0, legacyUpdates)
|
||||
assert.Equal(t, 1, canonicalUpdates)
|
||||
}
|
||||
|
||||
func TestSecurityNotificationHandler_DeprecatedRouteHeaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockService := &mockSecurityNotificationService{
|
||||
getSettingsFunc: func() (*models.NotificationConfig, error) {
|
||||
return &models.NotificationConfig{Enabled: true, MinLogLevel: "warn"}, nil
|
||||
},
|
||||
updateSettingsFunc: func(c *models.NotificationConfig) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
setAdminContext(c)
|
||||
c.Next()
|
||||
})
|
||||
router.GET("/api/v1/security/notifications/settings", handler.DeprecatedGetSettings)
|
||||
router.PUT("/api/v1/security/notifications/settings", handler.DeprecatedUpdateSettings)
|
||||
router.GET("/api/v1/notifications/settings/security", handler.GetSettings)
|
||||
router.PUT("/api/v1/notifications/settings/security", handler.UpdateSettings)
|
||||
|
||||
legacyGet := httptest.NewRecorder()
|
||||
legacyGetReq := httptest.NewRequest(http.MethodGet, "/api/v1/security/notifications/settings", http.NoBody)
|
||||
router.ServeHTTP(legacyGet, legacyGetReq)
|
||||
require.Equal(t, http.StatusOK, legacyGet.Code)
|
||||
assert.Equal(t, "true", legacyGet.Header().Get("X-Charon-Deprecated"))
|
||||
assert.Equal(t, "/api/v1/notifications/settings/security", legacyGet.Header().Get("X-Charon-Canonical-Endpoint"))
|
||||
|
||||
canonicalGet := httptest.NewRecorder()
|
||||
canonicalGetReq := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/settings/security", http.NoBody)
|
||||
router.ServeHTTP(canonicalGet, canonicalGetReq)
|
||||
require.Equal(t, http.StatusOK, canonicalGet.Code)
|
||||
assert.Empty(t, canonicalGet.Header().Get("X-Charon-Deprecated"))
|
||||
|
||||
body, err := json.Marshal(models.NotificationConfig{Enabled: true, MinLogLevel: "warn"})
|
||||
require.NoError(t, err)
|
||||
|
||||
legacyPut := httptest.NewRecorder()
|
||||
legacyPutReq := httptest.NewRequest(http.MethodPut, "/api/v1/security/notifications/settings", bytes.NewBuffer(body))
|
||||
legacyPutReq.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(legacyPut, legacyPutReq)
|
||||
require.Equal(t, http.StatusGone, legacyPut.Code)
|
||||
assert.Equal(t, "true", legacyPut.Header().Get("X-Charon-Deprecated"))
|
||||
assert.Equal(t, "/api/v1/notifications/settings/security", legacyPut.Header().Get("X-Charon-Canonical-Endpoint"))
|
||||
|
||||
var legacyBody map[string]string
|
||||
err = json.Unmarshal(legacyPut.Body.Bytes(), &legacyBody)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, legacyBody, 2)
|
||||
assert.Equal(t, "This endpoint is deprecated and no longer accepts updates", legacyBody["error"])
|
||||
assert.Equal(t, "/api/v1/notifications/settings/security", legacyBody["canonical_endpoint"])
|
||||
|
||||
canonicalPut := httptest.NewRecorder()
|
||||
canonicalPutReq := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/settings/security", bytes.NewBuffer(body))
|
||||
canonicalPutReq.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(canonicalPut, canonicalPutReq)
|
||||
require.Equal(t, http.StatusOK, canonicalPut.Code)
|
||||
}
|
||||
|
||||
func TestNormalizeEmailRecipients(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: " ",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "single valid",
|
||||
input: "admin@example.com",
|
||||
want: "admin@example.com",
|
||||
},
|
||||
{
|
||||
name: "multiple valid with spaces and blanks",
|
||||
input: " admin@example.com, , ops@example.com ,security@example.com ",
|
||||
want: "admin@example.com, ops@example.com, security@example.com",
|
||||
},
|
||||
{
|
||||
name: "duplicates and mixed case preserved",
|
||||
input: "Admin@Example.com, admin@example.com, Admin@Example.com",
|
||||
want: "Admin@Example.com, admin@example.com, Admin@Example.com",
|
||||
},
|
||||
{
|
||||
name: "invalid only",
|
||||
input: "not-an-email",
|
||||
wantErr: "invalid email recipients: not-an-email",
|
||||
},
|
||||
{
|
||||
name: "mixed invalid and valid",
|
||||
input: "admin@example.com, bad-address,ops@example.com",
|
||||
wantErr: "invalid email recipients: bad-address",
|
||||
},
|
||||
{
|
||||
name: "multiple invalids",
|
||||
input: "bad-address,also-bad",
|
||||
wantErr: "invalid email recipients: bad-address, also-bad",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := normalizeEmailRecipients(tt.input)
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tt.wantErr, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityNotificationHandler_DeprecatedUpdateSettings_AllFields tests that all JSON fields are returned
|
||||
func TestSecurityNotificationHandler_DeprecatedUpdateSettings_AllFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockService := &mockSecurityNotificationService{}
|
||||
handler := NewSecurityNotificationHandler(mockService)
|
||||
|
||||
body, err := json.Marshal(models.NotificationConfig{Enabled: true, MinLogLevel: "warn"})
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
setAdminContext(c)
|
||||
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/security/notifications/settings", bytes.NewBuffer(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.DeprecatedUpdateSettings(c)
|
||||
|
||||
assert.Equal(t, http.StatusGone, w.Code)
|
||||
assert.Equal(t, "true", w.Header().Get("X-Charon-Deprecated"))
|
||||
assert.Equal(t, "/api/v1/notifications/settings/security", w.Header().Get("X-Charon-Canonical-Endpoint"))
|
||||
|
||||
var response map[string]string
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify both JSON fields are present with exact values
|
||||
assert.Equal(t, "This endpoint is deprecated and no longer accepts updates", response["error"])
|
||||
assert.Equal(t, "/api/v1/notifications/settings/security", response["canonical_endpoint"])
|
||||
assert.Len(t, response, 2, "Should have exactly 2 fields in JSON response")
|
||||
}
|
||||
Reference in New Issue
Block a user