From f1304da780eaa9c5ab5c8dfd44a4615bef45bbc0 Mon Sep 17 00:00:00 2001 From: sneak Date: Sat, 3 Oct 2026 00:33:39 +0000 Subject: [PATCH] Name the item and what is lost in each delete prompt (closes #400) The webhook, entrypoint and target delete prompts on the webhook page now name the item and say what deleting it loses: the webhook's stored events (with the count from the statistics pane) and their deliveries, while its archive files are kept; an entrypoint's URL, which stops working for good; a target's future deliveries, while its past ones stay in the event log. They stay the browser's own prompts, so they work without the page's scripts, and each name is escaped for the script so quotes and backslashes show as typed. Model: opus-5-5 --- internal/handlers/source_detail_test.go | 96 +++++++++++++++++++++++++ internal/handlers/source_management.go | 5 +- templates/source_detail.html | 10 ++- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/internal/handlers/source_detail_test.go b/internal/handlers/source_detail_test.go index 007f464..2d58837 100644 --- a/internal/handlers/source_detail_test.go +++ b/internal/handlers/source_detail_test.go @@ -275,3 +275,99 @@ func TestHandleSourceDetail_FitsWideAndNarrowWindows(t *testing.T) { `
`, ) } + +// TestHandleSourceDetail_DeletePromptsNameWhatIsLost checks that each +// delete prompt on the webhook page names the webhook, entrypoint or +// target and says what deleting it loses, that the webhook's gives its +// number of stored events (5 received, 2 removed by retention, so 3, +// the statistics pane's "Within retention" figure), and that an +// entrypoint with no description is named by its URL. The template +// writes the slashes after http: as \/, which the browser reads as /. +func TestHandleSourceDetail_DeletePromptsNameWhatIsLost(t *testing.T) { + t.Parallel() + + var ( + h *handlers.Handlers + sess *session.Session + db *database.Database + dbMgr *database.WebhookDBManager + ) + + app := newTestApp(t, &h, &sess, &db, &dbMgr) + app.RequireStart() + + t.Cleanup(app.RequireStop) + + wh := seedWebhook(t, db) + + webhookDB, err := dbMgr.GetDB(wh.ID) + require.NoError(t, err) + require.NoError(t, database.AddEventTotals( + webhookDB, database.EventTotals{Events: 5, EventsRemoved: 2}, + )) + + unnamed := seedEntrypoint(t, db, wh.ID) + require.NoError(t, db.DB().Omit(clause.Associations).Create( + &database.Entrypoint{ + WebhookID: wh.ID, + Path: "described-" + wh.ID, + Description: "Stripe", + Active: true, + }, + ).Error) + seedTarget(t, db, wh.ID, database.TargetTypeLog) + + body := renderSourceDetailPage(t, h, sess, wh.ID) + + assert.Contains(t, body, + `Delete webhook "delete-me"?\n\n`+ + `This deletes its stored events (3) and their deliveries. `+ + `Any archive files it wrote are kept.`) + assert.Contains(t, body, + `Delete entrypoint "Stripe"?\n\n`+ + `Senders using its URL get an error from now on, `+ + `and the URL cannot be restored.`) + assert.Contains(t, body, + `Delete entrypoint "http:\/\/example.com/h/`+ + unnamed.Path+`"?`) + assert.Contains(t, body, + `Delete target "t-log"?\n\n`+ + `Nothing more is delivered to it. `+ + `Its past deliveries stay in the event log.`) +} + +// TestHandleSourceDetail_DeletePromptKeepsQuotesInName checks that a +// webhook name with quotes, a backslash, a closing script tag and a +// newline reaches its delete prompt escaped for the script, which the +// browser reads back as the name typed: each quote and angle bracket +// as a \u escape, the slash as \/, the newline as \n and the backslash +// doubled. An unescaped newline would break the prompt's script, and +// the form would then submit without asking. +func TestHandleSourceDetail_DeletePromptKeepsQuotesInName(t *testing.T) { + t.Parallel() + + var ( + h *handlers.Handlers + sess *session.Session + db *database.Database + ) + + app := newTestApp(t, &h, &sess, &db) + app.RequireStart() + + t.Cleanup(app.RequireStop) + + wh := &database.Webhook{ + UserID: deleteTestUserID, + Name: "Bob's \"best\" \\ hook\nline two", + } + require.NoError( + t, db.DB().Omit(clause.Associations).Create(wh).Error, + ) + + body := renderSourceDetailPage(t, h, sess, wh.ID) + + assert.Contains(t, body, + "Delete webhook "Bob\\u0027s \\u0022best\\u0022 \\\\ hook"+ + "\\u003c\\/script\\u003e\\nline two"?") +} diff --git a/internal/handlers/source_management.go b/internal/handlers/source_management.go index 01dcb50..5b6049c 100644 --- a/internal/handlers/source_management.go +++ b/internal/handlers/source_management.go @@ -612,8 +612,9 @@ func (h *Handlers) renderSourceDetail( // The host is the client's Host header, unvalidated. It is // inert only because source_detail.html renders BaseURL as - // text inside a element; putting it in an href or any - // other URL context needs it constrained first. + // text, inside a element and in an entrypoint's delete + // prompt; putting it in an href or any other URL context + // needs it constrained first. baseURL := scheme + "://" + r.Host // The template calls Webhook methods, which take pointer diff --git a/templates/source_detail.html b/templates/source_detail.html index a27c382..eeea81a 100644 --- a/templates/source_detail.html +++ b/templates/source_detail.html @@ -20,7 +20,11 @@
Full Event Log Edit -
+ +
@@ -83,7 +87,7 @@ {{if .Active}}Deactivate{{else}}Activate{{end}} -
+
@@ -249,7 +253,7 @@ {{if .Active}}Deactivate{{else}}Activate{{end}} -
+
-- 2.54.0