diff --git a/backend/internal/api/handlers/settings_handler.go b/backend/internal/api/handlers/settings_handler.go index dd1c1f5c..6239609b 100644 --- a/backend/internal/api/handlers/settings_handler.go +++ b/backend/internal/api/handlers/settings_handler.go @@ -177,18 +177,18 @@ func (h *SettingsHandler) UpdateSetting(c *gin.Context) { h.Cerberus.InvalidateCache() } - // Trigger async Caddy config reload (doesn't block HTTP response) + // Trigger sync Caddy config reload so callers can rely on deterministic applied state if h.CaddyManager != nil { - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() - if err := h.CaddyManager.ApplyConfig(ctx); err != nil { - logger.Log().WithError(err).Warn("Failed to reload Caddy config after security setting change") - } else { - logger.Log().WithField("setting_key", req.Key).Info("Caddy config reloaded after security setting change") - } - }() + if err := h.CaddyManager.ApplyConfig(ctx); err != nil { + logger.Log().WithError(err).Warn("Failed to reload Caddy config after security setting change") + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload configuration"}) + return + } + + logger.Log().WithField("setting_key", req.Key).Info("Caddy config reloaded after security setting change") } } @@ -283,18 +283,18 @@ func (h *SettingsHandler) PatchConfig(c *gin.Context) { h.Cerberus.InvalidateCache() } - // Trigger async Caddy config reload + // Trigger sync Caddy config reload so callers can rely on deterministic applied state if h.CaddyManager != nil { - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() - if err := h.CaddyManager.ApplyConfig(ctx); err != nil { - logger.Log().WithError(err).Warn("Failed to reload Caddy config after security settings change") - } else { - logger.Log().Info("Caddy config reloaded after security settings change") - } - }() + if err := h.CaddyManager.ApplyConfig(ctx); err != nil { + logger.Log().WithError(err).Warn("Failed to reload Caddy config after security settings change") + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload configuration"}) + return + } + + logger.Log().Info("Caddy config reloaded after security settings change") } } diff --git a/backend/internal/api/handlers/settings_handler_test.go b/backend/internal/api/handlers/settings_handler_test.go index 7060d478..94a92dc8 100644 --- a/backend/internal/api/handlers/settings_handler_test.go +++ b/backend/internal/api/handlers/settings_handler_test.go @@ -3,6 +3,7 @@ package handlers_test import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "net" @@ -22,6 +23,19 @@ import ( "github.com/Wikid82/charon/backend/internal/models" ) +type mockCaddyConfigManager struct { + applyFunc func(context.Context) error + calls int +} + +func (m *mockCaddyConfigManager) ApplyConfig(ctx context.Context) error { + m.calls++ + if m.applyFunc != nil { + return m.applyFunc(ctx) + } + return nil +} + func startTestSMTPServer(t *testing.T) (host string, port int) { t.Helper() @@ -295,6 +309,56 @@ func TestSettingsHandler_UpdateSetting_EnablesCerberusWhenACLEnabled(t *testing. assert.True(t, cfg.Enabled) } +func TestSettingsHandler_UpdateSetting_SecurityKeyAppliesConfigSynchronously(t *testing.T) { + gin.SetMode(gin.TestMode) + db := setupSettingsTestDB(t) + + mgr := &mockCaddyConfigManager{} + handler := handlers.NewSettingsHandlerWithDeps(db, mgr, nil, nil, "") + router := newAdminRouter() + router.POST("/settings", handler.UpdateSetting) + + payload := map[string]string{ + "key": "security.waf.enabled", + "value": "true", + } + body, _ := json.Marshal(payload) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, 1, mgr.calls) +} + +func TestSettingsHandler_UpdateSetting_SecurityKeyApplyFailureReturnsError(t *testing.T) { + gin.SetMode(gin.TestMode) + db := setupSettingsTestDB(t) + + mgr := &mockCaddyConfigManager{applyFunc: func(context.Context) error { + return fmt.Errorf("apply failed") + }} + handler := handlers.NewSettingsHandlerWithDeps(db, mgr, nil, nil, "") + router := newAdminRouter() + router.POST("/settings", handler.UpdateSetting) + + payload := map[string]string{ + "key": "security.waf.enabled", + "value": "true", + } + body, _ := json.Marshal(payload) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Equal(t, 1, mgr.calls) +} + func TestSettingsHandler_PatchConfig_SyncsAdminWhitelist(t *testing.T) { gin.SetMode(gin.TestMode) db := setupSettingsTestDB(t)