Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09b2115677 |
@@ -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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -94,10 +94,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 {
|
||||||
@@ -106,6 +106,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(
|
||||||
@@ -206,11 +207,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
|
||||||
|
|
||||||
@@ -264,6 +267,7 @@ type templateDataWrapper struct {
|
|||||||
User *UserInfo
|
User *UserInfo
|
||||||
CSRFToken string
|
CSRFToken string
|
||||||
Version string
|
Version string
|
||||||
|
Notice *notice
|
||||||
Data any
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,12 +312,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)
|
||||||
|
|
||||||
@@ -327,6 +334,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
|
||||||
}
|
}
|
||||||
@@ -335,6 +343,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 and resubmit's are beside those actions, with the reasons
|
||||||
|
// each can be refused.
|
||||||
|
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()
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,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,
|
||||||
@@ -738,6 +756,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
|
||||||
@@ -855,9 +898,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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,8 +931,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
|
||||||
|
|
||||||
@@ -903,16 +945,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()
|
||||||
@@ -930,16 +973,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
|
||||||
|
|
||||||
@@ -948,7 +994,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
|
||||||
|
|
||||||
@@ -957,7 +1003,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
|
||||||
|
|
||||||
@@ -968,8 +1017,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()
|
||||||
|
|
||||||
@@ -984,27 +1033,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
|
||||||
|
|
||||||
@@ -1014,8 +1065,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
|
||||||
|
|
||||||
@@ -1024,8 +1075,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
|
||||||
|
|
||||||
@@ -1061,9 +1115,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)
|
||||||
@@ -1299,10 +1353,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