Skip to content

Commit 4a47ef7

Browse files
committed
fix: acceptance tests - response_status_codes as strings, filter rule assertions
- Send response_status_codes as []string per API (RetryRule schema) - normalizeRulesForAPI: convert numeric codes to strings for --rules/--rules-file - assertFilterRuleFieldMatches: accept body/headers as string or map from API - assertResponseStatusCodesMatch: accept codes as string or number in responses - Update comments to avoid referencing local-only OpenAPI file Made-with: Cursor
1 parent e0303ba commit 4a47ef7

6 files changed

Lines changed: 100 additions & 35 deletions

File tree

pkg/cmd/connection_common.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func buildConnectionRules(f *connectionRuleFlags) ([]hookdeck.Rule, error) {
103103
if err := json.Unmarshal([]byte(f.Rules), &rules); err != nil {
104104
return nil, fmt.Errorf("invalid JSON for --rules: %w", err)
105105
}
106-
return rules, nil
106+
return normalizeRulesForAPI(rules), nil
107107
}
108108

109109
if f.RulesFile != "" {
@@ -115,7 +115,7 @@ func buildConnectionRules(f *connectionRuleFlags) ([]hookdeck.Rule, error) {
115115
if err := json.Unmarshal(data, &rules); err != nil {
116116
return nil, fmt.Errorf("invalid JSON in rules file: %w", err)
117117
}
118-
return rules, nil
118+
return normalizeRulesForAPI(rules), nil
119119
}
120120

121121
// Build each rule type (order matches create: deduplicate -> transform -> filter -> delay -> retry)
@@ -191,9 +191,10 @@ func buildConnectionRules(f *connectionRuleFlags) ([]hookdeck.Rule, error) {
191191
if f.RuleRetryInterval > 0 {
192192
rule["interval"] = f.RuleRetryInterval
193193
}
194+
// API expects response_status_codes as []string (RetryRule schema)
194195
if f.RuleRetryResponseStatusCode != "" {
195196
parts := strings.Split(f.RuleRetryResponseStatusCode, ",")
196-
intCodes := make([]int, 0, len(parts))
197+
strCodes := make([]string, 0, len(parts))
197198
for _, part := range parts {
198199
part = strings.TrimSpace(part)
199200
if part == "" {
@@ -206,16 +207,47 @@ func buildConnectionRules(f *connectionRuleFlags) ([]hookdeck.Rule, error) {
206207
if n < 100 || n > 599 {
207208
return nil, fmt.Errorf("invalid HTTP status code %d in --rule-retry-response-status-codes: must be between 100 and 599", n)
208209
}
209-
intCodes = append(intCodes, n)
210+
strCodes = append(strCodes, part)
210211
}
211-
rule["response_status_codes"] = intCodes
212+
rule["response_status_codes"] = strCodes
212213
}
213214
rules = append(rules, rule)
214215
}
215216

216217
return rules, nil
217218
}
218219

220+
// normalizeRulesForAPI ensures rules match the API schema: RetryRule.response_status_codes
221+
// must be []string; FilterRule body/headers may be string or object.
222+
func normalizeRulesForAPI(rules []hookdeck.Rule) []hookdeck.Rule {
223+
out := make([]hookdeck.Rule, len(rules))
224+
for i, r := range rules {
225+
out[i] = make(hookdeck.Rule)
226+
for k, v := range r {
227+
out[i][k] = v
228+
}
229+
if r["type"] == "retry" {
230+
if codes, ok := r["response_status_codes"].([]interface{}); ok && len(codes) > 0 {
231+
strCodes := make([]string, 0, len(codes))
232+
for _, c := range codes {
233+
switch v := c.(type) {
234+
case string:
235+
strCodes = append(strCodes, v)
236+
case float64:
237+
strCodes = append(strCodes, strconv.Itoa(int(v)))
238+
case int:
239+
strCodes = append(strCodes, strconv.Itoa(v))
240+
default:
241+
strCodes = append(strCodes, fmt.Sprintf("%v", c))
242+
}
243+
}
244+
out[i]["response_status_codes"] = strCodes
245+
}
246+
}
247+
}
248+
return out
249+
}
250+
219251
// parseJSONOrString attempts to parse s as a JSON object or array. Only values
220252
// starting with '{' or '[' (after trimming whitespace) are candidates for
221253
// parsing; bare primitives like "order", 123, or true are returned as-is so

test/acceptance/connection_rules_json_test.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ func TestConnectionCreateWithRulesJSONExactValues(t *testing.T) {
127127
assert.Equal(t, float64(3), rule["count"])
128128
assert.Equal(t, float64(10000), rule["interval"])
129129

130-
statusCodes, ok := rule["response_status_codes"].([]interface{})
131-
require.True(t, ok, "response_status_codes should be an array")
132-
assert.Equal(t, []interface{}{float64(500), float64(502), float64(503)}, statusCodes)
130+
statusCodes, ok := rule["response_status_codes"]
131+
require.True(t, ok, "response_status_codes should be present")
132+
assertResponseStatusCodesMatch(t, statusCodes, "500", "502", "503")
133133
}
134134
}
135135
assert.True(t, foundFilter, "Should have a filter rule")
@@ -205,10 +205,9 @@ func TestConnectionCreateWithRulesFileExactValues(t *testing.T) {
205205
assert.Equal(t, float64(5), rule["count"])
206206
assert.Equal(t, float64(30000), rule["interval"])
207207

208-
statusCodes, ok := rule["response_status_codes"].([]interface{})
209-
require.True(t, ok)
210-
expectedCodes := []interface{}{float64(500), float64(502), float64(503), float64(504)}
211-
assert.Equal(t, expectedCodes, statusCodes)
208+
statusCodes, ok := rule["response_status_codes"]
209+
require.True(t, ok, "response_status_codes should be present")
210+
assertResponseStatusCodesMatch(t, statusCodes, "500", "502", "503", "504")
212211
}
213212
}
214213
assert.True(t, foundFilter, "Should have a filter rule")

