package handlers_test
import (
"context"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"sneak.berlin/go/webhooker/internal/database"
"sneak.berlin/go/webhooker/internal/handlers"
"sneak.berlin/go/webhooker/internal/session"
)
// Template data keys the page templates read. The handlers package has
// its own unexported constants for these; this is the external test
// package, so it needs its own.
const (
dataKeyWebhook = "Webhook"
dataKeyError = "Error"
)
// renderPage renders a page template through the real template set as
// an authenticated user and returns the resulting HTML.
func renderPage(
t *testing.T,
h *handlers.Handlers,
sess *session.Session,
page string,
data map[string]any,
) string {
t.Helper()
cookies := authenticatedCookies(t, sess, "test-user-id", "testuser")
req := httptest.NewRequestWithContext(
context.Background(), http.MethodGet, "/", nil,
)
for _, c := range cookies {
req.AddCookie(c)
}
w := httptest.NewRecorder()
h.RenderTemplateForTest(w, req, page, data)
return w.Body.String()
}
// TestNavbarUsesWebhookTerminology pins the user-visible navigation
// label to "Webhooks". The /sources route is deliberately unchanged, so
// the assertion targets the link text rather than the href.
func TestNavbarUsesWebhookTerminology(t *testing.T) {
t.Parallel()
var h *handlers.Handlers
var sess *session.Session
app := newTestApp(t, &h, &sess)
app.RequireStart()
t.Cleanup(app.RequireStop)
body := renderPage(t, h, sess, "sources_list.html", map[string]any{
"Webhooks": []handlers.WebhookListItem{},
})
assert.Contains(t, body, `class="btn-text">Webhooks`)
assert.Contains(
t, body, `class="btn-text w-full text-left">Webhooks`,
)
assert.Contains(
t, body,
`
Webhooks
`,
)
assert.NotContains(
t, body, ">Sources<",
"no user-visible element may still be labelled Sources",
)
assert.Contains(
t, body, `href="/sources"`,
"the /sources route itself must not change",
)
}
// TestEditPageUsesWebhookTerminology pins the edit page's heading and
// its back link. The link's href still points at /source/{id}, which is
// intentional: only user-visible copy changes.
func TestEditPageUsesWebhookTerminology(t *testing.T) {
t.Parallel()
var h *handlers.Handlers
var sess *session.Session
app := newTestApp(t, &h, &sess)
app.RequireStart()
t.Cleanup(app.RequireStop)
// The webhook goes in as a pointer because source_edit.html calls
// Webhook.RetentionLabel, a pointer method: a map element is not
// addressable, so a value here renders an error instead of the
// page.
webhook := &database.Webhook{Name: "wh", RetentionDays: 14}
webhook.ID = "wh-1"
body := renderPage(t, h, sess, "source_edit.html", map[string]any{
dataKeyWebhook: webhook,
dataKeyError: "",
})
assert.Contains(t, body, "Edit Webhook")
assert.NotContains(t, body, ">Sources<")
assert.Contains(t, body, `href="/source/wh-1"`)
}
// TestCreateFormRetentionCopyMatchesBehaviour pins the create form's
// retention copy to what the code does: the reaper permanently deletes
// events past the cutoff, an empty field falls back to
// DefaultRetentionDays, and 0 is rewritten to the retain-forever
// sentinel by Webhook.BeforeSave.
func TestCreateFormRetentionCopyMatchesBehaviour(t *testing.T) {
t.Parallel()
var h *handlers.Handlers
var sess *session.Session
app := newTestApp(t, &h, &sess)
app.RequireStart()
t.Cleanup(app.RequireStop)
body := renderPage(t, h, sess, "sources_new.html", map[string]any{
"Name": "",
"Description": "",
"DefaultRetentionDays": database.DefaultRetentionDays,
dataKeyError: "",
})
assert.Contains(
t, body,
"permanently deletes events older than this",
"the form must say retention is enforced by deletion",
)
assert.Contains(t, body, "Enter 0 to retain events forever")
assert.Contains(
t, body,
"leave blank to use the default of "+
strconv.Itoa(database.DefaultRetentionDays)+" days",
"blank means the default, not forever",
)
}
// TestEditFormRetentionCopyMatchesBehaviour pins the edit form's
// retention copy, including that it states the stored policy via
// RetentionLabel and that an empty field leaves that policy unchanged
// rather than meaning forever.
func TestEditFormRetentionCopyMatchesBehaviour(t *testing.T) {
t.Parallel()
var h *handlers.Handlers
var sess *session.Session
app := newTestApp(t, &h, &sess)
app.RequireStart()
t.Cleanup(app.RequireStop)
finite := &database.Webhook{Name: "wh", RetentionDays: 14}
finite.ID = "wh-1"
body := renderPage(t, h, sess, "source_edit.html", map[string]any{
dataKeyWebhook: finite,
dataKeyError: "",
})
assert.Contains(t, body, "Currently 14 days.")
assert.Contains(
t, body,
"permanently deletes events older than this",
)
assert.Contains(t, body, "Enter 0 to retain events forever")
assert.Contains(
t, body,
"leave blank to keep the current setting",
"blank means unchanged, not forever",
)
forever := &database.Webhook{
Name: "wh",
RetentionDays: database.RetentionForeverDays,
}
forever.ID = "wh-2"
foreverBody := renderPage(
t, h, sess, "source_edit.html", map[string]any{
dataKeyWebhook: forever,
dataKeyError: "",
},
)
assert.Contains(
t, foreverBody, "Currently forever.",
"a retain-forever webhook must not read as a day count",
)
}
// TestEntrypointCopyButtonIsProgressiveEnhancement proves the copy
// affordance degrades: the button ships with the hidden attribute, so a
// browser that never runs app.js shows no dead control, and the URL is
// rendered as ordinary selectable text either way.
func TestEntrypointCopyButtonIsProgressiveEnhancement(t *testing.T) {
t.Parallel()
var h *handlers.Handlers
var sess *session.Session
app := newTestApp(t, &h, &sess)
app.RequireStart()
t.Cleanup(app.RequireStop)
entrypoint := database.Entrypoint{Path: "abc123"}
entrypoint.ID = "ep-1"
body := renderPage(t, h, sess, "source_detail.html", map[string]any{
"Webhook": database.Webhook{Name: "wh"},
"Entrypoints": []database.Entrypoint{entrypoint},
"Targets": []database.Target{},
"Events": []database.Event{},
"BaseURL": "https://hooks.example.com",
})
assert.Contains(
t, body,
`