Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,15 @@ Another flavour of JSON matching is to match only specific fields with [JSON Pat
| $[0].dyn | "$dyn" |
```

Response body can be matched with regular expression, named matches are treated as variables.

```gherkin
And I should have "some-service" response with body, that matches regular expression
"""
"time"\s*:\s*"(?P<year>\d{4})
"""
```

```gherkin

Status can be defined with either phrase or numeric code.
Expand Down
7 changes: 7 additions & 0 deletions _testdata/Dynamic.feature
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ Feature: Dynamic data is used in steps
"id":"$user_id"
"""

And I should have response with body, that matches regular expression
"""
"name"\s*:\s*"(?P<first_name>\w+)
"""

And variable $first_name equals to "John"

# Creating an order for that user with $user_id.
When I request HTTP endpoint with method "POST" and URI "/order/$user_id/?user_id=$user_id"
And I request HTTP endpoint with header "X-UserId: $user_id"
Expand Down
7 changes: 7 additions & 0 deletions _testdata/LocalClient.feature
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ Feature: HTTP Service
"""
"status":"failed"
"""
And I should have other responses with body, that matches regular expression
"""
"error"\s*:\s*"(?P<errorMessage>[A-Za-z]+)"
"""

And variable $errorMessage equals to "foo"


And I should have other responses with header "Content-Type: application/json"

Expand Down
106 changes: 106 additions & 0 deletions local_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"net/url"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -216,6 +217,7 @@
s.Step(`^I should have(.*) response with body, that matches JSON from file$`, l.iShouldHaveResponseWithBodyThatMatchesJSONFromFile)
s.Step(`^I should have(.*) response with body, that matches JSON$`, l.iShouldHaveResponseWithBodyThatMatchesJSON)
s.Step(`^I should have(.*) response with body, that matches JSON paths$`, l.iShouldHaveResponseWithBodyThatMatchesJSONPaths)
s.Step(`^I should have(.*) response with body, that matches regular expression$`, l.iShouldHaveResponseWithBodyThatMatchesRegexp)

s.Step(`^I should have(.*) other responses with status "([^"]*)"$`, l.iShouldHaveOtherResponsesWithStatus)
s.Step(`^I should have(.*) other responses with header "([^"]*): ([^"]*)"$`, l.iShouldHaveOtherResponsesWithHeader)
Expand All @@ -226,6 +228,7 @@
s.Step(`^I should have(.*) other responses with body, that matches JSON$`, l.iShouldHaveOtherResponsesWithBodyThatMatchesJSON)
s.Step(`^I should have(.*) other responses with body, that matches JSON from file$`, l.iShouldHaveOtherResponsesWithBodyThatMatchesJSONFromFile)
s.Step(`^I should have(.*) other responses with body, that matches JSON paths$`, l.iShouldHaveOtherResponsesWithBodyThatMatchesJSONPaths)
s.Step(`^I should have(.*) other responses with body, that matches regular expression$`, l.iShouldHaveOtherResponsesWithBodyThatMatchesRegexp)

s.After(l.afterScenario)
}
Expand Down Expand Up @@ -767,6 +770,99 @@
})
}

func (l *LocalClient) iShouldHaveResponseWithBodyThatMatchesRegexp(ctx context.Context, service, bodyDoc string) (context.Context, error) {
ctx = l.VS.PrepareContext(ctx)

return l.expectResponse(ctx, service, func(c *httpmock.Client) error {
return c.ExpectResponseBodyCallback(func(received []byte) error {
return l.matches(ctx, received, bodyDoc)
})
})
}

func (l *LocalClient) matches(ctx context.Context, received []byte, pattern string) error {
capture, matched, err := l.extractNamedMatches(received, pattern)
if err != nil {
return err
}

Check notice on line 787 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) on lines 785:787 are not covered by tests.

if !matched {
return augmentBodyErr(ctx, fmt.Errorf("%w %q in %q", errDoesNotContain, pattern, received))
}

Check notice on line 791 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) on lines 789:791 are not covered by tests.

_, vs := l.VS.Vars(ctx)

for k, v := range capture {
k := "$" + k

_, err := l.varCollected(vs, k, string(v))
if err != nil {
return augmentBodyErr(ctx, err)
}

Check notice on line 801 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) on lines 799:801 are not covered by tests.
}

return nil
}

func (l *LocalClient) extractNamedMatches(data []byte, pattern string) (map[string][]byte, bool, error) {
re, err := regexp.Compile(pattern)
if err != nil {
return nil, false, err
}

Check notice on line 811 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) on lines 809:811 are not covered by tests.

indices := re.FindSubmatchIndex(data)
if indices == nil {
return nil, false, nil // no full match
}

Check notice on line 816 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) on lines 814:816 are not covered by tests.

names := re.SubexpNames()
result := make(map[string][]byte, len(names)-1)

// i=0 → full match
// i≥1 → capture groups
for i, name := range names {
if i == 0 || name == "" {
continue // skip whole match and unnamed groups
}

start, end := indices[i*2], indices[i*2+1]
if start == -1 {
result[name] = nil // optional group didn't match

continue

Check notice on line 832 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

2 statement(s) on lines 829:832 are not covered by tests.
}

result[name] = data[start:end] // zero-copy []byte slice
}

return result, true, nil
}

func (l *LocalClient) varCollected(vs *shared.Vars, s string, v interface{}) (bool, error) {
if vs == nil || !vs.IsVar(s) {
return false, nil
}

Check notice on line 844 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) on lines 842:844 are not covered by tests.

if n, ok := v.(json.Number); ok {
v = shared.DecodeJSONNumber(n)
} else if f, ok := v.(float64); ok && f == float64(int64(f)) {
v = int64(f)
}

Check notice on line 850 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

2 statement(s) on lines 846:850 are not covered by tests.

fv, found := vs.Get(s)
if !found {
vs.Set(s, v)

return true, nil
}

if fv != v {
return false, fmt.Errorf("unexpected variable %s value, expected %v, received %v", s, fv, v)
}

Check notice on line 861 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

2 statement(s) are not covered by tests.

return false, nil

Check notice on line 863 in local_client.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) are not covered by tests.
}

func (l *LocalClient) iShouldHaveOtherResponsesWithBodyThatContains(ctx context.Context, service, bodyDoc string) (context.Context, error) {
ctx = l.VS.PrepareContext(ctx)

Expand Down Expand Up @@ -865,6 +961,16 @@
})
}

func (l *LocalClient) iShouldHaveOtherResponsesWithBodyThatMatchesRegexp(ctx context.Context, service, bodyDoc string) (context.Context, error) {
ctx = l.VS.PrepareContext(ctx)

return l.expectResponse(ctx, service, func(c *httpmock.Client) error {
return c.ExpectOtherResponsesBodyCallback(func(received []byte) error {
return l.matches(ctx, received, bodyDoc)
})
})
}

func (l *LocalClient) iShouldHaveOtherResponsesWithBodyThatMatchesJSONFromFile(ctx context.Context, service, filePath string) (context.Context, error) {
ctx = l.VS.PrepareContext(ctx)

Expand Down
7 changes: 6 additions & 1 deletion local_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (

"github.com/bool64/httpmock"
"github.com/cucumber/godog"
httpsteps "github.com/godogx/httpsteps"
"github.com/godogx/httpsteps"
"github.com/godogx/vars"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -26,6 +27,7 @@ func TestLocal_RegisterSteps(t *testing.T) {
concurrencyLevel := 5
setExpectations(mock, concurrencyLevel)

vs := vars.Steps{}
local := httpsteps.NewLocalClient(srvURL, func(client *httpmock.Client) {
client.Headers = map[string]string{
"X-Foo": "bar",
Expand All @@ -36,6 +38,7 @@ func TestLocal_RegisterSteps(t *testing.T) {

suite := godog.TestSuite{
ScenarioInitializer: func(s *godog.ScenarioContext) {
vs.Register(s)
local.RegisterSteps(s)
},
Options: &godog.Options{
Expand Down Expand Up @@ -226,10 +229,12 @@ func TestLocal_RegisterSteps_dynamic(t *testing.T) {
}))
defer srv.Close()

vs := vars.Steps{}
local := httpsteps.NewLocalClient(srv.URL)

suite := godog.TestSuite{
ScenarioInitializer: func(s *godog.ScenarioContext) {
vs.Register(s)
local.RegisterSteps(s)
},
Options: &godog.Options{
Expand Down
Loading