Files
webhooker/internal/delivery/target_http_query_test.go
T
clawbot ea8cba7264
check / check (push) Successful in 4m45s
Store and show an event's query string, and pass it on to an HTTP target when set (closes #312)
The receiver dropped the query string of every request it received, so a sender's URL parameters were silently lost. Each event now keeps it, as sent, in a new `raw_query` column of the per-webhook `events` table; a resubmitted copy carries its original's. The event log and the event's page show it in the shared request block, the event log leaving out one over 32 KiB with a link, as for headers. The archive and log targets carry it. HTTP targets gain "Pass the query string on to this target", off by default: on, deliveries, replays and resubmits append it to the target URL, joined with `&` to one already there. The access log still hides it.

Model: opus-5-5
2026-10-04 03:00:34 +02:00

173 lines
4.3 KiB
Go

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)
}