Repository: gavv/httpexpect (go)

Issue:
Add DefaultFormatter.EnableRequests and EnableResponses
Add two flags `EnableRequests` and `EnableResponses` to `DefaultFormatter`, which enable printing `http.Request` and `http.Response` on failure. New flags should be disabled by default.

Request and response instances are already present in `AssertionContext` passed to formatter. Formatted request and response should be added to `FormatData` and used in failure template. Formatting can be done using `httputil`, like we do it in `DebugPrinter`.

Formatted response should include round trip time (again like in `DebugPrinter`).

Related discussion: https://github.com/gavv/httpexpect/discussions/137

See also #160.

Pull request description:
Add DefaultFormatter.EnableRequests and EnableResponses
Closes #159 

Patch:
diff --git a/formatter.go b/formatter.go
index 3e0fe46..ebb0b0e 100644
--- a/formatter.go
+++ b/formatter.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"math"
+	"net/http/httputil"
 	"os"
 	"regexp"
 	"strconv"
@@ -56,6 +57,12 @@ type DefaultFormatter struct {
 	// Exclude diff from failure report.
 	DisableDiffs bool
 
+	// Enable printing of request on failure.
+	EnableRequests bool
+
+	// Enable printing of response on failure.
+	EnableResponses bool
+
 	// Thousand separator.
 	// Default is DigitSeparatorUnderscore.
 	DigitSeparator DigitSeparator
@@ -189,6 +196,9 @@ type FormatData struct {
 	ExpectedKind string
 	Expected     []string
 
+	Request  string
+	Response string
+
 	HaveReference bool
 	Reference     string
 
@@ -252,6 +262,9 @@ func (f *DefaultFormatter) buildFormatData(
 		data.AssertType = failure.Type.String()
 		data.AssertSeverity = failure.Severity.String()
 
+		f.fillRequest(&data, ctx, failure)
+		f.fillResponse(&data, ctx, failure)
+
 		f.fillErrors(&data, ctx, failure)
 
 		if failure.Actual != nil {
@@ -502,6 +515,34 @@ func (f *DefaultFormatter) fillDelta(
 	data.Delta = f.formatValue(failure.Delta.Value)
 }
 
+func (f *DefaultFormatter) fillRequest(
+	data *FormatData, ctx *AssertionContext, failure *AssertionFailure,
+) {
+	if f.EnableRequests && !refIsNil(ctx.Request) {
+		dump, err := httputil.DumpRequest(ctx.Request.httpReq, false)
+		if err != nil {
+			panic(err)
+		}
+		data.Request = string(dump)
+	}
+}
+
+func (f *DefaultFormatter) fillResponse(
+	data *FormatData, ctx *AssertionContext, failure *AssertionFailure,
+) {
+	if f.EnableResponses && !refIsNil(ctx.Request) {
+		dump, err := httputil.DumpResponse(ctx.Response.httpResp, false)
+		if err != nil {
+			panic(err)
+		}
+
+		text := strings.Replace(string(dump), "\r\n", "\n", -1)
+		lines := strings.SplitN(text, "\n", 2)
+
+		data.Response = fmt.Sprintf("%s %s\n%s", lines[0], ctx.Response.rtt, lines[1])
+	}
+}
+
 func (f *DefaultFormatter) formatValue(value interface{}) string {
 	if flt := extractFloat32(value); flt != nil {
 		return f.reformatNumber(f.formatFloatValue(*flt, 32))
@@ -927,6 +968,14 @@ test name: {{ .TestName | color $.EnableColors "Cyan" }}
 
 request name: {{ .RequestName | color $.EnableColors "Cyan" }}
 {{- end -}}
+{{- if .Request }}
+
+request: {{ .Request | indent | color $.EnableColors "HiMagenta" }}
+{{- end -}}
+{{- if .Response }}
+
+response: {{ .Response | indent | color $.EnableColors "HiMagenta" }}
+{{- end -}}
 {{- if .AssertPath }}
 
 assertion:


Test patch:
diff --git a/e2e_report_test.go b/e2e_report_test.go
index b5a2004..f12db95 100644
--- a/e2e_report_test.go
+++ b/e2e_report_test.go
@@ -311,3 +311,102 @@ func TestE2EReport_CustomTemplate(t *testing.T) {
 		})
 	})
 }
+
+func TestE2EReport_RequestAndResponse(t *testing.T) {
+	mux := http.NewServeMux()
+
+	mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte(`{"foo":{"bar":{"baz":[1,2,3]}}}`))
+	})
+
+	server := httptest.NewServer(mux)
+	defer server.Close()
+
+	tests := []struct {
+		name      string
+		formatter *DefaultFormatter
+	}{
+		{
+			name: "request and response enabled",
+			formatter: &DefaultFormatter{
+				EnableRequests:  true,
+				EnableResponses: true,
+			},
+		},
+		{
+			name: "request enabled, response disabled",
+			formatter: &DefaultFormatter{
+				EnableRequests:  true,
+				EnableResponses: false,
+			},
+		},
+		{
+			name: "request disabled, response enabled",
+			formatter: &DefaultFormatter{
+				EnableRequests:  false,
+				EnableResponses: true,
+			},
+		},
+		{
+			name: "request and response disabled",
+			formatter: &DefaultFormatter{
+				EnableRequests:  false,
+				EnableResponses: false,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			rep := &recordingReporter{}
+
+			e := WithConfig(Config{
+				TestName: "TestExample",
+				BaseURL:  server.URL,
+				AssertionHandler: &DefaultAssertionHandler{
+					Formatter: tt.formatter,
+					Reporter:  rep,
+				},
+			})
+
+			e.GET("/test").
+				Expect().
+				JSON().
+				Object().
+				Value("foo").
+				Object().
+				Value("bar").
+				Object().
+				Value("baz").
+				Array().
+				NotContains(1)
+
+			logs := rep.recorded
+
+			if tt.formatter.EnableRequests {
+				assert.Contains(t,
+					logs,
+					"GET /test HTTP/1.1",
+					"expected log output to contain request information")
+			} else {
+				assert.NotContains(t,
+					logs,
+					"GET /test HTTP/1.1",
+					"expected log output not to contain request information")
+			}
+
+			if tt.formatter.EnableResponses {
+				assert.Contains(t,
+					logs,
+					"HTTP/1.1 200 OK", "expected log output to contain response information")
+			} else {
+				assert.NotContains(t,
+					logs,
+					"HTTP/1.1 200 OK",
+					"expected log output not to contain response information")
+			}
+		})
+	}
+}