check / check (push) Successful in 3m18s
The event log had no way to list only the events whose delivery failed, and once it showed only the 50 newest, an older failure could not be found at all. It now has All, Failed (N) and Pending (N) links, carried in a `show` query parameter, so they work without the page's script library. Each filtered list keeps the 50-row limit and newest-first order, and lists an event once. It finds matching deliveries through `idx_deliveries_status` and looks their events up by ID, so its cost follows the matches, not the webhook's size. Replay returns to the list it was pressed in. The heading line says what a filter counts. Model: opus-5-5
648 lines
17 KiB
Go
648 lines
17 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
"sneak.berlin/go/webhooker/internal/handlers"
|
|
"sneak.berlin/go/webhooker/internal/session"
|
|
)
|
|
|
|
// paramDeliveryID is the chi URL parameter name the replay handler
|
|
// reads.
|
|
const paramDeliveryID = "deliveryID"
|
|
|
|
// replayTargetURL is a public destination, so a target configured with
|
|
// it is one the SSRF guard would accept. Nothing in these tests
|
|
// dispatches to it: the notifier is recorded, not run.
|
|
const replayTargetURL = "http://93.184.216.34/hook"
|
|
|
|
// seedFailedDelivery records an event, a terminally failed delivery of
|
|
// it to the given target, and the attempt that failed.
|
|
func seedFailedDelivery(
|
|
t *testing.T,
|
|
dbMgr *database.WebhookDBManager,
|
|
webhookID, targetID string,
|
|
) (*database.Event, *database.Delivery) {
|
|
t.Helper()
|
|
|
|
webhookDB, err := dbMgr.GetDB(webhookID)
|
|
require.NoError(t, err)
|
|
|
|
event := &database.Event{
|
|
WebhookID: webhookID,
|
|
EntrypointID: "entrypoint-" + webhookID,
|
|
Method: http.MethodPost,
|
|
Headers: `{"X-Test":["yes"]}`,
|
|
Body: `{"replay":"me"}`,
|
|
ContentType: contentTypeJSON,
|
|
}
|
|
|
|
require.NoError(t, webhookDB.Omit(
|
|
clause.Associations,
|
|
).Create(event).Error)
|
|
|
|
dlv := &database.Delivery{
|
|
EventID: event.ID,
|
|
TargetID: targetID,
|
|
Status: database.DeliveryStatusFailed,
|
|
}
|
|
|
|
require.NoError(t, webhookDB.Omit(
|
|
clause.Associations,
|
|
).Create(dlv).Error)
|
|
|
|
result := &database.DeliveryResult{
|
|
DeliveryID: dlv.ID,
|
|
AttemptNum: 1,
|
|
Success: false,
|
|
StatusCode: http.StatusBadGateway,
|
|
Error: "connection refused",
|
|
}
|
|
|
|
require.NoError(t, webhookDB.Omit(
|
|
clause.Associations,
|
|
).Create(result).Error)
|
|
|
|
return event, dlv
|
|
}
|
|
|
|
// loadDelivery reads a delivery back out of a webhook's database.
|
|
func loadDelivery(
|
|
t *testing.T, webhookDB *gorm.DB, deliveryID string,
|
|
) database.Delivery {
|
|
t.Helper()
|
|
|
|
var dlv database.Delivery
|
|
|
|
require.NoError(
|
|
t,
|
|
webhookDB.First(&dlv, "id = ?", deliveryID).Error,
|
|
)
|
|
|
|
return dlv
|
|
}
|
|
|
|
// listDeliveries reads every delivery of an event.
|
|
func listDeliveries(
|
|
t *testing.T, webhookDB *gorm.DB, eventID string,
|
|
) []database.Delivery {
|
|
t.Helper()
|
|
|
|
var deliveries []database.Delivery
|
|
|
|
require.NoError(t, webhookDB.Where(
|
|
"event_id = ?", eventID,
|
|
).Find(&deliveries).Error)
|
|
|
|
return deliveries
|
|
}
|
|
|
|
// theOtherDelivery returns the one delivery in the slice that is not
|
|
// excludeID. Identity is used rather than an ordering because the rows
|
|
// are minted milliseconds apart and their ids are random.
|
|
func theOtherDelivery(
|
|
t *testing.T,
|
|
deliveries []database.Delivery,
|
|
excludeID string,
|
|
) database.Delivery {
|
|
t.Helper()
|
|
|
|
var found []database.Delivery
|
|
|
|
for _, d := range deliveries {
|
|
if d.ID != excludeID {
|
|
found = append(found, d)
|
|
}
|
|
}
|
|
|
|
require.Len(t, found, 1)
|
|
|
|
return found[0]
|
|
}
|
|
|
|
// postReplay runs the real replay handler for one delivery.
|
|
func postReplay(
|
|
t *testing.T,
|
|
h *handlers.Handlers,
|
|
sess *session.Session,
|
|
webhookID, deliveryID string,
|
|
) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
|
|
req := postRequest(
|
|
"/hook/"+webhookID+"/deliveries/"+
|
|
deliveryID+"/replay",
|
|
authenticatedCookies(
|
|
t, sess, deleteTestUserID, deleteTestUsername,
|
|
),
|
|
map[string]string{
|
|
paramSourceID: webhookID,
|
|
paramDeliveryID: deliveryID,
|
|
},
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
h.HandleDeliveryReplay().ServeHTTP(w, req)
|
|
|
|
return w
|
|
}
|
|
|
|
// TestHandleDeliveryReplay_AppendsDeliveryAndLeavesOriginal is the
|
|
// core requirement: replaying a failed delivery succeeds, appends a
|
|
// new delivery, and leaves the original row and its recorded attempt
|
|
// exactly as they were.
|
|
//
|
|
// It also pins the two things a replay would be wrong to get from the
|
|
// original: the task carries the target's CURRENT configuration, which
|
|
// this test changes between the failure and the replay, and it carries
|
|
// the stored EVENT body rather than anything the failed attempt
|
|
// received back.
|
|
func TestHandleDeliveryReplay_AppendsDeliveryAndLeavesOriginal(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
notif *recordingNotifier
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db, &dbMgr, ¬if)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := seedWebhook(t, db)
|
|
tgt := seedConfiguredTarget(
|
|
t, db, wh.ID, database.TargetTypeHTTP,
|
|
`{"url":"`+replayTargetURL+`"}`,
|
|
)
|
|
|
|
event, original := seedFailedDelivery(
|
|
t, dbMgr, wh.ID, tgt.ID,
|
|
)
|
|
|
|
webhookDB, err := dbMgr.GetDB(wh.ID)
|
|
require.NoError(t, err)
|
|
|
|
before := loadDelivery(t, webhookDB, original.ID)
|
|
|
|
// The operator fixes the destination, which is the whole reason
|
|
// to replay. The replay must use this, not the config the
|
|
// original delivery ran against.
|
|
const fixedConfig = `{"url":"http://93.184.216.34/fixed"}`
|
|
|
|
require.NoError(t, db.DB().Model(&database.Target{}).
|
|
Where("id = ?", tgt.ID).
|
|
Update("config", fixedConfig).Error)
|
|
|
|
w := postReplay(t, h, sess, wh.ID, original.ID)
|
|
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
assert.Equal(
|
|
t,
|
|
"/hook/"+wh.ID+"/events?notice=replay-queued",
|
|
w.Header().Get("Location"),
|
|
)
|
|
|
|
deliveries := listDeliveries(t, webhookDB, event.ID)
|
|
require.Len(
|
|
t, deliveries, 2,
|
|
"replay must append a delivery, not reuse one",
|
|
)
|
|
|
|
replayed := theOtherDelivery(t, deliveries, original.ID)
|
|
assert.Equal(t, tgt.ID, replayed.TargetID)
|
|
assert.Equal(t, event.ID, replayed.EventID)
|
|
assert.Equal(
|
|
t, database.DeliveryStatusPending, replayed.Status,
|
|
)
|
|
|
|
assertDeliveryUntouched(t, webhookDB, before)
|
|
|
|
tasks := notif.Tasks()
|
|
require.Len(t, tasks, 1)
|
|
assertReplayTask(
|
|
t, tasks[0], wh.ID, event, tgt, replayed.ID, fixedConfig,
|
|
)
|
|
assertNoLeakedTarget(t, webhookDB)
|
|
}
|
|
|
|
// assertDeliveryUntouched proves a delivery row is exactly as it was
|
|
// read before: same terminal status, same timestamps, and the same
|
|
// recorded attempts.
|
|
func assertDeliveryUntouched(
|
|
t *testing.T,
|
|
webhookDB *gorm.DB,
|
|
before database.Delivery,
|
|
) {
|
|
t.Helper()
|
|
|
|
after := loadDelivery(t, webhookDB, before.ID)
|
|
assert.Equal(
|
|
t, before.Status, after.Status,
|
|
"replay must not resurrect the original delivery",
|
|
)
|
|
assert.Equal(t, before.UpdatedAt, after.UpdatedAt)
|
|
assert.Equal(t, before.CreatedAt, after.CreatedAt)
|
|
|
|
var attempts int64
|
|
|
|
require.NoError(t, webhookDB.
|
|
Model(&database.DeliveryResult{}).
|
|
Where("delivery_id = ?", before.ID).
|
|
Count(&attempts).Error)
|
|
assert.Equal(
|
|
t, int64(1), attempts,
|
|
"the original delivery's attempt history must stand",
|
|
)
|
|
}
|
|
|
|
// assertReplayTask proves the task handed to the delivery engine is
|
|
// the one the receiver would build for this event and this target, and
|
|
// that it carries wantConfig — the target's configuration as it stands
|
|
// now rather than as the original delivery ran against it.
|
|
func assertReplayTask(
|
|
t *testing.T,
|
|
task delivery.Task,
|
|
webhookID string,
|
|
event *database.Event,
|
|
target *database.Target,
|
|
wantDeliveryID, wantConfig string,
|
|
) {
|
|
t.Helper()
|
|
|
|
assert.Equal(t, wantDeliveryID, task.DeliveryID)
|
|
assert.Equal(t, event.ID, task.EventID)
|
|
assert.Equal(t, webhookID, task.WebhookID)
|
|
assert.Equal(t, event.EntrypointID, task.EntrypointID)
|
|
assert.Equal(t, target.ID, task.TargetID)
|
|
assert.Equal(t, target.Type, task.TargetType)
|
|
assert.JSONEq(
|
|
t, wantConfig, task.TargetConfig,
|
|
"replay must use the target's current configuration",
|
|
)
|
|
assert.Equal(t, event.Method, task.Method)
|
|
assert.Equal(t, event.Headers, task.Headers)
|
|
assert.Equal(t, event.ContentType, task.ContentType)
|
|
assert.Equal(t, 1, task.AttemptNum)
|
|
require.NotNil(t, task.Body)
|
|
assert.Equal(
|
|
t, event.Body, *task.Body,
|
|
"replay re-sends the stored event body",
|
|
)
|
|
}
|
|
|
|
// assertNoLeakedTarget proves the per-webhook database holds no target
|
|
// rows. AutoMigrate creates the table there because Delivery declares
|
|
// the relation, so it is a ROW that signals a leak: an association
|
|
// write would have upserted the whole target, plaintext config and
|
|
// all, into the event database. See
|
|
// https://git.eeqj.de/sneak/webhooker/issues/206.
|
|
func assertNoLeakedTarget(t *testing.T, webhookDB *gorm.DB) {
|
|
t.Helper()
|
|
|
|
var leaked int64
|
|
|
|
require.NoError(t, webhookDB.Unscoped().
|
|
Model(&database.Target{}).Count(&leaked).Error)
|
|
assert.Zero(
|
|
t, leaked,
|
|
"replay must not write the target into the event database",
|
|
)
|
|
}
|
|
|
|
// TestHandleDeliveryReplay_RefusesDeletedTarget proves the required
|
|
// refusal: a target deleted since the delivery ran is reported as
|
|
// deleted rather than erroring, and nothing is created or queued.
|
|
func TestHandleDeliveryReplay_RefusesDeletedTarget(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
notif *recordingNotifier
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db, &dbMgr, ¬if)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := seedWebhook(t, db)
|
|
tgt := seedConfiguredTarget(
|
|
t, db, wh.ID, database.TargetTypeHTTP,
|
|
`{"url":"`+replayTargetURL+`"}`,
|
|
)
|
|
|
|
event, original := seedFailedDelivery(
|
|
t, dbMgr, wh.ID, tgt.ID,
|
|
)
|
|
|
|
// Deletes are soft, so the delivery history outlives the target.
|
|
require.NoError(t, db.DB().Where(
|
|
"id = ?", tgt.ID,
|
|
).Delete(&database.Target{}).Error)
|
|
|
|
w := postReplay(t, h, sess, wh.ID, original.ID)
|
|
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
assert.Equal(
|
|
t,
|
|
"/hook/"+wh.ID+"/events?notice=replay-target-deleted",
|
|
w.Header().Get("Location"),
|
|
)
|
|
|
|
webhookDB, err := dbMgr.GetDB(wh.ID)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(
|
|
t, listDeliveries(t, webhookDB, event.ID), 1,
|
|
"a refused replay must create no delivery",
|
|
)
|
|
assert.Empty(
|
|
t, notif.Tasks(),
|
|
"a refused replay must queue nothing",
|
|
)
|
|
|
|
// The refusal is specific, which is why the target is looked up
|
|
// including soft-deleted rows: an id that never named a target
|
|
// is a different outcome, and a different message, from one the
|
|
// operator deleted.
|
|
_, orphan := seedFailedDelivery(
|
|
t, dbMgr, wh.ID, "target-that-never-existed",
|
|
)
|
|
|
|
missing := postReplay(t, h, sess, wh.ID, orphan.ID)
|
|
require.Equal(t, http.StatusSeeOther, missing.Code)
|
|
assert.Equal(
|
|
t,
|
|
"/hook/"+wh.ID+"/events?notice=replay-target-missing",
|
|
missing.Header().Get("Location"),
|
|
)
|
|
}
|
|
|
|
// TestHandleDeliveryReplay_RefusesWhileEarlierReplayInFlight proves
|
|
// the replay-storm guard: a second replay of the same event to the
|
|
// same target is refused while the first is still queued, so repeated
|
|
// submissions cannot stack copies of work the engine has not done.
|
|
func TestHandleDeliveryReplay_RefusesWhileEarlierReplayInFlight(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
notif *recordingNotifier
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db, &dbMgr, ¬if)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := seedWebhook(t, db)
|
|
tgt := seedConfiguredTarget(
|
|
t, db, wh.ID, database.TargetTypeHTTP,
|
|
`{"url":"`+replayTargetURL+`"}`,
|
|
)
|
|
|
|
event, original := seedFailedDelivery(
|
|
t, dbMgr, wh.ID, tgt.ID,
|
|
)
|
|
|
|
first := postReplay(t, h, sess, wh.ID, original.ID)
|
|
require.Equal(t, http.StatusSeeOther, first.Code)
|
|
require.Equal(
|
|
t,
|
|
"/hook/"+wh.ID+"/events?notice=replay-queued",
|
|
first.Header().Get("Location"),
|
|
)
|
|
|
|
second := postReplay(t, h, sess, wh.ID, original.ID)
|
|
require.Equal(t, http.StatusSeeOther, second.Code)
|
|
assert.Equal(
|
|
t,
|
|
"/hook/"+wh.ID+"/events?notice=replay-in-flight",
|
|
second.Header().Get("Location"),
|
|
)
|
|
|
|
webhookDB, err := dbMgr.GetDB(wh.ID)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(
|
|
t, listDeliveries(t, webhookDB, event.ID), 2,
|
|
"the refused second replay must add nothing",
|
|
)
|
|
assert.Len(
|
|
t, notif.Tasks(), 1,
|
|
"only the first replay reaches the delivery engine",
|
|
)
|
|
|
|
// A delivery the engine has not finished is not replayable
|
|
// either, which is the same rule seen from the other side.
|
|
queued := theOtherDelivery(
|
|
t, listDeliveries(t, webhookDB, event.ID), original.ID,
|
|
)
|
|
|
|
pending := postReplay(t, h, sess, wh.ID, queued.ID)
|
|
require.Equal(t, http.StatusSeeOther, pending.Code)
|
|
assert.Equal(
|
|
t,
|
|
"/hook/"+wh.ID+"/events?notice=replay-not-terminal",
|
|
pending.Header().Get("Location"),
|
|
)
|
|
}
|
|
|
|
// TestHandleDeliveryReplay_ReturnsToTheListItWasPressedIn proves a
|
|
// Replay pressed in the Failed list carries that list in its form and
|
|
// returns to it, and that a show value the event log does not know
|
|
// returns to the full log.
|
|
func TestHandleDeliveryReplay_ReturnsToTheListItWasPressedIn(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db, &dbMgr)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := seedWebhook(t, db)
|
|
tgt := seedConfiguredTarget(
|
|
t, db, wh.ID, database.TargetTypeHTTP,
|
|
`{"url":"`+replayTargetURL+`"}`,
|
|
)
|
|
|
|
_, original := seedFailedDelivery(t, dbMgr, wh.ID, tgt.ID)
|
|
|
|
assert.Contains(t, renderSourceLogsPageWithQuery(
|
|
t, h, sess, wh.ID, "?show=failed",
|
|
), `name="show" value="failed"`)
|
|
|
|
// The second replay is refused, as the first is still queued.
|
|
for _, tc := range []struct{ show, location string }{
|
|
{"failed", "/hook/" + wh.ID +
|
|
"/events?notice=replay-queued&show=failed"},
|
|
{"made-up", "/hook/" + wh.ID + "/events?notice=replay-in-flight"},
|
|
} {
|
|
req := postRequest(
|
|
"/hook/"+wh.ID+"/deliveries/"+original.ID+"/replay",
|
|
authenticatedCookies(
|
|
t, sess, deleteTestUserID, deleteTestUsername,
|
|
),
|
|
map[string]string{
|
|
paramSourceID: wh.ID,
|
|
paramDeliveryID: original.ID,
|
|
},
|
|
)
|
|
req.Body = io.NopCloser(strings.NewReader("show=" + tc.show))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.HandleDeliveryReplay().ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusSeeOther, w.Code, tc.show)
|
|
assert.Equal(t, tc.location, w.Header().Get("Location"), tc.show)
|
|
}
|
|
}
|
|
|
|
// TestHandleSourceLogs_RendersReplayControlAndBanner proves the action
|
|
// reaches the page it belongs on: a finished delivery renders a POST
|
|
// form carrying a CSRF token, and the outcome code a refusal redirects
|
|
// with becomes a readable message.
|
|
func TestHandleSourceLogs_RendersReplayControlAndBanner(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db, &dbMgr)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := seedWebhook(t, db)
|
|
tgt := seedConfiguredTarget(
|
|
t, db, wh.ID, database.TargetTypeHTTP,
|
|
`{"url":"`+replayTargetURL+`"}`,
|
|
)
|
|
|
|
_, original := seedFailedDelivery(t, dbMgr, wh.ID, tgt.ID)
|
|
|
|
body := renderSourceLogsPage(t, h, sess, wh.ID)
|
|
|
|
assert.Contains(
|
|
t, body,
|
|
`action="/hook/`+wh.ID+`/deliveries/`+
|
|
original.ID+`/replay"`,
|
|
)
|
|
assert.Contains(t, body, `method="POST"`)
|
|
assert.Contains(t, body, `name="csrf_token"`)
|
|
assert.Contains(t, body, ">Replay<")
|
|
|
|
refused := renderSourceLogsPageWithQuery(
|
|
t, h, sess, wh.ID, "?notice=replay-target-deleted",
|
|
)
|
|
|
|
assert.Contains(t, refused, "alert-error")
|
|
assert.Contains(
|
|
t, refused,
|
|
"has been deleted. Use Resubmit to send the event "+
|
|
"to the webhook",
|
|
)
|
|
|
|
// An outcome code nobody issued renders no banner at all.
|
|
unknown := renderSourceLogsPageWithQuery(
|
|
t, h, sess, wh.ID, "?notice=made-up",
|
|
)
|
|
|
|
assert.NotContains(t, unknown, "alert-error")
|
|
assert.NotContains(t, unknown, "alert-success")
|
|
assert.NotContains(t, unknown, "made-up")
|
|
}
|
|
|
|
// TestHandleDeliveryReplay_LabelsTheReplay proves a delivery created
|
|
// by Replay is labelled as a replay in the event's summary line in the
|
|
// event log, and in the list of the event's deliveries there and on
|
|
// the event's page, while the delivery it repeats is not.
|
|
func TestHandleDeliveryReplay_LabelsTheReplay(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db, &dbMgr)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := seedWebhook(t, db)
|
|
tgt := seedConfiguredTarget(
|
|
t, db, wh.ID, database.TargetTypeHTTP,
|
|
`{"url":"`+replayTargetURL+`"}`,
|
|
)
|
|
|
|
event, original := seedFailedDelivery(t, dbMgr, wh.ID, tgt.ID)
|
|
|
|
w := postReplay(t, h, sess, wh.ID, original.ID)
|
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
|
|
eventLog := renderSourceLogsPage(t, h, sess, wh.ID)
|
|
|
|
assert.Contains(t, eventLog, tgt.Name+": failed")
|
|
assert.Contains(t, eventLog, tgt.Name+" (replay): pending")
|
|
|
|
w = serveEventPage(t, h, sess, wh.ID, event.ID)
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// In each delivery list a row names the target, then the label if
|
|
// it is a replay, then its status: the replay is still pending, the
|
|
// original failed.
|
|
replayRow := tgt.Name + `</span> ` +
|
|
`<span class="text-xs text-gray-500">replay</span> ` +
|
|
`<span class="text-xs text-gray-400">pending</span>`
|
|
originalRow := tgt.Name + `</span> ` +
|
|
`<span class="text-xs text-red-600">failed</span>`
|
|
|
|
for _, page := range []string{eventLog, w.Body.String()} {
|
|
page = strings.Join(strings.Fields(page), " ")
|
|
|
|
assert.Contains(t, page, replayRow)
|
|
assert.Contains(t, page, originalRow)
|
|
}
|
|
}
|