gin icon indicating copy to clipboard operation
gin copied to clipboard

Unit testing middleware with CreateTestContext

Open ri-ch opened this issue 3 years ago • 3 comments

Description

Im trying to unit test some gin middeware. This middleware accepts a gin.Context and performs an action based on fields in the context.

I can see I can create a test context using gin.CreateTestContext() which returns a new context and an engine.

I would like to modify the context to set the preconditions for my test, but it is not clear how I would use the modified context.

How to reproduce

### Middleware

func ensureAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		cookie := c.Keys[userDataKey].(loginCookie)

		if !cookie.LoggedIn && c.FullPath() != "/login" {
                        c.Abort()
			c.Redirect(302, "/login")
			return
		}

		c.Next()
	}
}

The middleware test

func TestEnsureAuthRedirectsToLogin(t *testing.T) {
	cookie := createDefaultCookie()
	w := httptest.NewRecorder()
	ctx, engine := gin.CreateTestContext(w)
	ctx.Set("UserData", cookie)

	req, _ := http.NewRequest(http.MethodGet, "/", nil)

	// What do I do with `ctx`? Is there a way to inject this into my test?

	engine.Use(ensureAuth())
	engine.ServeHTTP(w, req)

	assert.Equal(t, 302, w.Result().StatusCode)
	assert.Equal(t, "/login", w.Result().Header.Get(HeaderLocation))
}

Expectations

I should be able to inject the modified context in order to test my middleware

Actual result

There doesn't appear to be a way to inject the context

Environment

  • go version: 1.16
  • gin version (or commit ref): 1.6.3
  • operating system: macOS BigSur / arm64

ri-ch avatar Aug 11 '21 08:08 ri-ch

@ri-ch I found the problem. When creating the context, you can see that the key is valuable at this time

image

But the problem lies in the function ServeHTTP()

image

You can see that the key in the current context has no value.

You can look at the source code of engine.pool.Get().(*Context)

// Get selects an arbitrary item from the Pool, removes it from the
// Pool, and returns it to the caller.
// Get may choose to ignore the pool and treat it as empty.
// Callers should not assume any relation between values passed to Put and
// the values returned by Get.
//
// If Get would otherwise return nil and p.New is non-nil, Get returns
// the result of calling p.New.
func (p *Pool) Get() interface{} {
	if race.Enabled {
		race.Disable()
	}
	l, pid := p.pin()
	x := l.private
	l.private = nil
	if x == nil {
		// Try to pop the head of the local shard. We prefer
		// the head over the tail for temporal locality of
		// reuse.
		x, _ = l.shared.popHead()
		if x == nil {
			x = p.getSlow(pid)
		}
	}
	runtime_procUnpin()
	if race.Enabled {
		race.Enable()
		if x != nil {
			race.Acquire(poolRaceAddr(x))
		}
	}
	if x == nil && p.New != nil {
		x = p.New()
	}
	return x
}

The context at this time is no longer the first context, it is random.

This is my code

func TestEnsureAuthRedirectsToLogin(t *testing.T) {
	cookie := http.Cookie{
		Name:       "",
		Value:      "",
		Path:       "/login",
		Domain:     "",
		Expires:    time.Time{},
		RawExpires: "",
		MaxAge:     0,
		Secure:     false,
		HttpOnly:   false,
		SameSite:   0,
		Raw:        "",
		Unparsed:   nil,
	}
	w := httptest.NewRecorder()
	ctx, engine := gin.CreateTestContext(w)
	ctx.Set("UserData", cookie)
	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil)

	// What do I do with `ctx`? Is there a way to inject this into my test?

	engine.Use(ensureAuth())
	engine.ServeHTTP(w, req)

	assert.Equal(t, 302, w.Result().StatusCode)
	assert.Equal(t, "/login", w.Result().Header.Get("Path"))
}

func ensureAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		get, exists := c.Get("UserData")
		if !exists {
			fmt.Println("UserData not exists")
			c.Abort()
			return
		}
		cookie := get.(http.Cookie)
		path := cookie.Path
		fmt.Println("cookie path is", path)
		if c.FullPath() != "/login" {
			c.Abort()
			c.Redirect(302, "/login")
			return
		}
		c.Next()
	}
}

Description

Im trying to unit test some gin middeware. This middleware accepts a gin.Context and performs an action based on fields in the context.

I can see I can create a test context using gin.CreateTestContext() which returns a new context and an engine.

I would like to modify the context to set the preconditions for my test, but it is not clear how I would use the modified context.

How to reproduce

### Middleware

func ensureAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		cookie := c.Keys[userDataKey].(loginCookie)

		if !cookie.LoggedIn && c.FullPath() != "/login" {
                        c.Abort()
			c.Redirect(302, "/login")
			return
		}

		c.Next()
	}
}

The middleware test

func TestEnsureAuthRedirectsToLogin(t *testing.T) {
	cookie := createDefaultCookie()
	w := httptest.NewRecorder()
	ctx, engine := gin.CreateTestContext(w)
	ctx.Set("UserData", cookie)

	req, _ := http.NewRequest(http.MethodGet, "/", nil)

	// What do I do with `ctx`? Is there a way to inject this into my test?

	engine.Use(ensureAuth())
	engine.ServeHTTP(w, req)

	assert.Equal(t, 302, w.Result().StatusCode)
	assert.Equal(t, "/login", w.Result().Header.Get(HeaderLocation))
}

Expectations

I should be able to inject the modified context in order to test my middleware

Actual result

There doesn't appear to be a way to inject the context

Environment

  • go version: 1.16
  • gin version (or commit ref): 1.6.3
  • operating system: macOS BigSur / arm64

jimbirthday avatar Aug 12 '21 06:08 jimbirthday

Just encountered the exact same issue. did you ever figure out a way to solve this? currently, it works for me by using HandleContext and not ServeHTTP, however not sure what's the difference.

c.Request, err = http.NewRequest("POST", path, ioReader)
require.NoError(t, err)
e.HandleContext(c)

cohendvir avatar Nov 08 '22 08:11 cohendvir

This seems to be duplicate of #1292 which has a workaround that works for me.

cyclops1982 avatar Dec 05 '23 06:12 cyclops1982