1
0
mirror of https://github.com/labstack/echo.git synced 2024-12-22 20:06:21 +02:00
echo/echo_test.go

1140 lines
30 KiB
Go
Raw Normal View History

package echo
import (
"bytes"
2018-04-03 13:09:21 +02:00
stdContext "context"
"errors"
"fmt"
2021-07-15 22:34:01 +02:00
"io/fs"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
2021-07-15 22:34:01 +02:00
type user struct {
ID int `json:"id" xml:"id" form:"id" query:"id" param:"id" header:"id"`
Name string `json:"name" xml:"name" form:"name" query:"name" param:"name" header:"name"`
}
const (
userJSON = `{"id":1,"name":"Jon Snow"}`
usersJSON = `[{"id":1,"name":"Jon Snow"}]`
userXML = `<user><id>1</id><name>Jon Snow</name></user>`
userForm = `id=1&name=Jon Snow`
invalidContent = "invalid content"
userJSONInvalidType = `{"id":"1","name":"Jon Snow"}`
userXMLConvertNumberError = `<user><id>Number one</id><name>Jon Snow</name></user>`
userXMLUnsupportedTypeError = `<user><>Number one</><name>Jon Snow</name></user>`
)
const userJSONPretty = `{
"id": 1,
"name": "Jon Snow"
}`
const userXMLPretty = `<user>
<id>1</id>
<name>Jon Snow</name>
</user>`
var dummyQuery = url.Values{"dummy": []string{"useless"}}
func TestEcho(t *testing.T) {
e := New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Router
assert.NotNil(t, e.Router())
2021-07-15 22:34:01 +02:00
e.HTTPErrorHandler(c, errors.New("error"))
assert.Equal(t, http.StatusInternalServerError, rec.Code)
}
2021-07-15 22:34:01 +02:00
func TestEcho_StaticFS(t *testing.T) {
2020-12-13 15:49:11 +02:00
var testCases = []struct {
name string
givenPrefix string
2021-07-15 22:34:01 +02:00
givenFs fs.FS
givenFsRoot string
2020-12-13 15:49:11 +02:00
whenURL string
expectStatus int
expectHeaderLocation string
expectBodyStartsWith string
}{
{
name: "ok",
givenPrefix: "/images",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("./_fixture/images"),
2020-12-13 15:49:11 +02:00
whenURL: "/images/walle.png",
expectStatus: http.StatusOK,
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
},
{
2021-07-15 22:34:01 +02:00
name: "ok, from sub fs",
givenPrefix: "/images",
2021-07-15 22:34:01 +02:00
givenFs: MustSubFS(os.DirFS("./_fixture/"), "images"),
whenURL: "/images/walle.png",
expectStatus: http.StatusOK,
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
},
2020-12-13 15:49:11 +02:00
{
name: "No file",
givenPrefix: "/images",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture/scripts"),
2020-12-13 15:49:11 +02:00
whenURL: "/images/bolt.png",
expectStatus: http.StatusNotFound,
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
},
{
name: "Directory",
givenPrefix: "/images",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture/images"),
2020-12-13 15:49:11 +02:00
whenURL: "/images/",
expectStatus: http.StatusNotFound,
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
},
{
name: "Directory Redirect",
givenPrefix: "/",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture/"),
2020-12-13 15:49:11 +02:00
whenURL: "/folder",
expectStatus: http.StatusMovedPermanently,
2020-12-16 03:37:26 +02:00
expectHeaderLocation: "/folder/",
expectBodyStartsWith: "",
},
{
name: "Directory Redirect with non-root path",
givenPrefix: "/static",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture"),
whenURL: "/static",
expectStatus: http.StatusMovedPermanently,
expectHeaderLocation: "/static/",
expectBodyStartsWith: "",
},
{
name: "Prefixed directory 404 (request URL without slash)",
givenPrefix: "/folder/", // trailing slash will intentionally not match "/folder"
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture"),
whenURL: "/folder", // no trailing slash
expectStatus: http.StatusNotFound,
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
},
{
name: "Prefixed directory redirect (without slash redirect to slash)",
givenPrefix: "/folder", // no trailing slash shall match /folder and /folder/*
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture"),
whenURL: "/folder", // no trailing slash
expectStatus: http.StatusMovedPermanently,
expectHeaderLocation: "/folder/",
expectBodyStartsWith: "",
},
2020-12-13 15:49:11 +02:00
{
name: "Directory with index.html",
givenPrefix: "/",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture"),
2020-12-13 15:49:11 +02:00
whenURL: "/",
expectStatus: http.StatusOK,
expectBodyStartsWith: "<!doctype html>",
},
{
name: "Prefixed directory with index.html (prefix ending with slash)",
givenPrefix: "/assets/",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture"),
whenURL: "/assets/",
expectStatus: http.StatusOK,
expectBodyStartsWith: "<!doctype html>",
},
{
name: "Prefixed directory with index.html (prefix ending without slash)",
givenPrefix: "/assets",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture"),
whenURL: "/assets/",
expectStatus: http.StatusOK,
expectBodyStartsWith: "<!doctype html>",
},
2020-12-13 15:49:11 +02:00
{
name: "Sub-directory with index.html",
givenPrefix: "/",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture"),
2020-12-13 15:49:11 +02:00
whenURL: "/folder/",
expectStatus: http.StatusOK,
expectBodyStartsWith: "<!doctype html>",
},
{
name: "do not allow directory traversal (backslash - windows separator)",
givenPrefix: "/",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture/"),
2020-12-13 15:49:11 +02:00
whenURL: `/..\\middleware/basic_auth.go`,
expectStatus: http.StatusNotFound,
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
},
{
name: "do not allow directory traversal (slash - unix separator)",
givenPrefix: "/",
2021-07-15 22:34:01 +02:00
givenFs: os.DirFS("_fixture/"),
2020-12-13 15:49:11 +02:00
whenURL: `/../middleware/basic_auth.go`,
expectStatus: http.StatusNotFound,
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
},
}
2020-12-13 15:49:11 +02:00
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
tmpFs := tc.givenFs
if tc.givenFsRoot != "" {
tmpFs = MustSubFS(tmpFs, tc.givenFsRoot)
}
e.StaticFS(tc.givenPrefix, tmpFs)
2020-12-13 15:49:11 +02:00
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
rec := httptest.NewRecorder()
2021-07-15 22:34:01 +02:00
2020-12-13 15:49:11 +02:00
e.ServeHTTP(rec, req)
2021-07-15 22:34:01 +02:00
2020-12-13 15:49:11 +02:00
assert.Equal(t, tc.expectStatus, rec.Code)
body := rec.Body.String()
if tc.expectBodyStartsWith != "" {
assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith))
} else {
assert.Equal(t, "", body)
}
2020-12-13 15:49:11 +02:00
if tc.expectHeaderLocation != "" {
assert.Equal(t, tc.expectHeaderLocation, rec.Result().Header["Location"][0])
} else {
_, ok := rec.Result().Header["Location"]
assert.False(t, ok)
}
})
}
}
2021-07-15 22:34:01 +02:00
func TestEcho_FileFS(t *testing.T) {
var testCases = []struct {
name string
whenPath string
whenFile string
whenFS fs.FS
givenURL string
expectCode int
expectStartsWith []byte
}{
{
name: "ok",
whenPath: "/walle",
whenFS: os.DirFS("_fixture/images"),
whenFile: "walle.png",
givenURL: "/walle",
expectCode: http.StatusOK,
expectStartsWith: []byte{0x89, 0x50, 0x4e},
},
{
name: "nok, requesting invalid path",
whenPath: "/walle",
whenFS: os.DirFS("_fixture/images"),
whenFile: "walle.png",
givenURL: "/walle.png",
expectCode: http.StatusNotFound,
expectStartsWith: []byte(`{"message":"Not Found"}`),
},
{
name: "nok, serving not existent file from filesystem",
whenPath: "/walle",
whenFS: os.DirFS("_fixture/images"),
whenFile: "not-existent.png",
givenURL: "/walle",
expectCode: http.StatusNotFound,
expectStartsWith: []byte(`{"message":"Not Found"}`),
},
}
2021-07-15 22:34:01 +02:00
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New()
e.FileFS(tc.whenPath, tc.whenFile, tc.whenFS)
2021-07-15 22:34:01 +02:00
req := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)
rec := httptest.NewRecorder()
2021-07-15 22:34:01 +02:00
e.ServeHTTP(rec, req)
2021-07-15 22:34:01 +02:00
assert.Equal(t, tc.expectCode, rec.Code)
2021-07-15 22:34:01 +02:00
body := rec.Body.Bytes()
if len(body) > len(tc.expectStartsWith) {
body = body[:len(tc.expectStartsWith)]
}
assert.Equal(t, tc.expectStartsWith, body)
})
}
}
2021-07-15 22:34:01 +02:00
func TestEcho_StaticPanic(t *testing.T) {
var testCases = []struct {
name string
givenRoot string
expectError string
}{
{
name: "panics for ../",
givenRoot: "../assets",
expectError: "can not create sub FS, invalid root given, err: sub ../assets: invalid name",
},
{
name: "panics for /",
givenRoot: "/assets",
expectError: "can not create sub FS, invalid root given, err: sub /assets: invalid name",
},
}
2021-07-15 22:34:01 +02:00
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New()
e.Filesystem = os.DirFS("./")
assert.PanicsWithError(t, tc.expectError, func() {
e.Static("../assets", tc.givenRoot)
})
})
}
2021-07-15 22:34:01 +02:00
}
2021-07-15 22:34:01 +02:00
func TestEchoStaticRedirectIndex(t *testing.T) {
e := New()
// HandlerFunc
ri := e.Static("/static", "_fixture")
assert.Equal(t, http.MethodGet, ri.Method())
assert.Equal(t, "/static*", ri.Path())
assert.Equal(t, "GET:/static*", ri.Name())
assert.Equal(t, []string{"*"}, ri.Params())
ctx, cancel := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)
defer cancel()
addr, err := startOnRandomPort(ctx, e)
if err != nil {
assert.Fail(t, err.Error())
}
2021-07-15 22:34:01 +02:00
code, body, err := doGet(fmt.Sprintf("http://%v/static", addr))
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(body, "<!doctype html>"))
assert.Equal(t, http.StatusOK, code)
}
func TestEchoFile(t *testing.T) {
var testCases = []struct {
name string
givenPath string
givenFile string
whenPath string
expectCode int
expectStartsWith string
}{
{
name: "ok",
givenPath: "/walle",
givenFile: "_fixture/images/walle.png",
whenPath: "/walle",
expectCode: http.StatusOK,
expectStartsWith: string([]byte{0x89, 0x50, 0x4e}),
},
{
name: "ok with relative path",
givenPath: "/",
givenFile: "./go.mod",
whenPath: "/",
expectCode: http.StatusOK,
expectStartsWith: "module github.com/labstack/echo/v",
},
{
name: "nok file does not exist",
givenPath: "/",
givenFile: "./this-file-does-not-exist",
whenPath: "/",
expectCode: http.StatusNotFound,
expectStartsWith: "{\"message\":\"Not Found\"}\n",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New() // we are using echo.defaultFS instance
e.File(tc.givenPath, tc.givenFile)
c, b := request(http.MethodGet, tc.whenPath, e)
assert.Equal(t, tc.expectCode, c)
if len(b) > len(tc.expectStartsWith) {
b = b[:len(tc.expectStartsWith)]
}
assert.Equal(t, tc.expectStartsWith, b)
})
}
}
func TestEchoMiddleware(t *testing.T) {
e := New()
buf := new(bytes.Buffer)
e.Pre(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
2021-07-15 22:34:01 +02:00
// before route match is found RouteInfo does not exist
assert.Equal(t, nil, c.RouteInfo())
buf.WriteString("-1")
return next(c)
}
})
e.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("1")
return next(c)
}
})
e.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("2")
return next(c)
}
})
e.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("3")
return next(c)
}
})
// Route
e.GET("/", func(c Context) error {
return c.String(http.StatusOK, "OK")
})
c, b := request(http.MethodGet, "/", e)
assert.Equal(t, "-1123", buf.String())
assert.Equal(t, http.StatusOK, c)
assert.Equal(t, "OK", b)
}
func TestEchoMiddlewareError(t *testing.T) {
e := New()
e.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
return errors.New("error")
}
})
2021-07-15 22:34:01 +02:00
e.GET("/", notFoundHandler)
c, _ := request(http.MethodGet, "/", e)
assert.Equal(t, http.StatusInternalServerError, c)
}
func TestEchoHandler(t *testing.T) {
e := New()
// HandlerFunc
e.GET("/ok", func(c Context) error {
return c.String(http.StatusOK, "OK")
})
c, b := request(http.MethodGet, "/ok", e)
assert.Equal(t, http.StatusOK, c)
assert.Equal(t, "OK", b)
}
func TestEchoWrapHandler(t *testing.T) {
e := New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
h := WrapHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("test"))
}))
if assert.NoError(t, h(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "test", rec.Body.String())
}
}
func TestEchoWrapMiddleware(t *testing.T) {
e := New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
buf := new(bytes.Buffer)
mw := WrapMiddleware(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf.Write([]byte("mw"))
h.ServeHTTP(w, r)
})
})
h := mw(func(c Context) error {
return c.String(http.StatusOK, "OK")
})
if assert.NoError(t, h(c)) {
assert.Equal(t, "mw", buf.String())
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "OK", rec.Body.String())
}
}
2021-07-15 22:34:01 +02:00
func TestEchoGet_routeInfoIsImmutable(t *testing.T) {
e := New()
ri := e.GET("/test", handlerFunc)
assert.Equal(t, "GET:/test", ri.Name())
riFromRouter, err := e.Router().Routes().FindByMethodPath(http.MethodGet, "/test")
assert.NoError(t, err)
assert.Equal(t, "GET:/test", riFromRouter.Name())
rInfo := ri.(routeInfo)
rInfo.name = "changed" // this change should not change other returned values
assert.Equal(t, "GET:/test", ri.Name())
riFromRouter, err = e.Router().Routes().FindByMethodPath(http.MethodGet, "/test")
assert.NoError(t, err)
assert.Equal(t, "GET:/test", riFromRouter.Name())
}
func TestEchoConnect(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
ri := e.CONNECT("/", func(c Context) error {
return c.String(http.StatusTeapot, "OK")
})
assert.Equal(t, http.MethodConnect, ri.Method())
assert.Equal(t, "/", ri.Path())
assert.Equal(t, http.MethodConnect+":/", ri.Name())
assert.Nil(t, ri.Params())
status, body := request(http.MethodConnect, "/", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "OK", body)
}
func TestEchoDelete(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
ri := e.DELETE("/", func(c Context) error {
return c.String(http.StatusTeapot, "OK")
})
assert.Equal(t, http.MethodDelete, ri.Method())
assert.Equal(t, "/", ri.Path())
assert.Equal(t, http.MethodDelete+":/", ri.Name())
assert.Nil(t, ri.Params())
status, body := request(http.MethodDelete, "/", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "OK", body)
}
func TestEchoGet(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
ri := e.GET("/", func(c Context) error {
return c.String(http.StatusTeapot, "OK")
})
assert.Equal(t, http.MethodGet, ri.Method())
assert.Equal(t, "/", ri.Path())
assert.Equal(t, http.MethodGet+":/", ri.Name())
assert.Nil(t, ri.Params())
status, body := request(http.MethodGet, "/", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "OK", body)
}
func TestEchoHead(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
ri := e.HEAD("/", func(c Context) error {
return c.String(http.StatusTeapot, "OK")
})
assert.Equal(t, http.MethodHead, ri.Method())
assert.Equal(t, "/", ri.Path())
assert.Equal(t, http.MethodHead+":/", ri.Name())
assert.Nil(t, ri.Params())
status, body := request(http.MethodHead, "/", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "OK", body)
}
func TestEchoOptions(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
ri := e.OPTIONS("/", func(c Context) error {
return c.String(http.StatusTeapot, "OK")
})
assert.Equal(t, http.MethodOptions, ri.Method())
assert.Equal(t, "/", ri.Path())
assert.Equal(t, http.MethodOptions+":/", ri.Name())
assert.Nil(t, ri.Params())
status, body := request(http.MethodOptions, "/", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "OK", body)
}
func TestEchoPatch(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
ri := e.PATCH("/", func(c Context) error {
return c.String(http.StatusTeapot, "OK")
})
assert.Equal(t, http.MethodPatch, ri.Method())
assert.Equal(t, "/", ri.Path())
assert.Equal(t, http.MethodPatch+":/", ri.Name())
assert.Nil(t, ri.Params())
status, body := request(http.MethodPatch, "/", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "OK", body)
}
func TestEchoPost(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
ri := e.POST("/", func(c Context) error {
return c.String(http.StatusTeapot, "OK")
})
assert.Equal(t, http.MethodPost, ri.Method())
assert.Equal(t, "/", ri.Path())
assert.Equal(t, http.MethodPost+":/", ri.Name())
assert.Nil(t, ri.Params())
status, body := request(http.MethodPost, "/", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "OK", body)
}
func TestEchoPut(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
ri := e.PUT("/", func(c Context) error {
return c.String(http.StatusTeapot, "OK")
})
assert.Equal(t, http.MethodPut, ri.Method())
assert.Equal(t, "/", ri.Path())
assert.Equal(t, http.MethodPut+":/", ri.Name())
assert.Nil(t, ri.Params())
status, body := request(http.MethodPut, "/", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "OK", body)
}
func TestEchoTrace(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
ri := e.TRACE("/", func(c Context) error {
return c.String(http.StatusTeapot, "OK")
})
assert.Equal(t, http.MethodTrace, ri.Method())
assert.Equal(t, "/", ri.Path())
assert.Equal(t, http.MethodTrace+":/", ri.Name())
assert.Nil(t, ri.Params())
status, body := request(http.MethodTrace, "/", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "OK", body)
}
func TestEchoAny(t *testing.T) { // JFC
e := New()
2021-07-15 22:34:01 +02:00
ris := e.Any("/", func(c Context) error {
return c.String(http.StatusOK, "Any")
})
2021-07-15 22:34:01 +02:00
assert.Len(t, ris, 11)
}
func TestEchoMatch(t *testing.T) { // JFC
e := New()
2021-07-15 22:34:01 +02:00
ris := e.Match([]string{http.MethodGet, http.MethodPost}, "/", func(c Context) error {
return c.String(http.StatusOK, "Match")
})
2021-07-15 22:34:01 +02:00
assert.Len(t, ris, 2)
}
2021-07-15 22:34:01 +02:00
func TestEcho_Routers_HandleHostsProperly(t *testing.T) {
2021-06-25 22:56:07 +02:00
e := New()
h := e.Host("route.com")
routes := []*Route{
2021-07-15 22:34:01 +02:00
{Method: http.MethodGet, Path: "/users/:user/events"},
{Method: http.MethodGet, Path: "/users/:user/events/public"},
{Method: http.MethodPost, Path: "/repos/:owner/:repo/git/refs"},
{Method: http.MethodPost, Path: "/repos/:owner/:repo/git/tags"},
2021-06-25 22:56:07 +02:00
}
for _, r := range routes {
h.Add(r.Method, r.Path, func(c Context) error {
return c.String(http.StatusOK, "OK")
})
}
2021-07-15 22:34:01 +02:00
routers := e.Routers()
routeCom, ok := routers["route.com"]
assert.True(t, ok)
if assert.Equal(t, len(routes), len(routeCom.Routes())) {
for _, r := range routeCom.Routes() {
2021-06-25 22:56:07 +02:00
found := false
for _, rr := range routes {
2021-07-15 22:34:01 +02:00
if r.Method() == rr.Method && r.Path() == rr.Path {
2021-06-25 22:56:07 +02:00
found = true
break
}
}
if !found {
2021-07-15 22:34:01 +02:00
t.Errorf("Route %s %s not found", r.Method(), r.Path())
2021-06-25 22:56:07 +02:00
}
}
}
}
func TestEchoServeHTTPPathEncoding(t *testing.T) {
e := New()
e.GET("/with/slash", func(c Context) error {
return c.String(http.StatusOK, "/with/slash")
})
e.GET("/:id", func(c Context) error {
2021-07-15 22:34:01 +02:00
return c.String(http.StatusOK, c.PathParam("id"))
})
var testCases = []struct {
name string
whenURL string
expectURL string
expectStatus int
}{
{
name: "url with encoding is not decoded for routing",
whenURL: "/with%2Fslash",
expectURL: "with%2Fslash", // `%2F` is not decoded to `/` for routing
expectStatus: http.StatusOK,
},
{
name: "url without encoding is used as is",
whenURL: "/with/slash",
expectURL: "/with/slash",
expectStatus: http.StatusOK,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, tc.expectStatus, rec.Code)
assert.Equal(t, tc.expectURL, rec.Body.String())
})
}
}
2021-06-25 22:56:07 +02:00
func TestEchoHost(t *testing.T) {
okHandler := func(c Context) error { return c.String(http.StatusOK, http.StatusText(http.StatusOK)) }
teapotHandler := func(c Context) error { return c.String(http.StatusTeapot, http.StatusText(http.StatusTeapot)) }
acceptHandler := func(c Context) error { return c.String(http.StatusAccepted, http.StatusText(http.StatusAccepted)) }
teapotMiddleware := MiddlewareFunc(func(next HandlerFunc) HandlerFunc { return teapotHandler })
e := New()
e.GET("/", acceptHandler)
e.GET("/foo", acceptHandler)
ok := e.Host("ok.com")
ok.GET("/", okHandler)
ok.GET("/foo", okHandler)
teapot := e.Host("teapot.com")
teapot.GET("/", teapotHandler)
teapot.GET("/foo", teapotHandler)
middle := e.Host("middleware.com", teapotMiddleware)
middle.GET("/", okHandler)
middle.GET("/foo", okHandler)
var testCases = []struct {
name string
whenHost string
whenPath string
expectBody string
expectStatus int
}{
{
name: "No Host Root",
whenHost: "",
whenPath: "/",
expectBody: http.StatusText(http.StatusAccepted),
expectStatus: http.StatusAccepted,
},
{
name: "No Host Foo",
whenHost: "",
whenPath: "/foo",
expectBody: http.StatusText(http.StatusAccepted),
expectStatus: http.StatusAccepted,
},
{
name: "OK Host Root",
whenHost: "ok.com",
whenPath: "/",
expectBody: http.StatusText(http.StatusOK),
expectStatus: http.StatusOK,
},
{
name: "OK Host Foo",
whenHost: "ok.com",
whenPath: "/foo",
expectBody: http.StatusText(http.StatusOK),
expectStatus: http.StatusOK,
},
{
name: "Teapot Host Root",
whenHost: "teapot.com",
whenPath: "/",
expectBody: http.StatusText(http.StatusTeapot),
expectStatus: http.StatusTeapot,
},
{
name: "Teapot Host Foo",
whenHost: "teapot.com",
whenPath: "/foo",
expectBody: http.StatusText(http.StatusTeapot),
expectStatus: http.StatusTeapot,
},
{
name: "Middleware Host",
whenHost: "middleware.com",
whenPath: "/",
expectBody: http.StatusText(http.StatusTeapot),
expectStatus: http.StatusTeapot,
},
{
name: "Middleware Host Foo",
whenHost: "middleware.com",
whenPath: "/foo",
expectBody: http.StatusText(http.StatusTeapot),
expectStatus: http.StatusTeapot,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tc.whenPath, nil)
req.Host = tc.whenHost
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
2021-07-15 22:34:01 +02:00
assert.Equal(t, tc.expectStatus, rec.Code)
assert.Equal(t, tc.expectBody, rec.Body.String())
2021-06-25 22:56:07 +02:00
})
}
}
func TestEchoGroup(t *testing.T) {
e := New()
buf := new(bytes.Buffer)
e.Use(MiddlewareFunc(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("0")
return next(c)
}
}))
h := func(c Context) error {
return c.NoContent(http.StatusOK)
}
//--------
// Routes
//--------
e.GET("/users", h)
// Group
g1 := e.Group("/group1")
g1.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("1")
return next(c)
}
})
g1.GET("", h)
// Nested groups with middleware
g2 := e.Group("/group2")
g2.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("2")
return next(c)
}
})
g3 := g2.Group("/group3")
g3.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
buf.WriteString("3")
return next(c)
}
})
g3.GET("", h)
request(http.MethodGet, "/users", e)
assert.Equal(t, "0", buf.String())
buf.Reset()
request(http.MethodGet, "/group1", e)
assert.Equal(t, "01", buf.String())
buf.Reset()
request(http.MethodGet, "/group2/group3", e)
assert.Equal(t, "023", buf.String())
}
func TestEchoNotFound(t *testing.T) {
e := New()
req := httptest.NewRequest(http.MethodGet, "/files", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, http.StatusNotFound, rec.Code)
}
func TestEchoMethodNotAllowed(t *testing.T) {
e := New()
e.GET("/", func(c Context) error {
return c.String(http.StatusOK, "Echo!")
})
req := httptest.NewRequest(http.MethodPost, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow))
}
2016-06-07 01:54:24 +02:00
func TestEchoContext(t *testing.T) {
e := New()
c := e.AcquireContext()
2021-07-15 22:34:01 +02:00
assert.IsType(t, new(DefaultContext), c)
2016-06-07 01:54:24 +02:00
e.ReleaseContext(c)
}
2021-07-15 22:34:01 +02:00
func TestEcho_Start(t *testing.T) {
e := New()
e.GET("/", func(c Context) error {
2021-07-15 22:34:01 +02:00
return c.String(http.StatusTeapot, "OK")
})
2021-07-15 22:34:01 +02:00
rndPort, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
defer rndPort.Close()
errChan := make(chan error, 1)
go func() {
2021-07-15 22:34:01 +02:00
errChan <- e.Start(rndPort.Addr().String())
}()
2021-07-15 22:34:01 +02:00
select {
case <-time.After(250 * time.Millisecond):
t.Fatal("start did not error out")
case err := <-errChan:
assert.Contains(t, err.Error(), "bind: address already in use")
}
}
2021-07-15 22:34:01 +02:00
func request(method, path string, e *Echo) (int, string) {
req := httptest.NewRequest(method, path, nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
return rec.Code, rec.Body.String()
}
2021-07-15 22:34:01 +02:00
func TestDefaultHTTPErrorHandler(t *testing.T) {
var testCases = []struct {
name string
givenExposeError bool
givenLoggerFunc bool
whenMethod string
whenError error
expectBody string
expectStatus int
expectLogged string
}{
{
2021-07-15 22:34:01 +02:00
name: "ok, expose error = true, HTTPError",
givenExposeError: true,
whenError: NewHTTPError(http.StatusTeapot, "my_error"),
expectStatus: http.StatusTeapot,
expectBody: `{"error":"code=418, message=my_error","message":"my_error"}` + "\n",
},
{
2021-07-15 22:34:01 +02:00
name: "ok, expose error = true, HTTPError + internal error",
givenExposeError: true,
whenError: NewHTTPError(http.StatusTeapot, "my_error").WithInternal(errors.New("internal_error")),
expectStatus: http.StatusTeapot,
expectBody: `{"error":"code=418, message=my_error, internal=internal_error","message":"my_error"}` + "\n",
},
{
2021-07-15 22:34:01 +02:00
name: "ok, expose error = true, HTTPError + internal HTTPError",
givenExposeError: true,
whenError: NewHTTPError(http.StatusTeapot, "my_error").WithInternal(NewHTTPError(http.StatusTooEarly, "early_error")),
expectStatus: http.StatusTooEarly,
expectBody: `{"error":"code=418, message=my_error, internal=code=425, message=early_error","message":"early_error"}` + "\n",
},
{
2021-07-15 22:34:01 +02:00
name: "ok, expose error = false, HTTPError",
whenError: NewHTTPError(http.StatusTeapot, "my_error"),
expectStatus: http.StatusTeapot,
expectBody: `{"message":"my_error"}` + "\n",
},
{
2021-07-15 22:34:01 +02:00
name: "ok, expose error = false, HTTPError + internal HTTPError",
whenError: NewHTTPError(http.StatusTeapot, "my_error").WithInternal(NewHTTPError(http.StatusTooEarly, "early_error")),
expectStatus: http.StatusTooEarly,
expectBody: `{"message":"early_error"}` + "\n",
},
{
2021-07-15 22:34:01 +02:00
name: "ok, expose error = true, Error",
givenExposeError: true,
whenError: fmt.Errorf("my errors wraps: %w", errors.New("internal_error")),
expectStatus: http.StatusInternalServerError,
expectBody: `{"error":"my errors wraps: internal_error","message":"Internal Server Error"}` + "\n",
},
{
2021-07-15 22:34:01 +02:00
name: "ok, expose error = false, Error",
whenError: fmt.Errorf("my errors wraps: %w", errors.New("internal_error")),
expectStatus: http.StatusInternalServerError,
expectBody: `{"message":"Internal Server Error"}` + "\n",
},
{
2021-07-15 22:34:01 +02:00
name: "ok, http.HEAD, expose error = true, Error",
givenExposeError: true,
whenMethod: http.MethodHead,
whenError: fmt.Errorf("my errors wraps: %w", errors.New("internal_error")),
expectStatus: http.StatusInternalServerError,
expectBody: ``,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
2021-07-15 22:34:01 +02:00
buf := new(bytes.Buffer)
e := New()
2021-07-15 22:34:01 +02:00
e.Logger = &jsonLogger{writer: buf}
e.Any("/path", func(c Context) error {
return tc.whenError
})
2021-07-15 22:34:01 +02:00
e.HTTPErrorHandler = DefaultHTTPErrorHandler(tc.givenExposeError)
2021-07-15 22:34:01 +02:00
method := http.MethodGet
if tc.whenMethod != "" {
method = tc.whenMethod
}
2021-07-15 22:34:01 +02:00
c, b := request(method, "/path", e)
2021-07-15 22:34:01 +02:00
assert.Equal(t, tc.expectStatus, c)
assert.Equal(t, tc.expectBody, b)
assert.Equal(t, tc.expectLogged, buf.String())
})
}
}
2021-07-15 22:34:01 +02:00
type myCustomContext struct {
DefaultContext
}
2021-07-15 22:34:01 +02:00
func (c *myCustomContext) QueryParam(name string) string {
return "prefix_" + c.DefaultContext.QueryParam(name)
}
2021-07-15 22:34:01 +02:00
func TestEcho_customContext(t *testing.T) {
e := New()
2021-07-15 22:34:01 +02:00
e.NewContextFunc = func(ec *Echo, pathParamAllocSize int) ServableContext {
return &myCustomContext{
DefaultContext: *NewDefaultContext(ec, pathParamAllocSize),
}
}
2021-07-15 22:34:01 +02:00
e.GET("/info/:id/:file", func(c Context) error {
return c.String(http.StatusTeapot, c.QueryParam("param"))
})
2021-07-15 22:34:01 +02:00
status, body := request(http.MethodGet, "/info/1/a.csv?param=123", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "prefix_123", body)
}
2021-07-15 22:34:01 +02:00
func benchmarkEchoRoutes(b *testing.B, routes []testRoute) {
e := New()
req := httptest.NewRequest("GET", "/", nil)
u := req.URL
w := httptest.NewRecorder()
b.ReportAllocs()
// Add routes
for _, route := range routes {
e.Add(route.Method, route.Path, func(c Context) error {
return nil
})
}
// Find routes
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, route := range routes {
req.Method = route.Method
u.Path = route.Path
e.ServeHTTP(w, req)
}
}
}
func BenchmarkEchoStaticRoutes(b *testing.B) {
benchmarkEchoRoutes(b, staticRoutes)
}
func BenchmarkEchoStaticRoutesMisses(b *testing.B) {
benchmarkEchoRoutes(b, staticRoutes)
}
func BenchmarkEchoGitHubAPI(b *testing.B) {
benchmarkEchoRoutes(b, gitHubAPI)
}
func BenchmarkEchoGitHubAPIMisses(b *testing.B) {
benchmarkEchoRoutes(b, gitHubAPI)
}
func BenchmarkEchoParseAPI(b *testing.B) {
benchmarkEchoRoutes(b, parseAPI)
}