Name the item and what is lost in each delete prompt (closes #400)
check / check (push) Successful in 3m18s
check / check (push) Successful in 3m18s
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
This commit is contained in:
@@ -275,3 +275,99 @@ func TestHandleSourceDetail_FitsWideAndNarrowWindows(t *testing.T) {
|
|||||||
`<div class="flex flex-wrap justify-between items-center gap-2 mt-2">`,
|
`<div class="flex flex-wrap justify-between items-center gap-2 mt-2">`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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</script>\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"?")
|
||||||
|
}
|
||||||
|
|||||||
@@ -612,8 +612,9 @@ func (h *Handlers) renderSourceDetail(
|
|||||||
|
|
||||||
// The host is the client's Host header, unvalidated. It is
|
// The host is the client's Host header, unvalidated. It is
|
||||||
// inert only because source_detail.html renders BaseURL as
|
// inert only because source_detail.html renders BaseURL as
|
||||||
// text inside a <code> element; putting it in an href or any
|
// text, inside a <code> element and in an entrypoint's delete
|
||||||
// other URL context needs it constrained first.
|
// prompt; putting it in an href or any other URL context
|
||||||
|
// needs it constrained first.
|
||||||
baseURL := scheme + "://" + r.Host
|
baseURL := scheme + "://" + r.Host
|
||||||
|
|
||||||
// The template calls Webhook methods, which take pointer
|
// The template calls Webhook methods, which take pointer
|
||||||
|
|||||||
@@ -20,7 +20,11 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a href="/hook/{{.Webhook.ID}}/events" class="btn-secondary">Full Event Log</a>
|
<a href="/hook/{{.Webhook.ID}}/events" class="btn-secondary">Full Event Log</a>
|
||||||
<a href="/hook/{{.Webhook.ID}}/edit" class="btn-secondary">Edit</a>
|
<a href="/hook/{{.Webhook.ID}}/edit" class="btn-secondary">Edit</a>
|
||||||
<form method="POST" action="/hook/{{.Webhook.ID}}/delete" onsubmit="return confirm('Delete this webhook and all its data?')">
|
<!-- The delete prompts are the browser's own, so they
|
||||||
|
work without the page's scripts. The template
|
||||||
|
escapes each name for the script, so a quote or a
|
||||||
|
backslash in it shows as typed. -->
|
||||||
|
<form method="POST" action="/hook/{{.Webhook.ID}}/delete" onsubmit="return confirm('Delete webhook "{{.Webhook.Name}}"?\n\nThis deletes its stored events{{with .Stats}} ({{.WithinRetention.Events}}){{end}} and their deliveries. Any archive files it wrote are kept.')">
|
||||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
<button type="submit" class="btn-danger">Delete</button>
|
<button type="submit" class="btn-danger">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -83,7 +87,7 @@
|
|||||||
{{if .Active}}Deactivate{{else}}Activate{{end}}
|
{{if .Active}}Deactivate{{else}}Activate{{end}}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="POST" action="/hook/{{$.Webhook.ID}}/entrypoints/{{.ID}}/delete" onsubmit="return confirm('Delete this entrypoint?')" class="inline">
|
<form method="POST" action="/hook/{{$.Webhook.ID}}/entrypoints/{{.ID}}/delete" onsubmit="return confirm('Delete entrypoint "{{if .Description}}{{.Description}}{{else}}{{$.BaseURL}}/h/{{.Path}}{{end}}"?\n\nSenders using its URL get an error from now on, and the URL cannot be restored.')" class="inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||||
<button type="submit" class="btn-small text-red-600" title="Delete">Delete</button>
|
<button type="submit" class="btn-small text-red-600" title="Delete">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -249,7 +253,7 @@
|
|||||||
{{if .Active}}Deactivate{{else}}Activate{{end}}
|
{{if .Active}}Deactivate{{else}}Activate{{end}}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="POST" action="/hook/{{$.Webhook.ID}}/targets/{{.ID}}/delete" onsubmit="return confirm('Delete this target?')" class="inline">
|
<form method="POST" action="/hook/{{$.Webhook.ID}}/targets/{{.ID}}/delete" onsubmit="return confirm('Delete target "{{.Name}}"?\n\nNothing more is delivered to it. Its past deliveries stay in the event log.')" class="inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||||
<button type="submit" class="btn-small text-red-600" title="Delete">Delete</button>
|
<button type="submit" class="btn-small text-red-600" title="Delete">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user