test/acceptance/connection_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,8 +1274,8 @@ func TestConnectionWithFilterRule(t *testing.T) {
12741274

12751275
rule := getConn.Rules[0]
12761276
assert.Equal(t, "filter", rule["type"], "Rule type should be filter")
1277-
assert.Equal(t, `{"type":"payment"}`, rule["body"], "Filter body should match input")
1278-
assert.Equal(t, `{"content-type":"application/json"}`, rule["headers"], "Filter headers should match input")
1277+
assertFilterRuleFieldMatches(t, rule["body"], `{"type":"payment"}`, "body")
1278+
assertFilterRuleFieldMatches(t, rule["headers"], `{"content-type":"application/json"}`, "headers")
12791279

12801280
t.Logf("Successfully created and verified connection with filter rule: %s", conn.ID)
12811281
}
@@ -1486,7 +1486,7 @@ func TestConnectionWithMultipleRules(t *testing.T) {
14861486
assert.Equal(t, "retry", getConn.Rules[2]["type"], "Third rule should be retry (logical order)")
14871487

14881488
// Verify filter rule details
1489-
assert.Equal(t, `{"type":"payment"}`, getConn.Rules[0]["body"], "Filter should have body expression")
1489+
assertFilterRuleFieldMatches(t, getConn.Rules[0]["body"], `{"type":"payment"}`, "body")
14901490

14911491
// Verify delay rule details
14921492
assert.Equal(t, float64(1000), getConn.Rules[1]["delay"], "Delay should be 1000 milliseconds")
@@ -2069,7 +2069,7 @@ func TestConnectionUpsertReplaceRules(t *testing.T) {
20692069
replacedRule := upserted.Rules[0]
20702070
assert.Equal(t, "filter", replacedRule["type"], "Rule should now be filter type")
20712071
assert.NotEqual(t, "retry", replacedRule["type"], "Retry rule should be replaced")
2072-
assert.Equal(t, filterBody, replacedRule["body"], "Filter body should match input")
2072+
assertFilterRuleFieldMatches(t, replacedRule["body"], filterBody, "body")
20732073

20742074
// Verify source and destination are preserved
20752075
assert.Equal(t, sourceName, upserted.Source.Name, "Source should be preserved in upsert output")
@@ -2083,7 +2083,7 @@ func TestConnectionUpsertReplaceRules(t *testing.T) {
20832083
assert.Len(t, fetched.Rules, 1, "Should have exactly one rule persisted")
20842084
fetchedRule := fetched.Rules[0]
20852085
assert.Equal(t, "filter", fetchedRule["type"], "Persisted rule should be filter type")
2086-
assert.Equal(t, filterBody, fetchedRule["body"], "Persisted filter body should match input")
2086+
assertFilterRuleFieldMatches(t, fetchedRule["body"], filterBody, "body")
20872087

20882088
t.Logf("Successfully replaced rules via upsert: %s", conn.ID)
20892089
}
@@ -2238,9 +2238,9 @@ func TestConnectionCreateRetryResponseStatusCodes(t *testing.T) {
22382238
if rule["type"] == "retry" {
22392239
foundRetry = true
22402240

2241-
statusCodes, ok := rule["response_status_codes"].([]interface{})
2242-
require.True(t, ok, "response_status_codes should be an array, got: %T (%v)", rule["response_status_codes"], rule["response_status_codes"])
2243-
assert.Len(t, statusCodes, 4, "Should have 4 status codes")
2241+
statusCodes, ok := rule["response_status_codes"]
2242+
require.True(t, ok, "response_status_codes should be present")
2243+
assertResponseStatusCodesMatch(t, statusCodes, "500", "502", "503", "504")
22442244
break
22452245
}
22462246
}

test/acceptance/connection_update_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ func TestConnectionUpdateFilterRule(t *testing.T) {
271271
for _, rule := range updated.Rules {
272272
if rule["type"] == "filter" {
273273
foundFilter = true
274-
assert.Equal(t, filterBody, rule["body"], "Filter body should match")
274+
assertFilterRuleFieldMatches(t, rule["body"], filterBody, "body")
275275
break
276276
}
277277
}
@@ -351,9 +351,9 @@ func TestConnectionUpdateRetryResponseStatusCodes(t *testing.T) {
351351
if rule["type"] == "retry" {
352352
foundRetry = true
353353

354-
statusCodes, ok := rule["response_status_codes"].([]interface{})
355-
require.True(t, ok, "response_status_codes should be an array, got: %T (%v)", rule["response_status_codes"], rule["response_status_codes"])
356-
assert.Len(t, statusCodes, 3, "Should have 3 status codes")
354+
statusCodes, ok := rule["response_status_codes"]
355+
require.True(t, ok, "response_status_codes should be present")
356+
assertResponseStatusCodesMatch(t, statusCodes, "500", "502", "503")
357357
break
358358
}
359359
}

test/acceptance/connection_upsert_test.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -732,17 +732,9 @@ func TestConnectionUpsertPartialUpdates(t *testing.T) {
732732
assert.Equal(t, float64(5), rule["count"], "count should be 5")
733733
assert.Equal(t, float64(10000), rule["interval"], "interval should be 10000")
734734

735-
statusCodes, ok := rule["response_status_codes"].([]interface{})
736-
require.True(t, ok, "response_status_codes should be an array, got %T: %v",
737-
rule["response_status_codes"], rule["response_status_codes"])
738-
require.Len(t, statusCodes, 4, "Should have exactly 4 status codes")
739-
740-
expectedCodes := []float64{500, 502, 503, 504}
741-
for i, expected := range expectedCodes {
742-
actual, ok := statusCodes[i].(float64)
743-
require.True(t, ok, "status code [%d] should be a number, got %T", i, statusCodes[i])
744-
assert.Equal(t, expected, actual, "status code [%d] should be %v", i, expected)
745-
}
735+
statusCodes, ok := rule["response_status_codes"]
736+
require.True(t, ok, "response_status_codes should be present")
737+
assertResponseStatusCodesMatch(t, statusCodes, "500", "502", "503", "504")
746738
break
747739
}
748740
assert.True(t, foundRetry, "Should have a retry rule")

test/acceptance/helpers.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"testing"
1414
"time"
1515

16+
"github.com/stretchr/testify/assert"
1617
"github.com/stretchr/testify/require"
1718
)
1819

@@ -194,6 +195,47 @@ func (r *CLIRunner) RunJSON(result interface{}, args ...string) error {
194195
return nil
195196
}
196197

198+
// assertFilterRuleFieldMatches asserts that a filter rule field (body or headers) matches the expected JSON.
199+
// The API may return the field as either a string or a parsed map; both are accepted.
200+
func assertFilterRuleFieldMatches(t *testing.T, actual interface{}, expectedJSON string, fieldName string) {
201+
t.Helper()
202+
var expectedMap map[string]interface{}
203+
require.NoError(t, json.Unmarshal([]byte(expectedJSON), &expectedMap), "expectedJSON should be valid JSON")
204+
var actualMap map[string]interface{}
205+
switch v := actual.(type) {
206+
case string:
207+
require.NoError(t, json.Unmarshal([]byte(v), &actualMap), "actual string should be valid JSON")
208+
case map[string]interface{}:
209+
actualMap = v
210+
default:
211+
t.Fatalf("%s should be string or map, got %T", fieldName, actual)
212+
}
213+
assert.Equal(t, expectedMap, actualMap, "%s should match expected JSON", fieldName)
214+
}
215+
216+
// assertResponseStatusCodesMatch asserts that response_status_codes from the API match expected values.
217+
// The API may return codes as strings or numbers; both are accepted.
218+
func assertResponseStatusCodesMatch(t *testing.T, statusCodes interface{}, expected ...string) {
219+
t.Helper()
220+
slice, ok := statusCodes.([]interface{})
221+
require.True(t, ok, "response_status_codes should be an array, got %T", statusCodes)
222+
require.Len(t, slice, len(expected), "response_status_codes length")
223+
for i, exp := range expected {
224+
var actual string
225+
switch v := slice[i].(type) {
226+
case string:
227+
actual = v
228+
case float64:
229+
actual = fmt.Sprintf("%.0f", v)
230+
case int:
231+
actual = fmt.Sprintf("%d", v)
232+
default:
233+
actual = fmt.Sprintf("%v", slice[i])
234+
}
235+
assert.Equal(t, exp, actual, "response_status_codes[%d]", i)
236+
}
237+
}
238+
197239
// generateTimestamp returns a timestamp string in the format YYYYMMDDHHMMSS plus microseconds
198240
// This is used for creating unique test resource names
199241
func generateTimestamp() string {

0 commit comments

Comments
 (0)