Specifying the Requirement for the Middleware With Unit Tests

In our application, we want middleware to achieve the following:

  1. Allow access to some routes only to authenticated users,
  2. Allow access to some routes only to unauthenticated users, and
  3. Set a flag for all requests to indicate the authentication status.

We will create three middleware functions named

  1. ensureLoggedIn,
  2. ensureNotLoggedIn, and
  3. setUserStatus.

Let's start by creating placeholders for these functions in middleware.auth.go, as follows:

// middleware.auth.go

// middleware.auth.go

package main

import "github.com/gin-gonic/gin"

func ensureLoggedIn() gin.HandlerFunc {
    return func(c *gin.Context) {

    }
}

func ensureNotLoggedIn() gin.HandlerFunc {
    return func(c *gin.Context) {

    }
}

func setUserStatus() gin.HandlerFunc {
    return func(c *gin.Context) {

    }
}

We will write tests in the middleware.auth_test.go file to create a specification for the middleware based on our requirements.

Before we do that, let's create a helper function in common_test.go that will simplify writing tests for the middleware.

// common_test.go

func testMiddlewareRequest(t *testing.T, r *gin.Engine, expectedHTTPCode int) {
    req, _ := http.NewRequest("GET", "/", nil)

    testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
        return w.Code == expectedHTTPCode
    })
}

This helper function checks whether middleware returns the expected HTTP status code. In addition to this function, common_test.go needs one more modification.

We want all test requests to use the setUserStatus middleware so that we can run authentication tests on the responses. To do this, we need to update the getRouter function in common_test.go as follows:

// common_test.go

func getRouter(withTemplates bool) *gin.Engine {
    r := gin.Default()
    if withTemplates {
        r.LoadHTMLGlob("templates/*")
        r.Use(setUserStatus()) // new line
    }
    return r
}

Now that we have the helper functions, let's start writing tests for the middleware. To test all the scenarios, we need to implement the following tests:

1. TestEnsureLoggedInUnauthenticated

This should test that the ensureLoggedIn middleware doesn't allow unauthenticated requests to continue execution.

// middleware.auth_test.go

func TestEnsureLoggedInUnauthenticated(t *testing.T) {
    r := getRouter(false)
    r.GET("/", setLoggedIn(false), ensureLoggedIn(), func(c *gin.Context) {
        t.Fail()
    })

    testMiddlewareRequest(t, r, http.StatusUnauthorized)
}

This test makes use of middleware setLoggedIn, used only during testing, which is implemented as follows:

// middleware.auth_test.go

func setLoggedIn(b bool) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set("is_logged_in", b)
    }
}

2. TestEnsureLoggedInAuthenticated

This should test that the ensureLoggedIn middleware allows authenticated requests to continue execution.

// middleware.auth_test.go

func TestEnsureLoggedInAuthenticated(t *testing.T) {
    r := getRouter(false)
    r.GET("/", setLoggedIn(true), ensureLoggedIn(), func(c *gin.Context) {
        c.Status(http.StatusOK)
    })

    testMiddlewareRequest(t, r, http.StatusOK)
}

3. TestEnsureNotLoggedInAuthenticated

This should test that the ensureNotLoggedIn middleware doesn't allow authenticated requests to continue execution.

// middleware.auth_test.go

func TestEnsureNotLoggedInAuthenticated(t *testing.T) {
    r := getRouter(false)
    r.GET("/", setLoggedIn(true), ensureNotLoggedIn(), func(c *gin.Context) {
        t.Fail()
    })

    testMiddlewareRequest(t, r, http.StatusUnauthorized)
}

4. TestEnsureNotLoggedInUnauthenticated

This tests whether the ensureNotLoggedIn middleware should allow an unauthenticated request to continue execution.

// middleware.auth_test.go

func TestEnsureNotLoggedInUnauthenticated(t *testing.T) {
    r := getRouter(false)
    r.GET("/", setLoggedIn(false), ensureNotLoggedIn(), func(c *gin.Context) {
        c.Status(http.StatusOK)
    })

    testMiddlewareRequest(t, r, http.StatusOK)
}

5. TestSetUserStatusAuthenticated

This tests that the setUserStatus middleware sets the is_logged_in flag in the context to true for authenticated requests.

// middleware.auth_test.go

func TestSetUserStatusAuthenticated(t *testing.T) {
    r := getRouter(false)
    r.GET("/", setUserStatus(), func(c *gin.Context) {
        loggedInInterface, exists := c.Get("is_logged_in")
        if !exists || !loggedInInterface.(bool) {
            t.Fail()
        }
    })

    w := httptest.NewRecorder()

    http.SetCookie(w, &http.Cookie{Name: "token", Value: "123"})

    req, _ := http.NewRequest("GET", "/", nil)
    req.Header = http.Header{"Cookie": w.HeaderMap["Set-Cookie"]}

    r.ServeHTTP(w, req)
}

6. TestSetUserStatusUnauthenticated

This should test that the setUserStatus middleware doesn't set the is_logged_in flag in the context, or sets it to false, for an unauthenticated requests.

// middleware.auth_test.go

