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:
- To have gone through the first part of the tutorial, and
- Go installed on your machine.
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 aRegister
menu, andtemplates/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 & 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, andtemplates/menu.html
will be updated with theLogin
andLogout
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, andtemplates/menu.html
will be updated with the newCreate 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:
- 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
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
- Use the
setUserStatus
middleware on all routes, - Use the
ensureLoggedIn
middleware on routes that require authentication, and - 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.
Modifying the Menu to Display Links Based on the Authorization Status
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:
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:
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.