Skip to content

Commit

Permalink
Merge pull request #2 from amad/feat/assert-header
Browse files Browse the repository at this point in the history
Add response header assertions and changes to TestCase schema
  • Loading branch information
amad authored Feb 28, 2020
2 parents 39afe74 e833bdf commit 9df157e
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 17 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 6,15 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: [1.13.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}

steps:
- name: Set up Go 1.13
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.13
go-version: ${{ matrix.go-version }}

- name: Checkout code
uses: actions/checkout@v2
Expand Down
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 82,13 @@ A test case can have all the following parameters to give you more control on wh
},
"assertions": {
"statusCode": 200,
"matchInBody": [
"body": [
"github",
"[a-z]"
]
],
"header": {
"Content-Type": "application/json"
}
}
}
]
Expand Down Expand Up @@ -130,13 133,15 @@ Example:
}
```

You can make assertion on HTTP status code, and also assert whether the response contains any match of the provided regular expression or simple string.
You can make assertion on HTTP status code, and also assert whether the response contains any match of the provided regular expression or simple string. You can also make assertion on response header.

Test case fails if the HTTP status code does not match, Or any of the assertion in body do not match.

The `assertions.statusCode` field accepts a HTTP status code to check. The default value for this field is `200`.

The `assertions.matchInBody` field accepts an array of strings that can contain simple string or regex. When this field isn't provided, Smoker does not make any assertions on response body.
The `assertions.body` field accepts an array of strings that can contain simple string or regex. When this field isn't provided, Smoker does not make any assertions on response body.

The `assertions.header` field accepts a map of strings. When this field isn't provided, Smoker does not make any assertions on response header. You can use regular expression to match header value. But, you must provide full header name.

Example:

Expand All @@ -148,11 153,15 @@ Example:
"url": "https://github.com/amad/NotFoundRepo",
"assertions": {
"statusCode": 404,
"matchInBody": [
"body": [
"Github",
"Page not found",
"[a-z]"
]
],
"header": {
"Content-Type": "application/json",
"X-Requestid": "*"
}
}
}
]
Expand Down
5 changes: 3 additions & 2 deletions core/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 35,7 @@ type TestCase struct {

// Assertions describes expectations on each test case.
type Assertions struct {
StatusCode int `json:"statusCode"`
MatchInBody []string `json:"matchInBody"`
StatusCode int `json:"statusCode"`
Body []string `json:"body"`
Header map[string]string `json:"header"`
}
26 changes: 24 additions & 2 deletions requester/requester.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 6,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/textproto"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -80,21 81,42 @@ func (r *Requester) Request(tc core.TestCase) (bool, error) {
return false, fmt.Errorf("expected status-code: %d received: %d", tc.Assertions.StatusCode, res.StatusCode)
}

if len(tc.Assertions.MatchInBody) != 0 {
if len(tc.Assertions.Body) != 0 {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return false, fmt.Errorf("unable to read the response body with with error: %w", err)
}

bodyStr := string(body)

for _, matchInBody := range tc.Assertions.MatchInBody {
for _, matchInBody := range tc.Assertions.Body {
res, err := regexp.MatchString(matchInBody, bodyStr)
if err != nil || !res {
return false, fmt.Errorf("can not match /%s/ in response body", matchInBody)
}
}
}

if len(tc.Assertions.Header) != 0 {
for expectedHeaderName, expectedHeaderValue := range tc.Assertions.Header {
canonicalHeaderName := textproto.CanonicalMIMEHeaderKey(expectedHeaderName)

headerValue, foundHeader := res.Header[canonicalHeaderName]

if !foundHeader {
return false, fmt.Errorf("unable to find response header %s", canonicalHeaderName)
}

if strings.EqualFold(expectedHeaderValue, headerValue[0]) {
continue
}

matched, err := regexp.MatchString(expectedHeaderValue, headerValue[0])
if err != nil || !matched {
return false, fmt.Errorf("expected response header %s:%s received %s:%s", canonicalHeaderName, expectedHeaderValue, canonicalHeaderName, headerValue[0])
}
}
}

return true, nil
}
54 changes: 49 additions & 5 deletions requester/requester_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 36,7 @@ func TestRequest(t *testing.T) {
tc core.TestCase
mockStatusCode int
mockResBody string
mockResHeader map[string]string
expectErr string
}{
{
Expand All @@ -47,8 48,8 @@ func TestRequest(t *testing.T) {
Headers: map[string]string{"Content-Type": "application/json"},
Body: "OK",
Assertions: core.Assertions{
StatusCode: 200,
MatchInBody: []string{"OK"},
StatusCode: 200,
Body: []string{"OK"},
},
},
mockStatusCode: 200,
Expand Down Expand Up @@ -96,7 97,7 @@ func TestRequest(t *testing.T) {
Name: "test",
URL: "example.com",
Assertions: core.Assertions{
MatchInBody: []string{"OK"},
Body: []string{"OK"},
},
},
mockStatusCode: 200,
Expand All @@ -109,12 110,49 @@ func TestRequest(t *testing.T) {
Name: "test",
URL: "example.com",
Assertions: core.Assertions{
MatchInBody: []string{"^[0-9]{3}-[0-9]{5}$"},
Body: []string{"^[0-9]{3}-[0-9]{5}$"},
},
},
mockStatusCode: 200,
mockResBody: "123-45678",
},
{
name: "errors when can not match header",
tc: core.TestCase{
Name: "test",
URL: "example.com",
Assertions: core.Assertions{
Header: map[string]string{"Content-Type": "application/json"},
},
},
mockStatusCode: 200,
mockResHeader: map[string]string{"Content-Type": "text/html"},
expectErr: "expected response header Content-Type:application/json received Content-Type:text/html",
},
{
name: "errors when expected header not found",
tc: core.TestCase{
Name: "test",
URL: "example.com",
Assertions: core.Assertions{
Header: map[string]string{"Content-Type": "application/json"},
},
},
mockStatusCode: 200,
expectErr: "unable to find response header Content-Type",
},
{
name: "can match header",
tc: core.TestCase{
Name: "test",
URL: "example.com",
Assertions: core.Assertions{
Header: map[string]string{"access-control-allow-origin": "*", "content-length": "[0-9] "},
},
},
mockStatusCode: 200,
mockResHeader: map[string]string{"access-control-allow-origin": "*", "content-length": "23432"},
},
}

for _, item := range tt {
Expand Down Expand Up @@ -154,10 192,16 @@ func TestRequest(t *testing.T) {
}
}

resHeader := make(http.Header)

for hn, hv := range item.mockResHeader {
resHeader.Add(hn, hv)
}

return &http.Response{
StatusCode: item.mockStatusCode,
Body: ioutil.NopCloser(bytes.NewBufferString(item.mockResBody)),
Header: make(http.Header),
Header: resHeader,
}
})
requester := &Requester{
Expand Down

0 comments on commit 9df157e

Please sign in to comment.