check / check (push) Successful in 4m30s
The recent events list on a webhook's page now shows the 50 newest events, each with its time relative to now (the full UTC timestamp on hover), its body size, its processing time and, when the webhook has exactly one HTTP target, that target's last HTTP status, colour-coded. Resubmitted copies are marked, as in the event log. Each event now records its body's size in bytes when it is stored, on receipt and on resubmit, and the list reads that instead of the body. Processing time is read from the existing delivery and attempt timestamps. The list loads its deliveries and their attempts in batched queries of its own, reading only the columns it shows. A failed load answers with an error instead of an empty list. Model: opus-5-5
244 lines
5.5 KiB
Go
244 lines
5.5 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"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"
|
|
)
|
|
|
|
// The secret path segments of a Slack incoming webhook URL.
|
|
// Holding them is enough to post to the channel forever, so
|
|
// they must never reach the rendered page.
|
|
const (
|
|
slackSecretPath = "/services/T00000000/B00000000/" +
|
|
"XXXXXXXXXXXXXXXXXXXXXXXX"
|
|
slackWebhookURL = "https://hooks.slack.com" +
|
|
slackSecretPath
|
|
)
|
|
|
|
// seedConfiguredTarget inserts a target with a stored config
|
|
// blob and returns it.
|
|
func seedConfiguredTarget(
|
|
t *testing.T,
|
|
db *database.Database,
|
|
webhookID string,
|
|
targetType database.TargetType,
|
|
config string,
|
|
) *database.Target {
|
|
t.Helper()
|
|
|
|
tgt := &database.Target{
|
|
WebhookID: webhookID,
|
|
Name: "t-" + string(targetType),
|
|
Type: targetType,
|
|
Active: true,
|
|
Config: config,
|
|
}
|
|
|
|
require.NoError(
|
|
t,
|
|
db.DB().Omit(clause.Associations).Create(tgt).Error,
|
|
)
|
|
|
|
return tgt
|
|
}
|
|
|
|
// renderSourceDetailPage runs the real source detail handler
|
|
// for a webhook and returns the rendered HTML.
|
|
func renderSourceDetailPage(
|
|
t *testing.T,
|
|
h *handlers.Handlers,
|
|
sess *session.Session,
|
|
webhookID string,
|
|
) string {
|
|
t.Helper()
|
|
|
|
w := serveSourceDetailPage(t, h, sess, webhookID)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
return w.Body.String()
|
|
}
|
|
|
|
// serveSourceDetailPage runs the real source detail handler for a
|
|
// webhook and returns its response, whatever its status.
|
|
func serveSourceDetailPage(
|
|
t *testing.T,
|
|
h *handlers.Handlers,
|
|
sess *session.Session,
|
|
webhookID string,
|
|
) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(),
|
|
http.MethodGet,
|
|
"/source/"+webhookID,
|
|
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.HandleSourceDetail().ServeHTTP(w, req)
|
|
|
|
return w
|
|
}
|
|
|
|
// TestHandleSourceDetail_MasksSlackWebhookURL is the
|
|
// load-bearing regression test for the credential leak: the
|
|
// rendered page must show the Slack target without any of the
|
|
// secret path segments of its webhook URL.
|
|
func TestHandleSourceDetail_MasksSlackWebhookURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := seedWebhook(t, db)
|
|
seedConfiguredTarget(
|
|
t, db, wh.ID,
|
|
database.TargetTypeSlack,
|
|
`{"webhookUrl":"`+slackWebhookURL+`"}`,
|
|
)
|
|
|
|
body := renderSourceDetailPage(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")
|
|
|
|
assert.Contains(t, body, "Webhook URL")
|
|
assert.Contains(t, body, "https://hooks.slack.com/...")
|
|
}
|
|
|
|
// TestHandleSourceDetail_MasksHTTPDestinationURL is the
|
|
// regression test for the same leak reached through the http
|
|
// target: its destination is routinely an incoming-webhook
|
|
// endpoint whose path segments are the credential, so the
|
|
// rendered page must not contain them.
|
|
func TestHandleSourceDetail_MasksHTTPDestinationURL(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := seedWebhook(t, db)
|
|
seedConfiguredTarget(
|
|
t, db, wh.ID,
|
|
database.TargetTypeHTTP,
|
|
`{"url":"`+slackWebhookURL+`"}`,
|
|
)
|
|
|
|
body := renderSourceDetailPage(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.Contains(t, body, "Destination URL")
|
|
assert.Contains(t, body, "https://hooks.slack.com/...")
|
|
}
|
|
|
|
// TestHandleSourceDetail_RendersNamedTargetFields proves the
|
|
// other target types render labelled fields rather than the
|
|
// stored blob.
|
|
func TestHandleSourceDetail_RendersNamedTargetFields(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
)
|
|
|
|
app := newTestApp(t, &h, &sess, &db)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
wh := seedWebhook(t, db)
|
|
|
|
seedConfiguredTarget(
|
|
t, db, wh.ID,
|
|
database.TargetTypeHTTP,
|
|
`{"url":"https://example.com/hook","timeout":30,`+
|
|
`"headers":{"Authorization":"Bearer sekrit"}}`,
|
|
)
|
|
seedConfiguredTarget(
|
|
t, db, wh.ID,
|
|
database.TargetTypeDatabase,
|
|
`{"expiry":"720h"}`,
|
|
)
|
|
seedConfiguredTarget(
|
|
t, db, wh.ID,
|
|
database.TargetType("carrier-pigeon"),
|
|
`{"beak":"sharp"}`,
|
|
)
|
|
|
|
body := renderSourceDetailPage(t, h, sess, wh.ID)
|
|
|
|
assert.Contains(t, body, "Destination URL")
|
|
assert.Contains(t, body, "https://example.com/...")
|
|
assert.Contains(t, body, "Timeout")
|
|
assert.Contains(t, body, "1 configured")
|
|
assert.NotContains(t, body, "sekrit")
|
|
|
|
assert.Contains(t, body, "Archive Expiry")
|
|
assert.Contains(t, body, "720h")
|
|
|
|
// An unknown type gets the neutral placeholder, never the
|
|
// stored blob.
|
|
assert.Contains(t, body, "(unavailable)")
|
|
assert.NotContains(t, body, "beak")
|
|
}
|