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, once // each, the Pending list exactly those with a delivery pending or // retrying, and any other show value every event; that each link // 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() // Each event is named by its content type. The failed one was // then replayed and delivered, so it has two deliveries. events := []struct { contentType string deliveries []database.DeliveryStatus }{ {"application/x-failed", []database.DeliveryStatus{ database.DeliveryStatusFailed, database.DeliveryStatusDelivered, }}, {"application/x-pending", []database.DeliveryStatus{ database.DeliveryStatusPending, }}, {"application/x-retrying", []database.DeliveryStatus{ database.DeliveryStatusRetrying, }}, {"application/x-delivered", []database.DeliveryStatus{ database.DeliveryStatusDelivered, }}, {"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 listed []string }{ {"", "All", all}, {"?show=failed", "Failed (1)", []string{"application/x-failed"}}, {"?show=pending", "Pending (2)", []string{ "application/x-pending", "application/x-retrying", }}, {"?show=unknown", "All", all}, } { body := renderSourceLogsPageWithQuery( t, f.h, f.sess, f.webhook.ID, tc.query, ) 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, "Failed (1)", tc.query) assert.Contains(t, body, "Pending (2)", tc.query) assert.Equal(t, 1, strings.Count(body, "aria-current"), tc.query) assert.Contains(t, body, `aria-current="page">`+tc.current+"", 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") } // 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) }