package handlers_test import ( "net/http" "net/http/httptest" "regexp" "strings" "testing" "time" "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" ) // failedHighlight is how the list marks a number of failed deliveries // that is not zero. const failedHighlight = `class="font-medium text-red-600"` // listWebhook adds a webhook with the given name, owned by the test // user. func listWebhook( t *testing.T, db *database.Database, name string, ) *database.Webhook { t.Helper() wh := &database.Webhook{UserID: deleteTestUserID, Name: name} require.NoError(t, db.DB().Omit(clause.Associations).Create(wh).Error) return wh } // renderWebhookList runs the real webhook list handler as the test user // and returns the rendered page. func renderWebhookList( t *testing.T, h *handlers.Handlers, sess *session.Session, ) string { t.Helper() cookies := authenticatedCookies( t, sess, deleteTestUserID, deleteTestUsername, ) w := httptest.NewRecorder() h.HandleSourceList().ServeHTTP( w, getRequest(t, "/hooks", cookies, nil), ) require.Equal(t, http.StatusOK, w.Code) return w.Body.String() } // listCard returns one webhook's entry in a rendered webhook list, its // markup as rendered and its text with the markup taken out and each // run of space made one space. func listCard(t *testing.T, page, webhookID string) (string, string) { t.Helper() _, card, found := strings.Cut(page, `href="/hook/`+webhookID+`"`) require.True(t, found, "the list has no entry for %s", webhookID) card, _, _ = strings.Cut(card, "") text := regexp.MustCompile(`<[^>]*>`).ReplaceAllString(card, " ") return card, strings.Join(strings.Fields(text), " ") } // TestSourceList_ShowsActivityOfEachWebhook checks the figures the list // shows for a webhook with recent failures, a healthy one, and one that // has never received an event. func TestSourceList_ShowsActivityOfEachWebhook(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) router := receiverRouter(h) now := time.Now() // Failing: two entrypoints, one inactive, and four targets, one // inactive. Three events each reach the three active targets. Two // deliveries failed in the last 24 hours, one 30 hours ago, and // one was delivered. failing := listWebhook(t, db, "failing") failingPath := statsEntrypoint(t, db, failing.ID, true) statsEntrypoint(t, db, failing.ID, false) first := seedTarget(t, db, failing.ID, database.TargetTypeLog) second := seedTarget(t, db, failing.ID, database.TargetTypeLog) seedTarget(t, db, failing.ID, database.TargetTypeLog) inactive := seedTarget(t, db, failing.ID, database.TargetTypeLog) require.NoError(t, db.DB().Model(inactive). Update("active", false).Error) for range 3 { require.Equal(t, http.StatusOK, postReceiver(t, router, failingPath)) } failingDB, err := dbMgr.GetDB(failing.ID) require.NoError(t, err) failingEvents := listEvents(t, failingDB) require.Len(t, failingEvents, 3) statsFinish(t, failingDB, statsDelivery(t, failingDB, failingEvents[0].ID, first.ID), database.DeliveryStatusFailed, now.Add(-30*time.Hour)) statsFinish(t, failingDB, statsDelivery(t, failingDB, failingEvents[1].ID, first.ID), database.DeliveryStatusFailed, now.Add(-time.Hour)) statsFinish(t, failingDB, statsDelivery(t, failingDB, failingEvents[2].ID, first.ID), database.DeliveryStatusFailed, now.Add(-time.Minute)) statsFinish(t, failingDB, statsDelivery(t, failingDB, failingEvents[2].ID, second.ID), database.DeliveryStatusDelivered, now.Add(-time.Minute)) // Healthy: one entrypoint and one target, both active, and two // events, both delivered. healthy := listWebhook(t, db, "healthy") healthyPath := statsEntrypoint(t, db, healthy.ID, true) only := seedTarget(t, db, healthy.ID, database.TargetTypeLog) for range 2 { require.Equal(t, http.StatusOK, postReceiver(t, router, healthyPath)) } healthyDB, err := dbMgr.GetDB(healthy.ID) require.NoError(t, err) healthyEvents := listEvents(t, healthyDB) require.Len(t, healthyEvents, 2) for _, ev := range healthyEvents { statsFinish(t, healthyDB, statsDelivery(t, healthyDB, ev.ID, only.ID), database.DeliveryStatusDelivered, now.Add(-time.Minute)) } // Quiet: nothing at all, and no event database. quiet := listWebhook(t, db, "quiet") page := renderWebhookList(t, h, sess) card, text := listCard(t, page, failing.ID) assert.Contains(t, text, "2 entrypoints, 1 inactive "+ "4 targets, 1 inactive "+ "3 events "+ "Last event "+lastEventText(failingEvents[2])+" "+ "2 failed deliveries in the last 24 hours") assert.Contains(t, card, failedHighlight+">2 failed deliveries in the last 24 hours<") card, text = listCard(t, page, healthy.ID) assert.Contains(t, text, "1 entrypoint "+ "1 target "+ "2 events "+ "Last event "+lastEventText(healthyEvents[1])+" "+ "0 failed deliveries in the last 24 hours") assert.NotContains(t, text, "inactive") assert.NotContains(t, card, failedHighlight) card, text = listCard(t, page, quiet.ID) assert.Contains(t, text, "0 entrypoints "+ "0 targets "+ "0 events "+ "No events yet "+ "0 failed deliveries in the last 24 hours") assert.NotContains(t, card, failedHighlight) assert.False(t, dbMgr.DBExists(quiet.ID), "showing the list must not create an event database") } // lastEventText is how the list shows the arrival of an event. func lastEventText(ev database.Event) string { return ev.CreatedAt.UTC().Format("2006-01-02 15:04:05 UTC") } // TestSourceList_UnreadableEventDatabase checks that a webhook whose // event database cannot be read says so in its entry instead of // showing zeros, and that the rest of the list is still shown. func TestSourceList_UnreadableEventDatabase(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) broken := listWebhook(t, db, "broken") statsEntrypoint(t, db, broken.ID, true) brokenDB, err := dbMgr.GetDB(broken.ID) require.NoError(t, err) require.NoError(t, brokenDB.Migrator().DropTable(&database.EventTotals{})) quiet := listWebhook(t, db, "quiet") page := renderWebhookList(t, h, sess) _, text := listCard(t, page, broken.ID) assert.Contains(t, text, "1 entrypoint 0 targets The event figures could not be read.") assert.NotContains(t, text, "events") assert.NotContains(t, text, "failed") _, text = listCard(t, page, quiet.ID) assert.Contains(t, text, "No events yet") }