diff --git a/hammy/httpassert/recorder.go b/hammy/httpassert/recorder.go new file mode 100644 index 0000000..7309938 --- /dev/null +++ b/hammy/httpassert/recorder.go @@ -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") +} diff --git a/hammy/httpassert/recorder_test.go b/hammy/httpassert/recorder_test.go new file mode 100644 index 0000000..78e71f2 --- /dev/null +++ b/hammy/httpassert/recorder_test.go @@ -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 =, wanted ") +} + +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 =, wanted containing ") +} + +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 , wanted equal to ") +} + +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 , wanted containing ") +} + +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 , wanted regexp ") +} + +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 +}