Skip to content

Commit 77c4831

Browse files
committed
feat: add name-based lookup and improve error handling for connection get
- Add support for passing connection name to 'connection get' command - Implement resolveConnectionID() to accept both ID and name - Improve 404 error handling with user-friendly messages - Add tests for name-based lookup and error cases - Update command usage and help text to reflect name support
1 parent 6201a18 commit 77c4831

File tree

2 files changed

+155
-7
lines changed

2 files changed

+155
-7
lines changed

pkg/cmd/connection_get.go

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8+
"strings"
89

910
"github.com/spf13/cobra"
1011

1112
"github.com/hookdeck/hookdeck-cli/pkg/ansi"
13+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
1214
"github.com/hookdeck/hookdeck-cli/pkg/validators"
1315
)
1416

@@ -22,14 +24,19 @@ func newConnectionGetCmd() *connectionGetCmd {
2224
cc := &connectionGetCmd{}
2325

2426
cc.cmd = &cobra.Command{
25-
Use: "get <connection-id>",
27+
Use: "get <connection-id-or-name>",
2628
Args: validators.ExactArgs(1),
2729
Short: "Get connection details",
2830
Long: `Get detailed information about a specific connection.
2931
32+
You can specify either a connection ID or name.
33+
3034
Examples:
31-
# Get connection details
32-
hookdeck connection get conn_abc123`,
35+
# Get connection by ID
36+
hookdeck connection get conn_abc123
37+
38+
# Get connection by name
39+
hookdeck connection get my-connection`,
3340
RunE: cc.runConnectionGetCmd,
3441
}
3542

@@ -43,14 +50,20 @@ func (cc *connectionGetCmd) runConnectionGetCmd(cmd *cobra.Command, args []strin
4350
return err
4451
}
4552

46-
connectionID := args[0]
47-
client := Config.GetAPIClient()
53+
connectionIDOrName := args[0]
54+
apiClient := Config.GetAPIClient()
4855
ctx := context.Background()
4956

57+
// Resolve connection ID from name or ID
58+
connectionID, err := resolveConnectionID(ctx, apiClient, connectionIDOrName)
59+
if err != nil {
60+
return err
61+
}
62+
5063
// Get connection by ID
51-
conn, err := client.GetConnection(ctx, connectionID)
64+
conn, err := apiClient.GetConnection(ctx, connectionID)
5265
if err != nil {
53-
return fmt.Errorf("failed to get connection: %w", err)
66+
return formatConnectionError(err, connectionIDOrName)
5467
}
5568

5669
if cc.output == "json" {
@@ -141,3 +154,62 @@ func (cc *connectionGetCmd) runConnectionGetCmd(cmd *cobra.Command, args []strin
141154

142155
return nil
143156
}
157+
158+
// resolveConnectionID accepts both connection names and IDs
159+
// Try as ID first (if it starts with conn_ or web_), then lookup by name
160+
func resolveConnectionID(ctx context.Context, client *hookdeck.Client, nameOrID string) (string, error) {
161+
// If it looks like a connection ID, try it directly
162+
if strings.HasPrefix(nameOrID, "conn_") || strings.HasPrefix(nameOrID, "web_") {
163+
// Try to get it to verify it exists
164+
_, err := client.GetConnection(ctx, nameOrID)
165+
if err == nil {
166+
return nameOrID, nil
167+
}
168+
// If we get a 404, fall through to name lookup
169+
// For other errors, format and return the error
170+
errMsg := strings.ToLower(err.Error())
171+
if !strings.Contains(errMsg, "404") && !strings.Contains(errMsg, "not found") {
172+
return "", err
173+
}
174+
// 404 on ID lookup - fall through to try name lookup
175+
}
176+
177+
// Try to find by name
178+
params := map[string]string{
179+
"name": nameOrID,
180+
}
181+
182+
result, err := client.ListConnections(ctx, params)
183+
if err != nil {
184+
return "", fmt.Errorf("failed to lookup connection by name '%s': %w", nameOrID, err)
185+
}
186+
187+
if result.Pagination.Limit == 0 || len(result.Models) == 0 {
188+
return "", fmt.Errorf("connection not found: '%s'\n\nPlease check the connection name or ID and try again", nameOrID)
189+
}
190+
191+
if len(result.Models) > 1 {
192+
return "", fmt.Errorf("multiple connections found with name '%s', please use the connection ID instead", nameOrID)
193+
}
194+
195+
return result.Models[0].ID, nil
196+
}
197+
198+
// formatConnectionError provides user-friendly error messages for connection get failures
199+
func formatConnectionError(err error, identifier string) error {
200+
errMsg := err.Error()
201+
202+
// Check for 404/not found errors (case-insensitive)
203+
errMsgLower := strings.ToLower(errMsg)
204+
if strings.Contains(errMsgLower, "404") || strings.Contains(errMsgLower, "not found") {
205+
return fmt.Errorf("connection not found: '%s'\n\nPlease check the connection name or ID and try again", identifier)
206+
}
207+
208+
// Check for network/timeout errors
209+
if strings.Contains(errMsg, "timeout") || strings.Contains(errMsg, "connection refused") {
210+
return fmt.Errorf("failed to connect to Hookdeck API: %w\n\nPlease check your network connection and try again", err)
211+
}
212+
213+
// Default to the original error with some context
214+
return fmt.Errorf("failed to get connection '%s': %w", identifier, err)
215+
}

test/acceptance/connection_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,82 @@ func TestConnectionCreateAndDelete(t *testing.T) {
6262
t.Logf("Successfully created and retrieved connection: %s", conn.Name)
6363
}
6464

65+
// TestConnectionGetByName tests that connection get works with connection name
66+
func TestConnectionGetByName(t *testing.T) {
67+
if testing.Short() {
68+
t.Skip("Skipping acceptance test in short mode")
69+
}
70+
71+
cli := NewCLIRunner(t)
72+
timestamp := generateTimestamp()
73+
74+
connName := "test-get-by-name-" + timestamp
75+
sourceName := "test-src-" + timestamp
76+
destName := "test-dst-" + timestamp
77+
78+
// Create a test connection
79+
var createResp Connection
80+
err := cli.RunJSON(&createResp,
81+
"connection", "create",
82+
"--name", connName,
83+
"--source-name", sourceName,
84+
"--source-type", "WEBHOOK",
85+
"--destination-name", destName,
86+
"--destination-type", "CLI",
87+
"--destination-cli-path", "/webhooks",
88+
)
89+
require.NoError(t, err, "Should create test connection")
90+
require.NotEmpty(t, createResp.ID, "Connection should have an ID")
91+
92+
// Cleanup
93+
t.Cleanup(func() {
94+
deleteConnection(t, cli, createResp.ID)
95+
})
96+
97+
// Test 1: Get by ID (original behavior)
98+
var getByID Connection
99+
err = cli.RunJSON(&getByID, "connection", "get", createResp.ID)
100+
require.NoError(t, err, "Should be able to get connection by ID")
101+
assert.Equal(t, createResp.ID, getByID.ID, "Connection ID should match")
102+
103+
// Test 2: Get by name (new behavior)
104+
var getByName Connection
105+
err = cli.RunJSON(&getByName, "connection", "get", connName)
106+
require.NoError(t, err, "Should be able to get connection by name")
107+
assert.Equal(t, createResp.ID, getByName.ID, "Connection ID should match when retrieved by name")
108+
assert.Equal(t, connName, getByName.Name, "Connection name should match")
109+
110+
// Test 3: Verify both methods return the same connection
111+
assert.Equal(t, getByID.ID, getByName.ID, "Getting by ID and name should return same connection")
112+
113+
t.Logf("Successfully tested connection get by both ID (%s) and name (%s)", createResp.ID, connName)
114+
}
115+
116+
// TestConnectionGetNotFound tests error handling for non-existent connections
117+
func TestConnectionGetNotFound(t *testing.T) {
118+
if testing.Short() {
119+
t.Skip("Skipping acceptance test in short mode")
120+
}
121+
122+
cli := NewCLIRunner(t)
123+
124+
// Test 1: Non-existent ID
125+
stdout, stderr, err := cli.Run("connection", "get", "conn_nonexistent123")
126+
require.Error(t, err, "Should error when connection ID doesn't exist")
127+
combinedOutput := stdout + stderr
128+
assert.Contains(t, combinedOutput, "connection not found", "Error should indicate connection not found")
129+
assert.Contains(t, combinedOutput, "Please check the connection name or ID", "Error should suggest checking the identifier")
130+
131+
// Test 2: Non-existent name
132+
stdout, stderr, err = cli.Run("connection", "get", "nonexistent-connection-name-xyz")
133+
require.Error(t, err, "Should error when connection name doesn't exist")
134+
combinedOutput = stdout + stderr
135+
assert.Contains(t, combinedOutput, "connection not found", "Error should indicate connection not found")
136+
assert.Contains(t, combinedOutput, "Please check the connection name or ID", "Error should suggest checking the identifier")
137+
138+
t.Logf("Successfully tested error handling for non-existent connections")
139+
}
140+
65141
// TestConnectionWithWebhookSource tests creating a connection with a WEBHOOK source
66142
func TestConnectionWithWebhookSource(t *testing.T) {
67143
if testing.Short() {

0 commit comments

Comments
 (0)