Say what each action did in a one-line notice (closes #383)
check / check (push) Waiting to run
check / check (push) Waiting to run
Saving, deleting, activating or deactivating a webhook, entrypoint or target, and signing out, now land on their page with a one-line notice such as "Webhook deleted." or "Signed out.". The redirect carries a fixed code that maps to fixed text; an unknown code shows nothing, so nothing from the URL is ever echoed. One partial in the page layout shows the notice on every page, and replay and resubmit now use the same codes and partial. Error pages show no notice. Model: opus-5-5
This commit was merged in pull request #420.
This commit is contained in:
@@ -333,7 +333,9 @@ func (h *Handlers) HandleLogout() http.HandlerFunc {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to login page
|
http.Redirect(
|
||||||
http.Redirect(w, r, "/pages/login", http.StatusSeeOther)
|
w, r, withNotice("/pages/login", signedOut),
|
||||||
|
http.StatusSeeOther,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,72 +11,37 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/delivery"
|
"sneak.berlin/go/webhooker/internal/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
// replayOutcomeParam is the query parameter the replay POST redirects
|
// The outcomes of a replay POST, as the notice codes its redirect
|
||||||
// with and the event log page reads its banner from.
|
// carries. noticeFor holds the line each one shows.
|
||||||
const replayOutcomeParam = "replay"
|
|
||||||
|
|
||||||
// replayOutcomeCode is the outcome of a replay POST. The redirect
|
|
||||||
// carries one of these fixed codes rather than a message, so nothing a
|
|
||||||
// client submits can reach the rendered page through it.
|
|
||||||
type replayOutcomeCode string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// replayQueued reports that a new delivery was created and handed
|
// replayQueued reports that a new delivery was created and handed
|
||||||
// to the delivery engine.
|
// to the delivery engine.
|
||||||
replayQueued replayOutcomeCode = "queued"
|
replayQueued noticeCode = "replay-queued"
|
||||||
|
|
||||||
// replayTargetDeleted reports a target that once existed and has
|
// replayTargetDeleted reports a target that once existed and has
|
||||||
// since been deleted. Deletes are soft and deliveries carry no
|
// since been deleted. Deletes are soft and deliveries carry no
|
||||||
// foreign key to the target row, so the history survives its
|
// foreign key to the target row, so the history survives its
|
||||||
// target and this is the ordinary case for an old event.
|
// target and this is the ordinary case for an old event.
|
||||||
replayTargetDeleted replayOutcomeCode = "target-deleted"
|
replayTargetDeleted noticeCode = "replay-target-deleted"
|
||||||
|
|
||||||
// replayTargetMissing reports a target id that names no row at
|
// replayTargetMissing reports a target id that names no row at
|
||||||
// all, deleted or otherwise.
|
// all, deleted or otherwise.
|
||||||
replayTargetMissing replayOutcomeCode = "target-missing"
|
replayTargetMissing noticeCode = "replay-target-missing"
|
||||||
|
|
||||||
// replayTargetInactive reports a target the operator has
|
// replayTargetInactive reports a target the operator has
|
||||||
// deactivated. A deactivated target receives no new deliveries, so
|
// deactivated. A deactivated target receives no new deliveries, so
|
||||||
// a replay to it would be a delivery they switched off.
|
// a replay to it would be a delivery they switched off.
|
||||||
replayTargetInactive replayOutcomeCode = "target-inactive"
|
replayTargetInactive noticeCode = "replay-target-inactive"
|
||||||
|
|
||||||
// replayNotTerminal reports a delivery the engine has not finished
|
// replayNotTerminal reports a delivery the engine has not finished
|
||||||
// with.
|
// with.
|
||||||
replayNotTerminal replayOutcomeCode = "not-terminal"
|
replayNotTerminal noticeCode = "replay-not-terminal"
|
||||||
|
|
||||||
// replayInFlight reports that an earlier replay of this event to
|
// replayInFlight reports that an earlier replay of this event to
|
||||||
// this target is still running.
|
// this target is still running.
|
||||||
replayInFlight replayOutcomeCode = "in-flight"
|
replayInFlight noticeCode = "replay-in-flight"
|
||||||
)
|
)
|
||||||
|
|
||||||
// replayOutcome returns the banner the event log page shows for an
|
|
||||||
// outcome code, and whether the replay was queued. An unrecognised
|
|
||||||
// code yields no banner.
|
|
||||||
func replayOutcome(code string) (string, bool) {
|
|
||||||
switch replayOutcomeCode(code) {
|
|
||||||
case replayQueued:
|
|
||||||
return "Replay queued: a new delivery was created against " +
|
|
||||||
"the target's current configuration.", true
|
|
||||||
case replayTargetDeleted:
|
|
||||||
return "Not replayed: the target this delivery was for has " +
|
|
||||||
"been deleted. Recreate the target, then replay.", false
|
|
||||||
case replayTargetMissing:
|
|
||||||
return "Not replayed: the target this delivery was for no " +
|
|
||||||
"longer exists.", false
|
|
||||||
case replayTargetInactive:
|
|
||||||
return "Not replayed: the target this delivery was for is " +
|
|
||||||
"deactivated. Activate it, then replay.", false
|
|
||||||
case replayNotTerminal:
|
|
||||||
return "Not replayed: this delivery has not finished yet.",
|
|
||||||
false
|
|
||||||
case replayInFlight:
|
|
||||||
return "Not replayed: a delivery of this event to this " +
|
|
||||||
"target is already in flight.", false
|
|
||||||
default:
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleDeliveryReplay re-sends a finished delivery's event to its
|
// HandleDeliveryReplay re-sends a finished delivery's event to its
|
||||||
// target.
|
// target.
|
||||||
//
|
//
|
||||||
@@ -140,14 +105,14 @@ func (h *Handlers) replayDelivery(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !original.Status.Terminal() {
|
if !original.Status.Terminal() {
|
||||||
h.finishReplay(w, r, webhook, replayNotTerminal)
|
redirectToEventLog(w, r, webhook, replayNotTerminal)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
target, code := h.replayTarget(webhook.ID, original.TargetID)
|
target, code := h.replayTarget(webhook.ID, original.TargetID)
|
||||||
if target == nil {
|
if target == nil {
|
||||||
h.finishReplay(w, r, webhook, code)
|
redirectToEventLog(w, r, webhook, code)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -200,7 +165,7 @@ func (h *Handlers) queueReplay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if inFlight > 0 {
|
if inFlight > 0 {
|
||||||
h.finishReplay(w, r, webhook, replayInFlight)
|
redirectToEventLog(w, r, webhook, replayInFlight)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -238,7 +203,7 @@ func (h *Handlers) queueReplay(
|
|||||||
"delivery_id", task.DeliveryID,
|
"delivery_id", task.DeliveryID,
|
||||||
)
|
)
|
||||||
|
|
||||||
h.finishReplay(w, r, webhook, replayQueued)
|
redirectToEventLog(w, r, webhook, replayQueued)
|
||||||
}
|
}
|
||||||
|
|
||||||
// replayTarget loads the delivery's target as it stands now.
|
// replayTarget loads the delivery's target as it stands now.
|
||||||
@@ -251,7 +216,7 @@ func (h *Handlers) queueReplay(
|
|||||||
// with the returned code saying why.
|
// with the returned code saying why.
|
||||||
func (h *Handlers) replayTarget(
|
func (h *Handlers) replayTarget(
|
||||||
webhookID, targetID string,
|
webhookID, targetID string,
|
||||||
) (*database.Target, replayOutcomeCode) {
|
) (*database.Target, noticeCode) {
|
||||||
var target database.Target
|
var target database.Target
|
||||||
|
|
||||||
err := h.db.DB().Unscoped().Where(
|
err := h.db.DB().Unscoped().Where(
|
||||||
@@ -361,17 +326,16 @@ func replayBody(body string) *string {
|
|||||||
return &body
|
return &body
|
||||||
}
|
}
|
||||||
|
|
||||||
// finishReplay redirects back to the event log the replay was
|
// redirectToEventLog redirects a replay or resubmit back to the event
|
||||||
// triggered from, carrying the outcome code the page turns into a
|
// log it was triggered from, carrying the outcome as its notice and
|
||||||
// banner and the page number the form submitted.
|
// the page number the form submitted.
|
||||||
func (h *Handlers) finishReplay(
|
func redirectToEventLog(
|
||||||
w http.ResponseWriter,
|
w http.ResponseWriter,
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
webhook database.Webhook,
|
webhook database.Webhook,
|
||||||
code replayOutcomeCode,
|
code noticeCode,
|
||||||
) {
|
) {
|
||||||
dest := "/hook/" + webhook.ID + "/events?" +
|
dest := withNotice("/hook/"+webhook.ID+"/events", code)
|
||||||
replayOutcomeParam + "=" + string(code)
|
|
||||||
|
|
||||||
// The page is read from the form rather than the query string:
|
// The page is read from the form rather than the query string:
|
||||||
// this is a POST, and its query string is what logs and Referer
|
// this is a POST, and its query string is what logs and Referer
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ func TestHandleDeliveryReplay_AppendsDeliveryAndLeavesOriginal(
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?replay=queued",
|
"/hook/"+wh.ID+"/events?notice=replay-queued",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ func TestHandleDeliveryReplay_RefusesDeletedTarget(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?replay=target-deleted",
|
"/hook/"+wh.ID+"/events?notice=replay-target-deleted",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -390,7 +390,7 @@ func TestHandleDeliveryReplay_RefusesDeletedTarget(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusSeeOther, missing.Code)
|
require.Equal(t, http.StatusSeeOther, missing.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?replay=target-missing",
|
"/hook/"+wh.ID+"/events?notice=replay-target-missing",
|
||||||
missing.Header().Get("Location"),
|
missing.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -431,7 +431,7 @@ func TestHandleDeliveryReplay_RefusesWhileEarlierReplayInFlight(
|
|||||||
require.Equal(t, http.StatusSeeOther, first.Code)
|
require.Equal(t, http.StatusSeeOther, first.Code)
|
||||||
require.Equal(
|
require.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?replay=queued",
|
"/hook/"+wh.ID+"/events?notice=replay-queued",
|
||||||
first.Header().Get("Location"),
|
first.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -439,7 +439,7 @@ func TestHandleDeliveryReplay_RefusesWhileEarlierReplayInFlight(
|
|||||||
require.Equal(t, http.StatusSeeOther, second.Code)
|
require.Equal(t, http.StatusSeeOther, second.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?replay=in-flight",
|
"/hook/"+wh.ID+"/events?notice=replay-in-flight",
|
||||||
second.Header().Get("Location"),
|
second.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -465,7 +465,7 @@ func TestHandleDeliveryReplay_RefusesWhileEarlierReplayInFlight(
|
|||||||
require.Equal(t, http.StatusSeeOther, pending.Code)
|
require.Equal(t, http.StatusSeeOther, pending.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?replay=not-terminal",
|
"/hook/"+wh.ID+"/events?notice=replay-not-terminal",
|
||||||
pending.Header().Get("Location"),
|
pending.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -509,7 +509,7 @@ func TestHandleSourceLogs_RendersReplayControlAndBanner(t *testing.T) {
|
|||||||
assert.Contains(t, body, ">Replay<")
|
assert.Contains(t, body, ">Replay<")
|
||||||
|
|
||||||
refused := renderSourceLogsPageWithQuery(
|
refused := renderSourceLogsPageWithQuery(
|
||||||
t, h, sess, wh.ID, "?replay=target-deleted",
|
t, h, sess, wh.ID, "?notice=replay-target-deleted",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.Contains(t, refused, "alert-error")
|
assert.Contains(t, refused, "alert-error")
|
||||||
@@ -517,7 +517,7 @@ func TestHandleSourceLogs_RendersReplayControlAndBanner(t *testing.T) {
|
|||||||
|
|
||||||
// An outcome code nobody issued renders no banner at all.
|
// An outcome code nobody issued renders no banner at all.
|
||||||
unknown := renderSourceLogsPageWithQuery(
|
unknown := renderSourceLogsPageWithQuery(
|
||||||
t, h, sess, wh.ID, "?replay=made-up",
|
t, h, sess, wh.ID, "?notice=made-up",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.NotContains(t, unknown, "alert-error")
|
assert.NotContains(t, unknown, "alert-error")
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -11,43 +10,19 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resubmitOutcomeParam is the query parameter the resubmit POST
|
// The outcomes of a resubmit POST, as the notice codes its redirect
|
||||||
// redirects with and the event log page reads its banner from.
|
// carries. noticeFor holds the line each one shows.
|
||||||
const resubmitOutcomeParam = "resubmit"
|
|
||||||
|
|
||||||
// resubmitOutcomeCode is the outcome of a resubmit POST. The redirect
|
|
||||||
// carries one of these fixed codes rather than a message, so nothing a
|
|
||||||
// client submits can reach the rendered page through it.
|
|
||||||
type resubmitOutcomeCode string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// resubmitQueued reports that a new event was stored and its
|
// resubmitQueued reports that a new event was stored and its
|
||||||
// deliveries handed to the delivery engine.
|
// deliveries handed to the delivery engine.
|
||||||
resubmitQueued resubmitOutcomeCode = "queued"
|
resubmitQueued noticeCode = "resubmit-queued"
|
||||||
|
|
||||||
// resubmitNoTargets reports a source with no active targets. The
|
// resubmitNoTargets reports a source with no active targets. The
|
||||||
// new event is stored either way, exactly as a received event
|
// new event is stored either way, exactly as a received event
|
||||||
// with no targets is.
|
// with no targets is.
|
||||||
resubmitNoTargets resubmitOutcomeCode = "no-targets"
|
resubmitNoTargets noticeCode = "resubmit-no-targets"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resubmitOutcome returns the banner the event log page shows for an
|
|
||||||
// outcome code, and whether the resubmit was queued. An unrecognised
|
|
||||||
// code yields no banner.
|
|
||||||
func resubmitOutcome(code string) (string, bool) {
|
|
||||||
switch resubmitOutcomeCode(code) {
|
|
||||||
case resubmitQueued:
|
|
||||||
return "Resubmitted: a new event was created from the stored " +
|
|
||||||
"one and queued to every active target.", true
|
|
||||||
case resubmitNoTargets:
|
|
||||||
return "Resubmitted: a new event was created, but this " +
|
|
||||||
"source has no active targets, so nothing was queued.",
|
|
||||||
true
|
|
||||||
default:
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resubmitSource is the stored event a resubmit copies. Its body is
|
// resubmitSource is the stored event a resubmit copies. Its body is
|
||||||
// read as bytes rather than as a string so the copy is byte-identical
|
// read as bytes rather than as a string so the copy is byte-identical
|
||||||
// to what was received, whatever the payload's encoding.
|
// to what was received, whatever the payload's encoding.
|
||||||
@@ -245,29 +220,5 @@ func (h *Handlers) queueResubmit(
|
|||||||
code = resubmitNoTargets
|
code = resubmitNoTargets
|
||||||
}
|
}
|
||||||
|
|
||||||
h.finishResubmit(w, r, webhook, code)
|
redirectToEventLog(w, r, webhook, code)
|
||||||
}
|
|
||||||
|
|
||||||
// finishResubmit redirects back to the event log the resubmit was
|
|
||||||
// triggered from, carrying the outcome code the page turns into a
|
|
||||||
// banner and the page number the form submitted.
|
|
||||||
func (h *Handlers) finishResubmit(
|
|
||||||
w http.ResponseWriter,
|
|
||||||
r *http.Request,
|
|
||||||
webhook database.Webhook,
|
|
||||||
code resubmitOutcomeCode,
|
|
||||||
) {
|
|
||||||
dest := "/hook/" + webhook.ID + "/events?" +
|
|
||||||
resubmitOutcomeParam + "=" + string(code)
|
|
||||||
|
|
||||||
// The page is read from the form rather than the query string:
|
|
||||||
// this is a POST, and its query string is what logs and Referer
|
|
||||||
// headers record.
|
|
||||||
if page := pageOrFirst(
|
|
||||||
r.PostFormValue("page"),
|
|
||||||
); page > 1 {
|
|
||||||
dest += "&page=" + strconv.Itoa(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, dest, http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func TestHandleEventResubmit_DeliversToTargetCreatedAfterTheEvent(
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?resubmit=queued",
|
"/hook/"+wh.ID+"/events?notice=resubmit-queued",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ func TestHandleEventResubmit_IsRepeatable(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?resubmit=queued",
|
"/hook/"+wh.ID+"/events?notice=resubmit-queued",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
"a resubmit must not be refused while an earlier "+
|
"a resubmit must not be refused while an earlier "+
|
||||||
"one is in flight",
|
"one is in flight",
|
||||||
@@ -436,7 +436,7 @@ func TestHandleEventResubmit_SkipsInactiveTarget(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?resubmit=queued",
|
"/hook/"+wh.ID+"/events?notice=resubmit-queued",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
"an inactive target is skipped, not an error",
|
"an inactive target is skipped, not an error",
|
||||||
)
|
)
|
||||||
@@ -482,7 +482,7 @@ func TestHandleEventResubmit_NoActiveTargetsStillStoresEvent(
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?resubmit=no-targets",
|
"/hook/"+wh.ID+"/events?notice=resubmit-no-targets",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ type Handlers struct {
|
|||||||
|
|
||||||
// parsePageTemplate parses a page-specific template set from the
|
// parsePageTemplate parses a page-specific template set from the
|
||||||
// embedded FS. Each page template is combined with the shared
|
// embedded FS. Each page template is combined with the shared
|
||||||
// base, htmlheader, and navbar templates, and with any further files
|
// base, htmlheader, navbar and notice templates, and with any further
|
||||||
// the page includes. The page file must be listed first so that its
|
// files the page includes. The page file must be listed first so that
|
||||||
// root action ({{template "base" .}}) becomes the template set's entry
|
// its root action ({{template "base" .}}) becomes the template set's
|
||||||
// point.
|
// entry point.
|
||||||
func parsePageTemplate(
|
func parsePageTemplate(
|
||||||
pageFile string, included ...string,
|
pageFile string, included ...string,
|
||||||
) *template.Template {
|
) *template.Template {
|
||||||
@@ -109,6 +109,7 @@ func parsePageTemplate(
|
|||||||
"base.html",
|
"base.html",
|
||||||
"htmlheader.html",
|
"htmlheader.html",
|
||||||
"navbar.html",
|
"navbar.html",
|
||||||
|
"notice.html",
|
||||||
}, included...)
|
}, included...)
|
||||||
|
|
||||||
return template.Must(
|
return template.Must(
|
||||||
@@ -209,11 +210,13 @@ func (s *Handlers) renderError(
|
|||||||
// served outside the routes where NoCache runs.
|
// served outside the routes where NoCache runs.
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|
||||||
|
// No notice: one would say an action worked above a page saying
|
||||||
|
// the request failed.
|
||||||
data := s.pageData(r, map[string]any{
|
data := s.pageData(r, map[string]any{
|
||||||
"Status": status,
|
"Status": status,
|
||||||
"StatusText": http.StatusText(status),
|
"StatusText": http.StatusText(status),
|
||||||
"Message": errorPageText(status),
|
"Message": errorPageText(status),
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
@@ -267,6 +270,7 @@ type templateDataWrapper struct {
|
|||||||
User *UserInfo
|
User *UserInfo
|
||||||
CSRFToken string
|
CSRFToken string
|
||||||
Version string
|
Version string
|
||||||
|
Notice *notice
|
||||||
Data any
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,12 +315,15 @@ func (s *Handlers) renderTemplate(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.executeTemplate(w, r, tmpl, s.pageData(r, data))
|
s.executeTemplate(w, r, tmpl, s.pageData(r, data, noticeFor(r)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// pageData adds the fields the shared layout renders to a page's own
|
// pageData adds the fields the shared layout renders to a page's own
|
||||||
// data.
|
// data. The layout shows the notice, when there is one, above the
|
||||||
func (s *Handlers) pageData(r *http.Request, data any) any {
|
// page.
|
||||||
|
func (s *Handlers) pageData(
|
||||||
|
r *http.Request, data any, pageNotice *notice,
|
||||||
|
) any {
|
||||||
userInfo := s.getUserInfo(r)
|
userInfo := s.getUserInfo(r)
|
||||||
csrfToken := middleware.CSRFToken(r)
|
csrfToken := middleware.CSRFToken(r)
|
||||||
|
|
||||||
@@ -330,6 +337,7 @@ func (s *Handlers) pageData(r *http.Request, data any) any {
|
|||||||
m["User"] = userInfo
|
m["User"] = userInfo
|
||||||
m["CSRFToken"] = csrfToken
|
m["CSRFToken"] = csrfToken
|
||||||
m["Version"] = version
|
m["Version"] = version
|
||||||
|
m["Notice"] = pageNotice
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
@@ -338,6 +346,7 @@ func (s *Handlers) pageData(r *http.Request, data any) any {
|
|||||||
User: userInfo,
|
User: userInfo,
|
||||||
CSRFToken: csrfToken,
|
CSRFToken: csrfToken,
|
||||||
Version: version,
|
Version: version,
|
||||||
|
Notice: pageNotice,
|
||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// noticeParam is the query parameter an action's redirect carries its
|
||||||
|
// notice code in.
|
||||||
|
const noticeParam = "notice"
|
||||||
|
|
||||||
|
// noticeCode names one of the fixed lines noticeFor knows. An action
|
||||||
|
// redirects with the code rather than the line, so nothing a client
|
||||||
|
// puts in the URL reaches the page: a code noticeFor does not know
|
||||||
|
// shows nothing.
|
||||||
|
type noticeCode string
|
||||||
|
|
||||||
|
// The codes of the actions on the webhook pages and of signing out.
|
||||||
|
// Replay's codes, with the reasons a replay can be refused, and
|
||||||
|
// resubmit's codes are defined beside those actions.
|
||||||
|
const (
|
||||||
|
webhookCreated noticeCode = "webhook-created"
|
||||||
|
webhookSaved noticeCode = "webhook-saved"
|
||||||
|
webhookDeleted noticeCode = "webhook-deleted"
|
||||||
|
entrypointAdded noticeCode = "entrypoint-added"
|
||||||
|
entrypointDeleted noticeCode = "entrypoint-deleted"
|
||||||
|
entrypointActivated noticeCode = "entrypoint-activated"
|
||||||
|
entrypointDeactivated noticeCode = "entrypoint-deactivated"
|
||||||
|
targetAdded noticeCode = "target-added"
|
||||||
|
targetSaved noticeCode = "target-saved"
|
||||||
|
targetDeleted noticeCode = "target-deleted"
|
||||||
|
targetActivated noticeCode = "target-activated"
|
||||||
|
targetDeactivated noticeCode = "target-deactivated"
|
||||||
|
signedOut noticeCode = "signed-out"
|
||||||
|
)
|
||||||
|
|
||||||
|
// notice is the line templates/notice.html shows above a page to say
|
||||||
|
// what an action did.
|
||||||
|
type notice struct {
|
||||||
|
Text string
|
||||||
|
|
||||||
|
// Failed shows the line as an error: the action was refused.
|
||||||
|
Failed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// noticeFor returns the notice the request's URL names, or nil when it
|
||||||
|
// names none or an unknown code.
|
||||||
|
func noticeFor(r *http.Request) *notice {
|
||||||
|
n, ok := map[noticeCode]notice{
|
||||||
|
webhookCreated: {Text: "Webhook created."},
|
||||||
|
webhookSaved: {Text: "Webhook saved."},
|
||||||
|
webhookDeleted: {Text: "Webhook deleted."},
|
||||||
|
entrypointAdded: {Text: "Entrypoint added."},
|
||||||
|
entrypointDeleted: {Text: "Entrypoint deleted."},
|
||||||
|
entrypointActivated: {Text: "Entrypoint activated."},
|
||||||
|
entrypointDeactivated: {Text: "Entrypoint deactivated."},
|
||||||
|
targetAdded: {Text: "Target added."},
|
||||||
|
targetSaved: {Text: "Target saved."},
|
||||||
|
targetDeleted: {Text: "Target deleted."},
|
||||||
|
targetActivated: {Text: "Target activated."},
|
||||||
|
targetDeactivated: {Text: "Target deactivated."},
|
||||||
|
signedOut: {Text: "Signed out."},
|
||||||
|
|
||||||
|
replayQueued: {
|
||||||
|
Text: "Replay queued: a new delivery was created " +
|
||||||
|
"against the target's current configuration.",
|
||||||
|
},
|
||||||
|
replayTargetDeleted: {
|
||||||
|
Text: "Not replayed: the target this delivery was for " +
|
||||||
|
"has been deleted. Recreate the target, then replay.",
|
||||||
|
Failed: true,
|
||||||
|
},
|
||||||
|
replayTargetMissing: {
|
||||||
|
Text: "Not replayed: the target this delivery was for " +
|
||||||
|
"no longer exists.",
|
||||||
|
Failed: true,
|
||||||
|
},
|
||||||
|
replayTargetInactive: {
|
||||||
|
Text: "Not replayed: the target this delivery was for " +
|
||||||
|
"is deactivated. Activate it, then replay.",
|
||||||
|
Failed: true,
|
||||||
|
},
|
||||||
|
replayNotTerminal: {
|
||||||
|
Text: "Not replayed: this delivery has not finished yet.",
|
||||||
|
Failed: true,
|
||||||
|
},
|
||||||
|
replayInFlight: {
|
||||||
|
Text: "Not replayed: a delivery of this event to this " +
|
||||||
|
"target is already in flight.",
|
||||||
|
Failed: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
resubmitQueued: {
|
||||||
|
Text: "Resubmitted: a new event was created from the " +
|
||||||
|
"stored one and queued to every active target.",
|
||||||
|
},
|
||||||
|
resubmitNoTargets: {
|
||||||
|
Text: "Resubmitted: a new event was created, but this " +
|
||||||
|
"source has no active targets, so nothing was queued.",
|
||||||
|
},
|
||||||
|
}[noticeCode(r.URL.Query().Get(noticeParam))]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &n
|
||||||
|
}
|
||||||
|
|
||||||
|
// withNotice returns path with code added as its notice.
|
||||||
|
func withNotice(path string, code noticeCode) string {
|
||||||
|
return path + "?" + noticeParam + "=" + string(code)
|
||||||
|
}
|
||||||
@@ -411,7 +411,9 @@ func TestHandleSourceDelete_RemovesConfigAndEventDatabase(
|
|||||||
h.HandleSourceDelete().ServeHTTP(w, req)
|
h.HandleSourceDelete().ServeHTTP(w, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(t, "/hooks", w.Header().Get("Location"))
|
assert.Equal(
|
||||||
|
t, "/hooks?notice=webhook-deleted", w.Header().Get("Location"),
|
||||||
|
)
|
||||||
|
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t, int64(0),
|
t, int64(0),
|
||||||
|
|||||||
@@ -322,7 +322,8 @@ func (h *Handlers) createWebhookWithEntrypoint(
|
|||||||
)
|
)
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
w, r, withNotice("/hook/"+webhook.ID, webhookCreated),
|
||||||
|
http.StatusSeeOther,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +580,8 @@ func (h *Handlers) applyWebhookEdit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
w, r, withNotice("/hook/"+webhook.ID, webhookSaved),
|
||||||
|
http.StatusSeeOther,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,7 +664,9 @@ func (h *Handlers) deleteWebhookResources(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/hooks", http.StatusSeeOther)
|
http.Redirect(
|
||||||
|
w, r, withNotice("/hooks", webhookDeleted), http.StatusSeeOther,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitWebhookDeletion soft-deletes a webhook's entrypoints,
|
// commitWebhookDeletion soft-deletes a webhook's entrypoints,
|
||||||
@@ -841,31 +845,16 @@ func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
|
|||||||
totalPages++
|
totalPages++
|
||||||
}
|
}
|
||||||
|
|
||||||
// The banner a replay or resubmit POST redirected back
|
|
||||||
// with. The message comes from a fixed set keyed by the
|
|
||||||
// outcome code, never from the query string itself.
|
|
||||||
replayMsg, replayOK := replayOutcome(
|
|
||||||
r.URL.Query().Get(replayOutcomeParam),
|
|
||||||
)
|
|
||||||
|
|
||||||
resubmitMsg, resubmitOK := resubmitOutcome(
|
|
||||||
r.URL.Query().Get(resubmitOutcomeParam),
|
|
||||||
)
|
|
||||||
|
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
tmplKeyWebhook: &webhook,
|
tmplKeyWebhook: &webhook,
|
||||||
"Events": evts,
|
"Events": evts,
|
||||||
"ReplayMessage": replayMsg,
|
"Page": page,
|
||||||
"ReplayQueued": replayOK,
|
"TotalPages": totalPages,
|
||||||
"ResubmitMessage": resubmitMsg,
|
"TotalEvents": total,
|
||||||
"ResubmitQueued": resubmitOK,
|
"HasPrev": page > 1,
|
||||||
"Page": page,
|
"HasNext": page < totalPages,
|
||||||
"TotalPages": totalPages,
|
"PrevPage": page - 1,
|
||||||
"TotalEvents": total,
|
"NextPage": page + 1,
|
||||||
"HasPrev": page > 1,
|
|
||||||
"HasNext": page < totalPages,
|
|
||||||
"PrevPage": page - 1,
|
|
||||||
"NextPage": page + 1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h.renderTemplate(w, r, "source_logs.html", data)
|
h.renderTemplate(w, r, "source_logs.html", data)
|
||||||
@@ -1254,7 +1243,8 @@ func (h *Handlers) HandleEntrypointCreate() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
w, r, withNotice("/hook/"+webhook.ID, entrypointAdded),
|
||||||
|
http.StatusSeeOther,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1365,7 +1355,8 @@ func (h *Handlers) processTargetCreate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
w, r, withNotice("/hook/"+webhook.ID, targetAdded),
|
||||||
|
http.StatusSeeOther,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1643,6 +1634,7 @@ func (h *Handlers) HandleEntrypointDelete() http.HandlerFunc {
|
|||||||
"entrypointID", &database.Entrypoint{},
|
"entrypointID", &database.Entrypoint{},
|
||||||
"failed to delete entrypoint",
|
"failed to delete entrypoint",
|
||||||
nil,
|
nil,
|
||||||
|
entrypointDeleted,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1655,18 +1647,21 @@ func (h *Handlers) HandleTargetDelete() http.HandlerFunc {
|
|||||||
"targetID", &database.Target{},
|
"targetID", &database.Target{},
|
||||||
"failed to delete target",
|
"failed to delete target",
|
||||||
h.evictArchiveWriterIfUnused,
|
h.evictArchiveWriterIfUnused,
|
||||||
|
targetDeleted,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteChildResource returns a handler that deletes a child
|
// deleteChildResource returns a handler that deletes a child
|
||||||
// resource (entrypoint or target) belonging to a webhook. The
|
// resource (entrypoint or target) belonging to a webhook. The
|
||||||
// optional afterDelete hook runs with the webhook's id once the
|
// optional afterDelete hook runs with the webhook's id once the
|
||||||
// delete has succeeded, before the redirect.
|
// delete has succeeded, before the redirect, which carries done as
|
||||||
|
// its notice.
|
||||||
func (h *Handlers) deleteChildResource(
|
func (h *Handlers) deleteChildResource(
|
||||||
idParam string,
|
idParam string,
|
||||||
model any,
|
model any,
|
||||||
errMsg string,
|
errMsg string,
|
||||||
afterDelete func(webhookID string),
|
afterDelete func(webhookID string),
|
||||||
|
done noticeCode,
|
||||||
) http.HandlerFunc {
|
) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := h.getUserID(r)
|
userID, ok := h.getUserID(r)
|
||||||
@@ -1708,7 +1703,7 @@ func (h *Handlers) deleteChildResource(
|
|||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r,
|
w, r,
|
||||||
"/hook/"+webhook.ID,
|
withNotice("/hook/"+webhook.ID, done),
|
||||||
http.StatusSeeOther,
|
http.StatusSeeOther,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1719,7 +1714,7 @@ func (h *Handlers) deleteChildResource(
|
|||||||
func (h *Handlers) HandleEntrypointToggle() http.HandlerFunc {
|
func (h *Handlers) HandleEntrypointToggle() http.HandlerFunc {
|
||||||
return h.toggleChildResource(
|
return h.toggleChildResource(
|
||||||
"entrypointID",
|
"entrypointID",
|
||||||
func(webhookID, childID string) error {
|
func(webhookID, childID string) (bool, error) {
|
||||||
var ep database.Entrypoint
|
var ep database.Entrypoint
|
||||||
|
|
||||||
err := h.db.DB().Where(
|
err := h.db.DB().Where(
|
||||||
@@ -1727,14 +1722,15 @@ func (h *Handlers) HandleEntrypointToggle() http.HandlerFunc {
|
|||||||
childID, webhookID,
|
childID, webhookID,
|
||||||
).First(&ep).Error
|
).First(&ep).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ep.Active = !ep.Active
|
ep.Active = !ep.Active
|
||||||
|
|
||||||
return h.db.DB().Save(&ep).Error
|
return ep.Active, h.db.DB().Save(&ep).Error
|
||||||
},
|
},
|
||||||
"failed to toggle entrypoint",
|
"failed to toggle entrypoint",
|
||||||
|
entrypointActivated, entrypointDeactivated,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1742,7 +1738,7 @@ func (h *Handlers) HandleEntrypointToggle() http.HandlerFunc {
|
|||||||
func (h *Handlers) HandleTargetToggle() http.HandlerFunc {
|
func (h *Handlers) HandleTargetToggle() http.HandlerFunc {
|
||||||
return h.toggleChildResource(
|
return h.toggleChildResource(
|
||||||
"targetID",
|
"targetID",
|
||||||
func(webhookID, childID string) error {
|
func(webhookID, childID string) (bool, error) {
|
||||||
var tgt database.Target
|
var tgt database.Target
|
||||||
|
|
||||||
err := h.db.DB().Where(
|
err := h.db.DB().Where(
|
||||||
@@ -1750,23 +1746,27 @@ func (h *Handlers) HandleTargetToggle() http.HandlerFunc {
|
|||||||
childID, webhookID,
|
childID, webhookID,
|
||||||
).First(&tgt).Error
|
).First(&tgt).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tgt.Active = !tgt.Active
|
tgt.Active = !tgt.Active
|
||||||
|
|
||||||
return h.db.DB().Save(&tgt).Error
|
return tgt.Active, h.db.DB().Save(&tgt).Error
|
||||||
},
|
},
|
||||||
"failed to toggle target",
|
"failed to toggle target",
|
||||||
|
targetActivated, targetDeactivated,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggleChildResource returns a handler that toggles the active
|
// toggleChildResource returns a handler that toggles the active
|
||||||
// state of a child resource belonging to a webhook.
|
// state of a child resource belonging to a webhook. toggleFn returns
|
||||||
|
// the new state, and the redirect carries activated or deactivated as
|
||||||
|
// its notice to match.
|
||||||
func (h *Handlers) toggleChildResource(
|
func (h *Handlers) toggleChildResource(
|
||||||
idParam string,
|
idParam string,
|
||||||
toggleFn func(webhookID, childID string) error,
|
toggleFn func(webhookID, childID string) (bool, error),
|
||||||
errMsg string,
|
errMsg string,
|
||||||
|
activated, deactivated noticeCode,
|
||||||
) http.HandlerFunc {
|
) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := h.getUserID(r)
|
userID, ok := h.getUserID(r)
|
||||||
@@ -1792,16 +1792,21 @@ func (h *Handlers) toggleChildResource(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = toggleFn(webhook.ID, childID)
|
active, err := toggleFn(webhook.ID, childID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.serverError(w, r, errMsg, err)
|
h.serverError(w, r, errMsg, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
done := deactivated
|
||||||
|
if active {
|
||||||
|
done = activated
|
||||||
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r,
|
w, r,
|
||||||
"/hook/"+webhook.ID,
|
withNotice("/hook/"+webhook.ID, done),
|
||||||
http.StatusSeeOther,
|
http.StatusSeeOther,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ func (h *Handlers) applyTargetEdit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
w, r, withNotice("/hook/"+webhook.ID, targetSaved),
|
||||||
|
http.StatusSeeOther,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,22 @@ func TestErrorPage_DeletedTarget(t *testing.T) {
|
|||||||
assertErrorPage(t, w, http.StatusNotFound, backToWebhooks)
|
assertErrorPage(t, w, http.StatusNotFound, backToWebhooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestErrorPage_ShowsNoNotice pins that a notice code in the URL of a
|
||||||
|
// page that fails is not shown above the error.
|
||||||
|
func TestErrorPage_ShowsNoNotice(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t)
|
||||||
|
|
||||||
|
userID, _ := env.seedUser(t, "owner", "somepassword")
|
||||||
|
cookies := env.authCookies(t, userID, "owner")
|
||||||
|
|
||||||
|
w := env.get("/hook/no-such-webhook?notice=webhook-saved", cookies)
|
||||||
|
|
||||||
|
assertErrorPage(t, w, http.StatusNotFound, backToWebhooks)
|
||||||
|
assert.NotContains(t, w.Body.String(), "Webhook saved.")
|
||||||
|
}
|
||||||
|
|
||||||
func TestErrorPage_UnknownPath(t *testing.T) {
|
func TestErrorPage_UnknownPath(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -263,6 +263,24 @@ func (e *testEnv) urlFrom(
|
|||||||
return html.UnescapeString(match[1])
|
return html.UnescapeString(match[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requireNotice requires w to redirect to dest carrying the notice
|
||||||
|
// code, then renders that page and requires it to show text.
|
||||||
|
func (e *testEnv) requireNotice(
|
||||||
|
t *testing.T,
|
||||||
|
w *httptest.ResponseRecorder,
|
||||||
|
dest, code, text string,
|
||||||
|
cookies []*http.Cookie,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
|
require.Equal(t, dest+"?notice="+code, w.Header().Get("Location"))
|
||||||
|
|
||||||
|
page := e.get(w.Header().Get("Location"), cookies)
|
||||||
|
require.Equal(t, http.StatusOK, page.Code)
|
||||||
|
assert.Contains(t, page.Body.String(), text)
|
||||||
|
}
|
||||||
|
|
||||||
// authCookies forges an authenticated session for the given user.
|
// authCookies forges an authenticated session for the given user.
|
||||||
func (e *testEnv) authCookies(
|
func (e *testEnv) authCookies(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
@@ -741,6 +759,31 @@ func TestPagesLogin_ReturnsToTheRequestedPage(t *testing.T) {
|
|||||||
assert.Equal(t, asked, w.Header().Get("Location"))
|
assert.Equal(t, asked, w.Header().Get("Location"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPagesLogout_SaysSignedOut signs out with the navbar's form and
|
||||||
|
// lands on the sign-in page, which says so.
|
||||||
|
func TestPagesLogout_SaysSignedOut(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t)
|
||||||
|
|
||||||
|
userID, _ := env.seedUser(t, "leaver", "somepassword")
|
||||||
|
token, cookies := env.csrfFrom(
|
||||||
|
t, "/hooks", env.authCookies(t, userID, "leaver"),
|
||||||
|
)
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("csrf_token", token)
|
||||||
|
|
||||||
|
w := env.post(
|
||||||
|
env.urlFrom(t, "/hooks", `action="(/pages/logout)"`, cookies),
|
||||||
|
form, cookies,
|
||||||
|
)
|
||||||
|
|
||||||
|
// The sign-in page is requested without the session cookie, which
|
||||||
|
// the logout told the browser to delete.
|
||||||
|
env.requireNotice(t, w, "/pages/login", "signed-out", "Signed out.", nil)
|
||||||
|
}
|
||||||
|
|
||||||
// --- /user/{username} group ---
|
// --- /user/{username} group ---
|
||||||
|
|
||||||
// TestPasswordChange_OversizeBody_RejectedAndPasswordUnchanged
|
// TestPasswordChange_OversizeBody_RejectedAndPasswordUnchanged
|
||||||
@@ -858,9 +901,9 @@ func TestHooks_ListAndNewWebhookForm(t *testing.T) {
|
|||||||
require.NoError(t,
|
require.NoError(t,
|
||||||
env.db.DB().Where("name = ?", "created").First(&created).Error,
|
env.db.DB().Where("name = ?", "created").First(&created).Error,
|
||||||
)
|
)
|
||||||
assert.Equal(
|
env.requireNotice(
|
||||||
t, "/hook/"+created.ID, w.Header().Get("Location"),
|
t, w, "/hook/"+created.ID, "webhook-created", "Webhook created.",
|
||||||
"creating a webhook should redirect to its page",
|
cookies,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,8 +934,7 @@ func TestHook_EditFormAndDelete(t *testing.T) {
|
|||||||
env.urlFrom(t, editPage, `action="(/hook/[^/"]+/edit)"`, cookies),
|
env.urlFrom(t, editPage, `action="(/hook/[^/"]+/edit)"`, cookies),
|
||||||
form, cookies,
|
form, cookies,
|
||||||
)
|
)
|
||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
env.requireNotice(t, w, page, "webhook-saved", "Webhook saved.", cookies)
|
||||||
assert.Equal(t, page, w.Header().Get("Location"))
|
|
||||||
|
|
||||||
var edited database.Webhook
|
var edited database.Webhook
|
||||||
|
|
||||||
@@ -906,16 +948,17 @@ func TestHook_EditFormAndDelete(t *testing.T) {
|
|||||||
env.urlFrom(t, page, `action="(/hook/[^/"]+/delete)"`, cookies),
|
env.urlFrom(t, page, `action="(/hook/[^/"]+/delete)"`, cookies),
|
||||||
form, cookies,
|
form, cookies,
|
||||||
)
|
)
|
||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
env.requireNotice(
|
||||||
assert.Equal(t, "/hooks", w.Header().Get("Location"))
|
t, w, "/hooks", "webhook-deleted", "Webhook deleted.", cookies,
|
||||||
|
)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t, http.StatusNotFound, env.get(page, cookies).Code,
|
t, http.StatusNotFound, env.get(page, cookies).Code,
|
||||||
"a deleted webhook's page should be gone",
|
"a deleted webhook's page should be gone",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHook_EntrypointActions adds, deactivates and deletes an
|
// TestHook_EntrypointActions adds, deactivates, activates and deletes
|
||||||
// entrypoint with the forms on the webhook page, each submitted to
|
// an entrypoint with the forms on the webhook page, each submitted to
|
||||||
// the action and with the token the page rendered.
|
// the action and with the token the page rendered.
|
||||||
func TestHook_EntrypointActions(t *testing.T) {
|
func TestHook_EntrypointActions(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -933,16 +976,19 @@ func TestHook_EntrypointActions(t *testing.T) {
|
|||||||
form.Set("csrf_token", token)
|
form.Set("csrf_token", token)
|
||||||
|
|
||||||
// submit posts the webhook page's form whose action pattern
|
// submit posts the webhook page's form whose action pattern
|
||||||
// captures, and requires the redirect back to that page.
|
// captures, and requires the redirect back to that page with the
|
||||||
submit := func(pattern string) {
|
// notice code, and the page to show text.
|
||||||
|
submit := func(pattern, code, text string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
w := env.post(env.urlFrom(t, page, pattern, cookies), form, cookies)
|
w := env.post(env.urlFrom(t, page, pattern, cookies), form, cookies)
|
||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
env.requireNotice(t, w, page, code, text, cookies)
|
||||||
require.Equal(t, page, w.Header().Get("Location"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submit(`action="(/hook/[^/"]+/entrypoints)"`)
|
toggle := `action="(/hook/[^/"]+/entrypoints/[^/"]+/toggle)"`
|
||||||
|
|
||||||
|
submit(`action="(/hook/[^/"]+/entrypoints)"`,
|
||||||
|
"entrypoint-added", "Entrypoint added.")
|
||||||
|
|
||||||
var added database.Entrypoint
|
var added database.Entrypoint
|
||||||
|
|
||||||
@@ -951,7 +997,7 @@ func TestHook_EntrypointActions(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.True(t, added.Active)
|
require.True(t, added.Active)
|
||||||
|
|
||||||
submit(`action="(/hook/[^/"]+/entrypoints/[^/"]+/toggle)"`)
|
submit(toggle, "entrypoint-deactivated", "Entrypoint deactivated.")
|
||||||
|
|
||||||
var toggled database.Entrypoint
|
var toggled database.Entrypoint
|
||||||
|
|
||||||
@@ -960,7 +1006,10 @@ func TestHook_EntrypointActions(t *testing.T) {
|
|||||||
)
|
)
|
||||||
assert.False(t, toggled.Active, "the toggle should deactivate it")
|
assert.False(t, toggled.Active, "the toggle should deactivate it")
|
||||||
|
|
||||||
submit(`action="(/hook/[^/"]+/entrypoints/[^/"]+/delete)"`)
|
submit(toggle, "entrypoint-activated", "Entrypoint activated.")
|
||||||
|
|
||||||
|
submit(`action="(/hook/[^/"]+/entrypoints/[^/"]+/delete)"`,
|
||||||
|
"entrypoint-deleted", "Entrypoint deleted.")
|
||||||
|
|
||||||
var left int64
|
var left int64
|
||||||
|
|
||||||
@@ -971,8 +1020,8 @@ func TestHook_EntrypointActions(t *testing.T) {
|
|||||||
|
|
||||||
// TestHook_TargetActions adds a target with the form on the webhook
|
// TestHook_TargetActions adds a target with the form on the webhook
|
||||||
// page, follows its Edit link to the target edit form and submits
|
// page, follows its Edit link to the target edit form and submits
|
||||||
// it, then deactivates and deletes it, every URL and token taken from
|
// it, then deactivates, activates and deletes it, every URL and token
|
||||||
// the rendered pages.
|
// taken from the rendered pages.
|
||||||
func TestHook_TargetActions(t *testing.T) {
|
func TestHook_TargetActions(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -987,27 +1036,29 @@ func TestHook_TargetActions(t *testing.T) {
|
|||||||
|
|
||||||
// submit posts form, with the token, to the action pattern
|
// submit posts form, with the token, to the action pattern
|
||||||
// captures on the page at from, and requires the redirect back to
|
// captures on the page at from, and requires the redirect back to
|
||||||
// the webhook page.
|
// the webhook page with the notice code, and that page to show
|
||||||
submit := func(from, pattern string, form url.Values) {
|
// text.
|
||||||
|
submit := func(from, pattern string, form url.Values, code, text string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
form.Set("csrf_token", token)
|
form.Set("csrf_token", token)
|
||||||
|
|
||||||
w := env.post(env.urlFrom(t, from, pattern, cookies), form, cookies)
|
w := env.post(env.urlFrom(t, from, pattern, cookies), form, cookies)
|
||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
env.requireNotice(t, w, page, code, text, cookies)
|
||||||
require.Equal(t, page, w.Header().Get("Location"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggle := `action="(/hook/[^/"]+/targets/[^/"]+/toggle)"`
|
||||||
|
|
||||||
submit(page, `action="(/hook/[^/"]+/targets)"`, url.Values{
|
submit(page, `action="(/hook/[^/"]+/targets)"`, url.Values{
|
||||||
"name": {"added"},
|
"name": {"added"},
|
||||||
"type": {string(database.TargetTypeLog)},
|
"type": {string(database.TargetTypeLog)},
|
||||||
})
|
}, "target-added", "Target added.")
|
||||||
|
|
||||||
editPage := env.urlFrom(
|
editPage := env.urlFrom(
|
||||||
t, page, `href="(/hook/[^/"]+/targets/[^/"]+/edit)"`, cookies,
|
t, page, `href="(/hook/[^/"]+/targets/[^/"]+/edit)"`, cookies,
|
||||||
)
|
)
|
||||||
submit(editPage, `action="(/hook/[^/"]+/targets/[^/"]+/edit)"`,
|
submit(editPage, `action="(/hook/[^/"]+/targets/[^/"]+/edit)"`,
|
||||||
url.Values{"name": {"renamed"}})
|
url.Values{"name": {"renamed"}}, "target-saved", "Target saved.")
|
||||||
|
|
||||||
var edited database.Target
|
var edited database.Target
|
||||||
|
|
||||||
@@ -1017,8 +1068,8 @@ func TestHook_TargetActions(t *testing.T) {
|
|||||||
assert.Equal(t, "renamed", edited.Name)
|
assert.Equal(t, "renamed", edited.Name)
|
||||||
require.True(t, edited.Active)
|
require.True(t, edited.Active)
|
||||||
|
|
||||||
submit(page, `action="(/hook/[^/"]+/targets/[^/"]+/toggle)"`,
|
submit(page, toggle, url.Values{},
|
||||||
url.Values{})
|
"target-deactivated", "Target deactivated.")
|
||||||
|
|
||||||
var toggled database.Target
|
var toggled database.Target
|
||||||
|
|
||||||
@@ -1027,8 +1078,11 @@ func TestHook_TargetActions(t *testing.T) {
|
|||||||
)
|
)
|
||||||
assert.False(t, toggled.Active, "the toggle should deactivate it")
|
assert.False(t, toggled.Active, "the toggle should deactivate it")
|
||||||
|
|
||||||
|
submit(page, toggle, url.Values{},
|
||||||
|
"target-activated", "Target activated.")
|
||||||
|
|
||||||
submit(page, `action="(/hook/[^/"]+/targets/[^/"]+/delete)"`,
|
submit(page, `action="(/hook/[^/"]+/targets/[^/"]+/delete)"`,
|
||||||
url.Values{})
|
url.Values{}, "target-deleted", "Target deleted.")
|
||||||
|
|
||||||
var left int64
|
var left int64
|
||||||
|
|
||||||
@@ -1064,9 +1118,9 @@ func TestHook_ResubmitFromEventLog(t *testing.T) {
|
|||||||
env.urlFrom(t, logsPath, `action="(/hook/[^"]+/resubmit)"`, cookies),
|
env.urlFrom(t, logsPath, `action="(/hook/[^"]+/resubmit)"`, cookies),
|
||||||
form, cookies,
|
form, cookies,
|
||||||
)
|
)
|
||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
env.requireNotice(
|
||||||
assert.Equal(
|
t, w, logsPath, "resubmit-no-targets",
|
||||||
t, logsPath+"?resubmit=no-targets", w.Header().Get("Location"),
|
"this source has no active targets", cookies,
|
||||||
)
|
)
|
||||||
|
|
||||||
webhookDB, err := env.dbMgr.GetDB(wh.ID)
|
webhookDB, err := env.dbMgr.GetDB(wh.ID)
|
||||||
@@ -1302,10 +1356,8 @@ func TestDeliveryReplay_PostOnlyAndCSRFProtected(t *testing.T) {
|
|||||||
html.UnescapeString(action[1]), form, cookies,
|
html.UnescapeString(action[1]), form, cookies,
|
||||||
)
|
)
|
||||||
|
|
||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
env.requireNotice(
|
||||||
assert.Equal(
|
t, w, logsPath, "replay-queued", "Replay queued:", cookies,
|
||||||
t, logsPath+"?replay=queued",
|
|
||||||
w.Header().Get("Location"),
|
|
||||||
)
|
)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t, int64(2), env.countDeliveries(t, wh.ID),
|
t, int64(2), env.countDeliveries(t, wh.ID),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<body class="bg-gray-50 min-h-screen flex flex-col">
|
<body class="bg-gray-50 min-h-screen flex flex-col">
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
{{template "navbar" .}}
|
{{template "navbar" .}}
|
||||||
|
{{template "notice" .}}
|
||||||
{{block "content" .}}{{end}}
|
{{block "content" .}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{{define "notice"}}
|
||||||
|
{{with .Notice}}
|
||||||
|
<div class="max-w-6xl mx-auto px-6 pt-4">
|
||||||
|
<div class="{{if .Failed}}alert-error{{else}}alert-success{{end}}">{{.Text}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
@@ -12,14 +12,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .ReplayMessage}}
|
|
||||||
<div class="{{if .ReplayQueued}}alert-success{{else}}alert-error{{end}}">{{.ReplayMessage}}</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if .ResubmitMessage}}
|
|
||||||
<div class="{{if .ResubmitQueued}}alert-success{{else}}alert-error{{end}}">{{.ResubmitMessage}}</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="divide-y divide-gray-100">
|
<div class="divide-y divide-gray-100">
|
||||||
{{range .Events}}
|
{{range .Events}}
|
||||||
|
|||||||
Reference in New Issue
Block a user