Repository: gavv/httpexpect
Pull request #176 (2022-11-27 08:10:09)

166 array and object every
Issue: #166 


Patch:
diff --git a/_examples/go.mod b/_examples/go.mod
index c9c4e54..eb5403f 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -5,12 +5,12 @@ go 1.17
 require (
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/fasthttp/websocket v1.5.0
-	github.com/gavv/httpexpect/v2 v2.6.0
+	github.com/gavv/httpexpect/v2 v2.6.1
 	github.com/gin-gonic/gin v1.8.1
 	github.com/gorilla/websocket v1.5.0
 	github.com/kataras/iris/v12 v12.1.8
 	github.com/labstack/echo v3.3.10+incompatible
-	github.com/valyala/fasthttp v1.41.0
+	github.com/valyala/fasthttp v1.42.0
 	google.golang.org/appengine v1.6.7
 )
 
diff --git a/_examples/go.sum b/_examples/go.sum
index 3f5a5ab..34c1b02 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -47,8 +47,8 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
 github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
-github.com/gavv/httpexpect/v2 v2.6.0 h1:whQo2O5pN4ypVJSUB/tJNF8QZhWpv6tzIhWKkp3O2KQ=
-github.com/gavv/httpexpect/v2 v2.6.0/go.mod h1:6ffPXr12lflKXEPjyjsbzg9n6dY1FchaG24VTyfAoFo=
+github.com/gavv/httpexpect/v2 v2.6.1 h1:vCoP1H++iiAV5VujGKECTLiJLtDNVAzdx9dm49DmSyE=
+github.com/gavv/httpexpect/v2 v2.6.1/go.mod h1:6ffPXr12lflKXEPjyjsbzg9n6dY1FchaG24VTyfAoFo=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
@@ -244,8 +244,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
 github.com/valyala/fasthttp v1.27.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA=
 github.com/valyala/fasthttp v1.33.0/go.mod h1:KJRK/MXx0J+yd0c5hlR+s1tIHD72sniU8ZJjl97LIw4=
 github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
-github.com/valyala/fasthttp v1.41.0 h1:zeR0Z1my1wDHTRiamBCXVglQdbUwgb9uWG3k1HQz6jY=
-github.com/valyala/fasthttp v1.41.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
+github.com/valyala/fasthttp v1.42.0 h1:LBMyqvJR8DEBgN79oI8dGbkuj5Lm9jbHESxH131TTN8=
+github.com/valyala/fasthttp v1.42.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
 github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
 github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
diff --git a/array.go b/array.go
index 30c642c..4c9da7f 100644
--- a/array.go
+++ b/array.go
@@ -212,6 +212,58 @@ func (a *Array) Iter() []Value {
 	return ret
 }
 
+// Every runs the passed function on all the Elements in the array.
+//
+// If assertion inside function fails, the original Array is marked failed.
+//
+// Every will execute the function for all values in the array irrespective
+// of assertion failures for some values in the array.
+//
+// Example:
+//
+//	array := NewArray(t, []interface{}{"foo", "bar"})
+//
+//	array.Every(func(index int, value *httpexpect.Value) {
+//		value.String().NotEmpty()
+//	})
+func (a *Array) Every(fn func(index int, value *Value)) *Array {
+	a.chain.enter("Every()")
+	defer a.chain.leave()
+
+	if a.chain.failed() {
+		return a
+	}
+
+	if fn == nil {
+		a.chain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected nil function argument"),
+			},
+		})
+		return a
+	}
+
+	chainFailure := false
+
+	for index, val := range a.value {
+		valueChain := a.chain.clone()
+		valueChain.replace("Every[%d]", index)
+
+		valueChain.setFailCallback(func() {
+			chainFailure = true
+		})
+
+		fn(index, newValue(valueChain, val))
+	}
+
+	if chainFailure {
+		a.chain.setFailed()
+	}
+
+	return a
+}
+
 // Empty succeeds if array is empty.
 //
 // Example:
