check / check (push) Waiting to run
The event log gains three links above the list: All, Failed (N) and Pending (N), carried in a `show` query parameter; any other value shows every event. Failed lists the events with a failed delivery, Pending those with one pending or retrying, each once, under the full list's 50-row limit and order. A filtered list finds the matching deliveries through `idx_deliveries_status`, looks up their events by ID and sorts them, and reads the rows of only the events shown; its counts read the deliveries alone. The line beside the heading says what a filter counts, and Replay returns to the list it was pressed in. The README and the comments naming the query parameters the service reads include `show`. Model: opus-5-5
358 lines
10 KiB
Go
358 lines
10 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi"
|
|
"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/session"
|
|
)
|
|
|
|
// contentTypeJSON is the content type the seeded events in this
|
|
// package carry. Shared across the seed helpers so the literal
|
|
// appears once.
|
|
const contentTypeJSON = "application/json"
|
|
|
|
// seedDeliveredEvent records an event and a delivery for it in
|
|
// the webhook's own database, so the log page has a delivery
|
|
// to render against the target.
|
|
func seedDeliveredEvent(
|
|
t *testing.T,
|
|
dbMgr *database.WebhookDBManager,
|
|
webhookID, targetID string,
|
|
) {
|
|
t.Helper()
|
|
|
|
webhookDB, err := dbMgr.GetDB(webhookID)
|
|
require.NoError(t, err)
|
|
|
|
event := &database.Event{
|
|
WebhookID: webhookID,
|
|
Method: http.MethodPost,
|
|
Body: `{"test":true}`,
|
|
ContentType: contentTypeJSON,
|
|
}
|
|
|
|
require.NoError(t, webhookDB.Omit(
|
|
clause.Associations,
|
|
).Create(event).Error)
|
|
|
|
dlv := &database.Delivery{
|
|
EventID: event.ID,
|
|
TargetID: targetID,
|
|
Status: database.DeliveryStatusDelivered,
|
|
}
|
|
|
|
require.NoError(t, webhookDB.Omit(
|
|
clause.Associations,
|
|
).Create(dlv).Error)
|
|
}
|
|
|
|
// renderSourceLogsPage runs the real event log handler for a
|
|
// webhook and returns the rendered HTML.
|
|
func renderSourceLogsPage(
|
|
t *testing.T,
|
|
h *handlers.Handlers,
|
|
sess *session.Session,
|
|
webhookID string,
|
|
) string {
|
|
t.Helper()
|
|
|
|
return renderSourceLogsPageWithQuery(
|
|
t, h, sess, webhookID, "",
|
|
)
|
|
}
|
|
|
|
// renderSourceLogsPageWithQuery is renderSourceLogsPage over a
|
|
// caller-supplied query string, for the page state a redirect back to
|
|
// the log carries in one.
|
|
func renderSourceLogsPageWithQuery(
|
|
t *testing.T,
|
|
h *handlers.Handlers,
|
|
sess *session.Session,
|
|
webhookID, query string,
|
|
) string {
|
|
t.Helper()
|
|
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(),
|
|
http.MethodGet,
|
|
"/hook/"+webhookID+"/events"+query,
|
|
nil,
|
|
)
|
|
|
|
for _, c := range authenticatedCookies(
|
|
t, sess, deleteTestUserID, deleteTestUsername,
|
|
) {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add(paramSourceID, webhookID)
|
|
|
|
req = req.WithContext(
|
|
context.WithValue(
|
|
req.Context(), chi.RouteCtxKey, rctx,
|
|
),
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
h.HandleSourceLogs().ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
return w.Body.String()
|
|
}
|
|
|
|
// TestHandleSourceLogs_MasksSlackWebhookURL proves the event
|
|
// log page is handed a display-safe projection of each target
|
|
// rather than the stored row, so the credential cannot be
|
|
// rendered from its template data.
|
|
func TestHandleSourceLogs_MasksSlackWebhookURL(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.TargetTypeSlack,
|
|
`{"webhookUrl":"`+slackWebhookURL+`"}`,
|
|
)
|
|
|
|
seedDeliveredEvent(t, dbMgr, wh.ID, tgt.ID)
|
|
|
|
body := renderSourceLogsPage(t, h, sess, wh.ID)
|
|
|
|
assert.NotContains(t, body, slackSecretPath)
|
|
assert.NotContains(t, body, "T00000000")
|
|
assert.NotContains(t, body, "B00000000")
|
|
assert.NotContains(
|
|
t, body, "XXXXXXXXXXXXXXXXXXXXXXXX",
|
|
)
|
|
assert.NotContains(t, body, "webhookUrl")
|
|
|
|
// The page still identifies the delivery's target.
|
|
assert.Contains(t, body, tgt.Name)
|
|
assert.Contains(t, body, "delivered")
|
|
}
|
|
|
|
// TestHandleSourceLogs_ShowsFiftyNewestEvents proves the event log
|
|
// holds the 50 newest events, newest first, and not one more, and says
|
|
// how many events there are in all.
|
|
func TestHandleSourceLogs_ShowsFiftyNewestEvents(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := newRecentEventsFixture(t)
|
|
base := time.Now().Add(-time.Hour)
|
|
|
|
for i := range 51 {
|
|
f.event(
|
|
t, fmt.Sprintf("application/x-log-%02d", i), "{}",
|
|
base.Add(time.Duration(i)*time.Second),
|
|
)
|
|
}
|
|
|
|
body := renderSourceLogsPage(t, f.h, f.sess, f.webhook.ID)
|
|
|
|
assert.Equal(t, 50, strings.Count(body, `role="button"`))
|
|
assert.NotContains(t, body, "application/x-log-00")
|
|
assert.Contains(t, body, "application/x-log-01")
|
|
assert.Less(
|
|
t,
|
|
strings.Index(body, "application/x-log-50"),
|
|
strings.Index(body, "application/x-log-49"),
|
|
)
|
|
assert.Contains(t, body, "50 most recent of 51 events")
|
|
}
|
|
|
|
// TestHandleSourceLogs_ShowsEventsByDeliveryStatus proves that the
|
|
// Failed list holds exactly the events with a failed delivery, the
|
|
// Pending list exactly those with a delivery pending or retrying, each
|
|
// once, and any other show value every event; that each link, and the
|
|
// line beside the heading, counts the events its list holds; and that
|
|
// the shown link is marked.
|
|
func TestHandleSourceLogs_ShowsEventsByDeliveryStatus(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := newRecentEventsFixture(t)
|
|
target := seedTarget(t, f.db, f.webhook.ID, database.TargetTypeLog)
|
|
now := time.Now()
|
|
|
|
const (
|
|
failed = database.DeliveryStatusFailed
|
|
delivered = database.DeliveryStatusDelivered
|
|
pending = database.DeliveryStatusPending
|
|
retrying = database.DeliveryStatusRetrying
|
|
)
|
|
|
|
// Each event is named by its content type. The first failed and
|
|
// was then replayed and delivered. The second failed, and so did
|
|
// its replay, and the fifth has one delivery pending and another
|
|
// retrying: each must still be listed and counted once.
|
|
events := []struct {
|
|
contentType string
|
|
deliveries []database.DeliveryStatus
|
|
}{
|
|
{"application/x-failed", []database.DeliveryStatus{failed, delivered}},
|
|
{"application/x-failed-twice", []database.DeliveryStatus{failed, failed}},
|
|
{"application/x-pending", []database.DeliveryStatus{pending}},
|
|
{"application/x-retrying", []database.DeliveryStatus{retrying}},
|
|
{"application/x-pending-retrying", []database.DeliveryStatus{pending, retrying}},
|
|
{"application/x-delivered", []database.DeliveryStatus{delivered}},
|
|
{"application/x-no-delivery", nil},
|
|
}
|
|
|
|
all := make([]string, len(events))
|
|
|
|
for i, e := range events {
|
|
event := f.event(
|
|
t, e.contentType, "{}", now.Add(time.Duration(i)*time.Second),
|
|
)
|
|
|
|
for _, status := range e.deliveries {
|
|
f.delivery(t, event, target.ID, status)
|
|
}
|
|
|
|
all[i] = e.contentType
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
query string
|
|
current string
|
|
heading string
|
|
listed []string
|
|
}{
|
|
{"", "All", "7 total events", all},
|
|
{"?show=failed", "Failed (2)", "2 events with a failed delivery",
|
|
[]string{"application/x-failed", "application/x-failed-twice"}},
|
|
{"?show=pending", "Pending (3)",
|
|
"3 events with a delivery pending or retrying", []string{
|
|
"application/x-pending", "application/x-retrying",
|
|
"application/x-pending-retrying",
|
|
}},
|
|
{"?show=unknown", "All", "7 total events", all},
|
|
} {
|
|
body := renderSourceLogsPageWithQuery(
|
|
t, f.h, f.sess, f.webhook.ID, tc.query,
|
|
)
|
|
|
|
// One row per listed event, so with each listed event shown
|
|
// no event is listed twice.
|
|
assert.Equal(t, len(tc.listed),
|
|
strings.Count(body, `role="button"`), tc.query)
|
|
|
|
for _, contentType := range all {
|
|
assert.Equal(t,
|
|
slices.Contains(tc.listed, contentType),
|
|
strings.Contains(body, ">"+contentType+"<"),
|
|
tc.query+" "+contentType)
|
|
}
|
|
|
|
assert.Contains(t, body, ">"+tc.heading+"<", tc.query)
|
|
assert.Contains(t, body, "Failed (2)", tc.query)
|
|
assert.Contains(t, body, "Pending (3)", tc.query)
|
|
assert.Equal(t, 1, strings.Count(body, "aria-current"), tc.query)
|
|
assert.Contains(t, body,
|
|
`aria-current="page">`+tc.current+"</a>", tc.query)
|
|
}
|
|
}
|
|
|
|
// TestHandleSourceLogs_FilteredListShowsFiftyNewest proves a filtered
|
|
// list holds the 50 newest matching events, as the full log does,
|
|
// while its link and heading count every matching event.
|
|
func TestHandleSourceLogs_FilteredListShowsFiftyNewest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := newRecentEventsFixture(t)
|
|
target := seedTarget(t, f.db, f.webhook.ID, database.TargetTypeLog)
|
|
base := time.Now().Add(-time.Hour)
|
|
|
|
for i := range 51 {
|
|
event := f.event(
|
|
t, fmt.Sprintf("application/x-failed-%02d", i), "{}",
|
|
base.Add(time.Duration(i)*time.Second),
|
|
)
|
|
f.delivery(t, event, target.ID, database.DeliveryStatusFailed)
|
|
}
|
|
|
|
// The newest event has no failed delivery.
|
|
f.event(t, "application/x-no-delivery", "{}", time.Now())
|
|
|
|
body := renderSourceLogsPageWithQuery(
|
|
t, f.h, f.sess, f.webhook.ID, "?show=failed",
|
|
)
|
|
|
|
assert.Equal(t, 50, strings.Count(body, `role="button"`))
|
|
assert.NotContains(t, body, "application/x-failed-00")
|
|
assert.NotContains(t, body, "application/x-no-delivery")
|
|
assert.Contains(t, body, "Failed (51)")
|
|
assert.Contains(t, body,
|
|
"50 most recent of 51 events with a failed delivery")
|
|
}
|
|
|
|
// TestHandleSourceLogs_EmptyFilteredList proves an empty filtered list
|
|
// says that no event matches rather than that none was recorded.
|
|
func TestHandleSourceLogs_EmptyFilteredList(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := newRecentEventsFixture(t)
|
|
target := seedTarget(t, f.db, f.webhook.ID, database.TargetTypeLog)
|
|
|
|
f.delivery(
|
|
t, f.event(t, contentTypeJSON, "{}", time.Now()),
|
|
target.ID, database.DeliveryStatusDelivered,
|
|
)
|
|
|
|
assert.Contains(t, renderSourceLogsPageWithQuery(
|
|
t, f.h, f.sess, f.webhook.ID, "?show=failed",
|
|
), "No event has a failed delivery.")
|
|
assert.Contains(t, renderSourceLogsPageWithQuery(
|
|
t, f.h, f.sess, f.webhook.ID, "?show=pending",
|
|
), "No event has a delivery pending or retrying.")
|
|
}
|
|
|
|
// TestHandleSourceLogs_OnlyNewestStartsExpanded proves that of the
|
|
// events in the log only the newest starts expanded.
|
|
func TestHandleSourceLogs_OnlyNewestStartsExpanded(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := newRecentEventsFixture(t)
|
|
now := time.Now()
|
|
|
|
f.event(t, "application/x-older", "{}", now.Add(-time.Minute))
|
|
f.event(t, "application/x-newer", "{}", now)
|
|
|
|
body := renderSourceLogsPage(t, f.h, f.sess, f.webhook.ID)
|
|
|
|
assert.Equal(t, 1, strings.Count(body, " data-open>"))
|
|
|
|
open := strings.Index(body, " data-open>")
|
|
newer := strings.Index(body, "application/x-newer")
|
|
older := strings.Index(body, "application/x-older")
|
|
|
|
assert.Less(t, open, newer, "the newest event is not the open one")
|
|
assert.Less(t, newer, older)
|
|
}
|