Repository: labstack/echo
Pull request #1279 (2019-02-13 20:38:00)

Increase Context tests coverage
Added some tests to increase coverage for Context.

Each tested case was added as a different commit for traceability and to easily revert it if for any reason is considered unnecessary.


Patch:
diff --git a/echo.go b/echo.go
index 6a41e56..4f8209b 100644
--- a/echo.go
+++ b/echo.go
@@ -43,6 +43,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/ioutil"
 	stdLog "log"
 	"net"
 	"net/http"
@@ -269,6 +270,7 @@ var (
 	ErrRendererNotRegistered       = errors.New("renderer not registered")
 	ErrInvalidRedirectCode         = errors.New("invalid redirect status code")
 	ErrCookieNotFound              = errors.New("cookie not found")
+	ErrInvalidCertOrKeyType        = errors.New("invalid cert or key type, must be string or []byte")
 )
 
 // Error handlers
@@ -605,20 +607,40 @@ func (e *Echo) Start(address string) error {
 }
 
 // StartTLS starts an HTTPS server.
-func (e *Echo) StartTLS(address string, certFile, keyFile string) (err error) {
-	if certFile == "" || keyFile == "" {
-		return errors.New("invalid tls configuration")
+// If `certFile` or `keyFile` is `string` the values are treated as file paths.
+// If `certFile` or `keyFile` is `[]byte` the values are treated as the certificate or key as-is.
+func (e *Echo) StartTLS(address string, certFile, keyFile interface{}) (err error) {
+	var cert []byte
+	if cert, err = filepathOrContent(certFile); err != nil {
+		return
 	}
+
+	var key []byte
+	if key, err = filepathOrContent(keyFile); err != nil {
+		return
+	}
+
 	s := e.TLSServer
 	s.TLSConfig = new(tls.Config)
 	s.TLSConfig.Certificates = make([]tls.Certificate, 1)
-	s.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
-	if err != nil {
+	if s.TLSConfig.Certificates[0], err = tls.X509KeyPair(cert, key); err != nil {
 		return
 	}
+
 	return e.startTLS(address)
 }
 
+func filepathOrContent(fileOrContent interface{}) (content []byte, err error) {
+	switch v := fileOrContent.(type) {
+	case string:
+		return ioutil.ReadFile(v)
+	case []byte:
+		return v, nil
+	default:
+		return nil, ErrInvalidCertOrKeyType
+	}
+}
+
 // StartAutoTLS starts an HTTPS server using certificates automatically installed from https://letsencrypt.org.
 func (e *Echo) StartAutoTLS(address string) error {
 	s := e.TLSServer
diff --git a/middleware/redirect.go b/middleware/redirect.go
index 30a2e40..813e5b8 100644
--- a/middleware/redirect.go
+++ b/middleware/redirect.go
@@ -21,7 +21,7 @@ type RedirectConfig struct {
 // 2) return the appropriate redirect url.
 type redirectLogic func(scheme, host, uri string) (ok bool, url string)
 
-const www = "www"
+const www = "www."
 
 // DefaultRedirectConfig is the default Redirect middleware config.
 var DefaultRedirectConfig = RedirectConfig{
@@ -60,7 +60,7 @@ func HTTPSWWWRedirect() echo.MiddlewareFunc {
 // See `HTTPSWWWRedirect()`.
 func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
 	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
-		if ok = scheme != "https" && host[:3] != www; ok {
+		if ok = scheme != "https" && host[:4] != www; ok {
 			url = "https://www." + host + uri
 		}
 		return
@@ -80,7 +80,7 @@ func HTTPSNonWWWRedirect() echo.MiddlewareFunc {
 func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
 	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
 		if ok = scheme != "https"; ok {
-			if host[:3] == www {
+			if host[:4] == www {
 				host = host[4:]
 			}
 			url = "https://" + host + uri
@@ -101,7 +101,7 @@ func WWWRedirect() echo.MiddlewareFunc {
 // See `WWWRedirect()`.
 func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
 	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
-		if ok = host[:3] != www; ok {
+		if ok = host[:4] != www; ok {
 			url = scheme + "://www." + host + uri
 		}
 		return
@@ -120,7 +120,7 @@ func NonWWWRedirect() echo.MiddlewareFunc {
 // See `NonWWWRedirect()`.
 func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
 	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
-		if ok = host[:3] == www; ok {
+		if ok = host[:4] == www; ok {
 			url = scheme + "://" + host[4:] + uri
 		}
 		return


Test patch:
diff --git a/context_test.go b/context_test.go
index 6118866..6caa270 100644
--- a/context_test.go
+++ b/context_test.go
@@ -2,9 +2,11 @@ package echo
 
 import (
 	"bytes"
+	"crypto/tls"
 	"encoding/json"
 	"encoding/xml"
 	"errors"
+	"fmt"
 	"io"
 	"mime/multipart"
 	"net/http"
@@ -72,6 +74,21 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, c Context)
 	return t.templates.ExecuteTemplate(w, name, data)
 }
 
+type responseWriterErr struct {
+}
+
+func (responseWriterErr) Header() http.Header {
+	return http.Header{}
+}
+
+func (responseWriterErr) Write([]byte) (int, error) {
+	return 0, errors.New("err")
+}
+
+func (responseWriterErr) WriteHeader(statusCode int) {
+
+}
+
 func TestContext(t *testing.T) {
 	e := New()
 	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
@@ -184,6 +201,12 @@ func TestContext(t *testing.T) {
 	err = c.XML(http.StatusOK, make(chan bool))
 	assert.Error(err)
 
+	// XML response write error
+	c = e.NewContext(req, rec).(*context)
+	c.response.Writer = responseWriterErr{}
+	err = c.XML(0, 0)
+	testify.Error(t, err)
+
 	// XMLPretty
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec).(*context)
@@ -431,6 +454,7 @@ func TestContextPathParam(t *testing.T) {
 
 	// Param
 	testify.Equal(t, "501", c.Param("fid"))
+	testify.Equal(t, "", c.Param("undefined"))
 }
 
 func TestContextFormValue(t *testing.T) {
@@ -455,6 +479,14 @@ func TestContextFormValue(t *testing.T) {
 			"email": []string{"jon@labstack.com"},
 		}, params)
 	}
+
+	// Multipart FormParams error
+	req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode()))
+	req.Header.Add(HeaderContentType, MIMEMultipartForm)
+	c = e.NewContext(req, nil)
+	params, err = c.FormParams()
+	testify.Nil(t, params)
+	testify.Error(t, err)
 }
 
 func TestContextQueryParam(t *testing.T) {
@@ -555,6 +587,232 @@ func TestContextHandler(t *testing.T) {
 	})
 	c := e.NewContext(nil, nil)
 	r.Find(http.MethodGet, "/handler", c)
-	c.Handler()(c)
+	err := c.Handler()(c)
 	testify.Equal(t, "handler", b.String())
+	testify.NoError(t, err)
+}
+
+func TestContext_SetHandler(t *testing.T) {
+	var c Context
+	c = new(context)
+
+	testify.Nil(t, c.Handler())
+
+	c.SetHandler(func(c Context) error {
+		return nil
+	})
+	testify.NotNil(t, c.Handler())
+}
+
+func TestContext_Path(t *testing.T) {
+	path := "/pa/th"
+
+	var c Context
+	c = new(context)
+
+	c.SetPath(path)
+	testify.Equal(t, path, c.Path())
+}
+
+type validator struct{}
+
+func (*validator) Validate(i interface{}) error {
+	return nil
+}
+
+func TestContext_Validate(t *testing.T) {
+	e := New()
+	c := e.NewContext(nil, nil)
+
+	testify.Error(t, c.Validate(struct{}{}))
+
+	e.Validator = &validator{}
+	testify.NoError(t, c.Validate(struct{}{}))
+}
+
+func TestContext_QueryString(t *testing.T) {
+	e := New()
+
+	queryString := "query=string&var=val"
+
+	req := httptest.NewRequest(GET, "/?"+queryString, nil)
+	c := e.NewContext(req, nil)
+
+	testify.Equal(t, queryString, c.QueryString())
+}
+
+func TestContext_Request(t *testing.T) {
+	var c Context
+	c = new(context)
+
+	testify.Nil(t, c.Request())
+
+	req := httptest.NewRequest(GET, "/path", nil)
+	c.SetRequest(req)
+
+	testify.Equal(t, req, c.Request())
+}
+
+func TestContext_Scheme(t *testing.T) {
+	tests := []struct {
+		c Context
+		s string
+	}{
+		{
+			&context{
+				request: &http.Request{
+					TLS: &tls.ConnectionState{},
+				},
+			},
+			"https",
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedProto: []string{"https"}},
+				},
+			},
+			"https",
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedProtocol: []string{"http"}},
+				},
+			},
+			"http",
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedSsl: []string{"on"}},
+				},
+			},
+			"https",
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXUrlScheme: []string{"https"}},
+				},
+			},
+			"https",
+		},
+		{
+			&context{
+				request: &http.Request{},
+			},
+			"http",
+		},
+	}
+
+	for _, tt := range tests {
+		testify.Equal(t, tt.s, tt.c.Scheme())
+	}
+}
+
+func TestContext_IsWebSocket(t *testing.T) {
+	tests := []struct {
+		c  Context
+		ws testify.BoolAssertionFunc
+	}{
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderUpgrade: []string{"websocket"}},
+				},
+			},
+			testify.True,
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderUpgrade: []string{"Websocket"}},
+				},
+			},
+			testify.True,
+		},
+		{
+			&context{
+				request: &http.Request{},
+			},
+			testify.False,
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderUpgrade: []string{"other"}},
+				},
+			},
+			testify.False,
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("test %d", i+1), func(t *testing.T) {
+			tt.ws(t, tt.c.IsWebSocket())
+		})
+	}
+}
+
+func TestContext_Bind(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(POST, "/", strings.NewReader(userJSON))
+	c := e.NewContext(req, nil)
+
+	var u *user
+
+	err := c.Bind(u)
+	testify.Error(t, err)
+	testify.Nil(t, u)
+
+	req.Header.Add(HeaderContentType, MIMEApplicationJSON)
+	err = c.Bind(&u)
+	testify.NoError(t, err)
+	testify.Equal(t, &user{1, "Jon Snow"}, u)
+}
+
+func TestContext_Logger(t *testing.T) {
+	e := New()
+	c := e.NewContext(nil, nil)
+
+	testify.NotNil(t, c.Logger())
+}
+
+func TestContext_RealIP(t *testing.T) {
+	tests := []struct {
+		c Context
+		s string
+	}{
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedFor: []string{"127.0.0.1, 127.0.1.1, "}},
+				},
+			},
+			"127.0.0.1",
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{
+						"X-Real-Ip": []string{"192.168.0.1"},
+					},
+				},
+			},
+			"192.168.0.1",
+		},
+		{
+			&context{
+				request: &http.Request{
+					RemoteAddr: "89.89.89.89:1654",
+				},
+			},
+			"89.89.89.89",
+		},
+	}
+
+	for _, tt := range tests {
+		testify.Equal(t, tt.s, tt.c.RealIP())
+	}
 }
