Skip to content

Commit 3d9f575

Browse files
authored
Merge pull request #1 from c4s4/develop
Added regular expression assertions
2 parents ed9e08b + 709671f commit 3d9f575

4 files changed

Lines changed: 321 additions & 37 deletions

File tree

executors/tavern/README.md

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,19 @@ For *multipartform*, *basicauth*, *ignoreverifyssl*, *proxy*, *resolve*, *nofoll
6363

6464
Possible fields are:
6565

66-
- **StatusCode**: expected status code
67-
- **Headers**: expected headers as a map (only defined headers are tested)
68-
- **Body**: expected text body
69-
- **Json**: expected JSON body as a structure
70-
- **JsonExcludes**: a list of paths to excludes from test in JSON response
66+
- **statusCode**: expected status code
67+
- **headers**: expected headers as a map (only defined headers are tested)
68+
- **headersRegexps**: list of headers assertions that are regexps
69+
- **body**: expected text body
70+
- **bodyRegexp**: regexp for expected body
71+
- **json**: expected JSON body as a structure
72+
- **jsonExcludes**: a list of paths to excludes from test in JSON response
73+
- **jsonRegexps**: list of json fields assertions that are regexps
7174

7275
Fields that are not set are not checked. Only defined headers are checked, thus Tavern executor won't complain about an additional header.
7376

77+
## Json Excludes
78+
7479
You can exclude paths from JSON during test. For instance, to exclude field `CreationDate` during response comparison, you might add in `response` field:
7580

7681
```yaml
@@ -105,6 +110,37 @@ Thus:
105110
- **\*** matches any entry
106111
- **\*\*** matches any entries in successive levels
107112

113+
## Regular Expressions
114+
115+
You can perform assertions with regular expressions.
116+
117+
To perform a body assertion with regular expression, you can use `bodyRegexp`. Thus you might write:
118+
119+
```yaml
120+
bodyRegexp: "Foo.*Bar"
121+
```
122+
123+
To perform regexp assertions on headers, you must declare headers to assert in `headers` clause, then list regexp fields in `headersRegexps`, as follows:
124+
125+
```yaml
126+
headers:
127+
Set-Cookie: "foo=bar; Path=/; Expires=.*?; HttpOnly; SameSite=None"
128+
headersRegexps:
129+
- "Set-Cookie"
130+
```
131+
132+
To perform regexp assertions on JSON structure, you must declare fields to assert in `json` clause, then list regexp fields in `jsonRegexps`, as follows:
133+
134+
```yaml
135+
json:
136+
foo: "bar"
137+
spam: "e.*s"
138+
jsonRegexps:
139+
- "spam"
140+
```
141+
142+
This will assert that *spam* JSON field matches `e.*s` regexp. Assertion on *foo* fields will be performed with equality as usual.
143+
108144
## Default Assertions
109145

110146
One default assertion is made in this executor:
@@ -158,6 +194,6 @@ testcases:
158194
FirstName: "Adeline"
159195
LastName: "Durand"
160196
jsonExcludes: *excludes
161-
...
197+
```
162198

163199
*Enjoy!*

