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, `