package delivery_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/database" "sneak.berlin/go/webhooker/internal/delivery" ) // eventQuery is the query string the events in these tests arrived // with. const eventQuery = "a=1&b=2" // httpTargetConfig is the stored configuration of an HTTP target at // targetURL. func httpTargetConfig( t *testing.T, targetURL string, forwardQuery bool, ) string { t.Helper() cfg, err := json.Marshal(delivery.HTTPTargetConfig{ URL: targetURL, ForwardQuery: forwardQuery, }) require.NoError(t, err) return string(cfg) } // deliverWithQuery sends one event that arrived with eventQuery to an // HTTP target configured with cfg, through the path a received event's // delivery takes, and returns the attempt it recorded. func deliverWithQuery(t *testing.T, cfg string) database.DeliveryResult { t.Helper() s := newISetup(t) event := iSeedEvent(t, s.WebhookDB, s.WebhookID, "{}") d := iSeedDelivery( t, s.WebhookDB, event.ID, uuid.NewString(), database.DeliveryStatusPending, ) task := iTask( d, event, s.WebhookID, d.TargetID, "query", cfg, 0, 1, &event.Body, ) task.RawQuery = eventQuery s.Engine.ExportProcessNewTask(context.TODO(), &task) var result database.DeliveryResult require.NoError(t, s.WebhookDB.Where( "delivery_id = ?", d.ID, ).First(&result).Error) return result } // TestDeliverHTTP_ForwardQuery proves the URL a delivery is sent to: // with the target's setting off, the target URL exactly as configured; // with it on, the event's query string appended, joined with "&" to a // query string the target URL already has. func TestDeliverHTTP_ForwardQuery(t *testing.T) { t.Parallel() // The target URL's path, without and with a query string of its // own. const ( plain = "/in" withQuery = "/in?key=k" ) tests := map[string]struct { path string forwardQuery bool want string }{ "off": { path: plain, want: plain, }, "off, the target URL has a query string": { path: withQuery, want: withQuery, }, "on": { path: plain, forwardQuery: true, want: plain + "?" + eventQuery, }, "on, the target URL has a query string": { path: withQuery, forwardQuery: true, want: withQuery + "&" + eventQuery, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { t.Parallel() received := make(chan string, 1) ts := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { received <- r.RequestURI w.WriteHeader(http.StatusOK) }, )) t.Cleanup(ts.Close) result := deliverWithQuery(t, httpTargetConfig( t, ts.URL+tc.path, tc.forwardQuery, )) assert.True(t, result.Success) require.Len(t, received, 1) assert.Equal(t, tc.want, <-received) }) } } // TestDeliverHTTP_ForwardedQueryKeepsTheTargetURLMasked proves the // credential in a target URL's own query string stays masked once the // event's query string is appended to it: in a response or error that // echoes the URL the target was sent, as the event log's Redactor shows // it, and in the error a failed connection stores. func TestDeliverHTTP_ForwardedQueryKeepsTheTargetURLMasked(t *testing.T) { t.Parallel() const secret = "s3cr3t" received := make(chan string, 1) ts := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { received <- r.RequestURI w.WriteHeader(http.StatusBadRequest) }, )) t.Cleanup(ts.Close) target := &database.Target{ Type: database.TargetTypeHTTP, Config: httpTargetConfig(t, ts.URL+"/in?token="+secret, true), } deliverWithQuery(t, target.Config) require.Len(t, received, 1) sent := <-received require.Equal(t, "/in?token="+secret+"&"+eventQuery, sent) redactor := delivery.NewRedactor(target) for _, echoed := range []string{sent, ts.URL + sent} { shown := redactor.Redact("rejected " + echoed) assert.NotContains(t, shown, secret, echoed) assert.Contains(t, shown, delivery.RedactionMarker, echoed) } // Nothing listens on port 1. failed := deliverWithQuery(t, httpTargetConfig( t, "http://127.0.0.1:1/in?token="+secret, true, )) require.NotEmpty(t, failed.Error) assert.NotContains(t, failed.Error, secret) assert.NotContains(t, failed.Error, eventQuery) }