From 6e6d0ece30e607eae88348198c248eccfb3ff3fb Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Wed, 28 Oct 2026 07:26:36 -0400 Subject: [PATCH 1/4] fix(listen): join the forwarding URL path and the event path with a single slash Co-Authored-By: Claude Sonnet 5.5 --- pkg/listen/proxy/forward_path_test.go | 62 +++++++++++++++++++++++++++ pkg/listen/proxy/proxy.go | 13 +++++- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 pkg/listen/proxy/forward_path_test.go 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 From 204e3bee04f7990e89f7b6fc6e3b345ea9e0ac5c Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 12 Nov 2026 00:26:08 +0000 Subject: [PATCH 2/4] fix(listen): print the same forwarded URL the request was sent to The request URL was joined with a single slash, but the URL printed in the compact and interactive output, the TUI event details and the TUI connection header was still concatenated by hand. A forwarding URL ending in / therefore printed /base//webhook for a request sent to /base/webhook, and the TUI details and header dropped the forwarding URL's path altogether. Move the join into pkg/listen/forwardurl and use it at all five places, with one test per place. Co-Authored-By: Claude Sonnet 5.5 --- pkg/listen/forwardurl/forwardurl.go | 28 +++++++ pkg/listen/forwardurl/forwardurl_test.go | 34 ++++++++ pkg/listen/proxy/proxy.go | 14 +--- .../proxy/renderer_forward_path_test.go | 81 +++++++++++++++++++ pkg/listen/proxy/renderer_interactive.go | 3 +- pkg/listen/proxy/renderer_simple.go | 3 +- pkg/listen/tui/forward_path_test.go | 77 ++++++++++++++++++ pkg/listen/tui/model.go | 3 +- pkg/listen/tui/view.go | 3 +- 9 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 pkg/listen/forwardurl/forwardurl.go create mode 100644 pkg/listen/forwardurl/forwardurl_test.go create mode 100644 pkg/listen/proxy/renderer_forward_path_test.go create mode 100644 pkg/listen/tui/forward_path_test.go diff --git a/pkg/listen/forwardurl/forwardurl.go b/pkg/listen/forwardurl/forwardurl.go new file mode 100644 index 00000000..fa8b529b --- /dev/null +++ b/pkg/listen/forwardurl/forwardurl.go @@ -0,0 +1,28 @@ +// Package forwardurl builds the URL that `hookdeck listen` forwards an event +// to. The request and every place that prints the URL share it, so what the +// CLI shows is what the CLI sent. +package forwardurl + +import ( + "net/url" + "strings" +) + +// JoinPath 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: a +// receiver may route on its own trailing slash, which is why path.Join is not +// used. +func JoinPath(base, eventPath string) string { + if strings.HasPrefix(eventPath, "/") { + base = strings.TrimSuffix(base, "/") + } + return base + eventPath +} + +// Build returns the full URL for an event: the forwarding URL's scheme, host +// and path, then the event's path. The forwarding URL's query string is not +// carried over. +func Build(target *url.URL, eventPath string) string { + return target.Scheme + "://" + target.Host + JoinPath(target.Path, eventPath) +} diff --git a/pkg/listen/forwardurl/forwardurl_test.go b/pkg/listen/forwardurl/forwardurl_test.go new file mode 100644 index 00000000..5373e6c1 --- /dev/null +++ b/pkg/listen/forwardurl/forwardurl_test.go @@ -0,0 +1,34 @@ +package forwardurl + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuild(t *testing.T) { + tests := []struct { + name string + target string + eventPath string + want string + }{ + {"forwarding URL with a trailing slash", "http://localhost:3000/base/", "/webhook", "http://localhost:3000/base/webhook"}, + {"forwarding URL without a trailing slash", "http://localhost:3000/base", "/webhook", "http://localhost:3000/base/webhook"}, + {"forwarding URL is the root", "http://localhost:3000/", "/webhook", "http://localhost:3000/webhook"}, + {"forwarding URL has no path", "http://localhost:3000", "/webhook", "http://localhost:3000/webhook"}, + {"event path keeps its own trailing slash", "http://localhost:3000/base/", "/webhook/", "http://localhost:3000/base/webhook/"}, + {"event path is empty", "http://localhost:3000/base/", "", "http://localhost:3000/base/"}, + {"forwarding URL query string is not carried over", "http://localhost:3000/base/?token=x", "/webhook", "http://localhost:3000/base/webhook"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + target, err := url.Parse(tt.target) + require.NoError(t, err) + assert.Equal(t, tt.want, Build(target, tt.eventPath)) + }) + } +} diff --git a/pkg/listen/proxy/proxy.go b/pkg/listen/proxy/proxy.go index af83a9ce..b37ad0c3 100644 --- a/pkg/listen/proxy/proxy.go +++ b/pkg/listen/proxy/proxy.go @@ -14,7 +14,6 @@ import ( "os" "os/signal" "strconv" - "strings" "sync" "sync/atomic" "syscall" @@ -23,6 +22,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/hookdeck/hookdeck-cli/pkg/hookdeck" + "github.com/hookdeck/hookdeck-cli/pkg/listen/forwardurl" "github.com/hookdeck/hookdeck-cli/pkg/listen/healthcheck" "github.com/hookdeck/hookdeck-cli/pkg/websocket" ) @@ -388,16 +388,6 @@ 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") @@ -411,7 +401,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 + joinForwardPath(p.cfg.URL.Path, webhookEvent.Body.Path) + url := forwardurl.Build(p.cfg.URL, webhookEvent.Body.Path) // Create request with context for timeout control timeout := webhookEvent.Body.Request.Timeout diff --git a/pkg/listen/proxy/renderer_forward_path_test.go b/pkg/listen/proxy/renderer_forward_path_test.go new file mode 100644 index 00000000..a8a8576f --- /dev/null +++ b/pkg/listen/proxy/renderer_forward_path_test.go @@ -0,0 +1,81 @@ +package proxy + +import ( + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/hookdeck/hookdeck-cli/pkg/listen/tui" + "github.com/hookdeck/hookdeck-cli/pkg/websocket" +) + +// The request is sent to a URL built in processAttempt, and each renderer +// prints the URL it believes was requested. These tests pin both renderers to +// the single-slash join, so a forwarding URL ending in "/" does not print +// "/base//webhook" for a request that went to "/base/webhook". + +func forwardPathRendererConfig(t *testing.T, forwardTo string) *RendererConfig { + t.Helper() + cfg := linksRendererConfig(t, "gateway") + target, err := url.Parse(forwardTo) + require.NoError(t, err) + cfg.TargetURL = target + return cfg +} + +func forwardPathAttempt(eventPath string) *websocket.Attempt { + return &websocket.Attempt{Body: websocket.AttemptBody{ + Path: eventPath, + EventID: "evt_1", + AttemptId: "atm_1", + ConnectionId: "web_1", + Request: websocket.AttemptRequest{Method: "POST"}, + }} +} + +var forwardPathRendererCases = []struct { + name string + forwardTo string + eventPath string + want string +}{ + {"trailing slash on the forwarding URL", "http://localhost:3000/base/", "/webhook", "POST http://localhost:3000/base/webhook "}, + {"no trailing slash on the forwarding URL", "http://localhost:3000/base", "/webhook", "POST http://localhost:3000/base/webhook "}, + {"forwarding URL is the root", "http://localhost:3000/", "/webhook", "POST http://localhost:3000/webhook "}, + {"event path keeps its own trailing slash", "http://localhost:3000/base/", "/webhook/", "POST http://localhost:3000/base/webhook/ "}, +} + +func TestSimpleRendererJoinsForwardingURLAndEventPath(t *testing.T) { + response := &EventResponse{StatusCode: 200, Duration: 5 * time.Millisecond} + + for _, tc := range forwardPathRendererCases { + t.Run(tc.name, func(t *testing.T) { + r := NewSimpleRenderer(forwardPathRendererConfig(t, tc.forwardTo), false) + out := captureStdout(t, func() { + r.OnEventComplete("evt_1", forwardPathAttempt(tc.eventPath), response, time.Now()) + }) + assert.Contains(t, out, tc.want) + }) + } +} + +func TestInteractiveRendererJoinsForwardingURLAndEventPath(t *testing.T) { + response := &EventResponse{StatusCode: 200, Duration: 5 * time.Millisecond} + + for _, tc := range forwardPathRendererCases { + t.Run(tc.name, func(t *testing.T) { + r, sent := recordingInteractiveRenderer() + r.cfg = forwardPathRendererConfig(t, tc.forwardTo) + + r.OnEventComplete("evt_1", forwardPathAttempt(tc.eventPath), response, time.Now()) + + require.Len(t, *sent, 1) + update, ok := (*sent)[0].(tui.UpdateEventMsg) + require.True(t, ok, "unexpected message %T", (*sent)[0]) + assert.Contains(t, update.LogLine, tc.want) + }) + } +} diff --git a/pkg/listen/proxy/renderer_interactive.go b/pkg/listen/proxy/renderer_interactive.go index aeb68fcd..91485bec 100644 --- a/pkg/listen/proxy/renderer_interactive.go +++ b/pkg/listen/proxy/renderer_interactive.go @@ -10,6 +10,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/hookdeck/hookdeck-cli/pkg/ansi" + "github.com/hookdeck/hookdeck-cli/pkg/listen/forwardurl" "github.com/hookdeck/hookdeck-cli/pkg/listen/tui" "github.com/hookdeck/hookdeck-cli/pkg/websocket" ) @@ -152,7 +153,7 @@ func (r *InteractiveRenderer) OnEventComplete(eventID string, attempt *websocket color.Faint(localTime), ansi.ColorizeStatus(response.StatusCode), attempt.Body.Request.Method, - r.cfg.TargetURL.Scheme+"://"+r.cfg.TargetURL.Host+r.cfg.TargetURL.Path+attempt.Body.Path, + forwardurl.Build(r.cfg.TargetURL, attempt.Body.Path), color.Faint(fmt.Sprintf("(%dms)", durationMs)), color.Faint("→"), color.Faint(displayURL), diff --git a/pkg/listen/proxy/renderer_simple.go b/pkg/listen/proxy/renderer_simple.go index db6bb52a..ee2c4aea 100644 --- a/pkg/listen/proxy/renderer_simple.go +++ b/pkg/listen/proxy/renderer_simple.go @@ -9,6 +9,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/hookdeck/hookdeck-cli/pkg/ansi" + "github.com/hookdeck/hookdeck-cli/pkg/listen/forwardurl" "github.com/hookdeck/hookdeck-cli/pkg/websocket" ) @@ -149,7 +150,7 @@ func (r *SimpleRenderer) OnEventComplete(eventID string, attempt *websocket.Atte color.Faint(localTime), ansi.ColorizeStatus(response.StatusCode), attempt.Body.Request.Method, - r.cfg.TargetURL.Scheme+"://"+r.cfg.TargetURL.Host+r.cfg.TargetURL.Path+attempt.Body.Path, + forwardurl.Build(r.cfg.TargetURL, attempt.Body.Path), color.Faint(fmt.Sprintf("(%dms)", durationMs)), color.Faint("→"), color.Faint(displayURL), diff --git a/pkg/listen/tui/forward_path_test.go b/pkg/listen/tui/forward_path_test.go new file mode 100644 index 00000000..8f1bb0a4 --- /dev/null +++ b/pkg/listen/tui/forward_path_test.go @@ -0,0 +1,77 @@ +package tui + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/hookdeck/hookdeck-cli/pkg/hookdeck" + "github.com/hookdeck/hookdeck-cli/pkg/websocket" +) + +// The TUI prints the forwarded URL in two places, and both used to leave the +// forwarding URL's path out or join it with a doubled "/". + +// The request line in the event details (and the copied request) is the URL the +// event was forwarded to. +func TestEventDetailsRequestLineJoinsForwardingURLAndEventPath(t *testing.T) { + for _, tc := range []struct { + name string + forwardTo string + eventPath string + want string + }{ + {"trailing slash on the forwarding URL", "http://localhost:3500/base/", "/webhook", "POST http://localhost:3500/base/webhook"}, + {"no trailing slash on the forwarding URL", "http://localhost:3500/base", "/webhook", "POST http://localhost:3500/base/webhook"}, + {"forwarding URL is the root", "http://localhost:3500/", "/webhook", "POST http://localhost:3500/webhook"}, + {"event path keeps its own trailing slash", "http://localhost:3500/base/", "/webhook/", "POST http://localhost:3500/base/webhook/"}, + } { + t.Run(tc.name, func(t *testing.T) { + m := newDetailsTestModel(t) + target, err := url.Parse(tc.forwardTo) + require.NoError(t, err) + m.cfg.TargetURL = target + + event := &EventInfo{ + ID: "evt_test", + Data: &websocket.Attempt{Body: websocket.AttemptBody{ + Path: tc.eventPath, + Request: websocket.AttemptRequest{Method: "POST", DataString: "{}"}, + }}, + } + + content, copyContent := m.buildEventDetailsContent(event) + + assert.Equal(t, tc.want, copyContent.requestLine) + assert.Contains(t, content, tc.want) + }) + } +} + +// The header lists each connection under its source as "Forwards to → ". +func TestConnectionInfoForwardsToJoinsForwardingURLAndConnectionPath(t *testing.T) { + for _, tc := range []struct { + name string + forwardTo string + cliPath string + want string + }{ + {"trailing slash on the forwarding URL", "http://localhost:3030/base/", "/webhook", "Forwards to → http://localhost:3030/base/webhook"}, + {"no trailing slash on the forwarding URL", "http://localhost:3030/base", "/webhook", "Forwards to → http://localhost:3030/base/webhook"}, + {"forwarding URL has no path", "http://localhost:3030", "/webhook", "Forwards to → http://localhost:3030/webhook"}, + } { + t.Run(tc.name, func(t *testing.T) { + m := newConnectionTestModel(t) + target, err := url.Parse(tc.forwardTo) + require.NoError(t, err) + m.cfg.TargetURL = target + m.cfg.Connections[0].Destination = &hookdeck.Destination{ + ID: "des_1", Type: "CLI", Config: map[string]interface{}{"path": tc.cliPath}, + } + + assert.Contains(t, m.renderConnectionInfo(), tc.want) + }) + } +} diff --git a/pkg/listen/tui/model.go b/pkg/listen/tui/model.go index a253b25b..d022c5b8 100644 --- a/pkg/listen/tui/model.go +++ b/pkg/listen/tui/model.go @@ -13,6 +13,7 @@ import ( tea "github.com/charmbracelet/bubbletea" configpkg "github.com/hookdeck/hookdeck-cli/pkg/config" "github.com/hookdeck/hookdeck-cli/pkg/hookdeck" + "github.com/hookdeck/hookdeck-cli/pkg/listen/forwardurl" "github.com/hookdeck/hookdeck-cli/pkg/login" "github.com/hookdeck/hookdeck-cli/pkg/websocket" ) @@ -347,7 +348,7 @@ func (m *Model) buildEventDetailsContent(event *EventInfo) (string, requestCopyC content.WriteString("\n\n") // HTTP request line: METHOD URL - requestURL := m.cfg.TargetURL.Scheme + "://" + m.cfg.TargetURL.Host + event.Data.Body.Path + requestURL := forwardurl.Build(m.cfg.TargetURL, event.Data.Body.Path) requestCopy.requestLine = event.Data.Body.Request.Method + " " + requestURL content.WriteString(requestCopy.requestLine + "\n\n") diff --git a/pkg/listen/tui/view.go b/pkg/listen/tui/view.go index 61a705b5..c528c814 100644 --- a/pkg/listen/tui/view.go +++ b/pkg/listen/tui/view.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/hookdeck/hookdeck-cli/pkg/hookdeck" + "github.com/hookdeck/hookdeck-cli/pkg/listen/forwardurl" "github.com/hookdeck/hookdeck-cli/pkg/listen/summary" ) @@ -410,7 +411,7 @@ func (m Model) renderConnectionInfo() string { if conns, exists := sourceConnections[source.ID]; exists { numConns := len(conns) for j, conn := range conns { - fullPath := m.cfg.TargetURL.Scheme + "://" + m.cfg.TargetURL.Host + conn.cliPath + fullPath := forwardurl.Build(m.cfg.TargetURL, conn.cliPath) connDisplay := "" if conn.destName != "" { From 0aede4006018e591ef3b9c4b9f0edb1d2e003abc Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 12 Nov 2026 00:32:59 +0000 Subject: [PATCH 3/4] test(listen): stop asserting that a query string on the forwarding URL is dropped Whether the query string should be forwarded or rejected is undecided (see #475), so the forwardurl test should not pin the current behaviour. Co-Authored-By: Claude Sonnet 5.5 --- pkg/listen/forwardurl/forwardurl.go | 3 +-- pkg/listen/forwardurl/forwardurl_test.go | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/listen/forwardurl/forwardurl.go b/pkg/listen/forwardurl/forwardurl.go index fa8b529b..a95e93ab 100644 --- a/pkg/listen/forwardurl/forwardurl.go +++ b/pkg/listen/forwardurl/forwardurl.go @@ -21,8 +21,7 @@ func JoinPath(base, eventPath string) string { } // Build returns the full URL for an event: the forwarding URL's scheme, host -// and path, then the event's path. The forwarding URL's query string is not -// carried over. +// and path, then the event's path. func Build(target *url.URL, eventPath string) string { return target.Scheme + "://" + target.Host + JoinPath(target.Path, eventPath) } diff --git a/pkg/listen/forwardurl/forwardurl_test.go b/pkg/listen/forwardurl/forwardurl_test.go index 5373e6c1..9a58281e 100644 --- a/pkg/listen/forwardurl/forwardurl_test.go +++ b/pkg/listen/forwardurl/forwardurl_test.go @@ -21,7 +21,6 @@ func TestBuild(t *testing.T) { {"forwarding URL has no path", "http://localhost:3000", "/webhook", "http://localhost:3000/webhook"}, {"event path keeps its own trailing slash", "http://localhost:3000/base/", "/webhook/", "http://localhost:3000/base/webhook/"}, {"event path is empty", "http://localhost:3000/base/", "", "http://localhost:3000/base/"}, - {"forwarding URL query string is not carried over", "http://localhost:3000/base/?token=x", "/webhook", "http://localhost:3000/base/webhook"}, } for _, tt := range tests { From 7cd909f5162f6f0083c4426d5d388a555caa51ad Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 12 Nov 2026 01:11:01 +0000 Subject: [PATCH 4/4] fix(listen): join the forwarding URL path in the startup banner too The compact startup banner built its "Forwards to" URL from the scheme and host only, so it left out the forwarding URL's path and could disagree with the URL the request is sent to. Build it with pkg/listen/forwardurl like the other places that print the forwarded URL. Co-Authored-By: Claude Sonnet 5.5 --- pkg/listen/printer.go | 3 +- pkg/listen/printer_forward_path_test.go | 44 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 pkg/listen/printer_forward_path_test.go diff --git a/pkg/listen/printer.go b/pkg/listen/printer.go index 8bd9fbfa..565e6b2c 100644 --- a/pkg/listen/printer.go +++ b/pkg/listen/printer.go @@ -10,6 +10,7 @@ import ( "github.com/hookdeck/hookdeck-cli/pkg/ansi" "github.com/hookdeck/hookdeck-cli/pkg/config" "github.com/hookdeck/hookdeck-cli/pkg/hookdeck" + "github.com/hookdeck/hookdeck-cli/pkg/listen/forwardurl" "github.com/hookdeck/hookdeck-cli/pkg/listen/links" "github.com/hookdeck/hookdeck-cli/pkg/listen/summary" ) @@ -61,7 +62,7 @@ func printSourcesWithConnections(config *config.Config, projectID string, source if cliPath != nil { path = *cliPath } - fullPath := targetURL.Scheme + "://" + targetURL.Host + path + fullPath := forwardurl.Build(targetURL, path) // Get connection name from FullName (format: "source -> destination") // Split on "->" and take the second part (destination) diff --git a/pkg/listen/printer_forward_path_test.go b/pkg/listen/printer_forward_path_test.go new file mode 100644 index 00000000..6c8bc003 --- /dev/null +++ b/pkg/listen/printer_forward_path_test.go @@ -0,0 +1,44 @@ +package listen + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/hookdeck/hookdeck-cli/pkg/hookdeck" +) + +// The startup banner lists "Forwards to → " for each connection. It has to +// show the URL the events will be forwarded to, including the forwarding URL's +// path, joined to the connection's path with a single "/". +func TestCompactBannerForwardsToJoinsForwardingURLAndConnectionPath(t *testing.T) { + for _, tc := range []struct { + name string + forwardTo string + cliPath string + want string + }{ + {"trailing slash on the forwarding URL", "http://localhost:3030/base/", "/webhook", "Forwards to → http://localhost:3030/base/webhook"}, + {"no trailing slash on the forwarding URL", "http://localhost:3030/base", "/webhook", "Forwards to → http://localhost:3030/base/webhook"}, + {"forwarding URL has no path", "http://localhost:3030", "/webhook", "Forwards to → http://localhost:3030/webhook"}, + {"connection path is the default root", "http://localhost:3030/base/", "/", "Forwards to → http://localhost:3030/base/"}, + } { + t.Run(tc.name, func(t *testing.T) { + resetColorState(t) + cfg, sources, connections, _ := printerFixture(t, 1) + targetURL, err := url.Parse(tc.forwardTo) + require.NoError(t, err) + connections[0].Destination = &hookdeck.Destination{ + ID: "des_1", Type: "CLI", Config: map[string]interface{}{"path": tc.cliPath}, + } + + out := capturePrinterOutput(t, func() { + printSourcesWithConnections(cfg, "tm_1", sources, connections, targetURL, "") + }) + + assert.Contains(t, out, tc.want) + }) + } +}