echo
echo copied to clipboard
No (Documented) way to test protected handlers
Issue Description
Doc has a Testing section. Examples there only works with public/ unprotected routes/ handlers. In a real-world app, most of the routes are protected. Same for my case. I'm using echojwt to protect routes. Unfortunately, I've failed to test those protected routes even after googling.
Checklist
- [x] Dependencies installed
- [x] No typos
- [x] Searched existing issues and docs
Expected behaviour
Need way/ (Doc) examples to be able to test protected handlers.
Actual behaviour
No examples/ guidelines for testing protected handlers in the doc
Steps to reproduce
- Create a new echo app
- Create 1 protected route with echojwt
- Tty to write a test for the protected handler.
Working code to debug
I don't know what this section is for
// main.go
package main
import (
"fmt"
"github.com/fatih/color"
"github.com/gookit/event"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/user/proj/config"
"github.com/user/proj/database"
"github.com/user/proj/database/seeders"
"github.com/user/proj/internal/bootstrap"
"github.com/user/proj/internal/hs"
middleware2 "github.com/user/proj/middleware"
"github.com/user/proj/routes"
"log"
"net/http"
"sort"
)
func init() {
// connect to DB
database.ConnectDB()
// register custom serializers (need this registration only if using this in tags)
//schema.RegisterSerializer("settingValue", serializers.SettingValue{})
// register events
bootstrap.RegisterEvents()
}
func main() {
// close async event chan
defer func() {
err := event.CloseWait()
if err != nil {
log.Fatal("Event close err:", err)
}
}()
e := echo.New()
isLocal := config.EnvDebug()
e.Debug = isLocal
e.Renderer = hs.GetTemplateMap()
e.Validator = &bootstrap.CustomValidator{}
e.Use(
middleware.BodyLimit("30M"),
middleware.GzipWithConfig(middleware.GzipConfig{
Level: 5,
}),
middleware.LoggerWithConfig(middleware.LoggerConfig{
//Format: "➡ ${method} ${uri} - ${status}\n",
Format: "➡ ${method}: ${host} ref:${referer} remote:${remote_ip} ${uri} - ${status}\n",
}),
middleware.RateLimiterWithConfig(middleware2.ThrottleConfig),
middleware.Recover(),
middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{config.EnvUrlUi(), config.EnvUrlAdmin()},
AllowMethods: []string{
http.MethodGet, http.MethodHead, http.MethodOptions,
http.MethodPatch, http.MethodPost, http.MethodDelete,
},
}),
)
// serve static
// like: http://domail.tld/s/path/file.ext
e.Static("/s/", "storage")
// home route (public)
e.GET("/", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, "Okay")
})
e.GET("/hc", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, "OK")
})
// setup router
routes.SetupRoutes(e)
log.Fatalln(
e.Start(":3000"),
)
}
// routes.go
package routes
import (
"github.com/labstack/echo/v4"
aclroutes "github.com/user/proj/internal/modules/acl/routes"
authRoutes "github.com/user/proj/internal/modules/auth/routes"
"github.com/user/proj/middleware"
"github.com/user/proj/pkg/router"
)
// register module routes here
// like - [route-segment]: module.Routes
var routes = router.RouteList{
"/auth": authRoutes.Routes, //-----------> these routes are protected with echojwt
"/acl": aclroutes.Routes,
}
func SetupRoutes(e *echo.Echo) {
routeGroup := e.Group("/v1")
router.SetupRoutes(
routeGroup,
routes,
// middlewares that'll apply in protected routes
middleware.JwtAuth(),
middleware.Acl,
)
}
// RouteList list of all app routes
type RouteList = map[string]ModuleRoutes
// SetupRoutes registers all public routes & private routs with given middlewares
func SetupRoutes(routeGroup *echo.Group, routes RouteList, protectedMiddlewares ...echo.MiddlewareFunc) {
// versioning
registerRoutes(routes, routeGroup, protectedMiddlewares)
}
// registers private & public routes
func registerRoutes(routes RouteList, group *echo.Group, protectedMiddlewares []echo.MiddlewareFunc) {
// setup public
for segment, r := range routes {
r.SetupPublic(group, segment)
}
// below this all routes will be private due to JwtAuth
group.Use(protectedMiddlewares...)
for segment, r := range routes {
r.SetupPrivate(group, segment)
}
}
Middlewares
// jwtAuth.go
package middleware
import (
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/user/proj/config"
)
func JwtAuth() echo.MiddlewareFunc {
return echojwt.WithConfig(echojwt.Config{
SigningKey: []byte(config.EnvKey()),
})
}
// acl.go
package middleware
import (
"github.com/labstack/echo/v4"
"github.com/user/proj/config"
"github.com/user/proj/internal/hs"
authservice "github.com/user/proj/internal/modules/auth/service"
"github.com/user/proj/internal/policies"
)
func Acl(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// get user
token, ok := hs.GetToken(c)
if !ok {
return policies.UnauthorizedResponse(c)
}
user, err := authservice.GetAuthUser(token)
if err != nil {
return policies.UnauthorizedResponse(c)
}
c.Set(config.AuthUserKeyName, user)
return next(c)
}
}
// hs.GetToken (helper fn)
func GetToken(c echo.Context) (token *jwt.Token, ok bool) {
token, ok = c.Get("user").(*jwt.Token)
if !ok {
log.Println("JWT token missing or invalid")
}
return
}
The handler func
func AuthUser(c echo.Context) error {
user := auth.ReqGetUser(c)
if user == nil { // -------> user is nil so getting 401 in the test
return policies.UnauthorizedResponse(c)
}
return c.JSON(http.StatusOK, hs.Res(hs.ResData{
Status: true,
D: user,
}))
}
// in a helper file
func ReqGetUser(c echo.Context) *model.User {
u := c.Get(config.AuthUserKeyName)
user, ok := u.(model.User)
if !ok {
return nil
}
return &user
}
The test
func TestAuthUser(t *testing.T) {
e := initEcho()
doSignup := func() (token string, user model.User) {
input := struct {
Name string `json:"name"`
Email string `json:"email"`
Pass string `json:"password"`
}{
Name: "Test User for " + t.Name(),
Email: "[email protected]",
Pass: "123456",
}
// prepare input as string
p, er := jsonutil.EncodeString(input)
if er != nil {
th.Fatalf(t, "Failed to encode json: %s", er)
}
// attach payload to request
req := httptest.NewRequest(http.MethodPost, "/v1/auth/sign-up", strings.NewReader(p))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
// create response writer & context
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
_ = SignUp(c)
if rec.Code != http.StatusCreated {
th.Fatalf(t, "Failed to signup")
}
// decode login response
type loginResponse struct {
Data struct {
Token string `json:"token"`
User model.User `json:"user"`
} `json:"d"`
}
res := new(loginResponse)
if err := json.Unmarshal(rec.Body.Bytes(), res); err != nil {
th.Fatal(t, "Failed to decode login response:", err)
}
if res.Data.Token == "" {
th.Fatal(t, "Login response missing token")
}
return res.Data.Token, res.Data.User
}
// login
authToken, user := doSignup() // --> this & the t.Cleanup below works fine
// at last delete the user
t.Cleanup(func() {
err := database.DB.Delete(&user).Error
if err != nil {
th.Fatal(t, "Failed to delete created user:", err)
}
})
// get auth user
tests := []struct {
name string
token string
wantResCode int
}{
{
name: "success with valid token",
token: authToken,
wantResCode: http.StatusOK,
},
{
name: "fail with invalid token",
token: "authToken",
wantResCode: http.StatusUnauthorized,
},
}
// --> Is this necessary here? Actual app has this registered for all protected routes
e.Use(
middleware2.JwtAuth(),
middleware2.Acl,
)
for _, tt := range tests {
// request with token
req := httptest.NewRequest(http.MethodGet, "/v1/auth/me", nil)
req.Header.Set(echo.HeaderAuthorization, "Bearer "+tt.token)
// create response writer & context
rec := httptest.NewRecorder()
//c := e.NewContext(req, rec)
t.Run(tt.name, func(t *testing.T) {
//_ = AuthUser(c) // --> Case1: doesn't triggers any middleware
e.ServeHTTP(rec, req) // --> Case2: triggers middleware but fails
// --> Case1: Fails with 401 as the middlewares not triggered so user is missing & this is a protected route
// --> Case2: Fails with 404 (I might messed up something here)
if rec.Code != tt.wantResCode {
th.Errorf(t, "Response Code %d want %d for user '%s'", rec.Code, tt.wantResCode, user.Email)
}
})
}
}
TL,DR: I'm new in Go & Echo. So please forgive my silly mistakes, I welcome any suggestion/ resource to learn more.
Please don't hesitate to ask any questions regarding this topic. I'm open to do what it takes to sort out this issue :).
Version/commit
go 1.22.5
github.com/labstack/echo-jwt/v4 v4.2.0
github.com/labstack/echo/v4 v4.12.0