Skip to content

No (Documented) way to test protected handlers #2676

Open
@apuatcfbd

Description

@apuatcfbd

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

  • Dependencies installed
  • No typos
  • 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions