Skip to content

Commit

Permalink
Support root path expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
Stein Fletcher committed Sep 6, 2020
1 parent 82f1d19 commit 9693678
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 0 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,39 @@ func fromAuthHeader(res *http.Response) (string, error) {
return res.Header.Get("Authorization"), nil
}
```

### Chain

`Chain` is used to provide several assertions at once

```go
Assert(
jsonpath.Chain().
Equal("a", "1").
NotEqual("b", "2").
Present("c").
End(),
).
```

### Root

`Root` is used to avoid duplicated paths in body expectations. For example, instead of writing:

```go
Assert(jsonpath.Equal("a.b.c.d", "a").
Assert(jsonpath.Equal("a.b.c.e", "b").
Assert(jsonpath.Equal("a.b.c.f", "c").
```

it is possible to define a root path like so

```go
Assert(
jsonpath.Root("$.a.b.c").
Equal("d", "a").
Equal("e", "b").
Equal("f", "c").
End(),
).
```
134 changes: 134 additions & 0 deletions jsonpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
regex "regexp"
"strings"
Expand Down Expand Up @@ -65,6 +66,7 @@ func NotEqual(expression string, expected interface{}) apitest.Assert {
}
}

// Len asserts that value is the expected length, determined by reflect.Len
func Len(expression string, expectedLength int) apitest.Assert {
return func(res *http.Response, req *http.Request) error {
value, err := jsonPath(res.Body, expression)
Expand All @@ -80,6 +82,7 @@ func Len(expression string, expectedLength int) apitest.Assert {
}
}

// GreaterThan asserts that value is greater than the given length, determined by reflect.Len
func GreaterThan(expression string, minimumLength int) apitest.Assert {
return func(res *http.Response, req *http.Request) error {
value, err := jsonPath(res.Body, expression)
Expand All @@ -95,6 +98,7 @@ func GreaterThan(expression string, minimumLength int) apitest.Assert {
}
}

// LessThan asserts that value is less than the given length, determined by reflect.Len
func LessThan(expression string, maximumLength int) apitest.Assert {
return func(res *http.Response, req *http.Request) error {
value, err := jsonPath(res.Body, expression)
Expand All @@ -110,6 +114,7 @@ func LessThan(expression string, maximumLength int) apitest.Assert {
}
}

// Present asserts that value returned by the expression is present
func Present(expression string) apitest.Assert {
return func(res *http.Response, req *http.Request) error {
value, _ := jsonPath(res.Body, expression)
Expand All @@ -120,6 +125,7 @@ func Present(expression string) apitest.Assert {
}
}

// NotPresent asserts that value returned by the expression is not present
func NotPresent(expression string) apitest.Assert {
return func(res *http.Response, req *http.Request) error {
value, _ := jsonPath(res.Body, expression)
Expand All @@ -130,6 +136,7 @@ func NotPresent(expression string) apitest.Assert {
}
}

// Matches asserts that the value matches the given regular expression
func Matches(expression string, regexp string) apitest.Assert {
return func(res *http.Response, req *http.Request) error {
pattern, err := regex.Compile(regexp)
Expand Down Expand Up @@ -167,6 +174,70 @@ func Matches(expression string, regexp string) apitest.Assert {
}
}

// Chain creates a new assertion chain
func Chain() *AssertionChain {
return &AssertionChain{rootExpression: ""}
}

// Root creates a new assertion chain prefixed with the given expression
func Root(expression string) *AssertionChain {
return &AssertionChain{rootExpression: expression + "."}
}

// AssertionChain supports chaining assertions and root expressions
type AssertionChain struct {
rootExpression string
assertions []apitest.Assert
}

// Equal adds an Equal assertion to the chain
func (r *AssertionChain) Equal(expression string, expected interface{}) *AssertionChain {
r.assertions = append(r.assertions, Equal(r.rootExpression+expression, expected))
return r
}

// NotEqual adds an NotEqual assertion to the chain
func (r *AssertionChain) NotEqual(expression string, expected interface{}) *AssertionChain {
r.assertions = append(r.assertions, NotEqual(r.rootExpression+expression, expected))
return r
}

// Contains adds an Contains assertion to the chain
func (r *AssertionChain) Contains(expression string, expected interface{}) *AssertionChain {
r.assertions = append(r.assertions, Contains(r.rootExpression+expression, expected))
return r
}

// Present adds an Present assertion to the chain
func (r *AssertionChain) Present(expression string) *AssertionChain {
r.assertions = append(r.assertions, Present(r.rootExpression+expression))
return r
}

// NotPresent adds an NotPresent assertion to the chain
func (r *AssertionChain) NotPresent(expression string) *AssertionChain {
r.assertions = append(r.assertions, NotPresent(r.rootExpression+expression))
return r
}

// Matches adds an Matches assertion to the chain
func (r *AssertionChain) Matches(expression, regexp string) *AssertionChain {
r.assertions = append(r.assertions, Matches(r.rootExpression+expression, regexp))
return r
}

// End returns an apitest.Assert which is a combination of the registered assertions
func (r *AssertionChain) End() apitest.Assert {
return func(res *http.Response, req *http.Request) error {
for _, assertion := range r.assertions {
if err := assertion(copyHttpResponse(res), copyHttpRequest(req)); err != nil {
return err
}
}
return nil
}
}

func isEmpty(object interface{}) bool {
if object == nil {
return true
Expand Down Expand Up @@ -260,3 +331,66 @@ func objectsAreEqual(expected, actual interface{}) bool {
}
return bytes.Equal(exp, act)
}

func copyHttpResponse(response *http.Response) *http.Response {
if response == nil {
return nil
}

var resBodyBytes []byte
if response.Body != nil {
resBodyBytes, _ = ioutil.ReadAll(response.Body)
response.Body = ioutil.NopCloser(bytes.NewBuffer(resBodyBytes))
}

resCopy := &http.Response{
Header: map[string][]string{},
StatusCode: response.StatusCode,
Status: response.Status,
Body: ioutil.NopCloser(bytes.NewBuffer(resBodyBytes)),
Proto: response.Proto,
ProtoMinor: response.ProtoMinor,
ProtoMajor: response.ProtoMajor,
ContentLength: response.ContentLength,
}

for name, values := range response.Header {
resCopy.Header[name] = values
}

return resCopy
}

func copyHttpRequest(request *http.Request) *http.Request {
resCopy := &http.Request{
Method: request.Method,
Host: request.Host,
Proto: request.Proto,
ProtoMinor: request.ProtoMinor,
ProtoMajor: request.ProtoMajor,
ContentLength: request.ContentLength,
RemoteAddr: request.RemoteAddr,
}

if request.Body != nil {
bodyBytes, _ := ioutil.ReadAll(request.Body)
resCopy.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
}

if request.URL != nil {
r2URL := new(url.URL)
*r2URL = *request.URL
resCopy.URL = r2URL
}

headers := make(http.Header)
for k, values := range request.Header {
for _, hValue := range values {
headers.Add(k, hValue)
}
}
resCopy.Header = headers

return resCopy
}
48 changes: 48 additions & 0 deletions jsonpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,54 @@ func TestApiTest_Matches(t *testing.T) {
}
}

func TestApiTest_Chain(t *testing.T) {
handler := http.NewServeMux()
handler.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{
"a": {
"b": {
"c": {
"d": 1,
"e": "2",
"f": [3, 4, 5]
}
}
}
}`))
if err != nil {
panic(err)
}
})

apitest.New().
Handler(handler).
Get("/hello").
Expect(t).
Assert(
jsonpath.Root("$.a.b.c").
Equal("d", float64(1)).
Equal("e", "2").
Contains("f", float64(5)).
End(),
).
End()

apitest.New().
Handler(handler).
Get("/hello").
Expect(t).
Assert(
jsonpath.Chain().
Equal("a.b.c.d", float64(1)).
Equal("a.b.c.e", "2").
Contains("a.b.c.f", float64(5)).
End(),
).
End()
}

func TestApiTest_Matches_FailCompile(t *testing.T) {
willFailToCompile := jsonpath.Matches(`$.b[? @.key=="c"].value`, `\`)
err := willFailToCompile(nil, nil)
Expand Down

0 comments on commit 9693678

Please sign in to comment.