executors/tavern/assert.go

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ func AssertResponse(actual interface{}, expected ...interface{}) error {
2222
if !ok {
2323
return fmt.Errorf("bad actual type: expected: Result, actual: %T", actual)
2424
}
25+
if err := CheckAssertions(result.Expected); err != nil {
26+
return err
27+
}
2528
// check status code
2629
if result.Expected.StatusCode != 0 {
2730
if result.Expected.StatusCode != result.Actual.StatusCode {
@@ -30,14 +33,25 @@ func AssertResponse(actual interface{}, expected ...interface{}) error {
3033
}
3134
}
3235
// check expected headers
33-
for k, v := range result.Expected.Headers {
34-
value, ok := result.Actual.Headers[k]
36+
for key, expected := range result.Expected.Headers {
37+
actual, ok := result.Actual.Headers[key]
3538
if !ok {
36-
return fmt.Errorf("header '%s' not found in response", k)
39+
return fmt.Errorf("header '%s' not found in response", key)
3740
}
38-
if value != v {
39-
return fmt.Errorf("bad header '%s' value: expected: '%s', actual: '%s'",
40-
k, v, value)
41+
if ElementInList(key, result.Expected.HeadersRegexps) {
42+
match, err := regexp.MatchString(expected, actual)
43+
if err != nil {
44+
return fmt.Errorf("bad headers regexp: %v", err)
45+
}
46+
if !match {
47+
return fmt.Errorf("bad header '%s' value: regexp: '%s', doesn't match: '%s'",
48+
key, expected, actual)
49+
}
50+
} else {
51+
if actual != expected {
52+
return fmt.Errorf("bad header '%s' value: expected: '%s', actual: '%s'",
53+
key, expected, actual)
54+
}
4155
}
4256
}
4357
// check expected body
@@ -47,6 +61,16 @@ func AssertResponse(actual interface{}, expected ...interface{}) error {
4761
result.Expected.Body, result.Actual.Body)
4862
}
4963
}
64+
// check expected body
65+
if result.Expected.BodyRegexp != "" {
66+
match, err := regexp.MatchString(result.Expected.BodyRegexp, result.Actual.Body)
67+
if err != nil {
68+
return fmt.Errorf("bad body regexp: %v", err)
69+
}
70+
if !match {
71+
return fmt.Errorf("body doesn't match regexp '%s'", result.Expected.BodyRegexp)
72+
}
73+
}
5074
// check expected JSON body
5175
if result.Expected.JSON != nil {
5276
if !reflect.DeepEqual(result.Expected.JSON, result.Actual.JSON) {
@@ -56,15 +80,22 @@ func AssertResponse(actual interface{}, expected ...interface{}) error {
5680
}
5781
if len(result.Expected.JSONExcludes) != 0 {
5882
var err error
59-
changelog, err = FilterChangelog(changelog, result.Expected.JSONExcludes)
83+
changelog, err = FilterChangelogExcludes(changelog, result.Expected.JSONExcludes)
84+
if err != nil {
85+
return err
86+
}
87+
}
88+
if len(result.Expected.JSONRegexps) != 0 {
89+
var err error
90+
changelog, err = FilterChangelogRegexps(changelog, result.Expected.JSONRegexps)
6091
if err != nil {
6192
return err
6293
}
6394
}
6495
if len(changelog) != 0 {
6596
var diffs []string
6697
for _, change := range changelog {
67-
diffs = append(diffs, ChangeMessage(change))
98+
diffs = append(diffs, ChangeMessage(change, result.Expected.JSONRegexps))
6899
}
69100
changes := strings.Join(diffs, "; ")
70101
return fmt.Errorf("diffs in json: %s", changes)
@@ -74,8 +105,8 @@ func AssertResponse(actual interface{}, expected ...interface{}) error {
74105
return nil
75106
}
76107

77-
// FilterChangelog filters changelog with JSON excluded fields
78-
func FilterChangelog(changelog []diff.Change, filters []string) ([]diff.Change, error) {
108+
// FilterChangelogExcludes filters changelog with JSON excluded fields
109+
func FilterChangelogExcludes(changelog []diff.Change, filters []string) ([]diff.Change, error) {
79110
var filteredChangelog []diff.Change
80111
for _, change := range changelog {
81112
filtered := false
@@ -97,10 +128,52 @@ func FilterChangelog(changelog []diff.Change, filters []string) ([]diff.Change,
97128
return filteredChangelog, nil
98129
}
99130

131+
// FilterChangelogRegexps filters changelog with Regexp fields
132+
func FilterChangelogRegexps(changelog []diff.Change, filters []string) ([]diff.Change, error) {
133+
var filteredChangelog []diff.Change
134+
for _, change := range changelog {
135+
filtered := false
136+
path := FormatPath(change.Path)
137+
for _, filter := range filters {
138+
match, err := regexp.MatchString(PathToRegexp(filter), path)
139+
if err != nil {
140+
return nil, fmt.Errorf("invalid filter regexp: %v", err)
141+
}
142+
if match {
143+
if change.Type == diff.UPDATE {
144+
from, ok := change.From.(string)
145+
if !ok {
146+
return nil, fmt.Errorf("regexp field %s is not a string", path)
147+
}
148+
to, ok := change.To.(string)
149+
if !ok {
150+
return nil, fmt.Errorf("regexp filter %s is not a string", path)
151+
}
152+
m, e := regexp.MatchString(from, to)
153+
if e != nil {
154+
return nil, fmt.Errorf("invalid field regexp: %v", err)
155+
}
156+
if m {
157+
filtered = true
158+
}
159+
}
160+
continue
161+
}
162+
}
163+
if !filtered {
164+
filteredChangelog = append(filteredChangelog, change)
165+
}
166+
}
167+
return filteredChangelog, nil
168+
}
169+
100170
// ChangeMessage generates human readable change message
101-
func ChangeMessage(change diff.Change) string {
171+
func ChangeMessage(change diff.Change, jsonRegexps []string) string {
102172
if change.Type == diff.UPDATE {
103173
path := FormatPath(change.Path)
174+
if ElementInList(path, jsonRegexps) {
175+
return fmt.Sprintf(`expected:%s = "%v" !~ actual:%s = "%v"`, path, change.From, path, change.To)
176+
}
104177
return fmt.Sprintf(`expected:%s = "%v" != actual:%s = "%v"`, path, change.From, path, change.To)
105178
}
106179
if change.Type == diff.CREATE {
@@ -127,10 +200,38 @@ func PathToRegexp(path string) string {
127200
if element == "*" {
128201
parts = append(parts, "[^/]+/")
129202
} else if element == "**" {
130-
parts = append(parts, "(.*?/)?")
203+
parts = append(parts, ".*/?")
131204
} else {
132205
parts = append(parts, element+"/")
133206
}
134207
}
135208
return "^" + strings.TrimSuffix(strings.Join(parts, ""), "/") + "$"
136209
}
210+
211+
// ElementInList tells if given path is in filters list
212+
func ElementInList(path string, filters[]string) bool {
213+
for _, filter := range filters {
214+
if filter == path {
215+
return true
216+
}
217+
}
218+
return false
219+
}
220+
221+
// CheckAssertions check incompatibles assertions
222+
func CheckAssertions(expected Response) error {
223+
if expected.Body != "" && expected.BodyRegexp != "" {
224+
return fmt.Errorf("you can set both body and bodyRegexps assertions")
225+
}
226+
for _, regexp := range expected.HeadersRegexps {
227+
if _, ok := expected.Headers[regexp]; !ok {
228+
return fmt.Errorf("field %s declared as regexp but not found in headers list", regexp)
229+
}
230+
}
231+
for _, regexp := range expected.JSONRegexps {
232+
if ElementInList(regexp, expected.JSONExcludes) {
233+
return fmt.Errorf("JSON field '%s' can't be excluded and declared as regexp", regexp)
234+
}
235+
}
236+
return nil
237+
}

0 commit comments

Comments
 (0)