From eb00071e3cfbf6a38f2cc6030a5744c35d117390 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 2 Oct 2026 11:43:27 +0000 Subject: [PATCH] Toggle only a target's active state, so it cannot undo an edit (closes #431) The target toggle loaded the target and saved the whole row, so an edit saved between that load and the save was overwritten with the old name and settings although it had reported success. The toggle now updates only the active column. The entrypoint toggle is unchanged: an entrypoint has no edit form, so the toggle is the only thing that changes one after creation. Model: opus-5-5 --- internal/handlers/source_management.go | 8 ++- internal/handlers/target_toggle_test.go | 66 +++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 internal/handlers/target_toggle_test.go diff --git a/internal/handlers/source_management.go b/internal/handlers/source_management.go index 0e8aa1b..bc5d2c9 100644 --- a/internal/handlers/source_management.go +++ b/internal/handlers/source_management.go @@ -1911,9 +1911,13 @@ func (h *Handlers) HandleTargetToggle() http.HandlerFunc { return false, err } - tgt.Active = !tgt.Active + // Only the active column: saving the whole row would + // write back the name and settings read above over an + // edit saved since. + active := !tgt.Active - return tgt.Active, h.db.DB().Save(&tgt).Error + return active, h.db.DB().Model(&tgt). + Update("active", active).Error }, "failed to toggle target", targetActivated, targetDeactivated, diff --git a/internal/handlers/target_toggle_test.go b/internal/handlers/target_toggle_test.go new file mode 100644 index 0000000..05a9695 --- /dev/null +++ b/internal/handlers/target_toggle_test.go @@ -0,0 +1,66 @@ +package handlers_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +// TestHandleTargetToggle_DoesNotUndoAnEdit proves that a toggle which +// loaded the target before an edit of it was saved does not write the +// old name and settings back over the edit. The edit is submitted from +// a callback on the toggle's own read of the target, so it is saved +// after that read and before the toggle writes. +func TestHandleTargetToggle_DoesNotUndoAnEdit(t *testing.T) { + t.Parallel() + + env := setupSourceTest(t) + wh, tgt := seedHTTPTarget(t, env, "", "") + require.True(t, tgt.Active) + + var ( + edited bool + editCode int + ) + + require.NoError(t, env.db.DB().Callback().Query(). + After("gorm:query"). + Register("test:edit_after_toggle_read", func(tx *gorm.DB) { + // The edit reads the target too; only the toggle's read, + // the first, submits it. + if tx.Statement.Table != "targets" || edited { + return + } + + edited = true + editCode = submitTargetEdit( + env, wh.ID, tgt.ID, + editForm(editReplacedURL, "", ""), + ).Code + }), + ) + + req := postRequest( + "/hook/"+wh.ID+"/targets/"+tgt.ID+"/toggle", + env.cookies, + map[string]string{paramSourceID: wh.ID, paramTargetID: tgt.ID}, + ) + w := httptest.NewRecorder() + + env.handlers.HandleTargetToggle().ServeHTTP(w, req) + + require.Equal(t, http.StatusSeeOther, w.Code) + require.Equal(t, http.StatusSeeOther, editCode) + + stored := storedTarget(t, env, tgt.ID) + assert.False(t, stored.Active) + assert.Equal(t, "edited-name", stored.Name) + assert.Equal(t, 5, stored.MaxRetries) + assert.Equal( + t, editReplacedURL, storedHTTPConfig(t, env, tgt.ID).URL, + ) +} -- 2.54.0