Skip to content

Commit 37df230

Browse files
Add httpassert header validation (#286)
## What - Add httpassert header validation ## References <!-- Add identifier for issue tickets, links to other PRs, etc. --> ## Checklist <!-- Remove this section if not applicable to your changes --> - [ ] Tests
2 parents 7ef2d65 + 7ae271a commit 37df230

File tree

4 files changed

+173
-49
lines changed

4 files changed

+173
-49
lines changed

pkg/httpassert/extracter.go

Lines changed: 109 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package httpassert
77
import (
88
"fmt"
99
"reflect"
10+
"regexp"
1011
"testing"
1112

1213
"github.com/stretchr/testify/assert"
@@ -21,68 +22,127 @@ type Extractor func(t *testing.T, actual any) any
2122
// request.Expect().JsonPath("$.data.id", httpassert.ExtractTo(&id))
2223
func ExtractTo(ptr any) Extractor {
2324
return func(t *testing.T, actual any) any {
24-
targetVal := reflect.ValueOf(ptr)
25-
if targetVal.Kind() != reflect.Ptr || targetVal.IsNil() {
26-
assert.Fail(t, "ExtractTo requires a non-nil pointer")
25+
return extractInto(t, ptr, actual, nil)
26+
}
27+
}
28+
29+
func ExtractRegexTo(value string, ptr any) Extractor {
30+
return func(t *testing.T, actual any) any {
31+
return extractInto(t, ptr, actual, func(t *testing.T, v any, dstType reflect.Type) (any, bool) {
32+
s, ok := v.(string)
33+
if !ok {
34+
assert.Fail(t, fmt.Sprintf("ExtractRegexTo expects actual to be string, got %T", v))
35+
return nil, false
36+
}
37+
38+
re := regexp.MustCompile(value)
39+
40+
m := re.FindStringSubmatch(s)
41+
if m == nil {
42+
assert.Fail(t, fmt.Sprintf("ExtractRegexTo no match for %q in %q", re.String(), s))
43+
return nil, false
44+
}
45+
46+
// m[0] full match, m[1:] groups
47+
groups := m[1:]
48+
if len(groups) == 0 {
49+
groups = []string{m[0]}
50+
}
51+
52+
if dstType.Kind() == reflect.Slice {
53+
return groups, true
54+
}
55+
return groups[0], true
56+
})
57+
}
58+
}
59+
60+
func extractInto(
61+
t *testing.T,
62+
ptr any,
63+
actual any,
64+
preprocess func(t *testing.T, actual any, dstType reflect.Type) (any, bool),
65+
) any {
66+
target := reflect.ValueOf(ptr)
67+
if target.Kind() != reflect.Ptr || target.IsNil() {
68+
assert.Fail(t, "ExtractTo requires a non-nil pointer")
69+
return nil
70+
}
71+
if actual == nil {
72+
assert.Fail(t, "ExtractTo actual value is nil")
73+
return nil
74+
}
75+
76+
dst := target.Elem()
77+
dstType := dst.Type()
78+
79+
if preprocess != nil {
80+
var ok bool
81+
actual, ok = preprocess(t, actual, dstType)
82+
if !ok {
2783
return nil
2884
}
29-
3085
if actual == nil {
3186
assert.Fail(t, "ExtractTo actual value is nil")
3287
return nil
3388
}
89+
}
3490

35-
outVal := reflect.ValueOf(actual)
36-
targetType := targetVal.Elem().Type()
91+
src := reflect.ValueOf(actual)
3792

38-
// Direct assign / convert for simple values (string, int, etc.)
39-
if outVal.Type().AssignableTo(targetType) {
40-
targetVal.Elem().Set(outVal)
41-
return ptr
42-
}
93+
// unwrap interface{}
94+
if src.IsValid() && src.Kind() == reflect.Interface && !src.IsNil() {
95+
src = reflect.ValueOf(src.Interface())
96+
}
4397

44-
if outVal.Type().ConvertibleTo(targetType) {
45-
targetVal.Elem().Set(outVal.Convert(targetType))
46-
return ptr
47-
}
98+
// direct assign / convert
99+
if src.IsValid() && src.Type().AssignableTo(dstType) {
100+
dst.Set(src)
101+
return ptr
102+
}
103+
if src.IsValid() && src.Type().ConvertibleTo(dstType) {
104+
dst.Set(src.Convert(dstType))
105+
return ptr
106+
}
48107

49-
// Special handling for slices, e.g. []interface{} -> []string
50-
if outVal.Kind() == reflect.Slice && targetType.Kind() == reflect.Slice {
51-
elemType := targetType.Elem()
52-
n := outVal.Len()
53-
dst := reflect.MakeSlice(targetType, n, n)
54-
55-
for i := 0; i < n; i++ {
56-
src := outVal.Index(i)
57-
58-
// If it's interface{}, unwrap to the underlying concrete value.
59-
if src.Kind() == reflect.Interface && !src.IsNil() {
60-
src = reflect.ValueOf(src.Interface())
61-
}
62-
63-
if src.Type().AssignableTo(elemType) {
64-
dst.Index(i).Set(src)
65-
continue
66-
}
67-
68-
if src.Type().ConvertibleTo(elemType) {
69-
dst.Index(i).Set(src.Convert(elemType))
70-
continue
71-
}
72-
73-
assert.Fail(t,
74-
fmt.Sprintf("ExtractTo slice element type mismatch at index %d: cannot assign %v to %v",
75-
i, src.Type(), elemType))
76-
return nil
108+
// slice handling: e.g. []interface{} -> []string
109+
if src.IsValid() && src.Kind() == reflect.Slice && dstType.Kind() == reflect.Slice {
110+
elemType := dstType.Elem()
111+
n := src.Len()
112+
out := reflect.MakeSlice(dstType, n, n)
113+
114+
for i := 0; i < n; i++ {
115+
s := src.Index(i)
116+
117+
// unwrap interface{} elements
118+
if s.Kind() == reflect.Interface && !s.IsNil() {
119+
s = reflect.ValueOf(s.Interface())
120+
}
121+
122+
if s.Type().AssignableTo(elemType) {
123+
out.Index(i).Set(s)
124+
continue
125+
}
126+
if s.Type().ConvertibleTo(elemType) {
127+
out.Index(i).Set(s.Convert(elemType))
128+
continue
77129
}
78130

79-
targetVal.Elem().Set(dst)
80-
return ptr
131+
assert.Fail(t, fmt.Sprintf(
132+
"ExtractTo slice element type mismatch at index %d: cannot assign %v to %v",
133+
i, s.Type(), elemType,
134+
))
135+
return nil
81136
}
82137

83-
assert.Fail(t,
84-
fmt.Sprintf("ExtractTo type mismatch: cannot assign %v to %v",
85-
outVal.Type(), targetType))
86-
return nil
138+
dst.Set(out)
139+
return ptr
140+
}
141+
142+
srcType := any("<invalid>")
143+
if src.IsValid() {
144+
srcType = src.Type()
87145
}
146+
assert.Fail(t, fmt.Sprintf("ExtractTo type mismatch: cannot assign %v to %v", srcType, dstType))
147+
return nil
88148
}

pkg/httpassert/matcher.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package httpassert
66

77
import (
88
"fmt"
9+
"regexp"
910
"testing"
1011

1112
"github.com/stretchr/testify/assert"
@@ -45,3 +46,13 @@ func Contains(v string) Matcher {
4546
return valid
4647
}
4748
}
49+
50+
// Regex checks if a string matches the given regular expression
51+
// Example: ExpectJsonPath("$.data.name", httpassert.Regex("^foo.*bar$"))
52+
func Regex(expr string) Matcher {
53+
re := regexp.MustCompile(expr)
54+
55+
return func(t *testing.T, value any) bool {
56+
return assert.Regexp(t, re, value)
57+
}
58+
}

pkg/httpassert/response.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,32 @@ type Response interface {
3636
JsonTemplateFile(path string, values map[string]any) Response
3737
JsonFile(path string) Response
3838

39+
Header(name string, value any) Response
40+
3941
Body(body string) Response
4042
GetJsonBodyObject(target any) Response
4143
GetBody() string
4244

4345
Log() Response
4446
}
4547

48+
func (r *responseImpl) Header(name string, value any) Response {
49+
out := r.response.Header().Get(name)
50+
51+
switch v := value.(type) {
52+
case Extractor:
53+
v(r.t, out)
54+
return r
55+
case Matcher:
56+
v(r.t, out)
57+
return r
58+
59+
default:
60+
assert.Equal(r.t, value, out)
61+
return r
62+
}
63+
}
64+
4665
func (r *responseImpl) StatusCode(expected int) Response {
4766
require.Equal(r.t, expected, r.response.Code)
4867
return r

pkg/httpassert/response_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,40 @@ func TestResponse(t *testing.T) {
143143
})
144144
})
145145

146+
t.Run("Header", func(t *testing.T) {
147+
router := http.NewServeMux()
148+
router.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
149+
w.Header().Set("Content-Type", "application/json")
150+
w.WriteHeader(http.StatusOK)
151+
})
152+
153+
t.Run("compare string", func(t *testing.T) {
154+
m := New(t, router)
155+
156+
m.Get("/api").
157+
Expect().
158+
Header("Content-Type", "application/json")
159+
})
160+
161+
t.Run("extract value to variable", func(t *testing.T) {
162+
m := New(t, router)
163+
164+
var value string
165+
m.Get("/api").
166+
Expect().
167+
Header("Content-Type", ExtractRegexTo(`application/(.*)`, &value))
168+
assert.Equal(t, "json", value)
169+
})
170+
171+
t.Run("use matcher", func(t *testing.T) {
172+
m := New(t, router)
173+
174+
m.Get("/api").
175+
Expect().
176+
Header("Content-Type", Regex("application/[json]"))
177+
})
178+
})
179+
146180
t.Run("POST basic JSON", func(t *testing.T) {
147181
request.Post("/json").
148182
ContentType("application/json").

0 commit comments

Comments
 (0)