diff --git a/assertion.go b/assertion.go
index 1f3dcd5..96bb132 100644
--- a/assertion.go
+++ b/assertion.go
@@ -28,7 +28,7 @@ const (
 	AssertNotEmpty
 
 	// Check expression: [Actual] is equal to [Expected]
-	// [Delta] specifies allowed difference between values (may be zero)
+	// If [Delta] is set, it specifies allowed difference between values
 	AssertEqual
 	AssertNotEqual
 
@@ -104,7 +104,7 @@ type AssertionContext struct {
 	Request *Request
 
 	// Response being matched
-	// May be nil if request was not yet received
+	// May be nil if response was not yet received
 	Response *Response
 
 	// Environment shared between tests
diff --git a/chain.go b/chain.go
index c35d459..2ffb054 100644
--- a/chain.go
+++ b/chain.go
@@ -96,6 +96,10 @@ func (c *chain) enter(name string, args ...interface{}) {
 	c.context.Path = append(c.context.Path, fmt.Sprintf(name, args...))
 }
 
+func (c *chain) replace(name string, args ...interface{}) {
+	c.context.Path[len(c.context.Path)-1] = fmt.Sprintf(name, args...)
+}
+
 func (c *chain) leave() {
 	if len(c.context.Path) == 0 {
 		panic("unpaired enter/leave")
@@ -125,6 +129,10 @@ func (c *chain) fail(failure AssertionFailure) {
 	}
 }
 
+func (c *chain) setFailed() {
+	c.failbit = true
+}
+
 func (c *chain) failed() bool {
 	return c.failbit
 }
diff --git a/expect.go b/expect.go
index 2a0f888..ab53474 100644
--- a/expect.go
+++ b/expect.go
@@ -99,21 +99,21 @@ type Config struct {
 	// TestName defines the name of the currently running test.
 	// May be empty.
 	//
-	// If empty, and if Config.Reporter implements Namer, name will be retrieved
-	// from it. In particular, this is true when Reporter is *testing.T.
+	// If non-empty, it will be included in failure report.
+	// Normally you set this value to t.Name().
 	TestName string
 
-	// BaseURL is a URL to prepended to all request.
+	// BaseURL is a URL to prepended to all requests.
 	// My be empty.
 	//
-	// If non-empty, trailing slash is allowed but not required and is appended
+	// If non-empty, trailing slash is allowed (but not required) and is appended
 	// automatically.
 	BaseURL string
 
 	// RequestFactory is used to pass in a custom *http.Request generation func.
 	// May be nil.
 	//
-	// If nil, DefaultRequestFactory is used.
+	// If nil, DefaultRequestFactory is used, which just calls http.NewRequest.
 	//
 	// You can use DefaultRequestFactory, or provide custom implementation.
 	// Useful for Google App Engine testing for example.
@@ -127,7 +127,7 @@ type Config struct {
 	//      Jar: httpexpect.NewJar(),
 	//  }
 	//
-	// You can use http.DefaultClient or http.Client, or provide
+	// You can use http.DefaultClient or your own http.Client, or provide
 	// custom implementation.
 	Client Client
 
@@ -153,36 +153,40 @@ type Config struct {
 	// Reporter is used to report formatted failure messages.
 	// Should NOT be nil, unless custom AssertionHandler is used.
 	//
-	// Reporter is used when AssertionHandler is nil or explicitly set to
-	// DefaultAssertionHandler.
+	// Config.Reporter is used by DefaultAssertionHandler, which is automatically
+	// constructed when AssertionHandler is nil.
 	//
 	// You can use AssertReporter, RequireReporter (they use testify),
 	// or *testing.T, or provide custom implementation.
 	Reporter Reporter
 
-	// Formatter is used to format failure message.
+	// Formatter is used to format success and failure messages.
 	// May be nil.
 	//
 	// If nil, DefaultFormatter is used.
 	//
-	// Formatter is used when AssertionHandler is nil or explicitly set to
-	// DefaultAssertionHandler.
+	// Config.Formatter is used by DefaultAssertionHandler, which is automatically
+	// constructed when AssertionHandler is nil.
 	//
-	// Usually you don't need custom formatter. Implementing one is
+	// Usually you don't need custom formatter. Implementing one is a
 	// relatively big task.
 	Formatter Formatter
 
-	// AssertionHandler handles both successful and failed assertions.
+	// AssertionHandler handles successful and failed assertions.
 	// May be nil.
 	//
-	// If nil, DefaultAssertionHandler is used.
+	// Every time an assertion is made, AssertionHandler is invoked with detailed
+	// info about the assertion. On failure, AssertionHandler is responsible to
+	// format error and report it to test suite.
 	//
-	// Every time an assertion is made, AssertionHandler in invoked with detailed
-	// info about the assertion and failure. On failure, AssertionHandler is
-	// responsible to format and report error.
+	// If AssertionHandler is nil, DefaultAssertionHandler is constructed, with
+	// Formatter set to Config.Formatter, Reporter set to Config.Reporter, and
+	// Logger set to nil. DefaultAssertionHandler will just delegates formatting
+	// and reporting to Formatter and Reporter.
 	//
-	// DefaultAssertionHandler uses Config.Formatter to format failure and
-	// Config.Reporter to report it.
+	// If you're happy with DefaultAssertionHandler, but want to enable logging
+	// of successful assertions and non-fatal failures, you can manually construct
+	// DefaultAssertionHandler and set its Logger field to non-nil value.
 	//
 	// Usually you don't need custom AssertionHandler and it's enough just to
 	// set Reporter. Use AssertionHandler for more precise control of reports.
@@ -192,7 +196,7 @@ type Config struct {
 	// May be nil.
 	//
 	// If printer implements WebsocketPrinter interface, it will be also used
-	// to print websocket messages.
+	// to print WebSocket messages.
 	//
 	// You can use CompactPrinter, DebugPrinter, CurlPrinter, or provide
 	// custom implementation.
@@ -204,11 +208,12 @@ type Config struct {
 	// Environment provides a container for arbitrary data shared between tests.
 	// May be nil.
 	//
-	// Environment is not by httpexpect itself, but can be used by tests to store
-	// and load arbitrary values. Tests can access Environment via Expect.Env()
-	// and AssertionContext.Env.
+	// Environment is not used by httpexpect itself, but can be used by tests to
+	// store and load arbitrary values. Tests can access Environment via
+	// Expect.Env(). It is also accessible in AssertionHandler via AssertionContext.
 	//
-	// If Environment is nil, a new environment is automatically created.
+	// If Environment is nil, a new empty environment is automatically created
+	// when Expect instance is constructed.
 	Environment *Environment
 }
 
@@ -330,8 +335,8 @@ func New(t LoggerReporter, baseURL string) *Expect {
 //
 // t is usually *testing.T, but can be any matching implementation.
 //
-// baseURL specifies URL to prepended to all request. My be empty. If non-empty,
-// trailing slash is allowed but not required and is appended automatically.
+// baseURL specifies URL to be prepended to all requests. My be empty. If non-empty,
+// trailing slash is allowed (but not required) and is appended automatically.
 //
 // Default is a shorthand for WithConfig. It uses:
 //   - baseURL for Config.BaseURL
diff --git a/object.go b/object.go
index b4d84ab..836333a 100644
--- a/object.go
+++ b/object.go
@@ -162,6 +162,59 @@ func (o *Object) Value(key string) *Value {
 	return newValue(o.chain, value)
 }
 
+// Every runs the passed function for all the key value pairs in the object
+//
+// If assertion inside function fails, the original Object is marked failed.
+//
+// Every will execute the function for all values in the object irrespective
+// of assertion failures for some values in the object.
+//
+// Example:
+//
+//	object := NewObject(t, map[string]interface{}{"foo": 123, "bar": 456})
+//
+//	object.Every(func(key string, value *httpexpect.Value) {
+//	  value.String().NotEmpty()
+//	})
+func (o *Object) Every(fn func(key string, value *Value)) *Object {
+	o.chain.enter("Every()")
+	defer o.chain.leave()
+
+	if o.chain.failed() {
+		return o
+	}
+
+	if fn == nil {
+		o.chain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected nil function argument"),
+			},
+		})
+		return o
+	}
+
+	chainFailure := false
+
+	for key, val := range o.value {
+		valueChain := o.chain.clone()
+		valueChain.replace("Every[%q]", key)
+
+		valueChain.setFailCallback(func() {
+			chainFailure = true
+		})
+
+		fn(key, newValue(valueChain, val))
+
+	}
+
+	if chainFailure {
+		o.chain.setFailed()
+	}
+
+	return o
+}
+
 // Empty succeeds if object is empty.
 //
 // Example:
diff --git a/request.go b/request.go
index 7805572..4e29d7c 100644
--- a/request.go
+++ b/request.go
@@ -1246,7 +1246,7 @@ func (r *Request) WithProto(proto string) *Request {
 //	req := NewRequest(config, "PUT", "http://example.com/upload")
 //	fh, _ := os.Open("data")
 //	defer fh.Close()
-//	req.WithHeader("Content-Type": "application/octet-stream")
+//	req.WithHeader("Content-Type", "application/octet-stream")
 //	req.WithChunked(fh)
 func (r *Request) WithChunked(reader io.Reader) *Request {
 	r.chain.enter("WithChunked()")


Test patch:
diff --git a/array_test.go b/array_test.go
index 83230c6..7069aa2 100644
--- a/array_test.go
+++ b/array_test.go
@@ -37,6 +37,9 @@ func TestArrayFailed(t *testing.T) {
 		value.NotContainsOnly("foo")
 		value.ContainsAny("foo")
 		value.NotContainsAny("foo")
+		value.Every(func(_ int, val *Value) {
+			val.String().NotEmpty()
+		})
 	}
 
 	t.Run("failed_chain", func(t *testing.T) {
@@ -748,3 +751,60 @@ func TestArrayConvertContains(t *testing.T) {
 	value.chain.assertFailed(t)
 	value.chain.reset()
 }
+
+func TestArrayEvery(t *testing.T) {
+	t.Run("Check validation", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		array := NewArray(reporter, []interface{}{2, 4, 6})
+		array.Every(func(_ int, val *Value) {
+			if v, ok := val.Raw().(float64); ok {
+				assert.Equal(ts, 0, int(v)%2)
+			}
+		})
+		array.chain.assertOK(ts)
+	})
+
+	t.Run("Empty array", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		array := NewArray(reporter, []interface{}{})
+		array.Every(func(_ int, val *Value) {})
+		array.chain.assertOK(ts)
+	})
+
+	t.Run("Test correct index", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		array := NewArray(reporter, []interface{}{1, 2, 3})
+		array.Every(
+			func(idx int, val *Value) {
+				if v, ok := val.Raw().(float64); ok {
+					assert.Equal(ts, idx, int(v)-1)
+				}
+			},
+		)
+		array.chain.assertOK(ts)
+	})
+
+	t.Run("Assertion failed for any", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		array := NewArray(reporter, []interface{}{"foo", "", "bar"})
+		invoked := 0
+		array.Every(func(_ int, val *Value) {
+			invoked++
+			val.String().NotEmpty()
+		})
+		assert.Equal(t, 3, invoked)
+		array.chain.assertFailed(ts)
+	})
+
+	t.Run("Assertion failed for all", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		array := NewArray(reporter, []interface{}{"", "", ""})
+		invoked := 0
+		array.Every(func(_ int, val *Value) {
+			invoked++
+			val.String().NotEmpty()
+		})
+		assert.Equal(t, 3, invoked)
+		array.chain.assertFailed(ts)
+	})
+}
diff --git a/object_test.go b/object_test.go
index a7b4c98..8df9183 100644
--- a/object_test.go
+++ b/object_test.go
@@ -29,6 +29,9 @@ func TestObjectFailed(t *testing.T) {
 		value.NotContainsSubset(nil)
 		value.ValueEqual("foo", nil)
 		value.NotValueEqual("foo", nil)
+		value.Every(func(_ string, value *Value) {
+			value.String().NotEmpty()
+		})
 	}
 
 	t.Run("failed_chain", func(t *testing.T) {
@@ -753,3 +756,73 @@ func TestObjectConvertValueEqual(t *testing.T) {
 	value.chain.assertFailed(t)
 	value.chain.reset()
 }
+
+func TestObjectEvery(t *testing.T) {
+	t.Run("Check Validation", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		object := NewObject(reporter, map[string]interface{}{
+			"foo": "123",
+			"bar": "456",
+			"baz": "b",
+		})
+		object.Every(func(_ string, value *Value) {
+			value.String().NotEmpty()
+		})
+		object.chain.assertOK(ts)
+	})
+
+	t.Run("Empty Object", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		object := NewObject(reporter, map[string]interface{}{})
+		object.Every(func(_ string, value *Value) {
+			value.String().NotEmpty()
+		})
+		object.chain.assertOK(ts)
+	})
+
+	t.Run("Test Keys", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		object := NewObject(reporter, map[string]interface{}{
+			"foo": "123",
+			"bar": "456",
+			"baz": "b",
+		})
+		object.Every(func(key string, value *Value) {
+			if v, ok := value.Raw().(string); ok {
+				switch v {
+				case "123":
+					assert.Equal(ts, "foo", key)
+				case "456":
+					assert.Equal(ts, "bar", key)
+				case "baz":
+					assert.Equal(ts, "baz", key)
+				}
+			}
+		})
+		object.chain.assertOK(ts)
+	})
+
+	t.Run("Assertion failed for any", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		object := NewObject(reporter, map[string]interface{}{"foo": "", "bar": "bar"})
+		invoked := 0
+		object.Every(func(_ string, val *Value) {
+			invoked++
+			val.String().NotEmpty()
+		})
+		object.chain.assertFailed(ts)
+		assert.Equal(t, 2, invoked)
+	})
+
+	t.Run("Assertion failed for all", func(ts *testing.T) {
+		reporter := newMockReporter(ts)
+		object := NewObject(reporter, map[string]interface{}{"foo": "", "bar": ""})
+		invoked := 0
+		object.Every(func(_ string, val *Value) {
+			invoked++
+			val.String().NotEmpty()
+		})
+		object.chain.assertFailed(ts)
+		assert.Equal(t, 2, invoked)
+	})
+}
diff --git a/request_test.go b/request_test.go
index 1b2d8c3..c805f9c 100644
--- a/request_test.go
+++ b/request_test.go
@@ -9,6 +9,7 @@ import (
 	"mime"
 	"mime/multipart"
 	"net/http"
+	"net/http/httptest"
 	"os"
 	"path/filepath"
 	"strings"
@@ -1601,3 +1602,616 @@ func TestRequestErrorConflictMultipart(t *testing.T) {
 	req3.WithFileBytes("a", "a", []byte("a"))
 	req3.chain.assertFailed(t)
 }
+
+func TestRequestRedirect(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	t.Run("dont follow redirects policy", func(t *testing.T) {
+		t.Run("request has no body", func(t *testing.T) {
+			tp := newMockTransportRedirect(t).
+				WithAssertFn(func(r *http.Request) bool {
+					return assert.Equal(t, http.NoBody, r.Body)
+				})
+
+			client := &http.Client{
+				Transport: tp,
+			}
+
+			config := Config{
+				Client:   client,
+				Reporter: reporter,
+			}
+
+			req := NewRequest(config, http.MethodPut, "/url").
+				WithRedirectPolicy(DontFollowRedirects)
+			req.chain.assertOK(t)
+
+			// Should return redirection response
+			resp := req.Expect().
+				Status(tp.redirectHTTPStatusCode).
+				Header("Location").
+				Equal("/redirect")
+			resp.chain.assertOK(t)
+
+			// Should set GetBody
+			assert.Nil(t, req.httpReq.GetBody)
+
+			// Should set CheckRedirect
+			httpClient, _ := req.config.Client.(*http.Client)
+			assert.NotNil(t, httpClient.CheckRedirect)
+			assert.Equal(t, http.ErrUseLastResponse, httpClient.CheckRedirect(req.httpReq, nil))
+
+			// Should do round trip
+			assert.Equal(t, 1, tp.tripCount)
+		})
+
+		t.Run("request has body", func(t *testing.T) {
+			tp := newMockTransportRedirect(t).
+				WithAssertFn(func(r *http.Request) bool {
+					b, err := ioutil.ReadAll(r.Body)
+					assert.NoError(t, err)
+					assert.Equal(t, "test body", string(b))
+
+					return true
+				})
+
+			client := &http.Client{
+				Transport: tp,
+			}
+
+			config := Config{
+				Client:   client,
+				Reporter: reporter,
+			}
+
+			req := NewRequest(config, http.MethodPut, "/url").
+				WithRedirectPolicy(DontFollowRedirects).
+				WithText("test body")
+			req.chain.assertOK(t)
+
+			// Should return redirection response
+			resp := req.Expect().
+				Status(tp.redirectHTTPStatusCode).
+				Header("Location").
+				Equal("/redirect")
+			resp.chain.assertOK(t)
+
+			// Should set GetBody
+			assert.Nil(t, req.httpReq.GetBody)
+
+			// Should set CheckRedirect
+			httpClient, _ := req.config.Client.(*http.Client)
+			assert.NotNil(t, httpClient.CheckRedirect)
+			assert.Equal(t, http.ErrUseLastResponse, httpClient.CheckRedirect(req.httpReq, nil))
+
+			// Should do round trip
+			assert.Equal(t, 1, tp.tripCount)
+		})
+	})
+
+	t.Run("follow all redirects policy", func(t *testing.T) {
+		t.Run("request has no body", func(t *testing.T) {
+			tp := newMockTransportRedirect(t).
+				WithAssertFn(func(r *http.Request) bool {
+					return assert.Equal(t, http.NoBody, r.Body)
+				}).WithMaxRedirect(1)
+
+			client := &http.Client{
+				Transport: tp,
+			}
+
+			config := Config{
+				Client:   client,
+				Reporter: reporter,
+			}
+
+			req := NewRequest(config, http.MethodPut, "/url").
+				WithRedirectPolicy(FollowAllRedirects).
+				WithMaxRedirects(1)
+			req.chain.assertOK(t)
+
+			// Should return OK response
+			resp := req.Expect().
+				Status(http.StatusOK)
+			resp.chain.assertOK(t)
+
+			// Should set GetBody
+			gb, err := req.httpReq.GetBody()
+			assert.NoError(t, err)
+			assert.Equal(t, http.NoBody, gb)
+
+			// Should set CheckRedirect
+			httpClient, _ := req.config.Client.(*http.Client)
+			assert.NotNil(t, httpClient.CheckRedirect)
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, nil))
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 1)))
+			assert.Equal(t,
+				errors.New("stopped after 1 redirects"),
+				httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 2)),
+			)
+
+			// Should do round trip
+			assert.Equal(t, 2, tp.tripCount)
+		})
+
+		t.Run("request has no body and too many redirects", func(t *testing.T) {
+			tp := newMockTransportRedirect(t).
+				WithAssertFn(func(r *http.Request) bool {
+					return assert.Equal(t, http.NoBody, r.Body)
+				})
+
+			client := &http.Client{
+				Transport: tp,
+			}
+
+			config := Config{
+				Client:   client,
+				Reporter: reporter,
+			}
+
+			req := NewRequest(config, http.MethodPut, "/url").
+				WithRedirectPolicy(FollowAllRedirects).
+				WithMaxRedirects(1)
+			req.chain.assertOK(t)
+
+			// Should error
+			resp := req.Expect()
+			resp.chain.assertFailed(t)
+
+			// Should set GetBody
+			gb, err := req.httpReq.GetBody()
+			assert.NoError(t, err)
+			assert.Equal(t, http.NoBody, gb)
+
+			// Should set CheckRedirect
+			httpClient, _ := req.config.Client.(*http.Client)
+			assert.NotNil(t, httpClient.CheckRedirect)
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, nil))
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 1)))
+			assert.Equal(t,
+				errors.New("stopped after 1 redirects"),
+				httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 2)),
+			)
+
+			// Should do round trip
+			assert.Equal(t, 2, tp.tripCount)
+		})
+
+		t.Run("request has body", func(t *testing.T) {
+			tp := newMockTransportRedirect(t).
+				WithAssertFn(func(r *http.Request) bool {
+					b, err := ioutil.ReadAll(r.Body)
+					assert.NoError(t, err)
+					assert.Equal(t, "test body", string(b))
+
+					return true
+				}).WithMaxRedirect(1)
+
+			client := &http.Client{
+				Transport: tp,
+			}
+
+			config := Config{
+				Client:   client,
+				Reporter: reporter,
+			}
+
+			req := NewRequest(config, http.MethodPut, "/url").
+				WithRedirectPolicy(FollowAllRedirects).
+				WithMaxRedirects(1).
+				WithText("test body")
+			req.chain.assertOK(t)
+
+			// Should return OK response
+			resp := req.Expect().
+				Status(http.StatusOK)
+			resp.chain.assertOK(t)
+
+			// Should set GetBody
+			gb, err := req.httpReq.GetBody()
+			assert.NoError(t, err)
+			b, err := ioutil.ReadAll(gb)
+			assert.NoError(t, err)
+			assert.Equal(t, "test body", string(b))
+
+			// Should set CheckRedirect
+			httpClient, _ := req.config.Client.(*http.Client)
+			assert.NotNil(t, httpClient.CheckRedirect)
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, nil))
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 1)))
+			assert.Equal(t,
+				errors.New("stopped after 1 redirects"),
+				httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 2)),
+			)
+
+			// Should do round trip
+			assert.Equal(t, 2, tp.tripCount)
+		})
+
+		t.Run("request has body and too many redirects", func(t *testing.T) {
+			tp := newMockTransportRedirect(t).
+				WithAssertFn(func(r *http.Request) bool {
+					b, err := ioutil.ReadAll(r.Body)
+					assert.NoError(t, err)
+					assert.Equal(t, "test body", string(b))
+
+					return true
+				})
+
+			client := &http.Client{
+				Transport: tp,
+			}
+
+			config := Config{
+				Client:   client,
+				Reporter: reporter,
+			}
+
+			req := NewRequest(config, http.MethodPut, "/url").
+				WithRedirectPolicy(FollowAllRedirects).
+				WithMaxRedirects(1).
+				WithText("test body")
+			req.chain.assertOK(t)
+
+			// Should error
+			resp := req.Expect()
+			resp.chain.assertFailed(t)
+
+			// Should set GetBody
+			gb, err := req.httpReq.GetBody()
+			assert.NoError(t, err)
+			b, err := ioutil.ReadAll(gb)
+			assert.NoError(t, err)
+			assert.Equal(t, "test body", string(b))
+
+			// Should set CheckRedirect
+			httpClient, _ := req.config.Client.(*http.Client)
+			assert.NotNil(t, httpClient.CheckRedirect)
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, nil))
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 1)))
+			assert.Equal(t,
+				errors.New("stopped after 1 redirects"),
+				httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 2)),
+			)
+
+			// Should do round trip
+			assert.Equal(t, 2, tp.tripCount)
+		})
+	})
+
+	t.Run("follow redirects without body policy", func(t *testing.T) {
+		t.Run("request has no body", func(t *testing.T) {
+			tp := newMockTransportRedirect(t).
+				WithAssertFn(func(r *http.Request) bool {
+					return assert.Contains(t, []interface{}{nil, http.NoBody}, r.Body)
+				}).WithMaxRedirect(1)
+
+			client := &http.Client{
+				Transport: tp,
+			}
+
+			config := Config{
+				Client:   client,
+				Reporter: reporter,
+			}
+
+			req := NewRequest(config, http.MethodPut, "/url").
+				WithRedirectPolicy(FollowRedirectsWithoutBody).
+				WithMaxRedirects(1)
+			req.chain.assertOK(t)
+
+			// Should return OK response
+			resp := req.Expect().
+				Status(http.StatusOK)
+			resp.chain.assertOK(t)
+
+			// Should set GetBody
+			assert.Nil(t, req.httpReq.GetBody)
+
+			// Should set CheckRedirect
+			httpClient, _ := req.config.Client.(*http.Client)
+			assert.NotNil(t, httpClient.CheckRedirect)
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, nil))
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 1)))
+			assert.Equal(t,
+				errors.New("stopped after 1 redirects"),
+				httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 2)),
+			)
+
+			// Should do round trip
+			assert.Equal(t, 2, tp.tripCount)
+		})
+
+		t.Run("request has no body and too many redirects", func(t *testing.T) {
+			tp := newMockTransportRedirect(t).
+				WithAssertFn(func(r *http.Request) bool {
+					return assert.Contains(t, []interface{}{nil, http.NoBody}, r.Body)
+				})
+
+			client := &http.Client{
+				Transport: tp,
+			}
+
+			config := Config{
+				Client:   client,
+				Reporter: reporter,
+			}
+
+			req := NewRequest(config, http.MethodPut, "/url").
+				WithRedirectPolicy(FollowRedirectsWithoutBody).
+				WithMaxRedirects(1)
+			req.chain.assertOK(t)
+
+			// Should error
+			resp := req.Expect()
+			resp.chain.assertFailed(t)
+
+			// Should set GetBody
+			assert.Nil(t, req.httpReq.GetBody)
+
+			// Should set CheckRedirect
+			httpClient, _ := req.config.Client.(*http.Client)
+			assert.NotNil(t, httpClient.CheckRedirect)
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, nil))
+			assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 1)))
+			assert.Equal(t,
+				errors.New("stopped after 1 redirects"),
+				httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 2)),
+			)
+
+			// Should do round trip
+			assert.Equal(t, 2, tp.tripCount)
+		})
+
+		t.Run("request has body and redirected with status permanent redirect",
+			func(t *testing.T) {
+				tp := newMockTransportRedirect(t).
+					WithAssertFn(func(r *http.Request) bool {
+						b, err := ioutil.ReadAll(r.Body)
+						assert.NoError(t, err)
+						assert.Equal(t, "test body", string(b))
+
+						return true
+					}).WithMaxRedirect(1)
+
+				client := &http.Client{
+					Transport: tp,
+				}
+
+				config := Config{
+					Client:   client,
+					Reporter: reporter,
+				}
+
+				req := NewRequest(config, http.MethodPut, "/url").
+					WithRedirectPolicy(FollowRedirectsWithoutBody).
+					WithMaxRedirects(1).
+					WithText("test body")
+				req.chain.assertOK(t)
+
+				// Should return redirection response
+				resp := req.Expect().
+					Status(tp.redirectHTTPStatusCode)
+				resp.chain.assertOK(t)
+
+				// Should set GetBody
+				assert.Nil(t, req.httpReq.GetBody)
+
+				// Should set CheckRedirect
+				httpClient, _ := req.config.Client.(*http.Client)
+				assert.NotNil(t, httpClient.CheckRedirect)
+				assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, nil))
+				assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 1)))
+				assert.Equal(t,
+					errors.New("stopped after 1 redirects"),
+					httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 2)),
+				)
+
+				// Should do round trip
+				assert.Equal(t, 1, tp.tripCount)
+			})
+
+		t.Run("request has body and redirected with status moved permanently",
+			func(t *testing.T) {
+				tp := newMockTransportRedirect(t).
+					WithAssertFn(func(r *http.Request) bool {
+						if r.URL.String() == "/url" {
+							assert.Equal(t, r.Method, http.MethodPut)
+
+							b, err := ioutil.ReadAll(r.Body)
+							assert.NoError(t, err)
+							assert.Equal(t, "test body", string(b))
+						} else if r.URL.String() == "/redirect" {
+							assert.Equal(t, r.Method, http.MethodGet)
+							assert.Nil(t, r.Body)
+						} else {
+							t.Fatalf("invalid request URL")
+						}
+
+						return true
+					}).WithRedirectHTTPStatusCode(http.StatusMovedPermanently).WithMaxRedirect(1)
+
+				client := &http.Client{
+					Transport: tp,
+				}
+
+				config := Config{
+					Client:   client,
+					Reporter: reporter,
+				}
+
+				req := NewRequest(config, http.MethodPut, "/url").
+					WithRedirectPolicy(FollowRedirectsWithoutBody).
+					WithMaxRedirects(1).
+					WithText("test body")
+				req.chain.assertOK(t)
+
+				// Should return OK response
+				resp := req.Expect().
+					Status(http.StatusOK)
+				resp.chain.assertOK(t)
+
+				// Should set GetBody
+				assert.Nil(t, req.httpReq.GetBody)
+
+				// Should set CheckRedirect
+				httpClient, _ := req.config.Client.(*http.Client)
+				assert.NotNil(t, httpClient.CheckRedirect)
+				assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, nil))
+				assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 1)))
+				assert.Equal(t,
+					errors.New("stopped after 1 redirects"),
+					httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 2)),
+				)
+
+				// Should do round trip
+				assert.Equal(t, 2, tp.tripCount)
+			})
+
+		t.Run(`request has body 
+		and redirected with status moved permanently 
+		and too many redirects`,
+			func(t *testing.T) {
+				tp := newMockTransportRedirect(t).
+					WithAssertFn(func(r *http.Request) bool {
+						if r.URL.String() == "/url" {
+							assert.Equal(t, r.Method, http.MethodPut)
+
+							b, err := ioutil.ReadAll(r.Body)
+							assert.NoError(t, err)
+							assert.Equal(t, "test body", string(b))
+						} else if r.URL.String() == "/redirect" {
+							assert.Equal(t, r.Method, http.MethodGet)
+							assert.Nil(t, r.Body)
+						} else {
+							t.Fatalf("invalid request URL")
+						}
+
+						return true
+					}).WithRedirectHTTPStatusCode(http.StatusMovedPermanently)
+
+				client := &http.Client{
+					Transport: tp,
+				}
+
+				config := Config{
+					Client:   client,
+					Reporter: reporter,
+				}
+
+				req := NewRequest(config, http.MethodPut, "/url").
+					WithRedirectPolicy(FollowRedirectsWithoutBody).
+					WithMaxRedirects(1).
+					WithText("test body")
+				req.chain.assertOK(t)
+
+				// Should error
+				resp := req.Expect()
+				resp.chain.assertFailed(t)
+
+				// Should set GetBody
+				assert.Nil(t, req.httpReq.GetBody)
+
+				// Should set CheckRedirect
+				httpClient, _ := req.config.Client.(*http.Client)
+				assert.NotNil(t, httpClient.CheckRedirect)
+				assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, nil))
+				assert.Equal(t, nil, httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 1)))
+				assert.Equal(t,
+					errors.New("stopped after 1 redirects"),
+					httpClient.CheckRedirect(req.httpReq, make([]*http.Request, 2)),
+				)
+
+				// Should do round trip
+				assert.Equal(t, 2, tp.tripCount)
+			})
+	})
+}
+
+// mockTransportRedirect mocks a transport that implements RoundTripper
+//
+// When tripCount < maxRedirect,
+// mockTransportRedirect responses with redirectHTTPStatusCode
+//
+// When tripCount = maxRedirect,
+// mockTransportRedirect responses with HTTP 200 OK
+type mockTransportRedirect struct {
+	t *testing.T
+
+	// assertFn asserts the HTTP request
+	assertFn func(*http.Request) bool
+
+	// redirectHTTPStatusCode indicates the HTTP status code of redirection response
+	redirectHTTPStatusCode int
+
+	// tripCount tracks the number of trip that has been done
+	tripCount int
+
+	// maxRedirect indicates the number of trip that can be done for redirection.
+	// -1 means always redirect.
+	maxRedirect int
+}
+
+func newMockTransportRedirect(
+	t *testing.T,
+) *mockTransportRedirect {
+	return &mockTransportRedirect{
+		t:                      t,
+		assertFn:               nil,
+		redirectHTTPStatusCode: http.StatusPermanentRedirect,
+		maxRedirect:            -1,
+	}
+}
+
+func (mt *mockTransportRedirect) RoundTrip(origReq *http.Request) (
+	*http.Response, error,
+) {
+	mt.tripCount++
+
+	if mt.assertFn != nil {
+		assert.True(mt.t, mt.assertFn(origReq))
+	}
+
+	res := httptest.NewRecorder()
+
+	if mt.maxRedirect == -1 || mt.tripCount <= mt.maxRedirect {
+		res.Result().StatusCode = mt.redirectHTTPStatusCode
+		res.Result().Header.Set("Location", "/redirect")
+	} else {
+		res.Result().StatusCode = http.StatusOK
+	}
+
+	return res.Result(), nil
+}
+
+func (mt *mockTransportRedirect) WithAssertFn(
+	fn func(*http.Request) bool,
+) *mockTransportRedirect {
+	mt.assertFn = fn
+
+	return mt
+}
+
+func (mt *mockTransportRedirect) WithRedirectHTTPStatusCode(
+	statusCode int,
+) *mockTransportRedirect {
+	if !(statusCode >= 300 && statusCode < 400) {
+		mt.t.Fatal("invalid redirect status code")
+	}
+
+	mt.redirectHTTPStatusCode = statusCode
+
+	return mt
+}
+
+func (mt *mockTransportRedirect) WithMaxRedirect(
+	maxRedirect int,
+) *mockTransportRedirect {
+	if maxRedirect != -1 && maxRedirect < 0 {
+		mt.t.Fatal("max redirect less than 0")
+	}
+
+	mt.maxRedirect = maxRedirect
+
+	return mt
+}