func TestSetUserStatusUnauthenticated(t *testing.T) {
    r := getRouter(false)
    r.GET("/", setUserStatus(), func(c *gin.Context) {
        loggedInInterface, exists := c.Get("is_logged_in")
        if exists && loggedInInterface.(bool) {
            t.Fail()
        }
    })

    w := httptest.NewRecorder()

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

    r.ServeHTTP(w, req)
}

Since the middleware hasn't been implemented yet, running tests should result in failure as follows:

=== RUN   TestShowIndexPageUnauthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |     203.507µs |  |   GET     /
--- PASS: TestShowIndexPageUnauthenticated (0.00s)
=== RUN   TestArticleUnauthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |     116.628µs |  |   GET     /article/view/1
--- PASS: TestArticleUnauthenticated (0.00s)
=== RUN   TestArticleListJSON
[GIN] 2016/09/04 - 11:38:23 | 200 |      44.179µs |  |   GET     /
--- PASS: TestArticleListJSON (0.00s)
=== RUN   TestArticleXML
[GIN] 2016/09/04 - 11:38:23 | 200 |      24.774µs |  |   GET     /article/view/1
--- PASS: TestArticleXML (0.00s)
=== RUN   TestArticleCreationAuthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |     129.521µs |  |   POST    /article/create
--- PASS: TestArticleCreationAuthenticated (0.00s)
=== RUN   TestShowRegistrationPageUnauthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |     119.253µs |  |   GET     /u/register
--- PASS: TestShowRegistrationPageUnauthenticated (0.00s)
=== RUN   TestRegisterUnauthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |      98.176µs |  |   POST    /u/register
--- PASS: TestRegisterUnauthenticated (0.00s)
=== RUN   TestRegisterUnauthenticatedUnavailableUsername
[GIN] 2016/09/04 - 11:38:23 | 400 |     114.461µs |  |   POST    /u/register
--- PASS: TestRegisterUnauthenticatedUnavailableUsername (0.00s)
=== RUN   TestShowLoginPageUnauthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |      93.892µs |  |   GET     /u/login
--- PASS: TestShowLoginPageUnauthenticated (0.00s)
=== RUN   TestLoginUnauthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |      92.823µs |  |   POST    /u/login
--- PASS: TestLoginUnauthenticated (0.00s)
=== RUN   TestLoginUnauthenticatedIncorrectCredentials
[GIN] 2016/09/04 - 11:38:23 | 400 |     112.536µs |  |   POST    /u/login
--- PASS: TestLoginUnauthenticatedIncorrectCredentials (0.00s)
=== RUN   TestEnsureLoggedInUnauthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |       1.171µs |  |   GET     /
--- FAIL: TestEnsureLoggedInUnauthenticated (0.00s)
=== RUN   TestEnsureLoggedInAuthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |         703ns |  |   GET     /
--- PASS: TestEnsureLoggedInAuthenticated (0.00s)
=== RUN   TestEnsureNotLoggedInAuthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |         743ns |  |   GET     /
--- FAIL: TestEnsureNotLoggedInAuthenticated (0.00s)
=== RUN   TestEnsureNotLoggedInUnauthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |         658ns |  |   GET     /
--- PASS: TestEnsureNotLoggedInUnauthenticated (0.00s)
=== RUN   TestSetUserStatusAuthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |         422ns |  |   GET     /
--- FAIL: TestSetUserStatusAuthenticated (0.00s)
=== RUN   TestSetUserStatusUnauthenticated
[GIN] 2016/09/04 - 11:38:23 | 200 |         272ns |  |   GET     /
--- PASS: TestSetUserStatusUnauthenticated (0.00s)
=== RUN   TestGetAllArticles
--- PASS: TestGetAllArticles (0.00s)
=== RUN   TestGetArticleByID
--- PASS: TestGetArticleByID (0.00s)
=== RUN   TestCreateNewArticle
--- PASS: TestCreateNewArticle (0.00s)
=== RUN   TestValidUserRegistration
--- PASS: TestValidUserRegistration (0.00s)
=== RUN   TestInvalidUserRegistration
--- PASS: TestInvalidUserRegistration (0.00s)
=== RUN   TestUsernameAvailability
--- PASS: TestUsernameAvailability (0.00s)
=== RUN   TestUserValidity
--- PASS: TestUserValidity (0.00s)
FAIL
exit status 1
FAIL    github.com/demo-apps/go-gin-app 0.013s

Creating the Middleware

Gin middleware is a function whose signature is similar to that of a route handler. In our application, we have created middleware as functions that return middleware function. This method has been used to highlight how we can develop flexible, general purpose middleware which can be customized, if required, by passing in the relevant parameters.

The setUserStatus middleware checks for the token cookie in the the context and sets the is_logged_in flag based on that.

// middleware.auth.go

func setUserStatus() gin.HandlerFunc {
    return func(c *gin.Context) {
        if token, err := c.Cookie("token"); err == nil || token != "" {
            c.Set("is_logged_in", true)
        } else {
            c.Set("is_logged_in", false)
        }
    }
}

