28 Sep 2016 ยท Software Engineering

    Test-driven Development of Go Web Applications with Gin

    41 min read
    Contents

    Introduction

    This the second part of the tutorial on building traditional web applications and microservices in Go using the Gin framework.

    In the first part of the tutorial, we set up a project and built a simple application using Gin that displayed a list of articles and the article details page.

    This part of the tutorial will add functionality like user registration, login, logout, article submission and authentication to the application to give you a clear idea of how to build real world applications using Gin and Go.

    Goals

    By the end of this tutorial, you will:

    • Learn how to add authentication to a web application built using Gin,
    • Learn how to add authorization to a web application built using Gin, and
    • Learn how continuously build and test this application

    Prerequisites

    For this tutorial, you will need:

    Adding Registration Functionality

    In this section, we’ll add functionality to allow a user to register in our application with a username and password. To do this, we’ll need to manage users and allow users to register.

    The code for this section will be organized as follows:

    • models.user.go will contain the user model,
    • handlers.user.go will contain the request handlers,
    • routes.go will be updated with the new routes,
    • templates/register.html will show the registration form,
    • templates/menu.html will be updated with a Register menu, and
    • templates/login-successful will be shown after successful registration.

    Let’s begin by creating a skeleton for the models.user.go and handlers.user.go files so that we can start writing tests.

    In models.user.go, we will define:

    • A user struct to hold a user’s details,
    • A userList array to hold the list of users,
    • A registerNewUser function to register a new user, and
    • A isUsernameAvailable function to check if username is available.

    With these additions, the models.user.go file should contain the following:

    // models.user.go
    
    package main
    
    import "errors"
    
    type user struct {
    	Username string json:"username"
    	Password string json:"-"
    }
    
    var userList = []user{
    	user{Username: "user1", Password: "pass1"},
    	user{Username: "user2", Password: "pass2"},
    	user{Username: "user3", Password: "pass3"},
    }
    
    func registerNewUser(username, password string) (*user, error) {
    	return nil, errors.New("placeholder error")
    }
    
    func isUsernameAvailable(username string) bool {
    	return false
    }

    In handlers.user.go, we will define a showRegistrationPage handler to show the registration page and a register handler to handle the registration request.

    With these additions, the handlers.user.go file should contain the following:

    // handlers.user.go
    
    package main
    
    import "github.com/gin-gonic/gin"
    
    func showRegistrationPage(c *gin.Context) {
    }
    
    func register(c *gin.Context) {
    }

    Writing Tests for This Functionality

    Based on our requirements, we know that our code should:

    • Prevent registration if a username is taken,
    • Allow registration if details are valid, and
    • Show the registration page to an unauthenticated user.

    Before we start writing tests, let’s modify common_test.go to add a temporary holder for users (tmpUserList) and modify the saveLists and restoreLists functions to process this new list. The changes in the updated common_test.go file should be as follows:

    var tmpUserList []user
    
    // existing code (not shown)
    // .
    // .
    // .
    
    func saveLists() {
    	tmpUserList = userList
    	tmpArticleList = articleList
    }
    
    func restoreLists() {
    	userList = tmpUserList
    	articleList = tmpArticleList
    }

    To test the functionality in models.user.go, we’ll need the following tests in models.user_test.go:

    1. TestUsernameAvailability

    This test will check that the isUsernameAvailable function returns true when a new username is passed in and it returns false when an existing username is passed in, as follows:

    // models.user_test.go
    
    func TestUsernameAvailability(t *testing.T) {
    	saveLists()
    
    	if !isUsernameAvailable("newuser") {
    		t.Fail()
    	}
    
    	if isUsernameAvailable("user1") {
    		t.Fail()
    	}
    
    	registerNewUser("newuser", "newpass")
    
    	if isUsernameAvailable("newuser") {
    		t.Fail()
    	}
    
    	restoreLists()
    }

    2. TestValidUserRegistration

    This test will check that the registerNewUser function successfully registers a new user with an unused username, as follows:

    // models.user_test.go
    
    func TestValidUserRegistration(t *testing.T) {
    	saveLists()
    
    	u, err := registerNewUser("newuser", "newpass")
    
    	if err != nil || u.Username == "" {
    		t.Fail()
    	}
    
    	restoreLists()
    }

    3. TestInvalidUserRegistration

    This test will check that the registerNewUser function doesn’t allow a user with an invalid username and password pair to register, as follows:

    // models.user_test.go
    
    func TestInvalidUserRegistration(t *testing.T) {
    	saveLists()
    
    	u, err := registerNewUser("user1", "pass1")
    
    	if err == nil || u != nil {
    		t.Fail()
    	}
    
    	u, err = registerNewUser("newuser", "")
    
    	if err == nil || u != nil {
    		t.Fail()
    	}
    
    	restoreLists()
    }

    We’ll add the tests for handlers.user.go in handlers.user_test.go. Before we do that, let’s create a couple of helper functions that will return username and password pairs for logging in and for registration:

    //handlers.user_test.go
    
    func getLoginPOSTPayload() string {
    	params := url.Values{}
    	params.Add("username", "user1")
    	params.Add("password", "pass1")
    
    	return params.Encode()
    }
    
    func getRegistrationPOSTPayload() string {
    	params := url.Values{}
    	params.Add("username", "u1")
    	params.Add("password", "p1")
    
    	return params.Encode()
    }

    Now, let’s add the following tests:

    1. TestShowRegistrationPageUnauthenticated

    This test will check that the registration page is shown to unauthenticated users. To implement this test, we first need to create a router and associate the /u/register route with the showRegistrationPage route handler. We will then create a new HTTP GET request to access this route.

    Finally, we will use the testHTTPResponse helper function (explained in part 1 of the tutorial) to check that the HTTP response code is 200 and that the registration page is served as expected.

    //handlers.user_test.go
    
    func TestShowRegistrationPageUnauthenticated(t *testing.T) {
    	r := getRouter(true)
    
    	r.GET("/u/register", showRegistrationPage)
    
    	req, _ := http.NewRequest("GET", "/u/register", nil)
    
    	testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
    		statusOK := w.Code == http.StatusOK
    
    		p, err := ioutil.ReadAll(w.Body)
    		pageOK := err == nil && strings.Index(string(p), "<title>Register</title>") > 0
    
    		return statusOK && pageOK
    	})
    }

    2. TestRegisterUnauthenticated

    This test will check that a registration request with a new username succeeds. In this test, we will create an HTTP POST request to test the /u/register route and the register route handler using an unused username.

    This test should pass if the HTTP status code is 200 and if the user is successfully registered and logged in.

    //handlers.user_test.go
    
    func TestRegisterUnauthenticated(t *testing.T) {
    	saveLists()
    	w := httptest.NewRecorder()
    
    	r := getRouter(true)
    
    	r.POST("/u/register", register)
    
    	registrationPayload := getRegistrationPOSTPayload()
    	req, _ := http.NewRequest("POST", "/u/register", strings.NewReader(registrationPayload))
    	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    	req.Header.Add("Content-Length", strconv.Itoa(len(registrationPayload)))
    
    	r.ServeHTTP(w, req)
    
    	if w.Code != http.StatusOK {
    		t.Fail()
    	}
    
    	p, err := ioutil.ReadAll(w.Body)
    	if err != nil || strings.Index(string(p), "<title>Successful registration &amp; Login</title>") < 0 {
    		t.Fail()
    	}
    	restoreLists()
    }

    3. TestRegisterUnauthenticatedUnavailableUsername

    This test will check that a user cannot register with a username that is already in use. In this test, we will create an HTTP POST request to test the /u/register route and the register route handler with an unavailable username.

    This test should pass if the request fails with an HTTP status code of 400.

    //handlers.user_test.go
    
    func TestRegisterUnauthenticatedUnavailableUsername(t *testing.T) {
    	saveLists()
    	w := httptest.NewRecorder()
    
    	r := getRouter(true)
    
    	r.POST("/u/register", register)
    
    	registrationPayload := getLoginPOSTPayload()
    	req, _ := http.NewRequest("POST", "/u/register", strings.NewReader(registrationPayload))
    	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    	req.Header.Add("Content-Length", strconv.Itoa(len(registrationPayload)))
    
    	r.ServeHTTP(w, req)
    
    	if w.Code != http.StatusBadRequest {
    		t.Fail()
    	}
    	restoreLists()
    }

    Now that we have the tests written, let’s run them to see what happens. In your project directory, execute the following command:

    go test -v

    Executing this command should show the following output:

    === RUN   TestShowIndexPageUnauthenticated
    [GIN] 2016/08/04 - 08:37:31 | 200 |     199.765ยตs |  |   GET     /
    --- PASS: TestShowIndexPageUnauthenticated (0.00s)
    === RUN   TestArticleUnauthenticated
    [GIN] 2016/08/04 - 08:37:31 | 200 |      81.056ยตs |  |   GET     /article/view/1
    --- FAIL: TestValidUserRegistration (0.00s)
    === RUN   TestInvalidUserRegistration
    --- PASS: TestInvalidUserRegistration (0.00s)
    === RUN   TestUsernameAvailability
    --- FAIL: TestUsernameAvailability (0.00s)
    --- PASS: TestArticleListJSON (0.00s)
    === RUN   TestArticleXML
    FAIL
    exit status 1
    FAIL--- PASS: TestArticleUnauthenticated (0.00s)
    === RUN   TestArticleListJSON
    [GIN] 2016/08/04 - 08:37:31 | 200 |      58.196ยตs |  |   GET     /
    [GIN] 2016/08/04 - 08:37:31 | 200 |      36.732ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleXML (0.00s)
    === RUN   TestShowRegistrationPageUnauthenticated
    [GIN] 2016/08/04 - 08:37:31 | 200 |         432ns |  |   GET     /u/register
    --- FAIL: TestShowRegistrationPageUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticated
    [GIN] 2016/08/04 - 08:37:31 | 200 |         366ns |  |   POST    /u/register
    --- FAIL: TestRegisterUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticatedUnavailableUsername
    [GIN] 2016/08/04 - 08:37:31 | 200 |         385ns |  |   POST    /u/register
    --- FAIL: TestRegisterUnauthenticatedUnavailableUsername (0.00s)
    === RUN   TestGetAllArticles
    --- PASS: TestGetAllArticles (0.00s)
    === RUN   TestGetArticleByID
    --- PASS: TestGetArticleByID (0.00s)
    === RUN   TestValidUserRegistration
    	github.com/demo-apps/go-gin-app	0.007s
    

    The new tests are failing, as expected, because we haven’t yet implemented the functionality in models.user.go and handlers.user.go.

    Setting Up the Routes

    The registration functionality will need two routes โ€” one for displaying the registration page and another to handle the POST request submitted from the registration page.

    While defining routes in Gin, we can group two or more routes together under a common parent if we want to. Since both the new routes are related, we can create a grouped route as shown below:

    // routes.go
    
    package main
    
    func initializeRoutes() {
    
    	router.GET("/", showIndexPage)
    
    	userRoutes := router.Group("/u")
    	{
    		userRoutes.GET("/register", showRegistrationPage)
    
    		userRoutes.POST("/register", register)
    	}
    
    	router.GET("/article/view/:article_id", getArticle)
    
    }

    Grouping routes together allows you to apply middleware on all routes in a group instead of doing so separately for each route. In the above snippet, the first route will use the showRegistrationPage function to display the registration page at the /u/register path. The second route will handle all the POST requests to the same path, making use of the register route handler.

    Creating the View Templates

    We need to create two new templates:

    1. templates/register.html

    This will display the registration page with the following contents:

    <!--register.html-->
    
    <!--Embed the header.html template at this location-->
    {{ template "header.html" .}}
    
    <h1>Register</h1>
    
    <div class="panel panel-default col-sm-6">
      <div class="panel-body">
        <!--If there's an error, display the error-->
        {{ if .ErrorTitle}}
        <p class="bg-danger">
          {{.ErrorTitle}}: {{.ErrorMessage}}
        </p>
        {{end}}
        <!--Create a form that POSTs to the /u/register route-->
        <form class="form" action="/u/register" method="POST">
          <div class="form-group">
            <label for="username">Username</label>
            <input type="text" class="form-control" id="username" name="username" placeholder="Username">
          </div>
          <div class="form-group">
            <label for="password">Password</label>
            <input type="password" name="password" class="form-control" id="password" placeholder="Password">
          </div>
          <button type="submit" class="btn btn-primary">Register</button>
        </form>
      </div>
    </div>  
    
    
    <!--Embed the footer.html template at this location-->
    {{ template "footer.html" .}}

    2. templates/login-successful.html

    This template will be used when a user registers successfully and is automatically logged in. Its contents are as follows:

    <!--login-successful.html-->
    
    <!--Embed the header.html template at this location-->
    {{ template "header.html" .}}
    
    <div>
      You have successfully logged in.
    </div>
    
    <!--Embed the footer.html template at this location-->
    {{ template "footer.html" .}}

    Additionally, we also need to modify the menu.html template to display a link to the registration page, as follows:

    <!--menu.html-->
    
    <nav class="navbar navbar-default">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="/">
            Home
          </a>
        </div>
        <ul class="nav navbar-nav">      
            <li><a href="/u/register">Register</a></li>
        </ul>
      </div>
    </nav>

    Implementing the Model

    In the user model, the registerNewUser function in models.user.go must create a new user if the input is valid and must reject the request if the input isn’t valid. The isUsernameAvailable function must check if the supplied username is available or is already in use.

    After implementing these functions, models.user.go should look as follows:

    // models.user.go
    
    package main
    
    import (
    	"errors"
    	"strings"
    )
    
    type user struct {
    	Username string json:"username"
    	Password string json:"-"
    }
    
    // For this demo, we're storing the user list in memory
    // We also have some users predefined.
    // In a real application, this list will most likely be fetched
    // from a database. Moreover, in production settings, you should
    // store passwords securely by salting and hashing them instead
    // of using them as we're doing in this demo
    var userList = []user{
    	user{Username: "user1", Password: "pass1"},
    	user{Username: "user2", Password: "pass2"},
    	user{Username: "user3", Password: "pass3"},
    }
    
    // Register a new user with the given username and password
    func registerNewUser(username, password string) (*user, error) {
    	if strings.TrimSpace(password) == "" {
    		return nil, errors.New("The password can't be empty")
    	} else if !isUsernameAvailable(username) {
    		return nil, errors.New("The username isn't available")
    	}
    
    	u := user{Username: username, Password: password}
    
    	userList = append(userList, u)
    
    	return &u, nil
    }
    
    // Check if the supplied username is available
    func isUsernameAvailable(username string) bool {
    	for _, u := range userList {
    		if u.Username == username {
    			return false
    		}
    	}
    	return true
    }

    Implementing the Route Handlers

    In the user route handler, the showRegistrationPage handler should show the registration page to unauthenticated users while the register handler should handle the registration request.

    After implementing these handlers, handlers.user.go should look as follows:

    // handlers.user.go
    
    package main
    
    import (
    	"math/rand"
    	"net/http"
    	"strconv"
    
    	"github.com/gin-gonic/gin"
    )
    
    func generateSessionToken() string {
    	// We're using a random 16 character string as the session token
    	// This is NOT a secure way of generating session tokens
    	// DO NOT USE THIS IN PRODUCTION
    	return strconv.FormatInt(rand.Int63(), 16)
    }
    
    func showRegistrationPage(c *gin.Context) {
    	// Call the render function with the name of the template to render
    	render(c, gin.H{
    		"title": "Register"}, "register.html")
    }
    
    func register(c *gin.Context) {
    	// Obtain the POSTed username and password values
    	username := c.PostForm("username")
    	password := c.PostForm("password")
    
    	if _, err := registerNewUser(username, password); err == nil {
    		// If the user is created, set the token in a cookie and log the user in
    		token := generateSessionToken()
    		c.SetCookie("token", token, 3600, "", "", false, true)
    		c.Set("is_logged_in", true)
    
    		render(c, gin.H{
    			"title": "Successful registration & Login"}, "login-successful.html")
    
    	} else {
    		// If the username/password combination is invalid,
    		// show the error message on the login page
    		c.HTML(http.StatusBadRequest, "register.html", gin.H{
    			"ErrorTitle":   "Registration Failed",
    			"ErrorMessage": err.Error()})
    
    	}
    }

    If we run the tests again, we’ll see the following output:

    === RUN   TestArticleXML
    [GIN] 2016/08/04 - 09:13:30 | 200 |      27.544ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleXML (0.00s)
    === RUN   TestShowRegistrationPageUnauthenticated
    [GIN] 2016/08/04 - 09:13:30 | 200 |      96.805ยตs |  |   GET     /u/register
    --- PASS: TestShowRegistrationPageUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticated
    [GIN] 2016/08/04 - 09:13:30 | 200 |     107.286ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticatedUnavailableUsername
    [GIN] 2016/08/04 - 09:13:30 | 400 |      116.63ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticatedUnavailableUsername (0.00s)
    === RUN   TestGetAllArticles
    --- PASS: TestGetAllArticles (0.00s)
    === RUN   TestGetArticleByID
    --- PASS: TestGetArticleByID (0.00s)
    === RUN   TestValidUserRegistration
    --- PASS: TestValidUserRegistration (0.00s)
    === RUN   TestInvalidUserRegistration
    --- PASS: TestInvalidUserRegistration (0.00s)
    === RUN   TestUsernameAvailability
    --- PASS: TestUsernameAvailability (0.00s)
    PASS
    ok  	github.com/demo-apps/go-gin-app	0.007s
    

    With the registration functionality complete, it’s time to allow users to log in and log out.

    Allowing Users to Log in and Log out

    In this section, we’ll add functionality that will allow registered users to log in and logged in users to log out of our application.

    The code for this section will be organized as follows:

    • models.user.go will be updated to check if the login credentials are valid,
    • handlers.user.go will be updated with the new request handlers,
    • routes.go will be updated with the new routes,
    • templates/login.html will display the login form, and
    • templates/menu.html will be updated with the Login and Logout menus.

    Let’s begin by creating a placeholder for the function to validate the login credentials, in models.user.go, as follows:

    // models.user.go
    
    func isUserValid(username, password string) bool {
    	return false
    }

    We also need to update handlers.user.go with:

    • A showLoginPage handler to show the login page,
    • A performLogin handler to handle the login request, and
    • A logout handler to handle the logout request.

    For now, let’s just add these handlers without any functionality so that we can write tests for them. Let’s update the handlers.user.go file with the following code:

    // handlers.user.go
    
    func showLoginPage(c *gin.Context) {}
    
    func performLogin(c *gin.Context) {}
    
    func logout(c *gin.Context) {}

    Testing Login and Logout Functionalities

    Based on the requirements for this functionality, this code should allow the use of a valid username/password combination and disallow the use of an invalid username/password combination.

    To test the isUserValid function in models.user.go, let’s update the models.user_test.go with the following test:

    // models.user_test.go
    
    func TestUserValidity(t *testing.T) {
    	if !isUserValid("user1", "pass1") {
    		t.Fail()
    	}
    
    	if isUserValid("user2", "pass1") {
    		t.Fail()
    	}
    
    	if isUserValid("user1", "") {
    		t.Fail()
    	}
    
    	if isUserValid("", "pass1") {
    		t.Fail()
    	}
    
    	if isUserValid("User1", "pass1") {
    		t.Fail()
    	}
    }

    This test uses several username/password combinations to test that the isUserValid function returns the expected result. Note that this uses the hard-coded list of users that we created earlier in models.user.go.

    The tests for the new handlers will be similar in structure to the ones added in the previous section. The new handlers added in handlers.user.go will need the following tests:

    1. TestShowLoginPageUnauthenticated

    This test will check that the login page is shown to unauthenticated users as expected.

    // handlers.user_test.go
    
    func TestShowLoginPageUnauthenticated(t *testing.T){
    	r := getRouter(true)
    
    	r.GET("/u/login", showLoginPage)
    
    	req, _ := http.NewRequest("GET", "/u/login", nil)
    
    	testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
    		statusOK := w.Code == http.StatusOK
    
    		p, err := ioutil.ReadAll(w.Body)
    		pageOK := err == nil && strings.Index(string(p), "<title>Login</title>") > 0
    
    		return statusOK && pageOK
    	})
    }

    2. TestLoginUnauthenticated

    This test will check that a POST request to login with the correct credentials returns a success message.

    //handlers.user_test.go
    
    func TestLoginUnauthenticated(t *testing.T) {
    	saveLists()
    	w := httptest.NewRecorder()
    	r := getRouter(true)
    
    	r.POST("/u/login", performLogin)
    
    	loginPayload := getLoginPOSTPayload()
    	req, _ := http.NewRequest("POST", "/u/login", strings.NewReader(loginPayload))
    	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    	req.Header.Add("Content-Length", strconv.Itoa(len(loginPayload)))
    
    	r.ServeHTTP(w, req)
    
    	if w.Code != http.StatusOK {
    		t.Fail()
    	}
    
    	p, err := ioutil.ReadAll(w.Body)
    	if err != nil || strings.Index(string(p), "<title>Successful Login</title>") < 0 {
    		t.Fail()
    	}
    	restoreLists()
    }

    3. TestLoginUnauthenticatedIncorrectCredentials

    This test will check that a POST request to login with incorrect credentials returns an error.

    //handlers.user_test.go
    
    func TestLoginUnauthenticatedIncorrectCredentials(t *testing.T) {
    	saveLists()
    	w := httptest.NewRecorder()
    	r := getRouter(true)
    
    	r.POST("/u/login", performLogin)
    
    	loginPayload := getRegistrationPOSTPayload()
    	req, _ := http.NewRequest("POST", "/u/login", strings.NewReader(loginPayload))
    	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    	req.Header.Add("Content-Length", strconv.Itoa(len(loginPayload)))
    
    	r.ServeHTTP(w, req)
    
    	if w.Code != http.StatusBadRequest {
    		t.Fail()
    	}
    	restoreLists()
    }

    With these tests written, let’s run them to see what happens. In your project directory, execute the following command:

    go test -v

    This should give the following output:

    === RUN   TestShowIndexPageUnauthenticated
    [GIN] 2016/09/04 - 08:33:22 | 200 |     277.294ยตs |  |   GET     /
    --- PASS: TestShowIndexPageUnauthenticated (0.00s)
    === RUN   TestArticleUnauthenticated
    [GIN] 2016/09/04 - 08:33:22 | 200 |     131.567ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleUnauthenticated (0.00s)
    === RUN   TestArticleListJSON
    [GIN] 2016/09/04 - 08:33:22 | 200 |      67.678ยตs |  |   GET     /
    --- PASS: TestArticleListJSON (0.00s)
    === RUN   TestArticleXML
    [GIN] 2016/09/04 - 08:33:22 | 200 |      26.344ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleXML (0.00s)
    === RUN   TestShowRegistrationPageUnauthenticated
    [GIN] 2016/09/04 - 08:33:22 | 200 |     130.407ยตs |  |   GET     /u/register
    --- PASS: TestShowRegistrationPageUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticated
    [GIN] 2016/09/04 - 08:33:22 | 200 |     176.598ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticatedUnavailableUsername
    [GIN] 2016/09/04 - 08:33:22 | 400 |     181.588ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticatedUnavailableUsername (0.00s)
    === RUN   TestShowLoginPageUnauthenticated
    [GIN] 2016/09/04 - 08:33:22 | 200 |         327ns |  |   GET     /u/login
    --- FAIL: TestShowLoginPageUnauthenticated (0.00s)
    === RUN   TestLoginUnauthenticated
    [GIN] 2016/09/04 - 08:33:22 | 200 |         316ns |  |   POST    /u/login
    --- FAIL: TestLoginUnauthenticated (0.00s)
    === RUN   TestLoginUnauthenticatedIncorrectCredentials
    [GIN] 2016/09/04 - 08:33:22 | 200 |         258ns |  |   POST    /u/login
    --- FAIL: TestLoginUnauthenticatedIncorrectCredentials (0.00s)
    === RUN   TestGetAllArticles
    --- PASS: TestGetAllArticles (0.00s)
    === RUN   TestGetArticleByID
    --- PASS: TestGetArticleByID (0.00s)
    === RUN   TestValidUserRegistration
    --- PASS: TestValidUserRegistration (0.00s)
    === RUN   TestInvalidUserRegistration
    --- PASS: TestInvalidUserRegistration (0.00s)
    === RUN   TestUsernameAvailability
    --- PASS: TestUsernameAvailability (0.00s)
    === RUN   TestUserValidity
    --- FAIL: TestUserValidity (0.00s)
    FAIL
    exit status 1
    FAIL	github.com/demo-apps/go-gin-app	0.009s
    

    As expected, the tests fail. Let’s now start implementing the functionality for login and logout.

    Setting Up the Routes

    For this section, we’ll need to add three routes to:

    • Display the login page,
    • Process the login request, and
    • Process the logout request.

    We’ll add these new routes to the previously created userRoutes group in routes.go as follows:

    // routes.go
    
    	userRoutes := router.Group("/u")
    	{
    		// . (existing content, not shown)
    		// .
    		userRoutes.GET("/login", showLoginPage)
    		userRoutes.POST("/login", performLogin)
    		userRoutes.GET("/logout", logout)
    	}

    Creating the View Template

    We need to create one new template to that will display the login page:

    <!--login.html-->
    
    {{ template "header.html" .}}
    
    <h1>Login</h1>
    
    
    <div class="panel panel-default col-sm-6">
      <div class="panel-body">
    
        {{ if .ErrorTitle}}
        <p class="bg-danger">
          {{.ErrorTitle}}: {{.ErrorMessage}}
        </p>
        {{end}}
    
        <form class="form" action="/u/login" method="POST">
          <div class="form-group">
            <label for="username">Username</label>
            <input type="text" class="form-control" id="username" name="username" placeholder="Username">
          </div>
          <div class="form-group">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" name="password" placeholder="Password">
          </div>
          <button type="submit" class="btn btn-primary">Login</button>
        </form>
      </div>
    </div>
    
    {{ template "footer.html" .}}

    Additionally, we also need to modify the menu.html template to display the login and logout links, as follows:

    <!--menu.html-->
    .
    .
        <ul class="nav navbar-nav">      
            <li><a href="/u/register">Register</a></li>
            <li><a href="/u/login">Login</a></li>
            <li><a href="/u/logout">Logout</a></li>
        </ul>
    .
    .

    Implementing the Model

    The isUserValid function should return true if the username/password combination passed in is valid. If not, it must return false. We’ll do this by going over the hardcoded list of users and matching the username/password combination, as follows:

    // models.user.go
    
    func isUserValid(username, password string) bool {
    	for _, u := range userList {
    		if u.Username == username && u.Password == password {
    			return true
    		}
    	}
    	return false
    }

    Now, run the following command in your shell to test that this function behaves as expected:

    go test -run=TestUserValidity

    This test should run successfully and show the following message:

    PASS
    ok  	github.com/demo-apps/go-gin-app	0.006s
    

    Implementing the Route Handlers

    The showLoginPage handler is similar to the showRegistrationPage handler from the previous section in that it displays a static page.

    The performLogin handler is largely similar to the register handler with one notable exception. The performLogin handler sets a cookie if the login is successful. Likewise, the logout handler removes this cookie when a user logs out.

    After implementing these handlers, handlers.user.go should contain the following additional code:

    // handlers.user.go
    
    func showLoginPage(c *gin.Context) {
    	render(c, gin.H{
    		"title": "Login",
    	}, "login.html")
    }
    
    func performLogin(c *gin.Context) {
    	username := c.PostForm("username")
    	password := c.PostForm("password")
    
    	if isUserValid(username, password) {
    		token := generateSessionToken()
    		c.SetCookie("token", token, 3600, "", "", false, true)
    
    		render(c, gin.H{
    			"title": "Successful Login"}, "login-successful.html")
    
    	} else {
    		c.HTML(http.StatusBadRequest, "login.html", gin.H{
    			"ErrorTitle":   "Login Failed",
    			"ErrorMessage": "Invalid credentials provided"})
    	}
    }
    
    func logout(c *gin.Context) {
    	c.SetCookie("token", "", -1, "", "", false, true)
    
    	c.Redirect(http.StatusTemporaryRedirect, "/")
    }

    With the handlers implemented, the tests should run successfully and result in the following output:

    === RUN   TestShowIndexPageUnauthenticated
    [GIN] 2016/09/04 - 09:16:54 | 200 |      269.85ยตs |  |   GET     /
    --- PASS: TestShowIndexPageUnauthenticated (0.00s)
    === RUN   TestArticleUnauthenticated
    [GIN] 2016/09/04 - 09:16:54 | 200 |     129.257ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleUnauthenticated (0.00s)
    === RUN   TestArticleListJSON
    [GIN] 2016/09/04 - 09:16:54 | 200 |      38.957ยตs |  |   GET     /
    --- PASS: TestArticleListJSON (0.00s)
    === RUN   TestArticleXML
    [GIN] 2016/09/04 - 09:16:54 | 200 |      29.553ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleXML (0.00s)
    === RUN   TestShowRegistrationPageUnauthenticated
    [GIN] 2016/09/04 - 09:16:54 | 200 |     114.762ยตs |  |   GET     /u/register
    --- PASS: TestShowRegistrationPageUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticated
    [GIN] 2016/09/04 - 09:16:54 | 200 |     182.793ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticatedUnavailableUsername
    [GIN] 2016/09/04 - 09:16:54 | 400 |     160.118ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticatedUnavailableUsername (0.00s)
    === RUN   TestShowLoginPageUnauthenticated
    [GIN] 2016/09/04 - 09:16:54 | 200 |     101.494ยตs |  |   GET     /u/login
    --- PASS: TestShowLoginPageUnauthenticated (0.00s)
    === RUN   TestLoginUnauthenticated
    [GIN] 2016/09/04 - 09:16:54 | 200 |      91.377ยตs |  |   POST    /u/login
    --- PASS: TestLoginUnauthenticated (0.00s)
    === RUN   TestLoginUnauthenticatedIncorrectCredentials
    [GIN] 2016/09/04 - 09:16:54 | 400 |     110.563ยตs |  |   POST    /u/login
    --- PASS: TestLoginUnauthenticatedIncorrectCredentials (0.00s)
    === RUN   TestGetAllArticles
    --- PASS: TestGetAllArticles (0.00s)
    === RUN   TestGetArticleByID
    --- PASS: TestGetArticleByID (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.010s
    

    Allowing Users to Post New Articles

    In this section, we’ll add functionality that will allow users to post new articles.

    The code for this functionality will be organized as follows:

    • models.article.go will be updated to create a new article,
    • handlers.article.go will be updated with the new route handlers to show the article creating page and to process the article creation request,
    • routes.go will be updated with the new routes,
    • templates/create-article.html will display the article creation form,
    • templates/submission-successful.html will be displayed after a successful article submission, and
    • templates/menu.html will be updated with the new Create Article link.

    Let’s begin by creating a placeholder for the function to create a new article in models.article.go, as follows:

    // models.article.go
    
    func createNewArticle(title, content string) (*article, error) {
    	return nil, nil
    }

    We need to update the handlers.article.go with a showArticleCreationPage handler to show the article creation page and a createArticle handler to process the article creation request.

    For now, we’ll add empty handlers so that we can write tests for them. The updated handlers.article.go file should contain the following code in addition to the previous one:

    // handlers.article.go
    
    func showArticleCreationPage(c *gin.Context) {}
    
    func createArticle(c *gin.Context) {}

    Testing the ‘Create Article’ Functionality

    The tests for this section of code should check that the articles are added to the existing list of articles.

    To test the createNewArticle function in models.article.go, update the models.article_test.go with the following test:

    // models.article_test.go
    
    func TestCreateNewArticle(t *testing.T) {
    	originalLength := len(getAllArticles())
    
    	a, err := createNewArticle("New test title", "New test content")
    
    	allArticles := getAllArticles()
    	newLength := len(allArticles)
    
    	if err != nil || newLength != originalLength+1 ||
    		a.Title != "New test title" || a.Content != "New test content" {
    
    		t.Fail()
    	}
    }
    

    This test uses the createNewArticle function to add a new article and checks that the article list contains the new article.

    The test for the handler that processes the article creation request will be similar to the test that tested the registration functionality.

    The handlers_article_test.go file should be updated to include the following test:

    // handlers.article_test.go
    
    func TestArticleCreationAuthenticated(t *testing.T) {
    	saveLists()
    	w := httptest.NewRecorder()
    
    	r := getRouter(true)
    
    	http.SetCookie(w, &http.Cookie{Name: "token", Value: "123"})
    
    	r.POST("/article/create", createArticle)
    
    	articlePayload := getArticlePOSTPayload()
    	req, _ := http.NewRequest("POST", "/article/create", strings.NewReader(articlePayload))
    	req.Header = http.Header{"Cookie": w.HeaderMap["Set-Cookie"]}
    	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    	req.Header.Add("Content-Length", strconv.Itoa(len(articlePayload)))
    
    	r.ServeHTTP(w, req)
    
    	if w.Code != http.StatusOK {
    		t.Fail()
    	}
    
    	p, err := ioutil.ReadAll(w.Body)
    	if err != nil || strings.Index(string(p), "<title>Submission Successful</title>") < 0 {
    		t.Fail()
    	}
    	restoreLists()
    }
    
    func getArticlePOSTPayload() string {
    	params := url.Values{}
    	params.Add("title", "Test Article Title")
    	params.Add("content", "Test Article Content")
    
    	return params.Encode()
    }

    Run these tests using the following command:

    go test -v

    The tests should fail and you should see something similar to the following

    === RUN   TestShowIndexPageUnauthenticated
    [GIN] 2016/09/04 - 10:04:52 | 200 |     211.568ยตs |  |   GET     /
    --- PASS: TestShowIndexPageUnauthenticated (0.00s)
    === RUN   TestArticleUnauthenticated
    [GIN] 2016/09/04 - 10:04:52 | 200 |     102.277ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleUnauthenticated (0.00s)
    === RUN   TestArticleListJSON
    [GIN] 2016/09/04 - 10:04:52 | 200 |      35.975ยตs |  |   GET     /
    --- PASS: TestArticleListJSON (0.00s)
    === RUN   TestArticleXML
    [GIN] 2016/09/04 - 10:04:52 | 200 |      25.814ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleXML (0.00s)
    === RUN   TestArticleCreationAuthenticated
    [GIN] 2016/09/04 - 10:04:52 | 200 |         426ns |  |   POST    /article/create
    --- FAIL: TestArticleCreationAuthenticated (0.00s)
    === RUN   TestShowRegistrationPageUnauthenticated
    [GIN] 2016/09/04 - 10:04:52 | 200 |     112.875ยตs |  |   GET     /u/register
    --- PASS: TestShowRegistrationPageUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticated
    [GIN] 2016/09/04 - 10:04:52 | 200 |     132.996ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticatedUnavailableUsername
    [GIN] 2016/09/04 - 10:04:52 | 400 |     116.116ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticatedUnavailableUsername (0.00s)
    === RUN   TestShowLoginPageUnauthenticated
    [GIN] 2016/09/04 - 10:04:52 | 200 |       92.27ยตs |  |   GET     /u/login
    --- PASS: TestShowLoginPageUnauthenticated (0.00s)
    === RUN   TestLoginUnauthenticated
    [GIN] 2016/09/04 - 10:04:52 | 200 |      97.427ยตs |  |   POST    /u/login
    --- PASS: TestLoginUnauthenticated (0.00s)
    === RUN   TestLoginUnauthenticatedIncorrectCredentials
    [GIN] 2016/09/04 - 10:04:52 | 400 |     107.117ยตs |  |   POST    /u/login
    --- PASS: TestLoginUnauthenticatedIncorrectCredentials (0.00s)
    === RUN   TestGetAllArticles
    --- PASS: TestGetAllArticles (0.00s)
    === RUN   TestGetArticleByID
    --- PASS: TestGetArticleByID (0.00s)
    === RUN   TestCreateNewArticle
    --- FAIL: 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.009s
    

    With the tests in place, let’s start implementing the functionality to allow article submission.

    Setting Up the Routes

    For this functionality, we’ll need to add two routes to display the article creation page and process the article creation request.

    This would be a good time to group the article related routes together in routes.go as follows:

    // routes.go
    
    	articleRoutes := router.Group("/article")
    	{
    		// route from Part 1 of the tutorial
    		articleRoutes.GET("/view/:article_id", getArticle)
    
    		articleRoutes.GET("/create", showArticleCreationPage)
    
    		articleRoutes.POST("/create", createArticle)
    	}
    

    Creating the View Templates

    For this functionality, we’ll need two new templates:

    1. templates/create-article.html

    This will display the article creation form using the following code:

    <!--create-article.html-->
    
    {{ template "header.html" .}}
    
    <h1>Create Article</h1>
    
    
    <div class="panel panel-default col-sm-12">
      <div class="panel-body">
        {{ if .ErrorTitle}}
        <p class="bg-danger">
          {{.ErrorTitle}}: {{.ErrorMessage}}
        </p>
        {{end}}
    
        <form class="form" action="/article/create" method="POST">
          <div class="form-group">
            <label for="title">Username</label>
            <input type="text" class="form-control" id="title" name="title" placeholder="Title">
          </div>
          <div class="form-group">
            <label for="content">Password</label>
            <textarea name="content" class="form-control" rows="10" id="content" laceholder="Article Content"></textarea>
          </div>
          <button type="submit" class="btn btn-primary">Submit</button>
        </form>
      </div>
    </div>
    
    
    {{ template "footer.html" .}}

    2. templates/submission-successful.html

    This will be displayed after a successful article submission.

    <!--submission-successful.html-->
    
    {{ template "header.html" .}}
    
    <div>
      <strong>The article was successfully submitted.</strong>
    
      <a href="/article/view/{{.payload.ID}}">{{.payload.Title}}</a>
    </div>
    
    {{ template "footer.html" .}}

    We also need to modify templates/menu.html to display the Create Article link, as follows:

    <!--menu.html-->
    .
    .
        <ul class="nav navbar-nav">      
            <li><a href="/article/create">Create Article</a></li>
            .
    				.
        </ul>
    .
    .

    Implementing the Model

    The createNewArticle function should create a new article based on the input supplied to it, add it to the list of articles and return it. This can be done as shown below:

    // models.article.go
    
    func createNewArticle(title, content string) (*article, error) {
    	a := article{ID: len(articleList) + 1, Title: title, Content: content}
    
    	articleList = append(articleList, a)
    
    	return &a, nil
    }

    Now, run the following command in your shell to test that this function behaves as expected:

    go test -run=TestCreateNewArticle

    This test should run successfully and show the following message:

    PASS
    ok  	github.com/demo-apps/go-gin-app	0.006s
    

    Implementing the Route handlers

    The showArticleCreationPage handler is similar to the the previously created handlers that display static pages.

    The createArticle is similar to the register handler created in a previous section.

    The updated handlers.article.go file should contain the following code:

    // handlers.article.go
    
    func showArticleCreationPage(c *gin.Context) {
    	render(c, gin.H{
    		"title": "Create New Article"}, "create-article.html")
    }
    
    func createArticle(c *gin.Context) {
    	title := c.PostForm("title")
    	content := c.PostForm("content")
    
    	if a, err := createNewArticle(title, content); err == nil {
    		render(c, gin.H{
    			"title":   "Submission Successful",
    			"payload": a}, "submission-successful.html")
    	} else {
    		c.AbortWithStatus(http.StatusBadRequest)
    	}
    }

    After implementing these handlers, the tests should execute successfully and show the following result:

    === RUN   TestShowIndexPageUnauthenticated
    [GIN] 2016/09/04 - 10:41:55 | 200 |     183.623ยตs |  |   GET     /
    --- PASS: TestShowIndexPageUnauthenticated (0.00s)
    === RUN   TestArticleUnauthenticated
    [GIN] 2016/09/04 - 10:41:55 | 200 |      99.933ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleUnauthenticated (0.00s)
    === RUN   TestArticleListJSON
    [GIN] 2016/09/04 - 10:41:55 | 200 |      32.855ยตs |  |   GET     /
    --- PASS: TestArticleListJSON (0.00s)
    === RUN   TestArticleXML
    [GIN] 2016/09/04 - 10:41:55 | 200 |      34.698ยตs |  |   GET     /article/view/1
    --- PASS: TestArticleXML (0.00s)
    === RUN   TestArticleCreationAuthenticated
    [GIN] 2016/09/04 - 10:41:55 | 200 |     155.667ยตs |  |   POST    /article/create
    --- PASS: TestArticleCreationAuthenticated (0.00s)
    === RUN   TestShowRegistrationPageUnauthenticated
    [GIN] 2016/09/04 - 10:41:55 | 200 |     108.247ยตs |  |   GET     /u/register
    --- PASS: TestShowRegistrationPageUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticated
    [GIN] 2016/09/04 - 10:41:55 | 200 |     115.109ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticated (0.00s)
    === RUN   TestRegisterUnauthenticatedUnavailableUsername
    [GIN] 2016/09/04 - 10:41:55 | 400 |     121.615ยตs |  |   POST    /u/register
    --- PASS: TestRegisterUnauthenticatedUnavailableUsername (0.00s)
    === RUN   TestShowLoginPageUnauthenticated
    [GIN] 2016/09/04 - 10:41:55 | 200 |     139.911ยตs |  |   GET     /u/login
    --- PASS: TestShowLoginPageUnauthenticated (0.00s)
    === RUN   TestLoginUnauthenticated
    [GIN] 2016/09/04 - 10:41:55 | 200 |     105.532ยตs |  |   POST    /u/login
    --- PASS: TestLoginUnauthenticated (0.00s)
    === RUN   TestLoginUnauthenticatedIncorrectCredentials
    [GIN] 2016/09/04 - 10:41:55 | 400 |     138.814ยตs |  |   POST    /u/login
    --- PASS: TestLoginUnauthenticatedIncorrectCredentials (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
    

    Now that we’ve implemented quite a bit of functionality in the application, let’s add some authorization checks to some of the functionality.

    Adding Authorization Checks

    Currently, anyone can add an article, see the login, logout, register and the article creation page. In a real application, we’d want to ensure that only authenticated users are allowed to log out and create an article, while only unauthenticated users are allowed to register and log in.

    In this section, we’ll add authorization to our application to achieve this. Note that while we will be using the authentication status to implement authorization, we can choose to use a complex roles and permissions structure too.

    Understanding the Requirement for Authorization

    While we have added a lot of functionality to our application, there are some obvious drawbacks that we highlighted in the previous sections. For instance, the Create Article, Register, Login and Logout links and functionalities are available to all users, regardless of their authentication status. Implementing authentication and authorization allows us to deal with these issues.

    As mentioned earlier, we will use the authentication status for authorization. In more complex applications, you can define roles and permissions and base the authorization rules on that.

    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
    

    Using the Middleware

    Middleware can be used in Gin in a number of ways. You can apply them to a single route, a group of routes or to all routes depending on your requirements. In our case, we want to

    1. Use the setUserStatus middleware on all routes,
    2. Use the ensureLoggedIn middleware on routes that require authentication, and
    3. Use the ensureNotLoggedIn middleware on routes that require users to be unauthenticated.

    Since we want to use the setUserStatus middleware on all routes, we can use the Use method of the router:

    router.Use(setUserStatus())

    In all the routes where we want to use a particular middleware, we can place it before the route handler in the route definition. For example, since we want to ensure that only authenticated users can see the Create Article page, we can modify that route definition from

    articleRoutes.GET("/create", showArticleCreationPage)

    to

    articleRoutes.GET("/create", ensureLoggedIn(), showArticleCreationPage)

    The updated routes.go file should have the following content:

    // routes.go
    
    package main
    
    func initializeRoutes() {
    
    	router.Use(setUserStatus())
    
    	router.GET("/", showIndexPage)
    
    	userRoutes := router.Group("/u")
    	{
    		userRoutes.GET("/login", ensureNotLoggedIn(), showLoginPage)
    
    		userRoutes.POST("/login", ensureNotLoggedIn(), performLogin)
    
    		userRoutes.GET("/logout", ensureLoggedIn(), logout)
    
    		userRoutes.GET("/register", ensureNotLoggedIn(), showRegistrationPage)
    
    		userRoutes.POST("/register", ensureNotLoggedIn(), register)
    	}
    
    	articleRoutes := router.Group("/article")
    	{
    		articleRoutes.GET("/view/:article_id", getArticle)
    
    		articleRoutes.GET("/create", ensureLoggedIn(), showArticleCreationPage)
    
    		articleRoutes.POST("/create", ensureLoggedIn(), createArticle)
    	}
    }

    After updating the routes, you should notice that the unauthenticated users won’t see the Create Article and Logout pages, and authenticated users won’t see the Register and Login pages.

    Now that we have implemented an authorization scheme, the only thing left to do is to ensure that the user interface reflects this scheme.

    We can use the is_logged_in variable in the menu.html to ensure that the appropriate links are displayed based on the user’s authentication status. The updated template will be as follows:

    <!--menu.html-->
    
    <nav class="navbar navbar-default">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="/">
            Home
          </a>
        </div>
        <ul class="nav navbar-nav">
          {{ if .is_logged_in }}
            <!--Display this link only when the user is logged in-->
            <li><a href="/article/create">Create Article</a></li>
          {{end}}
          {{ if not .is_logged_in }}
            <!--Display this link only when the user is not logged in-->
            <li><a href="/u/register">Register</a></li>
          {{end}}
          {{ if not .is_logged_in }}
            <!--Display this link only when the user is not logged in-->
            <li><a href="/u/login">Login</a></li>
          {{end}}
          {{ if .is_logged_in }}
            <!--Display this link only when the user is logged in-->
            <li><a href="/u/logout">Logout</a></li>
          {{end}}
        </ul>
      </div>
    </nav>

    With these changes, the Create Article and Logout links will be visible only to users who are logged in while the Register and the Login links will be visible only to unauthenticated users.

    Running, Testing and Continuously Building the Application with Semaphore

    Now that the application is ready, you can run it to see how it works.

    Starting the Application

    To start the application, go to your application directory and execute the following command:

    go build -o app

    This will build your application and create an executable named app which you can run using this command:

    ./app

    Your application should start serving on port 8080.

    Accessing the Application from the Browser

    Once you have started your application, it can be accessed in your browser at the following URL:

    http://localhost:8080/
    

    This will display the index page with the hard-coded list of articles as follows:

    Default index page

    As you can see in the menu on top, you have the option to register and login.

    Once you either register or log in, you will notice that the menu changes. You will see links to create a new article and to logout, as follows:

    Default index page after logging in

    Implementing Continuous Build Using Semaphore

    In just a few quick steps, you can use Semaphore to automatically build and test your code as soon as you push it to your repository. Here’s how you can add your GitHub or Bitbucket project and set up a Go project on Semaphore.

    The default configuration for a Go project takes care of the following:

    • Fetching the dependencies,
    • Building the project, and
    • Running the tests without any special tags.

    For this application, the default setup is all that is required. Just make sure that you choose the latest available version of Go. Once you’ve completed this process, you’ll be able to see the status of the latest builds and tests on your Semaphore dashboard. Every time you push any new changes, Semaphore will automatically detect that, build your project, run the tests and display the results on your dashboard.

    If you dig deeper and take a closer look at these build results, you’ll see the test results. For successful builds, the test results will be similar to the results we saw in the previous section. In case of failures, you’ll be able to see exactly which test failed.

    Next Steps

    Once you’ve built an application, you might want to automate the tasks of testing and deploying it every time it is updated. You can read more about implementing integration testing in Go microservices in this tutorial. Should you be interested, you can also read our tutorial about building and deploying Go web applications with Docker.

    Conclusion

    In this tutorial, we learned how to create a web application using the Gin framework. We also saw how we can add user management and authorization to our application to make them more user-friendly and robust.

    The code for the entire application is available in this Github repository.

    You should now be ready to use Gin to build high quality, well-tested web applications and microservices. If you have any questions, feel free to post them in the comments below.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Kulshekhar is an independent full stack engineer with 10+ years of experience in designing, developing, deploying and maintaining web applications.