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
109 changes: 109 additions & 0 deletions hammy/httpassert/recorder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package httpassert

import (
"net/http"
"net/http/httptest"
"regexp"
"strings"

"github.com/gogunit/gunit/hammy"
)

// Recorder wraps an httptest.ResponseRecorder with HTTP assertion helpers.
func Recorder(rec *httptest.ResponseRecorder) *RecorderAssert {
return &RecorderAssert{actual: rec}
}

// RecorderAssert provides HTTP assertions for an httptest.ResponseRecorder.
type RecorderAssert struct {
actual *httptest.ResponseRecorder
}

// Status asserts that the recorded response status equals expected.
func (r *RecorderAssert) Status(expected int) hammy.AssertionMessage {
resp, result := r.response()
if !result.IsSuccessful {
return hammy.Assert(false, "got nil response recorder, wanted status <%d>", expected)
}
return hammy.Assert(resp.StatusCode == expected, "got status <%d>, wanted <%d>", resp.StatusCode, expected)
}

// StatusInRange asserts that the recorded response status is between min and max, inclusive.
func (r *RecorderAssert) StatusInRange(min, max int) hammy.AssertionMessage {
if min > max {
return hammy.Assert(false, "got invalid status range <%d..%d>", min, max)
}
resp, result := r.response()
if !result.IsSuccessful {
return hammy.Assert(false, "got nil response recorder, wanted status in range <%d..%d>", min, max)
}
return hammy.Assert(resp.StatusCode >= min && resp.StatusCode <= max, "got status <%d>, wanted in range <%d..%d>", resp.StatusCode, min, max)
}

// Header asserts that the recorded response header equals expected.
func (r *RecorderAssert) Header(key, expected string) hammy.AssertionMessage {
resp, result := r.response()
if !result.IsSuccessful {
return hammy.Assert(false, "got nil response recorder, wanted header <%s> equal to <%s>", key, expected)
}
actual := resp.Header.Get(key)
return hammy.Assert(actual == expected, "got header <%s>=<%s>, wanted <%s>", key, actual, expected)
}

// HeaderContains asserts that the recorded response header contains expected.
func (r *RecorderAssert) HeaderContains(key, expected string) hammy.AssertionMessage {
resp, result := r.response()
if !result.IsSuccessful {
return hammy.Assert(false, "got nil response recorder, wanted header <%s> containing <%s>", key, expected)
}
actual := resp.Header.Get(key)
return hammy.Assert(strings.Contains(actual, expected), "got header <%s>=<%s>, wanted containing <%s>", key, actual, expected)
}

// BodyEqual asserts that the recorded response body equals expected.
func (r *RecorderAssert) BodyEqual(expected string) hammy.AssertionMessage {
actual, result := r.body()
if !result.IsSuccessful {
return hammy.Assert(false, "got nil response recorder, wanted response body")
}
return hammy.Assert(actual == expected, "got body <%s>, wanted equal to <%s>", actual, expected)
}

// BodyContains asserts that the recorded response body contains expected.
func (r *RecorderAssert) BodyContains(expected string) hammy.AssertionMessage {
actual, result := r.body()
if !result.IsSuccessful {
return hammy.Assert(false, "got nil response recorder, wanted response body")
}
return hammy.Assert(strings.Contains(actual, expected), "got body <%s>, wanted containing <%s>", actual, expected)
}

// BodyMatchesRegexp asserts that the recorded response body matches pattern.
func (r *RecorderAssert) BodyMatchesRegexp(pattern string) hammy.AssertionMessage {
actual, result := r.body()
if !result.IsSuccessful {
return hammy.Assert(false, "got nil response recorder, wanted response body")
}
re, err := regexp.Compile(pattern)
if err != nil {
return hammy.Assert(false, "invalid regexp <%s>: %v", pattern, err)
}
return hammy.Assert(re.MatchString(actual), "got body <%s>, wanted regexp <%s>", actual, pattern)
}

func (r *RecorderAssert) response() (*http.Response, hammy.AssertionMessage) {
if r == nil || r.actual == nil {
return nil, hammy.Assert(false, "got nil response recorder")
}
return r.actual.Result(), hammy.Assert(true, "got response recorder")
}

func (r *RecorderAssert) body() (string, hammy.AssertionMessage) {
if r == nil || r.actual == nil {
return "", hammy.Assert(false, "got nil response recorder")
}
if r.actual.Body == nil {
return "", hammy.Assert(true, "got response recorder body")
}
return r.actual.Body.String(), hammy.Assert(true, "got response recorder body")
}
170 changes: 170 additions & 0 deletions hammy/httpassert/recorder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package httpassert_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/gogunit/gunit/eye"
"github.com/gogunit/gunit/hammy"
"github.com/gogunit/gunit/hammy/httpassert"
)

func Test_Recorder_Status_success(t *testing.T) {
assert := hammy.New(t)
rec := newRecorder(http.StatusCreated, nil, "")

assert.Is(httpassert.Recorder(rec).Status(http.StatusCreated))
}

