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, + ) +}