echo/v4

//  - echo/proxy.go - 
package testecho

import (
	"net/url"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

// This is simple proxy middleware
// Example:
//    g := e.Group("/foo")
//  	g.Use(proxyRequests(barUrl))
func proxyRequests(base string) echo.MiddlewareFunc {
	upstream, err := url.Parse(base)
	if err != nil {
		panic(err)
	}

	proxy := middleware.ProxyWithConfig(middleware.ProxyConfig{
		Balancer: middleware.NewRandomBalancer([]*middleware.ProxyTarget{
			{
				Name: "endpoint-service-name",
				URL:  upstream,
			},
		}),
	})

	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			req := c.Request()
			req.Host = upstream.Host
			c.SetRequest(req)

			return proxy(next)(c)
		}
	}
}
//  - echo/helpers.go - 
package testecho

import (
	"time"

	"github.com/golang-jwt/jwt/v4"
)

var secret = []byte("secret")

type TokenClaim struct {
	*jwt.StandardClaims
	TokenType string
	Name      string
}

func verifyTokenWithKey(token *jwt.Token) (interface{}, error) {
	return secret, nil
}

func createToken(user string) (string, error) {
	token := jwt.New(jwt.SigningMethodHS256)

	// set our claims
	token.Claims = &TokenClaim{
		&jwt.StandardClaims{
			ExpiresAt: time.Now().Add(time.Minute * 1).Unix(),
		},
		"root",
		user,
	}

	return token.SignedString(secret)
}
//  - echo/handler.go - 
package testecho

import (
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/golang-jwt/jwt/v4"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type handler struct {
	users map[string]string
	echo  http.Handler
}

func Handler(db map[string]string) http.Handler {
	e := echo.New()

	e.GET("/public", func(ctx echo.Context) error {
		return ctx.String(http.StatusOK, "this is public endpoint")
	})

	e.POST("/authorize", func(ctx echo.Context) error {
		user := ctx.FormValue("user")
		pass := ctx.FormValue("pass")

		for u, p := range db {
			if user == u && pass == p {
				// create token
				token := jwt.New(jwt.SigningMethodHS256)

				// set our claims
				token.Claims = &TokenClaim{
					&jwt.StandardClaims{
						ExpiresAt: time.Now().Add(time.Minute * 1).Unix(),
					},
					"root",
					u,
				}
				// generate encoded token and send it as response
				t, err := token.SignedString(secret)
				if err != nil {
					return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
				}

				return ctx.JSON(http.StatusOK, map[string]string{
					"token": t,
				})
			}
		}

		return echo.ErrUnauthorized
	})

	g := e.Group("/restricted", middleware.JWTWithConfig(middleware.JWTConfig{

		ParseTokenFunc: func(auth string, c echo.Context) (interface{}, error) {
			// claims are of type `jwt.MapClaims` when token is created with `jwt.Parse`
			token, err := jwt.Parse(auth, verifyTokenWithKey)
			if err != nil {
				return nil, err
			}
			if !token.Valid {
				return nil, errors.New("invalid token")
			}

			return token, nil
		},
	}))

	g.POST("", func(ctx echo.Context) error {
		user := ctx.Get("user").(*jwt.Token)

		if claims, ok := user.Claims.(jwt.MapClaims); ok {
			return ctx.String(http.StatusOK, fmt.Sprintf("Welcome, %s\n", claims["Name"]))
		}

		return ctx.String(http.StatusOK, fmt.Sprintf("Welcome"))
	})

	return e
}
//  - echo/handler_test.go - 
package testecho

import (
	"httptesting"
	"testing"

	"github.com/stretchr/testify/suite"
)

func TestEchoHandler(t *testing.T) {
	suite.Run(t, &httptesting.HandlerTestSuite{
		Handler: Handler(map[string]string{"foo": "bar"}),
	})
}
//  - echo/error_handler_test.go - 
package testecho

import (
	"context"
	"errors"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/labstack/echo/v4"
	"github.com/stretchr/testify/assert"
)

func TestHTTPErrorHandler(t *testing.T) {
	e := echo.New()
	defer func() { _ = e.Close() }()

	tests := map[string]struct {
		err     error
		status  int
		message string
	}{
		"context.DeadlineExceeded": {
			context.DeadlineExceeded,
			408,
			`{"message":"context deadline exceeded"}` + "\n",
		},
		"context.Canceled": {
			context.Canceled,
			408,
			`{"message":"context canceled"}` + "\n",
		},
		`echo.HTTPError`: {
			&echo.HTTPError{
				Code:    418,
				Message: "I'm a teapot",
			},
			418,
			`{"message":"I'm a teapot"}` + "\n",
		},

		`regular_error`: {
			errors.New("I'm a teapot"),
			http.StatusInternalServerError,
			`{"message":"I'm a teapot"}` + "\n",
		},
	}

	for name, test := range tests {
		name, test := name, test

		t.Run(name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, "/", nil)
			rec := httptest.NewRecorder()

			errorHandler(test.err, e.NewContext(req, rec))

			b, _ := ioutil.ReadAll(rec.Body)

			assert.Equal(t, test.status, rec.Code, "Error Status Code not match")
			assert.Equal(t, test.message, string(b), "Error message not match")
		})
	}
}

type echoError struct {
	Message string `json:"message"`
}

func (e echoError) Error() string {
	return e.Message
}

func errorHandler(err error, c echo.Context) {
	if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
		c.JSON(http.StatusRequestTimeout, echoError{err.Error()})
		return
	}

	var echoErr *echo.HTTPError
	if errors.As(err, &echoErr) {
		c.JSON(echoErr.Code, echoErr)
		return
	}

	// Add another Error handling here
	c.JSON(http.StatusInternalServerError, echoError{err.Error()})
}