func Test_Recorder_Status_failure(t *testing.T) {
rec := newRecorder(http.StatusCreated, nil, "")
result := httpassert.Recorder(rec).Status(http.StatusOK)

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "got status <201>, wanted <200>")
}

func Test_Recorder_Status_failure_nil_recorder(t *testing.T) {
result := httpassert.Recorder(nil).Status(http.StatusOK)

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "got nil response recorder")
}

func Test_Recorder_StatusInRange_success(t *testing.T) {
assert := hammy.New(t)
rec := newRecorder(http.StatusNoContent, nil, "")

assert.Is(httpassert.Recorder(rec).StatusInRange(200, 299))
}

func Test_Recorder_StatusInRange_failure(t *testing.T) {
rec := newRecorder(http.StatusBadRequest, nil, "")
result := httpassert.Recorder(rec).StatusInRange(200, 299)

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "got status <400>, wanted in range <200..299>")
}

func Test_Recorder_StatusInRange_failure_invalid_range(t *testing.T) {
rec := newRecorder(http.StatusOK, nil, "")
result := httpassert.Recorder(rec).StatusInRange(299, 200)

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "got invalid status range <299..200>")
}

func Test_Recorder_Header_success(t *testing.T) {
assert := hammy.New(t)
rec := newRecorder(http.StatusOK, http.Header{"Content-Type": {"application/json"}}, "")

assert.Is(httpassert.Recorder(rec).Header("Content-Type", "application/json"))
}

func Test_Recorder_Header_failure(t *testing.T) {
rec := newRecorder(http.StatusOK, http.Header{"Content-Type": {"text/plain"}}, "")
result := httpassert.Recorder(rec).Header("Content-Type", "application/json")

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "got header <Content-Type>=<text/plain>, wanted <application/json>")
}

func Test_Recorder_HeaderContains_success(t *testing.T) {
assert := hammy.New(t)
rec := newRecorder(http.StatusOK, http.Header{"Content-Type": {"application/json; charset=utf-8"}}, "")

assert.Is(httpassert.Recorder(rec).HeaderContains("Content-Type", "application/json"))
}

func Test_Recorder_HeaderContains_failure(t *testing.T) {
rec := newRecorder(http.StatusOK, http.Header{"Content-Type": {"text/plain"}}, "")
result := httpassert.Recorder(rec).HeaderContains("Content-Type", "application/json")

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "got header <Content-Type>=<text/plain>, wanted containing <application/json>")
}

func Test_Recorder_BodyEqual_success(t *testing.T) {
assert := hammy.New(t)
rec := newRecorder(http.StatusOK, nil, "hello world")

assert.Is(httpassert.Recorder(rec).BodyEqual("hello world"))
}

func Test_Recorder_BodyEqual_failure(t *testing.T) {
rec := newRecorder(http.StatusOK, nil, "hello world")
result := httpassert.Recorder(rec).BodyEqual("goodbye")

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "got body <hello world>, wanted equal to <goodbye>")
}

func Test_Recorder_BodyContains_success(t *testing.T) {
assert := hammy.New(t)
rec := newRecorder(http.StatusOK, nil, "hello world")

assert.Is(httpassert.Recorder(rec).BodyContains("world"))
}

func Test_Recorder_BodyContains_failure(t *testing.T) {
rec := newRecorder(http.StatusOK, nil, "hello world")
result := httpassert.Recorder(rec).BodyContains("goodbye")

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "got body <hello world>, wanted containing <goodbye>")
}

func Test_Recorder_BodyMatchesRegexp_success(t *testing.T) {
assert := hammy.New(t)
rec := newRecorder(http.StatusOK, nil, "status 204")

assert.Is(httpassert.Recorder(rec).BodyMatchesRegexp(`status \d+`))
}

func Test_Recorder_BodyMatchesRegexp_failure(t *testing.T) {
rec := newRecorder(http.StatusOK, nil, "status 204")
result := httpassert.Recorder(rec).BodyMatchesRegexp(`status 5\d\d`)

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "got body <status 204>, wanted regexp <status 5\\d\\d>")
}

func Test_Recorder_BodyMatchesRegexp_failure_invalid_pattern(t *testing.T) {
rec := newRecorder(http.StatusOK, nil, "status 204")
result := httpassert.Recorder(rec).BodyMatchesRegexp(`(`)

aSpy := eye.Spy()
hammy.New(aSpy).Is(result)
aSpy.HadErrorContaining(t, "invalid regexp <(>")
}

func Test_Recorder_Body_assertions_restore_body(t *testing.T) {
assert := hammy.New(t)
recorderAssert := httpassert.Recorder(newRecorder(http.StatusOK, nil, "hello world"))

assert.Is(recorderAssert.BodyContains("hello"))
assert.Is(recorderAssert.BodyEqual("hello world"))
assert.Is(recorderAssert.BodyMatchesRegexp(`world$`))
}

func newRecorder(status int, headers http.Header, body string) *httptest.ResponseRecorder {
recorder := httptest.NewRecorder()
for key, values := range headers {
for _, value := range values {
recorder.Header().Add(key, value)
}
}
recorder.WriteHeader(status)
_, _ = recorder.WriteString(body)
return recorder
}
Loading