Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ $ hookdeck connection list

# Filter by source or destination
$ hookdeck connection list --source src_abc123
$ hookdeck connection list --destination dest_xyz789
$ hookdeck connection list --destination des_xyz789

# Filter by name pattern
$ hookdeck connection list --name "production-*"
Expand Down
4 changes: 2 additions & 2 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,7 @@ hookdeck connection list
hookdeck connection list --source-id src_abc123

# Filter by destination ID
hookdeck connection list --destination-id dest_xyz789
hookdeck connection list --destination-id des_xyz789

# Filter by connection name
hookdeck connection list --name "production-connection"
Expand Down Expand Up @@ -1586,7 +1586,7 @@ hookdeck attempt list
hookdeck attempt list --event-id evt_123

# List attempts for a destination
hookdeck attempt list --destination-id dest_456
hookdeck attempt list --destination-id des_456

# Filter by status
hookdeck attempt list --status FAILED
Expand Down
33 changes: 32 additions & 1 deletion pkg/cmd/connection_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ func (cc *connectionCreateCmd) runConnectionCreateCmd(cmd *cobra.Command, args [
// Single API call to create the connection
connection, err := client.CreateConnection(context.Background(), req)
if err != nil {
return fmt.Errorf("failed to create connection: %w", err)
return cc.enhanceCreateError(err)
}

// Display results
Expand Down Expand Up @@ -1049,3 +1049,34 @@ func (cc *connectionCreateCmd) buildRulesArray(cmd *cobra.Command) ([]hookdeck.R

return rules, nil
}

// enhanceCreateError adds helpful hints to API errors based on the flags used
func (cc *connectionCreateCmd) enhanceCreateError(err error) error {
return cc.enhanceConnectionError(err, "create")
}

// enhanceConnectionError adds helpful hints to API errors based on the flags used
// This is shared between create and upsert commands
func (cc *connectionCreateCmd) enhanceConnectionError(err error, operation string) error {
errStr := err.Error()

// Check if this is a "Not Found" error, which commonly indicates an invalid resource ID
isNotFound := strings.Contains(errStr, "Not Found") || strings.Contains(errStr, "404")

if isNotFound {
var hints []string

if cc.sourceID != "" {
hints = append(hints, fmt.Sprintf(" - --source-id '%s' was provided. Source IDs typically start with 'src_'.", cc.sourceID))
}
if cc.destinationID != "" {
hints = append(hints, fmt.Sprintf(" - --destination-id '%s' was provided. Destination IDs typically start with 'des_'.", cc.destinationID))
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The destination ID prefix in the hint message is incorrect. According to the documentation (REFERENCE.md:906) and unit tests (pkg/hookdeck/connections_test.go:270), destination IDs use the prefix 'dest_', not 'des_'. The hint message should say "Destination IDs typically start with 'dest_'." to be consistent with the actual API.

Suggested change
hints = append(hints, fmt.Sprintf(" - --destination-id '%s' was provided. Destination IDs typically start with 'des_'.", cc.destinationID))
hints = append(hints, fmt.Sprintf(" - --destination-id '%s' was provided. Destination IDs typically start with 'dest_'.", cc.destinationID))

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot you are incorrect. destination IDs start with des_. Where did you get your information from?
CleanShot 2026-02-05 at 11 10 49

}

if len(hints) > 0 {
return fmt.Errorf("failed to %s connection: %w\n\nHints:\n%s\n\nPlease verify the resource IDs exist and are the correct type.", operation, err, strings.Join(hints, "\n"))
}
}

return fmt.Errorf("failed to %s connection: %w", operation, err)
}
2 changes: 1 addition & 1 deletion pkg/cmd/connection_upsert.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func (cu *connectionUpsertCmd) runConnectionUpsertCmd(cmd *cobra.Command, args [

connection, err := client.UpsertConnection(context.Background(), req)
if err != nil {
return fmt.Errorf("failed to upsert connection: %w", err)
return cu.enhanceConnectionError(err, "upsert")
}

// Display results
Expand Down
4 changes: 2 additions & 2 deletions pkg/hookdeck/connections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestListConnections(t *testing.T) {
"disabled": "false",
"paused": "false",
"source_id": "src_123",
"destination": "dest_123",
"destination": "des_123",
},
mockResponse: ConnectionListResponse{
Models: []Connection{
Expand Down Expand Up @@ -267,7 +267,7 @@ func TestCreateConnection(t *testing.T) {
Name: stringPtr("test-connection"),
Description: stringPtr("test description"),
SourceID: stringPtr("src_123"),
DestinationID: stringPtr("dest_123"),
DestinationID: stringPtr("des_123"),
},
mockResponse: Connection{
ID: "conn_123",
Expand Down
255 changes: 255 additions & 0 deletions test/acceptance/connection_error_hints_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package acceptance

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestConnectionCreateWithNonExistentSourceID tests error hints when source ID doesn't exist
func TestConnectionCreateWithNonExistentSourceID(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test in short mode")
}

cli := NewCLIRunner(t)
timestamp := generateTimestamp()

connName := "test-bad-src-" + timestamp
destName := "test-dst-" + timestamp
fakeSourceID := "src_nonexistent123"

// Try to create connection with non-existent source ID
stdout, stderr, err := cli.Run("connection", "create",
"--name", connName,
"--source-id", fakeSourceID,
"--destination-name", destName,
"--destination-type", "CLI",
"--destination-cli-path", "/webhooks",
)

require.Error(t, err, "Should fail when source ID doesn't exist")
combinedOutput := stdout + stderr

// Verify error message contains helpful hints
assert.Contains(t, combinedOutput, "failed to create connection", "Should indicate connection creation failed")
assert.Contains(t, combinedOutput, "Hints:", "Should contain hints section")
assert.Contains(t, combinedOutput, "--source-id", "Hint should mention --source-id flag")
assert.Contains(t, combinedOutput, fakeSourceID, "Hint should include the provided source ID")
assert.Contains(t, combinedOutput, "src_", "Hint should mention source ID prefix format")

t.Logf("Successfully verified error hints for non-existent source ID")
}

// TestConnectionCreateWithNonExistentDestinationID tests error hints when destination ID doesn't exist
func TestConnectionCreateWithNonExistentDestinationID(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test in short mode")
}

cli := NewCLIRunner(t)
timestamp := generateTimestamp()

connName := "test-bad-dst-" + timestamp
sourceName := "test-src-" + timestamp
fakeDestinationID := "des_nonexistent123"
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fake destination ID uses an incorrect prefix. According to the documentation (REFERENCE.md:906) and unit tests (pkg/hookdeck/connections_test.go:270), destination IDs should use the prefix 'dest_', not 'des_'. Change this to "dest_nonexistent123" to match the actual API format.

Copilot uses AI. Check for mistakes.

// Try to create connection with non-existent destination ID
stdout, stderr, err := cli.Run("connection", "create",
"--name", connName,
"--source-name", sourceName,
"--source-type", "WEBHOOK",
"--destination-id", fakeDestinationID,
)

require.Error(t, err, "Should fail when destination ID doesn't exist")
combinedOutput := stdout + stderr

// Verify error message contains helpful hints
assert.Contains(t, combinedOutput, "failed to create connection", "Should indicate connection creation failed")
assert.Contains(t, combinedOutput, "Hints:", "Should contain hints section")
assert.Contains(t, combinedOutput, "--destination-id", "Hint should mention --destination-id flag")
assert.Contains(t, combinedOutput, fakeDestinationID, "Hint should include the provided destination ID")
assert.Contains(t, combinedOutput, "des_", "Hint should mention destination ID prefix format")
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test assertion checks for the incorrect prefix 'des_'. According to the documentation (REFERENCE.md:906) and unit tests (pkg/hookdeck/connections_test.go:270), destination IDs should use the prefix 'dest_', not 'des_'. Update this assertion to check for "dest_" instead.

Suggested change
assert.Contains(t, combinedOutput, "des_", "Hint should mention destination ID prefix format")
assert.Contains(t, combinedOutput, "dest_", "Hint should mention destination ID prefix format")

Copilot uses AI. Check for mistakes.

t.Logf("Successfully verified error hints for non-existent destination ID")
}

// TestConnectionCreateWithWrongIDType tests error hints when wrong ID type is provided
// This reproduces the bug from issue #204 where a connection ID was passed as source ID
func TestConnectionCreateWithWrongIDType(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test in short mode")
}

cli := NewCLIRunner(t)
timestamp := generateTimestamp()

connName := "test-wrong-id-type-" + timestamp
destName := "test-dst-" + timestamp
// Using a connection ID format (web_) instead of source ID format (src_)
wrongIDType := "web_y0A7nz0tRxZy"

// Try to create connection with wrong ID type
stdout, stderr, err := cli.Run("connection", "create",
"--name", connName,
"--source-id", wrongIDType,
"--destination-name", destName,
"--destination-type", "HTTP",
"--destination-url", "https://example.com/webhooks",
)

require.Error(t, err, "Should fail when wrong ID type is provided")
combinedOutput := stdout + stderr

// Verify error message contains helpful hints about correct ID format
assert.Contains(t, combinedOutput, "failed to create connection", "Should indicate connection creation failed")
assert.Contains(t, combinedOutput, "Hints:", "Should contain hints section")
assert.Contains(t, combinedOutput, "--source-id", "Hint should mention --source-id flag")
assert.Contains(t, combinedOutput, wrongIDType, "Hint should include the provided ID")
assert.Contains(t, combinedOutput, "src_", "Hint should mention correct source ID prefix")
assert.Contains(t, combinedOutput, "verify the resource IDs", "Should suggest verifying resource IDs")

t.Logf("Successfully verified error hints for wrong ID type (issue #204 scenario)")
}

// TestConnectionUpsertWithNonExistentSourceID tests error hints for upsert with non-existent source ID
func TestConnectionUpsertWithNonExistentSourceID(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test in short mode")
}

cli := NewCLIRunner(t)
timestamp := generateTimestamp()

connName := "test-upsert-bad-src-" + timestamp
destName := "test-dst-" + timestamp
fakeSourceID := "src_nonexistent456"

// Try to upsert connection with non-existent source ID
stdout, stderr, err := cli.Run("connection", "upsert", connName,
"--source-id", fakeSourceID,
"--destination-name", destName,
"--destination-type", "CLI",
"--destination-cli-path", "/webhooks",
)

require.Error(t, err, "Should fail when source ID doesn't exist")
combinedOutput := stdout + stderr

// Verify error message contains helpful hints
assert.Contains(t, combinedOutput, "failed to upsert connection", "Should indicate connection upsert failed")
assert.Contains(t, combinedOutput, "Hints:", "Should contain hints section")
assert.Contains(t, combinedOutput, "--source-id", "Hint should mention --source-id flag")
assert.Contains(t, combinedOutput, fakeSourceID, "Hint should include the provided source ID")
assert.Contains(t, combinedOutput, "src_", "Hint should mention source ID prefix format")

t.Logf("Successfully verified error hints for upsert with non-existent source ID")
}

// TestConnectionUpsertWithNonExistentDestinationID tests error hints for upsert with non-existent destination ID
func TestConnectionUpsertWithNonExistentDestinationID(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test in short mode")
}

cli := NewCLIRunner(t)
timestamp := generateTimestamp()

connName := "test-upsert-bad-dst-" + timestamp
sourceName := "test-src-" + timestamp
fakeDestinationID := "des_nonexistent456"
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fake destination ID uses an incorrect prefix. According to the documentation (REFERENCE.md:906) and unit tests (pkg/hookdeck/connections_test.go:270), destination IDs should use the prefix 'dest_', not 'des_'. Change this to "dest_nonexistent456" to match the actual API format.

Copilot uses AI. Check for mistakes.

// Try to upsert connection with non-existent destination ID
stdout, stderr, err := cli.Run("connection", "upsert", connName,
"--source-name", sourceName,
"--source-type", "WEBHOOK",
"--destination-id", fakeDestinationID,
)

require.Error(t, err, "Should fail when destination ID doesn't exist")
combinedOutput := stdout + stderr

// Verify error message contains helpful hints
assert.Contains(t, combinedOutput, "failed to upsert connection", "Should indicate connection upsert failed")
assert.Contains(t, combinedOutput, "Hints:", "Should contain hints section")
assert.Contains(t, combinedOutput, "--destination-id", "Hint should mention --destination-id flag")
assert.Contains(t, combinedOutput, fakeDestinationID, "Hint should include the provided destination ID")
assert.Contains(t, combinedOutput, "des_", "Hint should mention destination ID prefix format")
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test assertion checks for the incorrect prefix 'des_'. According to the documentation (REFERENCE.md:906) and unit tests (pkg/hookdeck/connections_test.go:270), destination IDs should use the prefix 'dest_', not 'des_'. Update this assertion to check for "dest_" instead.

Suggested change
assert.Contains(t, combinedOutput, "des_", "Hint should mention destination ID prefix format")
assert.Contains(t, combinedOutput, "dest_", "Hint should mention destination ID prefix format")

Copilot uses AI. Check for mistakes.

t.Logf("Successfully verified error hints for upsert with non-existent destination ID")
}

// TestConnectionCreateWithExistingSourceID tests that create works correctly with a valid existing source ID
// This is a positive test to ensure the --source-id flag works when the source exists
func TestConnectionCreateWithExistingSourceID(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test in short mode")
}

cli := NewCLIRunner(t)
timestamp := generateTimestamp()

// First, create a connection to get a source we can reuse
initialConnName := "test-initial-" + timestamp
sourceName := "test-reusable-src-" + timestamp
initialDestName := "test-initial-dst-" + timestamp

var initialConn Connection
err := cli.RunJSON(&initialConn,
"connection", "create",
"--name", initialConnName,
"--source-name", sourceName,
"--source-type", "WEBHOOK",
"--destination-name", initialDestName,
"--destination-type", "CLI",
"--destination-cli-path", "/initial",
)
require.NoError(t, err, "Should create initial connection")
require.NotEmpty(t, initialConn.ID, "Initial connection should have ID")

// Get the source ID from the created connection
var connDetails map[string]interface{}
err = cli.RunJSON(&connDetails, "connection", "get", initialConn.ID)
require.NoError(t, err, "Should get connection details")

source, ok := connDetails["source"].(map[string]interface{})
require.True(t, ok, "Should have source in connection")
sourceID, ok := source["id"].(string)
require.True(t, ok && sourceID != "", "Should have source ID")

t.Logf("Created initial connection with source ID: %s", sourceID)

// Cleanup initial connection
t.Cleanup(func() {
deleteConnection(t, cli, initialConn.ID)
})

// Now create a new connection using the existing source ID
newConnName := "test-with-src-id-" + timestamp
newDestName := "test-new-dst-" + timestamp

var newConn Connection
err = cli.RunJSON(&newConn,
"connection", "create",
"--name", newConnName,
"--source-id", sourceID,
"--destination-name", newDestName,
"--destination-type", "CLI",
"--destination-cli-path", "/new",
)
require.NoError(t, err, "Should create connection with existing source ID")
require.NotEmpty(t, newConn.ID, "New connection should have ID")

// Cleanup new connection
t.Cleanup(func() {
deleteConnection(t, cli, newConn.ID)
})

// Verify the connection uses the same source
assert.Equal(t, sourceName, newConn.Source.Name, "Should use the existing source")
assert.Equal(t, newDestName, newConn.Destination.Name, "Should have new destination")

t.Logf("Successfully created connection %s using existing source ID %s", newConn.ID, sourceID)
}