Compare commits

1 Commits
Author SHA1 Message Date
sneak 8adb7cd043 Name the item and what is lost in each delete prompt (closes #400)
check / check (push) Successful in 3m21s
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
2026-10-03 00:33:48 +00:00
4 changed files with 10 additions and 112 deletions
-4
View File
@@ -1899,10 +1899,6 @@ retries) is individually logged for full observability.
| `error` | string | Error message (on failure) | | `error` | string | Error message (on failure) |
| `duration` | integer | Request duration in milliseconds | | `duration` | integer | Request duration in milliseconds |
A `database` or `log` target sends no HTTP request, so in the event log and
on the event's page its attempts show no status: a successful one reads
"archived" or "written to the log".
**Relations:** Belongs to Delivery. **Relations:** Belongs to Delivery.
#### EventTotals, TargetTotals and EntrypointTotals #### EventTotals, TargetTotals and EntrypointTotals
@@ -1,89 +0,0 @@
package handlers_test
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm/clause"
"sneak.berlin/go/webhooker/internal/database"
)
// TestDeliveryAttempts_ReadInTheTargetTypesOwnTerms proves, on the
// event's page and in the event log, that an http or slack attempt
// shows its status as before, while a database or log attempt, which
// sends no HTTP request, says what it did and shows no status.
func TestDeliveryAttempts_ReadInTheTargetTypesOwnTerms(t *testing.T) {
t.Parallel()
cases := []struct {
targetType database.TargetType
success bool
statusCode int
errText string
outcome string
status string // "" when the attempt must show no status
}{
{
database.TargetTypeHTTP, false, 0, "",
"failure", "Status: — (no response)",
},
{
database.TargetTypeSlack, true, http.StatusOK, "",
"success", "Status: 200",
},
{database.TargetTypeDatabase, true, 0, "", "archived", ""},
{
database.TargetTypeDatabase, false, 0,
"opening archive database: disk full", "failure", "",
},
{database.TargetTypeLog, true, 0, "", "written to the log", ""},
}
for _, tc := range cases {
t.Run(string(tc.targetType), func(t *testing.T) {
t.Parallel()
f := newRecentEventsFixture(t)
target := seedTarget(t, f.db, f.webhook.ID, tc.targetType)
event := f.event(t, contentTypeJSON, "{}", time.Now())
dlv := f.delivery(
t, event, target.ID, database.DeliveryStatusDelivered,
)
require.NoError(t, f.webhookDB.Omit(clause.Associations).Create(
&database.DeliveryResult{
DeliveryID: dlv.ID,
AttemptNum: 1,
Success: tc.success,
StatusCode: tc.statusCode,
Error: tc.errText,
},
).Error)
w := serveEventPage(t, f.h, f.sess, f.webhook.ID, event.ID)
require.Equal(t, http.StatusOK, w.Code)
pages := []string{
w.Body.String(),
renderSourceLogsPage(t, f.h, f.sess, f.webhook.ID),
}
for _, page := range pages {
assert.Contains(t, page, ">"+tc.outcome+"</span>")
if tc.errText != "" {
assert.Contains(t, page, "Error: "+tc.errText)
}
if tc.status == "" {
assert.NotContains(t, page, "Status:")
} else {
assert.Contains(t, page, tc.status)
}
}
})
}
}
+9 -14
View File
@@ -279,10 +279,9 @@ func TestHandleSourceDetail_FitsWideAndNarrowWindows(t *testing.T) {
// TestHandleSourceDetail_DeletePromptsNameWhatIsLost checks that each // TestHandleSourceDetail_DeletePromptsNameWhatIsLost checks that each
// delete prompt on the webhook page names the webhook, entrypoint or // delete prompt on the webhook page names the webhook, entrypoint or
// target and says what deleting it loses, that the webhook's gives its // 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, // number of stored events, and that an entrypoint with no description
// the statistics pane's "Within retention" figure), and that an // is named by its URL. The template writes the slashes after http: as
// entrypoint with no description is named by its URL. The template // \/, which the browser reads as /.
// writes the slashes after http: as \/, which the browser reads as /.
func TestHandleSourceDetail_DeletePromptsNameWhatIsLost(t *testing.T) { func TestHandleSourceDetail_DeletePromptsNameWhatIsLost(t *testing.T) {
t.Parallel() t.Parallel()
@@ -303,7 +302,7 @@ func TestHandleSourceDetail_DeletePromptsNameWhatIsLost(t *testing.T) {
webhookDB, err := dbMgr.GetDB(wh.ID) webhookDB, err := dbMgr.GetDB(wh.ID)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, database.AddEventTotals( require.NoError(t, database.AddEventTotals(
webhookDB, database.EventTotals{Events: 5, EventsRemoved: 2}, webhookDB, database.EventTotals{Events: 3},
)) ))
unnamed := seedEntrypoint(t, db, wh.ID) unnamed := seedEntrypoint(t, db, wh.ID)
@@ -337,12 +336,9 @@ func TestHandleSourceDetail_DeletePromptsNameWhatIsLost(t *testing.T) {
} }
// TestHandleSourceDetail_DeletePromptKeepsQuotesInName checks that a // TestHandleSourceDetail_DeletePromptKeepsQuotesInName checks that a
// webhook name with quotes, a backslash, a closing script tag and a // webhook name with quotes and a backslash reaches its delete prompt
// newline reaches its delete prompt escaped for the script, which the // escaped for the script, each quote as a \u escape and the backslash
// browser reads back as the name typed: each quote and angle bracket // doubled, which the browser reads back as the name typed.
// 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) { func TestHandleSourceDetail_DeletePromptKeepsQuotesInName(t *testing.T) {
t.Parallel() t.Parallel()
@@ -359,7 +355,7 @@ func TestHandleSourceDetail_DeletePromptKeepsQuotesInName(t *testing.T) {
wh := &database.Webhook{ wh := &database.Webhook{
UserID: deleteTestUserID, UserID: deleteTestUserID,
Name: "Bob's \"best\" \\ hook</script>\nline two", Name: `Bob's "best" \ hook`,
} }
require.NoError( require.NoError(
t, db.DB().Omit(clause.Associations).Create(wh).Error, t, db.DB().Omit(clause.Associations).Create(wh).Error,
@@ -368,6 +364,5 @@ func TestHandleSourceDetail_DeletePromptKeepsQuotesInName(t *testing.T) {
body := renderSourceDetailPage(t, h, sess, wh.ID) body := renderSourceDetailPage(t, h, sess, wh.ID)
assert.Contains(t, body, assert.Contains(t, body,
"Delete webhook &quot;Bob\\u0027s \\u0022best\\u0022 \\\\ hook"+ "Delete webhook &quot;Bob\\u0027s \\u0022best\\u0022 \\\\ hook&quot;?")
"\\u003c\\/script\\u003e\\nline two&quot;?")
} }
+1 -5
View File
@@ -5,15 +5,11 @@
<p class="text-xs text-gray-500">{{.AttemptsOmitted}} attempt{{if ne .AttemptsOmitted 1}}s{{end}} omitted between the first and last shown.</p> <p class="text-xs text-gray-500">{{.AttemptsOmitted}} attempt{{if ne .AttemptsOmitted 1}}s{{end}} omitted between the first and last shown.</p>
{{end}} {{end}}
{{range .Results}} {{range .Results}}
{{/* Inside this loop, dot is one attempt and $ the whole delivery. */}}
<div class="rounded-md bg-white border border-gray-200 p-2"> <div class="rounded-md bg-white border border-gray-200 p-2">
<div class="flex flex-wrap items-center gap-3 text-xs"> <div class="flex flex-wrap items-center gap-3 text-xs">
<span class="text-gray-500">Attempt {{.AttemptNum}}</span> <span class="text-gray-500">Attempt {{.AttemptNum}}</span>
<span class="{{if .Success}}text-green-600{{else}}text-red-600{{end}}">{{if not .Success}}failure{{else if eq $.Target.Type "database"}}archived{{else if eq $.Target.Type "log"}}written to the log{{else}}success{{end}}</span> <span class="{{if .Success}}text-green-600{{else}}text-red-600{{end}}">{{if .Success}}success{{else}}failure{{end}}</span>
{{/* A database or log target sends no HTTP request, so its attempts have no status code. */}}
{{if not (eq $.Target.Type "database" "log")}}
<span class="text-gray-500">Status: {{if .HasStatusCode}}{{.StatusCode}}{{else}}&mdash; (no response){{end}}</span> <span class="text-gray-500">Status: {{if .HasStatusCode}}{{.StatusCode}}{{else}}&mdash; (no response){{end}}</span>
{{end}}
<span class="text-gray-500">Duration: {{.DurationMS}} ms</span> <span class="text-gray-500">Duration: {{.DurationMS}} ms</span>
</div> </div>
{{if .Error}} {{if .Error}}