check / check (push) Waiting to run
The entrypoint list showed no sign of whether anything uses an entrypoint, so an operator with several could not tell which senders are live before deactivating or deleting one. Each entrypoint now shows when its last event arrived, relative with the UTC time on hover, or "never", and how many events arrived through it within the webhook's retention. The last-event time comes from a new entrypoint_totals row written in the transaction that stores the event and left by retention, so a sender quieter than the retention period does not read "never". The count is one grouped query over a new index. Resubmitted copies count in neither. Pre-1.0: schema changed in place. Model: opus-5-5
198 lines
5.8 KiB
Go
198 lines
5.8 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm/clause"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/handlers"
|
|
"sneak.berlin/go/webhooker/internal/logger"
|
|
"sneak.berlin/go/webhooker/internal/session"
|
|
)
|
|
|
|
// entrypointRow returns the part of a rendered webhook page from an
|
|
// entrypoint's URL to the next entrypoint's, which holds its figures.
|
|
func entrypointRow(t *testing.T, page, entrypointID string) string {
|
|
t.Helper()
|
|
|
|
_, row, found := strings.Cut(page, `id="entrypoint-url-`+entrypointID+`"`)
|
|
require.True(t, found)
|
|
|
|
row, _, _ = strings.Cut(row, `id="entrypoint-url-`)
|
|
|
|
return row
|
|
}
|
|
|
|
// lastEventShown matches an entrypoint row's last event arriving at at.
|
|
func lastEventShown(at time.Time) string {
|
|
return `Last Event:</span>\s*<span title="` +
|
|
at.UTC().Format(time.DateTime) + ` UTC">[^<]+</span>`
|
|
}
|
|
|
|
// eventsShown matches an entrypoint row's count of n events.
|
|
func eventsShown(n int) string {
|
|
return `Events Within Retention:</span>\s*<span>` +
|
|
strconv.Itoa(n) + `</span>`
|
|
}
|
|
|
|
// TestHandleSourceDetail_ShowsEntrypointEvents proves each entrypoint
|
|
// on the webhook page shows its own figures: how many events arrived
|
|
// through it within the webhook's retention period, leaving out one
|
|
// older than that, and when the newest arrived, or "never" for an
|
|
// entrypoint with none.
|
|
func TestHandleSourceDetail_ShowsEntrypointEvents(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 := &database.Webhook{
|
|
UserID: deleteTestUserID, Name: "figures", RetentionDays: 7,
|
|
}
|
|
require.NoError(t, db.DB().Omit(clause.Associations).Create(wh).Error)
|
|
|
|
webhookDB, err := dbMgr.GetDB(wh.ID)
|
|
require.NoError(t, err)
|
|
|
|
entrypoint := func() *database.Entrypoint {
|
|
ep := &database.Entrypoint{
|
|
WebhookID: wh.ID, Path: uuid.New().String(), Active: true,
|
|
}
|
|
require.NoError(t,
|
|
db.DB().Omit(clause.Associations).Create(ep).Error)
|
|
|
|
return ep
|
|
}
|
|
|
|
// event stores an event that arrived on ep's URL age ago and
|
|
// records it as ep's last event, as the receiver does.
|
|
event := func(ep *database.Entrypoint, age time.Duration) time.Time {
|
|
e := &database.Event{
|
|
WebhookID: wh.ID,
|
|
EntrypointID: ep.ID,
|
|
Method: http.MethodPost,
|
|
}
|
|
e.CreatedAt = time.Now().Add(-age)
|
|
require.NoError(t,
|
|
webhookDB.Omit(clause.Associations).Create(e).Error)
|
|
require.NoError(t, database.AddEntrypointTotals(webhookDB,
|
|
database.EntrypointTotals{
|
|
EntrypointID: ep.ID, LastEventAt: e.CreatedAt,
|
|
}))
|
|
|
|
return e.CreatedAt
|
|
}
|
|
|
|
busy, quiet, unused := entrypoint(), entrypoint(), entrypoint()
|
|
|
|
event(busy, 8*24*time.Hour) // older than the 7 days kept
|
|
event(busy, 3*time.Hour)
|
|
busyLast := event(busy, time.Hour)
|
|
quietLast := event(quiet, 2*24*time.Hour)
|
|
|
|
body := renderSourceDetailPage(t, h, sess, wh.ID)
|
|
|
|
assert.Regexp(t, lastEventShown(busyLast), entrypointRow(t, body, busy.ID))
|
|
assert.Regexp(t, eventsShown(2), entrypointRow(t, body, busy.ID))
|
|
assert.Regexp(t, lastEventShown(quietLast), entrypointRow(t, body, quiet.ID))
|
|
assert.Regexp(t, eventsShown(1), entrypointRow(t, body, quiet.ID))
|
|
assert.Regexp(t, `Last Event:</span>\s*<span>never</span>`,
|
|
entrypointRow(t, body, unused.ID))
|
|
assert.Regexp(t, eventsShown(0), entrypointRow(t, body, unused.ID))
|
|
}
|
|
|
|
// TestHandleSourceDetail_EntrypointLastEventSurvivesRetention checks
|
|
// that once retention has removed every event that arrived on an
|
|
// entrypoint's URL, the entrypoint still shows when the last one
|
|
// arrived rather than "never".
|
|
func TestHandleSourceDetail_EntrypointLastEventSurvivesRetention(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
log *logger.Logger
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db, &dbMgr, &log)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := &database.Webhook{
|
|
UserID: deleteTestUserID, Name: "swept", RetentionDays: 1,
|
|
}
|
|
require.NoError(t, db.DB().Omit(clause.Associations).Create(wh).Error)
|
|
|
|
ep := seedEntrypoint(t, db, wh.ID)
|
|
webhookDB, events := receiveEvents(t, h, dbMgr, wh.ID, ep.Path, 1)
|
|
arrived := events[0].CreatedAt
|
|
|
|
statsAge(t, webhookDB, events[0].ID, time.Now().Add(-50*time.Hour))
|
|
statsPrune(t, db, dbMgr, log, webhookDB)
|
|
require.Empty(t, listEvents(t, webhookDB))
|
|
|
|
row := entrypointRow(t, renderSourceDetailPage(t, h, sess, wh.ID), ep.ID)
|
|
assert.Regexp(t, lastEventShown(arrived), row)
|
|
assert.Regexp(t, eventsShown(0), row)
|
|
}
|
|
|
|
// TestHandleSourceDetail_ResubmitLeavesEntrypointFigures checks that a
|
|
// resubmitted copy, which did not arrive on the entrypoint's URL,
|
|
// changes neither the entrypoint's last event nor its count.
|
|
func TestHandleSourceDetail_ResubmitLeavesEntrypointFigures(
|
|
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)
|
|
ep := seedEntrypoint(t, db, wh.ID)
|
|
webhookDB, events := receiveEvents(t, h, dbMgr, wh.ID, ep.Path, 1)
|
|
arrived := events[0].CreatedAt
|
|
|
|
require.Equal(t, http.StatusSeeOther,
|
|
postResubmit(t, h, sess, wh.ID, events[0].ID).Code)
|
|
require.Len(t, listEvents(t, webhookDB), 2)
|
|
|
|
var totals database.EntrypointTotals
|
|
|
|
require.NoError(t, webhookDB.Take(&totals).Error)
|
|
assert.True(t, arrived.Equal(totals.LastEventAt))
|
|
|
|
row := entrypointRow(t, renderSourceDetailPage(t, h, sess, wh.ID), ep.ID)
|
|
assert.Regexp(t, lastEventShown(arrived), row)
|
|
assert.Regexp(t, eventsShown(1), row)
|
|
}
|