Specifying the Requirement for the Middleware With Unit Tests
In our application, we want middleware to achieve the following:
- Allow access to some routes only to authenticated users,
- Allow access to some routes only to unauthenticated users, and
- Set a flag for all requests to indicate the authentication status.
We will create three middleware functions named
ensureLoggedIn
,ensureNotLoggedIn
, andsetUserStatus
.
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