Skip to content

Commit e5931e5

Browse files
cli http tests: allow response header capture
1 parent 88b9586 commit e5931e5

5 files changed

Lines changed: 211 additions & 13 deletions

File tree

checks/http.go

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,16 @@ func runHTTPRequest(
7676
req.SetBasicAuth(requestStep.Request.BasicAuth.Username, requestStep.Request.BasicAuth.Password)
7777
}
7878

79-
resp, err := client.Do(req)
79+
requestClient := client
80+
if requestStep.Request.FollowRedirects != nil && !*requestStep.Request.FollowRedirects {
81+
clientCopy := *client
82+
clientCopy.CheckRedirect = func(req *http.Request, via []*http.Request) error {
83+
return http.ErrUseLastResponse
84+
}
85+
requestClient = &clientCopy
86+
}
87+
88+
resp, err := requestClient.Do(req)
8089
if err != nil {
8190
errString := fmt.Sprintf("Failed to fetch: %s", err.Error())
8291
result = api.HTTPRequestResult{Err: errString}
@@ -100,7 +109,12 @@ func runHTTPRequest(
100109
trailers[k] = strings.Join(v, ",")
101110
}
102111

103-
parseVariables(body, requestStep.ResponseVariables, variables)
112+
if err := parseVariables(body, requestStep.ResponseVariables, variables); err != nil {
113+
return api.HTTPRequestResult{Err: fmt.Sprintf("Failed to parse response variable: %s", err)}
114+
}
115+
if err := parseHeaderVariables(headers, requestStep.ResponseHeaderVariables, variables); err != nil {
116+
return api.HTTPRequestResult{Err: fmt.Sprintf("Failed to parse response header variable: %s", err)}
117+
}
104118

105119
result = api.HTTPRequestResult{
106120
StatusCode: resp.StatusCode,
@@ -186,15 +200,57 @@ func truncateAndStringifyBody(body []byte) string {
186200

187201
func parseVariables(body []byte, vardefs []api.HTTPRequestResponseVariable, variables map[string]string) error {
188202
for _, vardef := range vardefs {
189-
val, err := valFromJqPath(vardef.Path, string(body))
203+
vals, err := valsFromJqPath(vardef.Path, string(body))
190204
if err != nil {
191205
return err
192206
}
193-
variables[vardef.Name] = fmt.Sprintf("%v", val)
207+
if len(vals) != 1 || vals[0] == nil {
208+
continue
209+
}
210+
variables[vardef.Name] = fmt.Sprintf("%v", vals[0])
194211
}
195212
return nil
196213
}
197214

215+
func parseHeaderVariables(headers map[string]string, vardefs []api.HTTPRequestResponseHeaderVariable, variables map[string]string) error {
216+
for _, vardef := range vardefs {
217+
headerValue, ok := findHeaderValue(headers, vardef.Header)
218+
if !ok || headerValue == "" {
219+
continue
220+
}
221+
222+
value := headerValue
223+
if vardef.Regex != "" {
224+
re, err := regexp.Compile(vardef.Regex)
225+
if err != nil {
226+
return err
227+
}
228+
if re.NumSubexp() != 1 {
229+
return fmt.Errorf("regex for header variable %q must have exactly one capture group", vardef.Name)
230+
}
231+
232+
matches := re.FindStringSubmatch(headerValue)
233+
if len(matches) != 2 || matches[1] == "" {
234+
continue
235+
}
236+
value = matches[1]
237+
}
238+
239+
variables[vardef.Name] = value
240+
}
241+
242+
return nil
243+
}
244+
245+
func findHeaderValue(headers map[string]string, key string) (string, bool) {
246+
for actualKey, value := range headers {
247+
if strings.EqualFold(actualKey, key) {
248+
return value, true
249+
}
250+
}
251+
return "", false
252+
}
253+
198254
func InterpolateVariables(template string, vars map[string]string) string {
199255
r := regexp.MustCompile(`\$\{([^}]+)\}`)
200256
return r.ReplaceAllStringFunc(template, func(m string) string {

checks/http_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,103 @@ func TestTruncateAndStringifyBodyCapsBinaryBody(t *testing.T) {
126126
t.Fatalf("len(truncateAndStringifyBody(binary)) = %d, want %d", len(got), 16*1024)
127127
}
128128
}
129+
130+
func TestRunHTTPRequestCapturesResponseHeaderVariableAndDoesNotFollowRedirect(t *testing.T) {
131+
followRedirects := false
132+
133+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
134+
if r.URL.Path != "/login" {
135+
t.Errorf("path = %q, want /login", r.URL.Path)
136+
return
137+
}
138+
139+
w.Header().Set("Set-Cookie", "session_id=abc123; Path=/; HttpOnly")
140+
w.Header().Set("Location", "/account")
141+
w.WriteHeader(http.StatusFound)
142+
_, _ = w.Write([]byte("Found. Redirecting to /account"))
143+
}))
144+
defer server.Close()
145+
146+
variables := map[string]string{}
147+
requestStep := api.CLIStepHTTPRequest{
148+
ResponseHeaderVariables: []api.HTTPRequestResponseHeaderVariable{{
149+
Name: "sessionID",
150+
Header: "Set-Cookie",
151+
Regex: "session_id=([^;]+)",
152+
}},
153+
Request: api.HTTPRequest{
154+
Method: http.MethodPost,
155+
FullURL: api.BaseURLPlaceholder + "/login",
156+
FollowRedirects: &followRedirects,
157+
BodyForm: map[string]string{
158+
"email": "pacifica@example.com",
159+
"password": "password123",
160+
"returnTo": "/account",
161+
},
162+
},
163+
}
164+
165+
result := runHTTPRequest(server.Client(), server.URL, variables, requestStep)
166+
if result.Err != "" {
167+
t.Fatalf("unexpected request error: %s", result.Err)
168+
}
169+
if result.StatusCode != http.StatusFound {
170+
t.Fatalf("StatusCode = %d, want %d", result.StatusCode, http.StatusFound)
171+
}
172+
if result.ResponseHeaders["Set-Cookie"] == "" {
173+
t.Fatalf("expected Set-Cookie response header")
174+
}
175+
if result.Variables["sessionID"] != "abc123" {
176+
t.Fatalf("captured sessionID = %q, want abc123", result.Variables["sessionID"])
177+
}
178+
}
179+
180+
func TestParseVariablesLeavesMissingValuesUnset(t *testing.T) {
181+
variables := map[string]string{}
182+
err := parseVariables(
183+
[]byte(`{"token":"abc123","missing":null}`),
184+
[]api.HTTPRequestResponseVariable{
185+
{Name: "token", Path: ".token"},
186+
{Name: "missing", Path: ".missing"},
187+
{Name: "notFound", Path: ".not_found"},
188+
},
189+
variables,
190+
)
191+
if err != nil {
192+
t.Fatalf("unexpected parseVariables error: %v", err)
193+
}
194+
if variables["token"] != "abc123" {
195+
t.Fatalf("token = %q, want abc123", variables["token"])
196+
}
197+
if _, ok := variables["missing"]; ok {
198+
t.Fatalf("expected null variable to remain unset")
199+
}
200+
if _, ok := variables["notFound"]; ok {
201+
t.Fatalf("expected missing variable to remain unset")
202+
}
203+
}
204+
205+
func TestParseHeaderVariablesLeavesMissingValuesUnset(t *testing.T) {
206+
variables := map[string]string{}
207+
err := parseHeaderVariables(
208+
map[string]string{"Set-Cookie": "session_id=abc123; Path=/; HttpOnly"},
209+
[]api.HTTPRequestResponseHeaderVariable{
210+
{Name: "sessionID", Header: "Set-Cookie", Regex: "session_id=([^;]+)"},
211+
{Name: "missingHeader", Header: "X-Missing"},
212+
{Name: "missingMatch", Header: "Set-Cookie", Regex: "missing=([^;]+)"},
213+
},
214+
variables,
215+
)
216+
if err != nil {
217+
t.Fatalf("unexpected parseHeaderVariables error: %v", err)
218+
}
219+
if variables["sessionID"] != "abc123" {
220+
t.Fatalf("sessionID = %q, want abc123", variables["sessionID"])
221+
}
222+
if _, ok := variables["missingHeader"]; ok {
223+
t.Fatalf("expected missing header variable to remain unset")
224+
}
225+
if _, ok := variables["missingMatch"]; ok {
226+
t.Fatalf("expected non-matching header variable to remain unset")
227+
}
228+
}

client/lessons.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,11 @@ const (
7272
)
7373

7474
type CLIStepHTTPRequest struct {
75-
ResponseVariables []HTTPRequestResponseVariable
76-
Tests []HTTPRequestTest
77-
Request HTTPRequest
78-
SleepAfterMs *int
75+
ResponseVariables []HTTPRequestResponseVariable
76+
ResponseHeaderVariables []HTTPRequestResponseHeaderVariable
77+
Tests []HTTPRequestTest
78+
Request HTTPRequest
79+
SleepAfterMs *int
7980
}
8081

8182
type Sleepable interface {
@@ -93,11 +94,12 @@ func (h *CLIStepHTTPRequest) GetSleepAfterMs() *int {
9394
const BaseURLPlaceholder = "${baseURL}"
9495

9596
type HTTPRequest struct {
96-
Method string
97-
FullURL string
98-
Headers map[string]string
99-
BodyJSON map[string]any
100-
BodyForm map[string]string
97+
Method string
98+
FullURL string
99+
Headers map[string]string
100+
BodyJSON map[string]any
101+
BodyForm map[string]string
102+
FollowRedirects *bool
101103

102104
BasicAuth *HTTPBasicAuth
103105
}
@@ -112,6 +114,12 @@ type HTTPRequestResponseVariable struct {
112114
Path string
113115
}
114116

117+
type HTTPRequestResponseHeaderVariable struct {
118+
Name string
119+
Header string
120+
Regex string
121+
}
122+
115123
// HTTPRequestTest should have only one field set
116124
type HTTPRequestTest struct {
117125
StatusCode *int

render/variables.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ func savedVariablesForHTTPResult(result api.HTTPRequestResult) []variableEntry {
5252
description: "JSON Body " + responseVariable.Path,
5353
})
5454
}
55+
for _, responseHeaderVariable := range result.Request.ResponseHeaderVariables {
56+
value := result.Variables[responseHeaderVariable.Name]
57+
if value == "" {
58+
continue
59+
}
60+
entries = append(entries, variableEntry{
61+
name: responseHeaderVariable.Name,
62+
value: value,
63+
description: responseHeaderVariableDescription(responseHeaderVariable),
64+
})
65+
}
5566
return entries
5667
}
5768

@@ -66,9 +77,25 @@ func missingSaveVariablesForHTTPResult(result api.HTTPRequestResult) []variableE
6677
description: "JSON Body " + responseVariable.Path,
6778
})
6879
}
80+
for _, responseHeaderVariable := range result.Request.ResponseHeaderVariables {
81+
if result.Variables[responseHeaderVariable.Name] != "" {
82+
continue
83+
}
84+
entries = append(entries, variableEntry{
85+
name: responseHeaderVariable.Name,
86+
description: responseHeaderVariableDescription(responseHeaderVariable),
87+
})
88+
}
6989
return entries
7090
}
7191

