package handlers_test import ( "net/http" "net/http/httptest" "strconv" "strings" "testing" "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/delivery" "sneak.berlin/go/webhooker/internal/handlers" "sneak.berlin/go/webhooker/internal/session" ) // responseCap is the number of response bytes the event log // page is allowed to render for one delivery attempt. const responseCap = handlers.MaxRenderedResponseBytesForTest // failedAttempt describes the failed delivery every test in // this file seeds. The values are distinctive so that finding // them in the rendered page cannot be a coincidence. const ( attemptStatusCode = 502 attemptDurationMS = 1234 attemptNumber = 3 attemptError = "upstream returned 502 Bad Gateway" ) // seedFailedDelivery records an event, a failed delivery // against targetID, and one delivery result carrying the // given response body. It returns the delivery. func seedFailedDelivery( t *testing.T, dbMgr *database.WebhookDBManager, webhookID, targetID, responseBody string, ) *database.Delivery { t.Helper() webhookDB, err := dbMgr.GetDB(webhookID) require.NoError(t, err) event := &database.Event{ WebhookID: webhookID, Method: http.MethodPost, Body: `{"test":true}`, ContentType: "application/json", } require.NoError(t, webhookDB.Omit( clause.Associations, ).Create(event).Error) dlv := &database.Delivery{ EventID: event.ID, TargetID: targetID, Status: database.DeliveryStatusFailed, } require.NoError(t, webhookDB.Omit( clause.Associations, ).Create(dlv).Error) result := &database.DeliveryResult{ DeliveryID: dlv.ID, AttemptNum: attemptNumber, Success: false, StatusCode: attemptStatusCode, ResponseBody: responseBody, Error: attemptError, Duration: attemptDurationMS, } require.NoError(t, webhookDB.Omit( clause.Associations, ).Create(result).Error) return dlv } // seedFailureAndRender seeds a failed delivery against a // target of the given type and config, and returns the // rendered event log page. func seedFailureAndRender( t *testing.T, targetType database.TargetType, config, responseBody string, ) string { t.Helper() 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, targetType, config, ) seedFailedDelivery(t, dbMgr, wh.ID, tgt.ID, responseBody) return renderSourceLogsPage(t, h, sess, wh.ID) } // TestHandleSourceLogs_RendersFailedAttempt is the regression // test for the reported gap: a failed delivery used to render // as the status word alone, so diagnosing it meant opening the // per-webhook SQLite file by hand. func TestHandleSourceLogs_RendersFailedAttempt(t *testing.T) { t.Parallel() body := seedFailureAndRender( t, database.TargetTypeHTTP, `{"url":"https://example.com/hook/abc"}`, "upstream exploded", ) assert.Contains( t, body, strconv.Itoa(attemptStatusCode), "the attempt's status code must reach the page", ) assert.Contains( t, body, attemptError, "the attempt's error must reach the page", ) assert.Contains( t, body, strconv.Itoa(attemptDurationMS), "the attempt's duration must reach the page", ) assert.Contains( t, body, "Attempt "+strconv.Itoa(attemptNumber), "the attempt number must reach the page", ) assert.Contains( t, body, "upstream exploded", "the attempt's response body must reach the page", ) } // TestHandleSourceLogs_EscapesResponseBody proves the // response body is treated as the untrusted remote content it // is. The remote chooses these bytes and the page is rendered // inside the operator's authenticated origin, where the // application's own CSP allows inline script from 'self'. func TestHandleSourceLogs_EscapesResponseBody(t *testing.T) { t.Parallel() const payload = `` body := seedFailureAndRender( t, database.TargetTypeHTTP, `{"url":"https://example.com/hook/abc"}`, payload, ) assert.NotContains(t, body, payload) assert.NotContains(t, body, "