Keep what was typed when a target or webhook edit is refused (closes #381)
check / check (push) Successful in 3m17s

A refused save on the target edit page answered with a bare text page, losing the form and everything typed, and the webhook edit page came back with the stored values instead of the submitted ones. A refused target edit now shows the edit form again with the reason above it and every value submitted, with the same status codes as before; a refused webhook edit keeps the submitted name, description and retention. Target edits use the same validation as new targets, with no second copy; an encoding or database failure stays a logged 500. The browser test covers a refused save on both pages, and its main function is now a plain list of checks.

Model: opus-5-5
This commit was merged in pull request #474.
This commit is contained in:
2026-10-03 00:51:56 +02:00
parent 61371d388e
commit f282c6363d
11 changed files with 467 additions and 269 deletions
+71
View File
@@ -565,6 +565,73 @@ func assertEditRejectsTimeout(
)
}
// TestHandleTargetEditSubmit_RefusedFormComesBack refuses an edit of
// a target of each type and checks that the edit form comes back with
// the reason and every value submitted, and that nothing is saved.
func TestHandleTargetEditSubmit_RefusedFormComesBack(t *testing.T) {
t.Parallel()
env := setupSourceTest(t)
// fields is what the operator submitted, as a query string.
cases := []struct {
targetType database.TargetType
fields string
reason string
}{
{
database.TargetTypeHTTP,
"name=edited&url=" + editBlockedURL +
"&headers=X-Edited:+kept&timeout=12&max_retries=3",
"Invalid target URL",
},
{
database.TargetTypeSlack,
"name=edited&url=" + editOriginalURL + "&max_retries=25",
"Invalid max retries",
},
{
database.TargetTypeDatabase, "name=edited&expiry=7d",
"Invalid archive expiry",
},
{database.TargetTypeLog, "name=", "Name is required"},
}
for _, tc := range cases {
t.Run(string(tc.targetType), func(t *testing.T) {
t.Parallel()
webhook := seedWebhookWithRetention(t, env.db, 30)
target := seedTarget(t, env.db, webhook.ID, tc.targetType)
form, err := url.ParseQuery(tc.fields)
require.NoError(t, err)
w := submitTargetEdit(env, webhook.ID, target.ID, form)
assert.Equal(t, http.StatusBadRequest, w.Code)
page := w.Body.String()
assert.Contains(t, page, `class="alert-error">`+tc.reason)
// headers is the form's one textarea; every other field is
// an input.
for field := range form {
shown := `name="` + field + `" value="` + form.Get(field) + `"`
if field == "headers" {
shown = ">" + form.Get(field) + "</textarea>"
}
assert.Contains(t, page, shown)
}
assert.Equal(
t, target.Name, storedTarget(t, env, target.ID).Name,
"a refused edit must save nothing",
)
})
}
}
// TestHandleTargetEdit_Scoping keeps the edit routes scoped the way
// the delete and toggle routes are: ownership is decided by the
// webhook, and the target is then scoped to it.
@@ -700,6 +767,10 @@ func TestHandleTargetEditSubmit_RenamesArchive(t *testing.T) {
w = submitTargetEdit(env, wh.ID, archive.ID, again)
require.Equal(t, http.StatusConflict, w.Code)
assert.Contains(t, w.Body.String(), "archive-taken.db")
assert.Contains(
t, w.Body.String(), `name="name" value="Again"`,
"the form comes back with the name submitted",
)
assert.Equal(
t, renamedTargetName, storedTarget(t, env, archive.ID).Name,
)