92+
func responseHeaderVariableDescription(v api.HTTPRequestResponseHeaderVariable) string {
93+
if v.Regex == "" {
94+
return "Response Header " + v.Header
95+
}
96+
return fmt.Sprintf("Response Header %s matching %s", v.Header, v.Regex)
97+
}
98+
7299
func availableVariablesForHTTPResult(result api.HTTPRequestResult) (entries []variableEntry, expectsVariables bool) {
73100
seen := map[string]bool{}
74101

render/variables_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@ func TestHTTPVariableSections(t *testing.T) {
1212
Variables: map[string]string{
1313
"authToken": "token-123",
1414
"shortCode": "abc123",
15+
"sessionID": "session-123",
1516
},
1617
Request: api.CLIStepHTTPRequest{
1718
ResponseVariables: []api.HTTPRequestResponseVariable{
1819
{Name: "shortCode", Path: ".short_code"},
1920
{Name: "missingCode", Path: ".missing_code"},
2021
},
22+
ResponseHeaderVariables: []api.HTTPRequestResponseHeaderVariable{
23+
{Name: "sessionID", Header: "Set-Cookie", Regex: "session_id=([^;]+)"},
24+
{Name: "missingSessionID", Header: "Set-Cookie", Regex: "missing=([^;]+)"},
25+
},
2126
Request: api.HTTPRequest{
2227
FullURL: "${baseURL}/api/links/${shortCode}",
2328
Headers: map[string]string{
@@ -37,9 +42,11 @@ func TestHTTPVariableSections(t *testing.T) {
3742

3843
wantContains := []string{
3944
"Variables Saved:",
45+
"sessionID: session-123 (Response Header Set-Cookie matching session_id=([^;]+))",
4046
"shortCode: abc123 (JSON Body .short_code)",
4147
"Variables Missing:",
4248
"missingCode: [not found] (JSON Body .missing_code)",
49+
"missingSessionID: [not found] (Response Header Set-Cookie matching missing=([^;]+))",
4350
"Variables Available:",
4451
"authToken: token-123 (Request Header \"Authorization\")",
4552
"shortCode: abc123 (Request URL)",

0 commit comments

Comments
 (0)