Skip to content

Commit 7b6397c

Browse files
cli http tests: allow response header capture
1 parent ef8f638 commit 7b6397c

5 files changed

Lines changed: 154 additions & 11 deletions

File tree

checks/http.go

Lines changed: 53 additions & 2 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,
@@ -195,6 +209,43 @@ func parseVariables(body []byte, vardefs []api.HTTPRequestResponseVariable, vari
195209
return nil
196210
}
197211

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

checks/http_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,53 @@ 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+
}

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)