Resubmit a stored event as a new undelivered event (closes #250)
All checks were successful
check / check (push) Successful in 3m3s
All checks were successful
check / check (push) Successful in 3m3s
Capturing real webhook traffic and firing it repeatedly at a backend under development is a primary function of this service, and per-delivery replay cannot do it: it only ever resolves the delivery's own original target, so a target created for a dev backend has no prior delivery and nothing can be replayed to it. The event log now offers a per-event Resubmit action. It stores a NEW event copying the stored one's method, headers, body and content type verbatim, and fans it out to the webhook's currently ACTIVE targets, resolved fresh by the query the receiver uses -- so a target created long after the original event arrived receives it. The original event's deliveries have no bearing on where the copy goes, inactive targets are skipped as the receiver skips them, and the action is repeatable: replay's in-flight refusal is deliberately not ported, because firing one captured event over and over is the point. The receiver and the resubmit path share one construction and one fan-out site. An eventSource value carries where the fields came from, live request or stored event, and createAndFanOut writes the event and its pending deliveries in one transaction and hands the tasks to the same Notifier, so a resubmitted delivery is retried, SSRF-guarded and circuit-broken exactly as a first one is. buildDeliveryTasks returns an error instead of writing a response, which is what lets both callers share it. The stored event is read once, before the write transaction, with a cast to blob, so a body over delivery.MaxInlineBodySize is copied byte for byte and the engine loads it from the new event row. A nullable resubmitted_from_id records provenance -- empty for an event that arrived on the receiver -- and the event log reports the relationship in both directions, without which the log is unreadable after a few resubmits of one event. The route sits in the owned-source group, so auth, CSRF and the body cap apply, with its own rate limit bucket and an events_resubmitted_total counter. Inbound signature verification is not re-run: there is no inbound signature to check on a copy an authenticated, CSRF-protected operator action submits. Per-delivery replay is unchanged; it serves recovery, which resubmit does not replace. The README claimed in four places that replay was unimplemented, one of them telling the operator that a delivery stranded by a target type change was lost; all four are corrected and resubmit is documented beside replay.
This commit is contained in:
603
internal/handlers/event_resubmit_test.go
Normal file
603
internal/handlers/event_resubmit_test.go
Normal file
@@ -0,0 +1,603 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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"
|
||||
)
|
||||
|
||||
// resubmitTargetURL 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 resubmitTargetURL = "http://93.184.216.34/hook"
|
||||
|
||||
// resubmitEventHeaders is the stored header JSON a seeded event
|
||||
// carries, so a test can prove the copy takes it verbatim.
|
||||
const resubmitEventHeaders = `{"X-Test":["yes"],"X-Trace":["abc"]}`
|
||||
|
||||
// seedStoredEvent records one event in a webhook's own database with
|
||||
// no deliveries at all, which is the state a captured event is in when
|
||||
// the operator has yet to create the target to test.
|
||||
func seedStoredEvent(
|
||||
t *testing.T,
|
||||
dbMgr *database.WebhookDBManager,
|
||||
webhookID, body string,
|
||||
) *database.Event {
|
||||
t.Helper()
|
||||
|
||||
webhookDB, err := dbMgr.GetDB(webhookID)
|
||||
require.NoError(t, err)
|
||||
|
||||
event := &database.Event{
|
||||
WebhookID: webhookID,
|
||||
EntrypointID: "entrypoint-" + webhookID,
|
||||
Method: http.MethodPost,
|
||||
Headers: resubmitEventHeaders,
|
||||
Body: body,
|
||||
ContentType: contentTypeJSON,
|
||||
}
|
||||
|
||||
require.NoError(t, webhookDB.Omit(
|
||||
clause.Associations,
|
||||
).Create(event).Error)
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// postResubmit runs the real resubmit handler for one event.
|
||||
func postResubmit(
|
||||
t *testing.T,
|
||||
h *handlers.Handlers,
|
||||
sess *session.Session,
|
||||
webhookID, eventID string,
|
||||
) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
req := postRequest(
|
||||
"/source/"+webhookID+"/events/"+eventID+"/resubmit",
|
||||
authenticatedCookies(
|
||||
t, sess, deleteTestUserID, deleteTestUsername,
|
||||
),
|
||||
map[string]string{
|
||||
paramSourceID: webhookID,
|
||||
paramEventID: eventID,
|
||||
},
|
||||
)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.HandleEventResubmit().ServeHTTP(w, req)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// listEvents reads every event in a webhook's database, oldest first.
|
||||
func listEvents(
|
||||
t *testing.T, webhookDB *gorm.DB,
|
||||
) []database.Event {
|
||||
t.Helper()
|
||||
|
||||
var events []database.Event
|
||||
|
||||
require.NoError(t, webhookDB.
|
||||
Order("created_at ASC, id ASC").
|
||||
Find(&events).Error)
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
// theOtherEvent returns the one event that is not excludeID.
|
||||
func theOtherEvent(
|
||||
t *testing.T, events []database.Event, excludeID string,
|
||||
) database.Event {
|
||||
t.Helper()
|
||||
|
||||
var found []database.Event
|
||||
|
||||
for _, e := range events {
|
||||
if e.ID != excludeID {
|
||||
found = append(found, e)
|
||||
}
|
||||
}
|
||||
|
||||
require.Len(t, found, 1)
|
||||
|
||||
return found[0]
|
||||
}
|
||||
|
||||
// TestHandleEventResubmit_DeliversToTargetCreatedAfterTheEvent is the
|
||||
// core of the feature and the thing per-delivery replay cannot do: the
|
||||
// event was captured before the target existed, so it has no delivery
|
||||
// to replay, and the resubmit must still reach the new target.
|
||||
func TestHandleEventResubmit_DeliversToTargetCreatedAfterTheEvent(
|
||||
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)
|
||||
original := seedStoredEvent(
|
||||
t, dbMgr, wh.ID, `{"captured":"traffic"}`,
|
||||
)
|
||||
|
||||
// The dev backend is registered only now, after the traffic was
|
||||
// captured. It has no prior delivery of anything.
|
||||
tgt := seedConfiguredTarget(
|
||||
t, db, wh.ID, database.TargetTypeHTTP,
|
||||
`{"url":"`+resubmitTargetURL+`"}`,
|
||||
)
|
||||
|
||||
w := postResubmit(t, h, sess, wh.ID, original.ID)
|
||||
|
||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||
assert.Equal(
|
||||
t,
|
||||
"/source/"+wh.ID+"/logs?resubmit=queued",
|
||||
w.Header().Get("Location"),
|
||||
)
|
||||
|
||||
webhookDB, err := dbMgr.GetDB(wh.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
events := listEvents(t, webhookDB)
|
||||
require.Len(
|
||||
t, events, 2,
|
||||
"resubmit must create a new event, not mark the old one",
|
||||
)
|
||||
|
||||
fresh := theOtherEvent(t, events, original.ID)
|
||||
assertEventCopy(t, original, fresh)
|
||||
|
||||
// The delivery hangs off the NEW event, and the original event
|
||||
// still has none.
|
||||
assert.Empty(
|
||||
t, listDeliveries(t, webhookDB, original.ID),
|
||||
"the original event must be left untouched",
|
||||
)
|
||||
|
||||
deliveries := listDeliveries(t, webhookDB, fresh.ID)
|
||||
require.Len(t, deliveries, 1)
|
||||
assert.Equal(t, tgt.ID, deliveries[0].TargetID)
|
||||
assert.Equal(
|
||||
t, database.DeliveryStatusPending, deliveries[0].Status,
|
||||
)
|
||||
|
||||
tasks := notif.Tasks()
|
||||
require.Len(t, tasks, 1)
|
||||
assert.Equal(t, deliveries[0].ID, tasks[0].DeliveryID)
|
||||
assertResubmitTask(t, tasks[0], wh.ID, &fresh, tgt)
|
||||
|
||||
assertNoLeakedTarget(t, webhookDB)
|
||||
}
|
||||
|
||||
// assertEventCopy proves the resubmitted event copies every stored
|
||||
// field of the event it came from, and records the provenance that
|
||||
// keeps the log readable. It also pins that a received event carries
|
||||
// no source event of its own.
|
||||
func assertEventCopy(
|
||||
t *testing.T, original *database.Event, fresh database.Event,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, original.Method, fresh.Method)
|
||||
assert.Equal(t, original.Headers, fresh.Headers)
|
||||
assert.Equal(t, original.Body, fresh.Body)
|
||||
assert.Equal(t, original.ContentType, fresh.ContentType)
|
||||
assert.Equal(t, original.EntrypointID, fresh.EntrypointID)
|
||||
assert.Equal(t, original.WebhookID, fresh.WebhookID)
|
||||
assert.NotEqual(t, original.ID, fresh.ID)
|
||||
|
||||
require.NotNil(t, fresh.ResubmittedFromID)
|
||||
assert.Equal(t, original.ID, *fresh.ResubmittedFromID)
|
||||
|
||||
assert.Nil(
|
||||
t, original.ResubmittedFromID,
|
||||
"a received event records no source event",
|
||||
)
|
||||
}
|
||||
|
||||
// assertResubmitTask proves the task handed to the delivery engine is
|
||||
// the one the receiver would build for the NEW event and this target.
|
||||
func assertResubmitTask(
|
||||
t *testing.T,
|
||||
task delivery.Task,
|
||||
webhookID string,
|
||||
fresh *database.Event,
|
||||
target *database.Target,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, fresh.ID, task.EventID)
|
||||
assert.Equal(t, webhookID, task.WebhookID)
|
||||
assert.Equal(t, fresh.EntrypointID, task.EntrypointID)
|
||||
assert.Equal(t, target.ID, task.TargetID)
|
||||
assert.Equal(t, target.Type, task.TargetType)
|
||||
assert.Equal(t, fresh.Method, task.Method)
|
||||
assert.Equal(t, fresh.Headers, task.Headers)
|
||||
assert.Equal(t, fresh.ContentType, task.ContentType)
|
||||
assert.Equal(t, 1, task.AttemptNum)
|
||||
|
||||
require.NotNil(t, task.Body)
|
||||
assert.Equal(t, fresh.Body, *task.Body)
|
||||
}
|
||||
|
||||
// TestHandleEventResubmit_IsRepeatable proves the requirement replay
|
||||
// deliberately does not meet: firing the same captured event at a
|
||||
// backend over and over must work, with no in-flight refusal, and each
|
||||
// press must produce its own event and its own delivery.
|
||||
func TestHandleEventResubmit_IsRepeatable(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":"`+resubmitTargetURL+`"}`,
|
||||
)
|
||||
original := seedStoredEvent(t, dbMgr, wh.ID, `{"fire":"again"}`)
|
||||
|
||||
// Nothing between the presses marks the earlier deliveries
|
||||
// finished, so every one of these is submitted while the last is
|
||||
// still pending.
|
||||
const presses = 5
|
||||
|
||||
for range presses {
|
||||
w := postResubmit(t, h, sess, wh.ID, original.ID)
|
||||
|
||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||
assert.Equal(
|
||||
t,
|
||||
"/source/"+wh.ID+"/logs?resubmit=queued",
|
||||
w.Header().Get("Location"),
|
||||
"a resubmit must not be refused while an earlier "+
|
||||
"one is in flight",
|
||||
)
|
||||
}
|
||||
|
||||
webhookDB, err := dbMgr.GetDB(wh.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
events := listEvents(t, webhookDB)
|
||||
require.Len(t, events, presses+1)
|
||||
|
||||
tasks := notif.Tasks()
|
||||
require.Len(t, tasks, presses)
|
||||
|
||||
seen := make(map[string]struct{}, presses)
|
||||
|
||||
for _, task := range tasks {
|
||||
assert.Equal(t, tgt.ID, task.TargetID)
|
||||
assert.NotEqual(
|
||||
t, original.ID, task.EventID,
|
||||
"each resubmit delivers its own new event",
|
||||
)
|
||||
|
||||
_, dup := seen[task.EventID]
|
||||
assert.False(t, dup, "each resubmit creates its own event")
|
||||
|
||||
seen[task.EventID] = struct{}{}
|
||||
|
||||
require.Len(t, listDeliveries(t, webhookDB, task.EventID), 1)
|
||||
}
|
||||
|
||||
// Every copy names the same source event, so twenty presses stay
|
||||
// traceable to the one captured request.
|
||||
for _, e := range events {
|
||||
if e.ID == original.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
require.NotNil(t, e.ResubmittedFromID)
|
||||
assert.Equal(t, original.ID, *e.ResubmittedFromID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleEventResubmit_OversizeBodySurvivesIntact covers the
|
||||
// non-inline case: a body above delivery.MaxInlineBodySize is not
|
||||
// carried on the task at all, so it has to be copied into the new
|
||||
// event row byte-identically for the engine to load it from there.
|
||||
func TestHandleEventResubmit_OversizeBodySurvivesIntact(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)
|
||||
|
||||
// Well over the inline limit, and not text: a multibyte rune, a
|
||||
// NUL and a byte that is not valid UTF-8, so a copy that went
|
||||
// through a re-encode or a truncation is visible in the compare.
|
||||
const sentinel = "TAIL-SENTINEL-1f4a9c"
|
||||
|
||||
stored := strings.Repeat("A", delivery.MaxInlineBodySize) +
|
||||
"é\x00\xff" +
|
||||
strings.Repeat("B", 4096) + sentinel
|
||||
|
||||
require.Greater(t, len(stored), delivery.MaxInlineBodySize)
|
||||
|
||||
wh := seedWebhook(t, db)
|
||||
seedConfiguredTarget(
|
||||
t, db, wh.ID, database.TargetTypeHTTP,
|
||||
`{"url":"`+resubmitTargetURL+`"}`,
|
||||
)
|
||||
original := seedStoredEvent(t, dbMgr, wh.ID, stored)
|
||||
|
||||
w := postResubmit(t, h, sess, wh.ID, original.ID)
|
||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||
|
||||
webhookDB, err := dbMgr.GetDB(wh.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
fresh := theOtherEvent(t, listEvents(t, webhookDB), original.ID)
|
||||
|
||||
assert.Len(t, fresh.Body, len(stored))
|
||||
assert.Equal(
|
||||
t, stored, fresh.Body,
|
||||
"the stored body must be copied byte for byte",
|
||||
)
|
||||
|
||||
tasks := notif.Tasks()
|
||||
require.Len(t, tasks, 1)
|
||||
assert.Nil(
|
||||
t, tasks[0].Body,
|
||||
"a body over the inline limit is fetched from the new "+
|
||||
"event row rather than carried on the task",
|
||||
)
|
||||
|
||||
// The engine's own read of the body, against the new event id:
|
||||
// what it would send is what was stored.
|
||||
var loaded database.Event
|
||||
|
||||
require.NoError(t, webhookDB.Select("body").
|
||||
First(&loaded, "id = ?", tasks[0].EventID).Error)
|
||||
assert.Equal(t, stored, loaded.Body)
|
||||
}
|
||||
|
||||
// TestHandleEventResubmit_SkipsInactiveTarget proves a deactivated
|
||||
// target is skipped exactly as the receiver skips it — not an error,
|
||||
// and not a delivery the operator switched off.
|
||||
func TestHandleEventResubmit_SkipsInactiveTarget(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)
|
||||
active := seedConfiguredTarget(
|
||||
t, db, wh.ID, database.TargetTypeHTTP,
|
||||
`{"url":"`+resubmitTargetURL+`"}`,
|
||||
)
|
||||
off := seedConfiguredTarget(
|
||||
t, db, wh.ID, database.TargetTypeHTTP,
|
||||
`{"url":"`+resubmitTargetURL+`/off"}`,
|
||||
)
|
||||
|
||||
require.NoError(t, db.DB().Model(&database.Target{}).
|
||||
Where("id = ?", off.ID).
|
||||
Update("active", false).Error)
|
||||
|
||||
original := seedStoredEvent(t, dbMgr, wh.ID, `{"skip":"one"}`)
|
||||
|
||||
w := postResubmit(t, h, sess, wh.ID, original.ID)
|
||||
|
||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||
assert.Equal(
|
||||
t,
|
||||
"/source/"+wh.ID+"/logs?resubmit=queued",
|
||||
w.Header().Get("Location"),
|
||||
"an inactive target is skipped, not an error",
|
||||
)
|
||||
|
||||
tasks := notif.Tasks()
|
||||
require.Len(t, tasks, 1)
|
||||
assert.Equal(t, active.ID, tasks[0].TargetID)
|
||||
|
||||
webhookDB, err := dbMgr.GetDB(wh.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
fresh := theOtherEvent(t, listEvents(t, webhookDB), original.ID)
|
||||
require.Len(t, listDeliveries(t, webhookDB, fresh.ID), 1)
|
||||
}
|
||||
|
||||
// TestHandleEventResubmit_NoActiveTargetsStillStoresEvent proves a
|
||||
// source with nothing to deliver to behaves as the receiver does: the
|
||||
// event is stored, nothing is queued, and the operator is told so
|
||||
// rather than being shown an error.
|
||||
func TestHandleEventResubmit_NoActiveTargetsStillStoresEvent(
|
||||
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)
|
||||
original := seedStoredEvent(t, dbMgr, wh.ID, `{"no":"targets"}`)
|
||||
|
||||
w := postResubmit(t, h, sess, wh.ID, original.ID)
|
||||
|
||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||
assert.Equal(
|
||||
t,
|
||||
"/source/"+wh.ID+"/logs?resubmit=no-targets",
|
||||
w.Header().Get("Location"),
|
||||
)
|
||||
|
||||
assert.Empty(t, notif.Tasks())
|
||||
|
||||
webhookDB, err := dbMgr.GetDB(wh.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, listEvents(t, webhookDB), 2)
|
||||
}
|
||||
|
||||
// TestHandleEventResubmit_RefusesEventOfAnotherWebhook proves the
|
||||
// route cannot re-inject an event out of a webhook the session's user
|
||||
// does not own, and reports the same 404 for an id that names nothing.
|
||||
func TestHandleEventResubmit_RefusesEventOfAnotherWebhook(
|
||||
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)
|
||||
|
||||
theirs := seedWebhookFor(t, db, otherTestUserID)
|
||||
theirEvent := seedStoredEvent(t, dbMgr, theirs.ID, `{"not":"mine"}`)
|
||||
|
||||
mine := seedWebhook(t, db)
|
||||
seedConfiguredTarget(
|
||||
t, db, mine.ID, database.TargetTypeHTTP,
|
||||
`{"url":"`+resubmitTargetURL+`"}`,
|
||||
)
|
||||
seedStoredEvent(t, dbMgr, mine.ID, `{"mine":true}`)
|
||||
|
||||
// Their webhook, as its owner would address it.
|
||||
w := postResubmit(t, h, sess, theirs.ID, theirEvent.ID)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// Their event id, addressed through a webhook the user does own.
|
||||
w = postResubmit(t, h, sess, mine.ID, theirEvent.ID)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// An id that names no event at all.
|
||||
w = postResubmit(t, h, sess, mine.ID, uuid.NewString())
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// A malformed id never reaches the query.
|
||||
w = postResubmit(t, h, sess, mine.ID, "not-a-uuid")
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
assert.Empty(
|
||||
t, notif.Tasks(),
|
||||
"a refused resubmit must queue nothing",
|
||||
)
|
||||
}
|
||||
|
||||
// TestHandleSourceLogs_ShowsResubmitProvenance proves the event log
|
||||
// reports the relationship in both directions, which is what keeps it
|
||||
// readable once one captured event has been fired repeatedly.
|
||||
func TestHandleSourceLogs_ShowsResubmitProvenance(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)
|
||||
seedConfiguredTarget(
|
||||
t, db, wh.ID, database.TargetTypeHTTP,
|
||||
`{"url":"`+resubmitTargetURL+`"}`,
|
||||
)
|
||||
original := seedStoredEvent(t, dbMgr, wh.ID, `{"trace":"me"}`)
|
||||
|
||||
for range 2 {
|
||||
require.Equal(
|
||||
t,
|
||||
http.StatusSeeOther,
|
||||
postResubmit(t, h, sess, wh.ID, original.ID).Code,
|
||||
)
|
||||
}
|
||||
|
||||
webhookDB, err := dbMgr.GetDB(wh.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
events := listEvents(t, webhookDB)
|
||||
require.Len(t, events, 3)
|
||||
|
||||
body := renderSourceLogsPage(t, h, sess, wh.ID)
|
||||
|
||||
assert.Contains(
|
||||
t, body, "Resubmitted as 2 new events",
|
||||
"the source event must show it has been resubmitted",
|
||||
)
|
||||
assert.Contains(
|
||||
t, body, "Resubmitted from event",
|
||||
"a copy must show where it came from",
|
||||
)
|
||||
assert.Contains(
|
||||
t, body,
|
||||
"/source/"+wh.ID+"/events/"+original.ID+"/resubmit",
|
||||
"the log must offer the resubmit action per event",
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user