diff --git a/pkg/listen/proxy/forward_path_test.go b/pkg/listen/proxy/forward_path_test.go new file mode 100644 index 00000000..3cac012e --- /dev/null +++ b/pkg/listen/proxy/forward_path_test.go @@ -0,0 +1,62 @@ +package proxy + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/hookdeck/hookdeck-cli/pkg/websocket" +) + +func TestProcessAttemptJoinsForwardingURLPathAndEventPath(t *testing.T) { + tests := []struct { + name string + forwardTo string + eventPath string + expectPath string + }{ + {"forwarding URL with a trailing slash", "/base/", "/webhook", "/base/webhook"}, + {"forwarding URL without a trailing slash", "/base", "/webhook", "/base/webhook"}, + {"forwarding URL is the root", "/", "/webhook", "/webhook"}, + {"forwarding URL has no path", "", "/webhook", "/webhook"}, + {"event path keeps its own trailing slash", "/base/", "/webhook/", "/base/webhook/"}, + {"event path is empty", "/base/", "", "/base/"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + received := make(chan string, 1) + local := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + received <- r.URL.Path + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(local.Close) + + target, err := url.Parse(local.URL + tt.forwardTo) + require.NoError(t, err) + p := New(&Config{URL: target, NoHealthcheck: true}, nil, newRecordingRenderer()) + + p.processAttempt(websocket.IncomingMessage{Attempt: &websocket.Attempt{ + Body: websocket.AttemptBody{ + Path: tt.eventPath, + EventID: "evt_test", + AttemptId: "evt_test", + Request: websocket.AttemptRequest{Method: http.MethodPost, DataString: "{}", Headers: json.RawMessage(`{}`)}, + }, + }}) + + select { + case got := <-received: + assert.Equal(t, tt.expectPath, got) + case <-time.After(5 * time.Second): + t.Fatal("local server never received the request") + } + }) + } +} diff --git a/pkg/listen/proxy/proxy.go b/pkg/listen/proxy/proxy.go index d47b5756..0bed1288 100644 --- a/pkg/listen/proxy/proxy.go +++ b/pkg/listen/proxy/proxy.go @@ -14,6 +14,7 @@ import ( "os" "os/signal" "strconv" + "strings" "sync" "sync/atomic" "syscall" @@ -379,6 +380,16 @@ func (p *Proxy) createSession(ctx context.Context) (hookdeck.Session, error) { return session, err } +// joinForwardPath appends the event's path to the forwarding URL's path with a +// single "/" between them. A trailing "/" on the forwarding URL is dropped only +// when the event path starts with one, and the event path itself is left as is. +func joinForwardPath(base, eventPath string) string { + if strings.HasPrefix(eventPath, "/") { + base = strings.TrimSuffix(base, "/") + } + return base + eventPath +} + func (p *Proxy) processAttempt(msg websocket.IncomingMessage) { if msg.Attempt == nil { p.cfg.Log.Debug("WebSocket specified for Events received unexpected event") @@ -392,7 +403,7 @@ func (p *Proxy) processAttempt(msg websocket.IncomingMessage) { "prefix": "proxy.Proxy.processAttempt", }).Debugf("Processing webhook event") - url := p.cfg.URL.Scheme + "://" + p.cfg.URL.Host + p.cfg.URL.Path + webhookEvent.Body.Path + url := p.cfg.URL.Scheme + "://" + p.cfg.URL.Host + joinForwardPath(p.cfg.URL.Path, webhookEvent.Body.Path) // Create request with context for timeout control timeout := webhookEvent.Body.Request.Timeout