The ensureLoggedIn middleware checks whether the is_logged_in flag is set. If it is not set, the middleware aborts the request with an HTTP unauthorized error and prevents control from reaching the route handler.

// middleware.auth.go

func ensureLoggedIn() gin.HandlerFunc {
    return func(c *gin.Context) {
        loggedInInterface, _ := c.Get("is_logged_in")
        loggedIn := loggedInInterface.(bool)
        if !loggedIn {
            c.AbortWithStatus(http.StatusUnauthorized)
        }
    }
}

The ensureNotLoggedIn middleware checks whether the is_logged_in flag is set. If it is set, the middleware aborts the request with an HTTP unauthorized error and prevents control from reaching the route handler.

// middleware.auth.go

func ensureNotLoggedIn() gin.HandlerFunc {
    return func(c *gin.Context) {
        loggedInInterface, _ := c.Get("is_logged_in")
        loggedIn := loggedInInterface.(bool)
        if loggedIn {
            c.AbortWithStatus(http.StatusUnauthorized)
        }
    }
}

With the middleware implemented, the tests should now run successfully.

=== RUN   TestShowIndexPageUnauthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |      186.54µs |  |   GET     /
--- PASS: TestShowIndexPageUnauthenticated (0.00s)
=== RUN   TestArticleUnauthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |     101.192µs |  |   GET     /article/view/1
--- PASS: TestArticleUnauthenticated (0.00s)
=== RUN   TestArticleListJSON
[GIN] 2016/09/04 - 11:40:43 | 200 |      31.624µs |  |   GET     /
--- PASS: TestArticleListJSON (0.00s)
=== RUN   TestArticleXML
[GIN] 2016/09/04 - 11:40:43 | 200 |      25.522µs |  |   GET     /article/view/1
--- PASS: TestArticleXML (0.00s)
=== RUN   TestArticleCreationAuthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |     154.322µs |  |   POST    /article/create
--- PASS: TestArticleCreationAuthenticated (0.00s)
=== RUN   TestShowRegistrationPageUnauthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |     107.503µs |  |   GET     /u/register
--- PASS: TestShowRegistrationPageUnauthenticated (0.00s)
=== RUN   TestRegisterUnauthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |     100.203µs |  |   POST    /u/register
--- PASS: TestRegisterUnauthenticated (0.00s)
=== RUN   TestRegisterUnauthenticatedUnavailableUsername
[GIN] 2016/09/04 - 11:40:43 | 400 |     111.629µs |  |   POST    /u/register
--- PASS: TestRegisterUnauthenticatedUnavailableUsername (0.00s)
=== RUN   TestShowLoginPageUnauthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |     118.653µs |  |   GET     /u/login
--- PASS: TestShowLoginPageUnauthenticated (0.00s)
=== RUN   TestLoginUnauthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |     112.039µs |  |   POST    /u/login
--- PASS: TestLoginUnauthenticated (0.00s)
=== RUN   TestLoginUnauthenticatedIncorrectCredentials
[GIN] 2016/09/04 - 11:40:43 | 400 |     113.413µs |  |   POST    /u/login
--- PASS: TestLoginUnauthenticatedIncorrectCredentials (0.00s)
=== RUN   TestEnsureLoggedInUnauthenticated
[GIN] 2016/09/04 - 11:40:43 | 401 |       1.183µs |  |   GET     /
--- PASS: TestEnsureLoggedInUnauthenticated (0.00s)
=== RUN   TestEnsureLoggedInAuthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |         809ns |  |   GET     /
--- PASS: TestEnsureLoggedInAuthenticated (0.00s)
=== RUN   TestEnsureNotLoggedInAuthenticated
[GIN] 2016/09/04 - 11:40:43 | 401 |         751ns |  |   GET     /
--- PASS: TestEnsureNotLoggedInAuthenticated (0.00s)
=== RUN   TestEnsureNotLoggedInUnauthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |         647ns |  |   GET     /
--- PASS: TestEnsureNotLoggedInUnauthenticated (0.00s)
=== RUN   TestSetUserStatusAuthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |       2.181µs |  |   GET     /
--- PASS: TestSetUserStatusAuthenticated (0.00s)
=== RUN   TestSetUserStatusUnauthenticated
[GIN] 2016/09/04 - 11:40:43 | 200 |         822ns |  |   GET     /
--- PASS: TestSetUserStatusUnauthenticated (0.00s)
=== RUN   TestGetAllArticles
--- PASS: TestGetAllArticles (0.00s)
=== RUN   TestGetArticleByID
--- PASS: TestGetArticleByID (0.00s)
=== RUN   TestCreateNewArticle
--- PASS: TestCreateNewArticle (0.00s)
=== RUN   TestValidUserRegistration
--- PASS: TestValidUserRegistration (0.00s)
=== RUN   TestInvalidUserRegistration
--- PASS: TestInvalidUserRegistration (0.00s)
=== RUN   TestUsernameAvailability
--- PASS: TestUsernameAvailability (0.00s)
=== RUN   TestUserValidity
--- PASS: TestUserValidity (0.00s)
PASS
ok      github.com/demo-apps/go-gin-app 0.011s

results matching ""

    No results matching ""