echo icon indicating copy to clipboard operation
echo copied to clipboard

No (Documented) way to test protected handlers

Open apuatcfbd opened this issue 6 months ago • 2 comments

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

  1. Create a new echo app
  2. Create 1 protected route with echojwt
  3. 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

apuatcfbd avatar Aug 23 '24 15:08 apuatcfbd