diff --git a/echo_test.go b/echo_test.go
index 3ac10a7..dec713e 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	stdContext "context"
 	"errors"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
@@ -12,6 +13,7 @@ import (
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 type (
@@ -428,6 +430,71 @@ func TestEchoStartTLS(t *testing.T) {
 	e.Close()
 }
 
+func TestEchoStartTLSByteString(t *testing.T) {
+	cert, err := ioutil.ReadFile("_fixture/certs/cert.pem")
+	require.NoError(t, err)
+	key, err := ioutil.ReadFile("_fixture/certs/key.pem")
+	require.NoError(t, err)
+
+	testCases := []struct {
+		cert        interface{}
+		key         interface{}
+		expectedErr error
+		name        string
+	}{
+		{
+			cert:        "_fixture/certs/cert.pem",
+			key:         "_fixture/certs/key.pem",
+			expectedErr: nil,
+			name:        `ValidCertAndKeyFilePath`,
+		},
+		{
+			cert:        cert,
+			key:         key,
+			expectedErr: nil,
+			name:        `ValidCertAndKeyByteString`,
+		},
+		{
+			cert:        cert,
+			key:         1,
+			expectedErr: ErrInvalidCertOrKeyType,
+			name:        `InvalidKeyType`,
+		},
+		{
+			cert:        0,
+			key:         key,
+			expectedErr: ErrInvalidCertOrKeyType,
+			name:        `InvalidCertType`,
+		},
+		{
+			cert:        0,
+			key:         1,
+			expectedErr: ErrInvalidCertOrKeyType,
+			name:        `InvalidCertAndKeyTypes`,
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.name, func(t *testing.T) {
+			e := New()
+			e.HideBanner = true
+
+			go func() {
+				err := e.StartTLS(":0", test.cert, test.key)
+				if test.expectedErr != nil {
+					require.EqualError(t, err, test.expectedErr.Error())
+				} else if err != http.ErrServerClosed { // Prevent the test to fail after closing the servers
+					require.NoError(t, err)
+				}
+			}()
+			time.Sleep(200 * time.Millisecond)
+
+			require.NoError(t, e.Close())
+		})
+	}
+}
+
 func TestEchoStartAutoTLS(t *testing.T) {
 	e := New()
 	errChan := make(chan error, 0)