Skip to content

Commit ade84bf

Browse files
committed
fix: address PR #262 review feedback
- parseJSONOrString: only parse JSON objects/arrays (starting with { or [), not bare primitives like "order", 123, true that could be JQ expressions - Add HTTP status code range validation (100-599) for --rule-retry-response-status-codes - Fix UpsertRetryResponseStatusCodesAsArray acceptance test: status codes are now integers (float64 after JSON round-trip), not strings - Upgrade assert.True to require.True for map type assertions to prevent nil-pointer panics on failure - Update filter flag help text to mention JSON object support alongside JQ - Remove unused strings import from acceptance test https://claude.ai/code/session_01No6Vv4niFtYoF56at7uL2P
1 parent c3a7e2e commit ade84bf

3 files changed

Lines changed: 49 additions & 19 deletions

File tree

pkg/cmd/connection_common.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ func addConnectionRuleFlags(cmd *cobra.Command, f *connectionRuleFlags) {
7777
cmd.Flags().IntVar(&f.RuleRetryInterval, "rule-retry-interval", 0, "Interval between retries in milliseconds")
7878
cmd.Flags().StringVar(&f.RuleRetryResponseStatusCode, "rule-retry-response-status-codes", "", "Comma-separated HTTP status codes to retry on")
7979

80-
cmd.Flags().StringVar(&f.RuleFilterBody, "rule-filter-body", "", "JQ expression to filter on request body")
81-
cmd.Flags().StringVar(&f.RuleFilterHeaders, "rule-filter-headers", "", "JQ expression to filter on request headers")
82-
cmd.Flags().StringVar(&f.RuleFilterQuery, "rule-filter-query", "", "JQ expression to filter on request query parameters")
83-
cmd.Flags().StringVar(&f.RuleFilterPath, "rule-filter-path", "", "JQ expression to filter on request path")
80+
cmd.Flags().StringVar(&f.RuleFilterBody, "rule-filter-body", "", "JSON object or JQ expression to filter on request body")
81+
cmd.Flags().StringVar(&f.RuleFilterHeaders, "rule-filter-headers", "", "JSON object or JQ expression to filter on request headers")
82+
cmd.Flags().StringVar(&f.RuleFilterQuery, "rule-filter-query", "", "JSON object or JQ expression to filter on request query parameters")
83+
cmd.Flags().StringVar(&f.RuleFilterPath, "rule-filter-path", "", "JSON object or JQ expression to filter on request path")
8484

8585
cmd.Flags().StringVar(&f.RuleTransformName, "rule-transform-name", "", "Name or ID of the transformation to apply")
8686
cmd.Flags().StringVar(&f.RuleTransformCode, "rule-transform-code", "", "Transformation code (if creating inline)")
@@ -203,6 +203,9 @@ func buildConnectionRules(f *connectionRuleFlags) ([]hookdeck.Rule, error) {
203203
if err != nil {
204204
return nil, fmt.Errorf("invalid HTTP status code %q in --rule-retry-response-status-codes: must be an integer", part)
205205
}
206+
if n < 100 || n > 599 {
207+
return nil, fmt.Errorf("invalid HTTP status code %d in --rule-retry-response-status-codes: must be between 100 and 599", n)
208+
}
206209
intCodes = append(intCodes, n)
207210
}
208211
rule["response_status_codes"] = intCodes
@@ -213,11 +216,18 @@ func buildConnectionRules(f *connectionRuleFlags) ([]hookdeck.Rule, error) {
213216
return rules, nil
214217
}
215218

216-
// parseJSONOrString attempts to parse s as JSON. If successful it returns the
217-
// parsed value (object, array, number, bool, etc.); otherwise it returns s as
218-
// a plain string. This lets filter flags accept both JSON objects and JQ
219-
// expressions transparently.
219+
// parseJSONOrString attempts to parse s as a JSON object or array. Only values
220+
// starting with '{' or '[' (after trimming whitespace) are candidates for
221+
// parsing; bare primitives like "order", 123, or true are returned as-is so
222+
// that JQ expressions and other plain strings are never misinterpreted.
220223
func parseJSONOrString(s string) interface{} {
224+
trimmed := strings.TrimSpace(s)
225+
if len(trimmed) == 0 {
226+
return s
227+
}
228+
if trimmed[0] != '{' && trimmed[0] != '[' {
229+
return s
230+
}
221231
var v interface{}
222232
if err := json.Unmarshal([]byte(s), &v); err == nil {
223233
return v

pkg/cmd/connection_upsert_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestBuildConnectionRulesFilterHeadersJSON(t *testing.T) {
3333
assert.False(t, isString, "headers should be a parsed object, not a string")
3434

3535
headersMap, isMap := headers.(map[string]interface{})
36-
assert.True(t, isMap, "headers should be map[string]interface{}, got %T", headers)
36+
require.True(t, isMap, "headers should be map[string]interface{}, got %T", headers)
3737
assert.Contains(t, headersMap, "x-shopify-topic")
3838
})
3939

@@ -51,6 +51,24 @@ func TestBuildConnectionRulesFilterHeadersJSON(t *testing.T) {
5151
assert.True(t, isString, "non-JSON value should remain a string")
5252
})
5353

54+
t.Run("bare JSON primitives should remain as strings", func(t *testing.T) {
55+
// Values like "order", 123, true are valid JSON primitives but should
56+
// NOT be parsed — they are likely JQ expressions or literal strings.
57+
for _, input := range []string{`"order"`, `123`, `true`} {
58+
flags := connectionRuleFlags{
59+
RuleFilterHeaders: input,
60+
}
61+
rules, err := buildConnectionRules(&flags)
62+
require.NoError(t, err)
63+
require.Len(t, rules, 1)
64+
65+
headers := rules[0]["headers"]
66+
_, isString := headers.(string)
67+
assert.True(t, isString, "input %q should remain a string, got %T", input, headers)
68+
assert.Equal(t, input, headers, "value should be unchanged")
69+
}
70+
})
71+
5472
t.Run("filter body JSON should also be parsed", func(t *testing.T) {
5573
flags := connectionRuleFlags{
5674
RuleFilterBody: `{"event_type":"payment"}`,
@@ -64,7 +82,7 @@ func TestBuildConnectionRulesFilterHeadersJSON(t *testing.T) {
6482
_, isString := body.(string)
6583
assert.False(t, isString, "body should be a parsed object, not a string")
6684
_, isMap := body.(map[string]interface{})
67-
assert.True(t, isMap, "body should be map[string]interface{}, got %T", body)
85+
require.True(t, isMap, "body should be map[string]interface{}, got %T", body)
6886
})
6987
}
7088

test/acceptance/connection_upsert_test.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package acceptance
22

33
import (
4-
"strings"
54
"testing"
65

76
"github.com/stretchr/testify/assert"
@@ -373,14 +372,17 @@ func TestConnectionUpsertPartialUpdates(t *testing.T) {
373372
require.True(t, ok, "response_status_codes should be array, got: %T (%v)", rule["response_status_codes"], rule["response_status_codes"])
374373
assert.Len(t, statusCodes, 4, "Should have 4 status codes")
375374

376-
codes := make([]string, len(statusCodes))
375+
// After JSON round-trip, numbers are float64
376+
codes := make([]float64, len(statusCodes))
377377
for i, c := range statusCodes {
378-
codes[i] = strings.TrimSpace(c.(string))
378+
n, ok := c.(float64)
379+
require.True(t, ok, "status code should be a number, got %T", c)
380+
codes[i] = n
379381
}
380-
assert.Contains(t, codes, "500")
381-
assert.Contains(t, codes, "502")
382-
assert.Contains(t, codes, "503")
383-
assert.Contains(t, codes, "504")
382+
assert.Contains(t, codes, float64(500))
383+
assert.Contains(t, codes, float64(502))
384+
assert.Contains(t, codes, float64(503))
385+
assert.Contains(t, codes, float64(504))
384386
break
385387
}
386388
}
@@ -504,7 +506,7 @@ func TestConnectionUpsertPartialUpdates(t *testing.T) {
504506
"--rule-filter-headers should store JSON as an object, not an escaped string; got: %v", headers)
505507

506508
headersMap, isMap := headers.(map[string]interface{})
507-
assert.True(t, isMap,
509+
require.True(t, isMap,
508510
"headers should be a JSON object (map[string]interface{}), got %T", headers)
509511
assert.Contains(t, headersMap, "x-shopify-topic",
510512
"headers object should contain the expected key")
@@ -564,7 +566,7 @@ func TestConnectionUpsertPartialUpdates(t *testing.T) {
564566
"--rule-filter-body should store JSON as an object, not an escaped string; got: %v", body)
565567

566568
bodyMap, isMap := body.(map[string]interface{})
567-
assert.True(t, isMap, "body should be a JSON object, got %T", body)
569+
require.True(t, isMap, "body should be a JSON object, got %T", body)
568570
assert.Contains(t, bodyMap, "event_type", "body object should contain the expected key")
569571
break
570572
}

0 commit comments

Comments
 (0)