From 53094c9f5e74e14d46e0d7ed99e22d485985897b Mon Sep 17 00:00:00 2001 From: Theo Beers Date: Thu, 11 Jun 2026 10:53:33 -0400 Subject: [PATCH 1/2] make http response var capture more generic --- checks/http.go | 42 +++++++++++++++++++++++++++++++--------- checks/http_test.go | 32 ++++++++++++++++++++++++++++++ client/lessons.go | 5 +++-- render/variables.go | 15 ++++++++++++-- render/variables_test.go | 11 ++++++++--- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/checks/http.go b/checks/http.go index 09a8b30..953d0d9 100644 --- a/checks/http.go +++ b/checks/http.go @@ -199,16 +199,40 @@ func truncateAndStringifyBody(body []byte) string { } func parseVariables(body []byte, vardefs []api.HTTPRequestResponseVariable, variables map[string]string) error { + bodyString := string(body) + for _, vardef := range vardefs { - vals, err := valsFromJqPath(vardef.Path, string(body)) - if err != nil { - return err - } - if len(vals) != 1 || vals[0] == nil { - continue + switch { + case vardef.Path != "" && vardef.BodyRegex != "": + return fmt.Errorf("invalid response variable configuration") + + case vardef.BodyRegex != "": + re, err := regexp.Compile(vardef.BodyRegex) + if err != nil { + return fmt.Errorf("invalid response body variable configuration") + } + if re.NumSubexp() != 1 { + return fmt.Errorf("invalid response body variable configuration") + } + matches := re.FindStringSubmatch(bodyString) + if len(matches) == 2 && matches[1] != "" { + variables[vardef.Name] = matches[1] + } + + case vardef.Path != "": + vals, err := valsFromJqPath(vardef.Path, bodyString) + if err != nil { + return err + } + if len(vals) == 1 && vals[0] != nil { + variables[vardef.Name] = fmt.Sprintf("%v", vals[0]) + } + + default: + return fmt.Errorf("invalid response variable configuration") } - variables[vardef.Name] = fmt.Sprintf("%v", vals[0]) } + return nil } @@ -223,10 +247,10 @@ func parseHeaderVariables(headers map[string]string, vardefs []api.HTTPRequestRe if vardef.Regex != "" { re, err := regexp.Compile(vardef.Regex) if err != nil { - return err + return fmt.Errorf("invalid response header variable configuration") } if re.NumSubexp() != 1 { - return fmt.Errorf("regex for header variable %q must have exactly one capture group", vardef.Name) + return fmt.Errorf("invalid response header variable configuration") } matches := re.FindStringSubmatch(headerValue) diff --git a/checks/http_test.go b/checks/http_test.go index f85e26c..a7bfd1b 100644 --- a/checks/http_test.go +++ b/checks/http_test.go @@ -202,6 +202,38 @@ func TestParseVariablesLeavesMissingValuesUnset(t *testing.T) { } } +func TestParseVariablesCapturesBodyRegex(t *testing.T) { + variables := map[string]string{} + err := parseVariables( + []byte(`reset`), + []api.HTTPRequestResponseVariable{ + {Name: "resetToken", BodyRegex: `/password-reset/([a-z0-9]+)`}, + }, + variables, + ) + if err != nil { + t.Fatalf("unexpected parseVariables error: %v", err) + } + if variables["resetToken"] != "abc123" { + t.Fatalf("resetToken = %q, want abc123", variables["resetToken"]) + } +} + +func TestParseVariablesRequiresCaptureSource(t *testing.T) { + variables := map[string]string{} + err := parseVariables( + []byte(`{"token":"abc123"}`), + []api.HTTPRequestResponseVariable{{Name: "token"}}, + variables, + ) + if err == nil { + t.Fatal("expected parseVariables error") + } + if err.Error() != "invalid response variable configuration" { + t.Fatalf("error = %q, want invalid response variable configuration", err.Error()) + } +} + func TestParseHeaderVariablesLeavesMissingValuesUnset(t *testing.T) { variables := map[string]string{} err := parseHeaderVariables( diff --git a/client/lessons.go b/client/lessons.go index 2b5ea9b..34a8693 100644 --- a/client/lessons.go +++ b/client/lessons.go @@ -110,8 +110,9 @@ type HTTPBasicAuth struct { } type HTTPRequestResponseVariable struct { - Name string `yaml:"name"` - Path string `yaml:"path"` + Name string `yaml:"name"` + Path string `yaml:"path"` + BodyRegex string `yaml:"bodyRegex"` } type HTTPRequestResponseHeaderVariable struct { diff --git a/render/variables.go b/render/variables.go index 7639c47..67c33cb 100644 --- a/render/variables.go +++ b/render/variables.go @@ -46,10 +46,12 @@ func savedVariablesForHTTPResult(result api.HTTPRequestResult) []variableEntry { if value == "" { continue } + + description := responseVariableDescription(responseVariable) entries = append(entries, variableEntry{ name: responseVariable.Name, value: value, - description: "JSON Body " + responseVariable.Path, + description: description, }) } for _, responseHeaderVariable := range result.Request.ResponseHeaderVariables { @@ -72,9 +74,11 @@ func missingSaveVariablesForHTTPResult(result api.HTTPRequestResult) []variableE if result.Variables[responseVariable.Name] != "" { continue } + + description := responseVariableDescription(responseVariable) entries = append(entries, variableEntry{ name: responseVariable.Name, - description: "JSON Body " + responseVariable.Path, + description: description, }) } for _, responseHeaderVariable := range result.Request.ResponseHeaderVariables { @@ -205,3 +209,10 @@ func availableVariablesForCLIResult(result api.CLICommandResult) (entries []vari return entries, expectsVariables } + +func responseVariableDescription(v api.HTTPRequestResponseVariable) string { + if v.BodyRegex != "" { + return "Response Body pattern" + } + return "JSON Body " + v.Path +} diff --git a/render/variables_test.go b/render/variables_test.go index 1570b5c..16d84ce 100644 --- a/render/variables_test.go +++ b/render/variables_test.go @@ -10,14 +10,17 @@ import ( func TestHTTPVariableSections(t *testing.T) { result := api.HTTPRequestResult{ Variables: map[string]string{ - "authToken": "token-123", - "shortCode": "abc123", - "sessionID": "session-123", + "authToken": "token-123", + "resetToken": "reset-123", + "shortCode": "abc123", + "sessionID": "session-123", }, Request: api.CLIStepHTTPRequest{ ResponseVariables: []api.HTTPRequestResponseVariable{ {Name: "shortCode", Path: ".short_code"}, + {Name: "resetToken", BodyRegex: `/password-reset/([a-z0-9]+)`}, {Name: "missingCode", Path: ".missing_code"}, + {Name: "missingResetToken", BodyRegex: `/missing/([a-z0-9]+)`}, }, ResponseHeaderVariables: []api.HTTPRequestResponseHeaderVariable{ {Name: "sessionID", Header: "Set-Cookie", Regex: "session_id=([^;]+)"}, @@ -42,10 +45,12 @@ func TestHTTPVariableSections(t *testing.T) { wantContains := []string{ "Variables Saved:", + "resetToken: reset-123 (Response Body pattern)", "sessionID: session-123 (Response Header Set-Cookie matching session_id=([^;]+))", "shortCode: abc123 (JSON Body .short_code)", "Variables Missing:", "missingCode: [not found] (JSON Body .missing_code)", + "missingResetToken: [not found] (Response Body pattern)", "missingSessionID: [not found] (Response Header Set-Cookie matching missing=([^;]+))", "Variables Available:", "authToken: token-123 (Request Header \"Authorization\")", From 07adc547e0f1bb9534917ca71a46a47963304f01 Mon Sep 17 00:00:00 2001 From: Theo Beers Date: Thu, 11 Jun 2026 11:17:16 -0400 Subject: [PATCH 2/2] make messages less detailed --- render/variables.go | 2 +- render/variables_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/render/variables.go b/render/variables.go index 67c33cb..21bcf4a 100644 --- a/render/variables.go +++ b/render/variables.go @@ -97,7 +97,7 @@ func responseHeaderVariableDescription(v api.HTTPRequestResponseHeaderVariable) if v.Regex == "" { return "Response Header " + v.Header } - return fmt.Sprintf("Response Header %s matching %s", v.Header, v.Regex) + return "Response Header " + v.Header + " pattern" } func availableVariablesForHTTPResult(result api.HTTPRequestResult) (entries []variableEntry, expectsVariables bool) { diff --git a/render/variables_test.go b/render/variables_test.go index 16d84ce..f95c12f 100644 --- a/render/variables_test.go +++ b/render/variables_test.go @@ -46,12 +46,12 @@ func TestHTTPVariableSections(t *testing.T) { wantContains := []string{ "Variables Saved:", "resetToken: reset-123 (Response Body pattern)", - "sessionID: session-123 (Response Header Set-Cookie matching session_id=([^;]+))", + "sessionID: session-123 (Response Header Set-Cookie pattern)", "shortCode: abc123 (JSON Body .short_code)", "Variables Missing:", "missingCode: [not found] (JSON Body .missing_code)", "missingResetToken: [not found] (Response Body pattern)", - "missingSessionID: [not found] (Response Header Set-Cookie matching missing=([^;]+))", + "missingSessionID: [not found] (Response Header Set-Cookie pattern)", "Variables Available:", "authToken: token-123 (Request Header \"Authorization\")", "shortCode: abc123 (Request URL)",