CRUD RESTful API with Go, GORM, JWT, MySQL and Testing

Go, also known as Golang, is a programming language developed by Google. It was created by Robert Griesemer, Rob Pike, and Ken Thompson and was first announced in 2009. Go was designed with the goal of providing a simple and efficient programming language that is easy to learn, write, and read.

In this blog post:

We will build a blog application where a user can:

  • Signup (Register)
  • Edit his account
  • Shutdown (Delete his account)
  • Create a blog post
  • Edit blog post created by him
  • View all blog posts
  • View a particular blog post
  • View other blog posts published by other users
  • Delete blog post created by him

This API will be built with:

  • Go
  • GORM (A Golang ORM)
  • JWT
  • Mysql
  • Gorilla Mux (For HTTP routing and URL matcher)

All methods and endpoints will tested in in the next post. Table tests will also be used to test every possible case for a particular functionality.

In Future Articles:

This article is the first part. There will be future articles that will explain how to:

  • Creating The API
  • Testing the API
  • Dockerize the API
  • Deploy on Kubernetes

In case you just want to see the code, check this at github

Step 1. Setup Go Project and create Database

Create the directory the project will live in. This can be created anywhere on your computer. Let’s call it skillpedia

mkdir skillpedia && cd skillpedia

Initiate go modules which makes dependency version information explicit and easier to manage

go mod init github.com/{username}/{projectdir}

Where {username} is your github username and {projectdir} is the directory created above. For my case:

go mod init github.com/Sangwan70/skillpedia

We will have to use third party packages in this application. If you are setting this for the first time, run the following commands:

go get github.com/badoux/checkmail
go get github.com/jinzhu/gorm
go get golang.org/x/crypto/bcrypt
go get github.com/dgrijalva/jwt-go
go get github.com/gorilla/mux
go get github.com/jinzhu/gorm/dialects/mysql
go get github.com/joho/godotenv
go get gopkg.in/go-playground/assert.v1
go get golang.org/x/text

This command add all modules to go.mod in the project directory. Your go.mod will look like:

Next, create two directories, api and tests, inside the skillpedia directory

mkdir api tests

Create the .env file to setup environmental variables required by our project:

# vi .env
# DB_HOST=skillpedia-mysql   # will be required for docker container                   
DB_HOST=127.0.0.1         
DB_DRIVER=mysql
API_SECRET=8tY6ew32U      # Any Random string Required for JWT.
DB_USER=sangwan
DB_PASSWORD=skpDBPass#12
DB_NAME=skillpedia_api
DB_PORT=3306

# MySQL Test
# TEST_DB_HOST=mysql_test  # will be required for docker container                     
TEST_DB_HOST=127.0.0.1
TEST_DB_DRIVER=mysql
TEST_API_SECRET=8tY6ew32U
TEST_DB_USER=sangwan
TEST_DB_PASSWORD=skpDBPass#12
TEST_DB_NAME=skillpedia_api_test
TEST_DB_PORT=3306

Note the MySQL database connection details. You will have to create a MySQL Database with same name, user id and password as given.

You can user this script to install mysql on RHEL 8/Rocky Linux 8. Simply download and run the script as root user.

After logging in as root user to mysql, use the following commands to create required users and databases:

mysql> create user sangwan@localhost identified by 'skpDBPass#12';
Query OK, 0 rows affected (0.05 sec)

 

mysql> create database skillpedia_api;
Query OK, 1 row affected (0.01 sec)

 

mysql> create database skillpedia_api_test;
Query OK, 1 row affected (0.00 sec)

 

mysql> grant all privileges on skillpedia_api.* to sangwan@localhost;
Query OK, 0 rows affected (0.09 sec)

 

mysql> grant all privileges on skillpedia_api_test.* to sangwan@localhost;
Query OK, 0 rows affected (0.00 sec)

At this point, this is the structure we have:

[root@server skillpedia]# tree -a
.
├── api
├── .env
├── go.mod
├── go.sum
└── tests

Step 2: Create Models

Create a directory, modules inside the api

mkdir api/models

For our blog, we will have two model files: “User” to handle business logic for users and “Post” to handle business logic for posts.

The User model:

Create the user model in api/models:

vi api/models/User.go

package models
import (
   "errors"
   "html"
   "log"
   "strings"
   "time"
   "github.com/badoux/checkmail"
   "github.com/jinzhu/gorm"
   "golang.org/x/crypto/bcrypt"
)
type User struct {
   ID        uint32    `gorm:"primary_key;auto_increment" json:"id"`
   Nickname  string    `gorm:"size:255;not null;unique" json:"nickname"`
   Email     string    `gorm:"size:100;not null;unique" json:"email"`
   Password  string    `gorm:"size:100;not null;" json:"password"`
   CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
   UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
}
func Hash(password string) ([]byte, error) {
   return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 }
func VerifyPassword(hashedPassword, password string) error {
      return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
func (u *User) BeforeSave() error {
      hashedPassword, err := Hash(u.Password)
      if err != nil {
            return err
      }
      u.Password = string(hashedPassword)
      return nil
}
func (u *User) Prepare() {
      u.ID = 0
      u.Nickname = html.EscapeString(strings.TrimSpace(u.Nickname))
      u.Email = html.EscapeString(strings.TrimSpace(u.Email))
      u.CreatedAt = time.Now()
      u.UpdatedAt = time.Now()
}
func (u *User) Validate(action string) error {
      switch strings.ToLower(action) {
      case "update":
            if u.Nickname == "" {
                  return errors.New("Required Nickname")
            }
            if u.Password == "" {
                  return errors.New("Required Password")
            }
            if u.Email == "" {
                  return errors.New("Required Email")
            }
            if err := checkmail.ValidateFormat(u.Email); err != nil {
                  return errors.New("Invalid Email")
            }
            return nil
      case "login":
            if u.Password == "" {
                  return errors.New("Required Password")
            }
            if u.Email == "" {
                  return errors.New("Required Email")
            }
            if err := checkmail.ValidateFormat(u.Email); err != nil {
                  return errors.New("Invalid Email")
            }
            return nil
      default:
            if u.Nickname == "" {
                  return errors.New("Required Nickname")
            }
            if u.Password == "" {
                  return errors.New("Required Password")
            }
            if u.Email == "" {
                  return errors.New("Required Email")
            }
            if err := checkmail.ValidateFormat(u.Email); err != nil {
                  return errors.New("Invalid Email")
            }
            return nil
      }
}
func (u *User) SaveUser(db *gorm.DB) (*User, error) {
      var err error
      err = db.Debug().Create(&u).Error
      if err != nil {
            return &User{}, err
      }
      return u, nil
}
func (u *User) FindAllUsers(db *gorm.DB) (*[]User, error) {
      var err error
      users := []User{}
      err = db.Debug().Model(&User{}).Limit(100).Find(&users).Error
      if err != nil {
            return &[]User{}, err
      }
      return &users, err
}

func (u *User) FindUserByID(db *gorm.DB, uid uint32) (*User, error) {
      var err error
      err = db.Debug().Model(User{}).Where("id = ?", uid).Take(&u).Error
      if err != nil {
            return &User{}, err
      }
      if gorm.IsRecordNotFoundError(err) {
            return &User{}, errors.New("User Not Found")
      }
      return u, err
}

func (u *User) UpdateAUser(db *gorm.DB, uid uint32) (*User, error) {
      // To hash the password
      err := u.BeforeSave()
     if err != nil {
           log.Fatal(err)
     }
      db = db.Debug().Model(&User{}).Where("id = ?", uid).Take(&User{}).UpdateColumns(
            map[string]interface{}{
                  "password":  u.Password,
                  "nickname":  u.Nickname,
                  "email":     u.Email,
                  "update_at": time.Now(),
            },
      )
      if db.Error != nil {
            return &User{}, db.Error
      }
      // This is the display the updated user
      err = db.Debug().Model(&User{}).Where("id = ?", uid).Take(&u).Error
      if err != nil {
            return &User{}, err
      }
      return u, nil
}

func (u *User) DeleteAUser(db *gorm.DB, uid uint32) (int64, error) {

      db = db.Debug().Model(&User{}).Where("id = ?", uid).Take(&User{}).Delete(&User{})

      if db.Error != nil {

            return 0, db.Error

      }

      return db.RowsAffected, nil

}

 

The Post model:

Create the post model in the path api/models

vi api/models/Post.go

package models

 

import (

   "errors"

   "html"

   "strings"

   "time"

 

   "github.com/jinzhu/gorm"

)

 

type Post struct {

   ID        uint64    `gorm:"primary_key;auto_increment" json:"id"`

   Title     string    `gorm:"size:255;not null;unique" json:"title"`

   Content   string    `gorm:"size:255;not null;" json:"content"`

   Author    User      `json:"author"`

   AuthorID  uint32    `gorm:"not null" json:"author_id"`

   CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`

   UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`

}

 

func (p *Post) Prepare() {

   p.ID = 0

   p.Title = html.EscapeString(strings.TrimSpace(p.Title))

   p.Content = html.EscapeString(strings.TrimSpace(p.Content))

   p.Author = User{}

   p.CreatedAt = time.Now()

   p.UpdatedAt = time.Now()

}

 

func (p *Post) Validate() error {

 

   if p.Title == "" {

      return errors.New("Required Title")

   }

   if p.Content == "" {

      return errors.New("Required Content")

   }

   if p.AuthorID < 1 {

      return errors.New("Required Author")

   }

   return nil

}

 

func (p *Post) SavePost(db *gorm.DB) (*Post, error) {

   var err error

   err = db.Debug().Model(&Post{}).Create(&p).Error

   if err != nil {

      return &Post{}, err

   }

   if p.ID != 0 {

      err = db.Debug().Model(&User{}).Where("id = ?", p.AuthorID).Take(&p.Author).Error

      if err != nil {

            return &Post{}, err

      }

   }

   return p, nil

}

 

func (p *Post) FindAllPosts(db *gorm.DB) (*[]Post, error) {

   var err error

   posts := []Post{}

   err = db.Debug().Model(&Post{}).Limit(100).Find(&posts).Error

   if err != nil {

      return &[]Post{}, err

   }

   if len(posts) > 0 {

      for i, _ := range posts {

            err := db.Debug().Model(&User{}).Where("id = ?", posts[i].AuthorID).Take(&posts[i].Author).Error

            if err != nil {

                  return &[]Post{}, err

            }

      }

   }

   return &posts, nil

}

 

func (p *Post) FindPostByID(db *gorm.DB, pid uint64) (*Post, error) {

   var err error

   err = db.Debug().Model(&Post{}).Where("id = ?", pid).Take(&p).Error

   if err != nil {

      return &Post{}, err

   }

   if p.ID != 0 {

      err = db.Debug().Model(&User{}).Where("id = ?", p.AuthorID).Take(&p.Author).Error

      if err != nil {

            return &Post{}, err

      }

   }

   return p, nil

}

 

func (p *Post) UpdateAPost(db *gorm.DB, pid uint64) (*Post, error) {

 

   var err error

   db = db.Debug().Model(&Post{}).Where("id = ?", pid).Take(&Post{}).UpdateColumns(

      map[string]interface{}{

            "title":      p.Title,

            "content":    p.Content,

            "updated_at": time.Now(),

      },

   )

   err = db.Debug().Model(&Post{}).Where("id = ?", pid).Take(&p).Error

   if err != nil {

      return &Post{}, err

   }

   if p.ID != 0 {

      err = db.Debug().Model(&User{}).Where("id = ?", p.AuthorID).Take(&p.Author).Error

      if err != nil {

            return &Post{}, err

      }

   }

   return p, nil

}

 

func (p *Post) DeleteAPost(db *gorm.DB, pid uint64, uid uint32) (int64, error) {

 

   db = db.Debug().Model(&Post{}).Where("id = ? and author_id = ?", pid, uid).Take(&Post{}).Delete(&Post{})

 

   if db.Error != nil {

      if gorm.IsRecordNotFoundError(db.Error) {

            return 0, errors.New("Post not found")

      }

      return 0, db.Error

   }

   return db.RowsAffected, nil

}

Step 3. Create a Custom Response

Before we create controllers that will interact with models defined above, we will create a custom response package. This will be used for the http responses.

Inside the api directory, create the responses directory

mkdir api/responses

Then create the json.go file in the path: api/responses

vi api/responses/json.go

package responses

 

import (

        "encoding/json"

        "fmt"

        "net/http"

)

 

func JSON(w http.ResponseWriter, statusCode int, data interface{}) {

        w.WriteHeader(statusCode)

        err := json.NewEncoder(w).Encode(data)

        if err != nil {

                fmt.Fprintf(w, "%s", err.Error())

        }

}

 

func ERROR(w http.ResponseWriter, statusCode int, err error) {

        if err != nil {

                JSON(w, statusCode, struct {

                        Error string `json:"error"`

                }{

                        Error: err.Error(),

                })

                return

        }

        JSON(w, http.StatusBadRequest, nil)

}

Step 4. Create JSON Web Tokens (JWT)

Remember that users need to be authenticated before they can: Update or Shutdown their accounts, Create, Update, and Delete Posts.

Let’s write a package that will help us generate a JWT token that will enable the user to perform the above actions.

Create the auth package (directory) inside the api directory.

mkdir api/auth

Create the token.go file in the path: api/auth

vi api/auth/token.go

package auth

 

import (

      "encoding/json"

      "fmt"

      "log"

      "net/http"

      "os"

      "strconv"

      "strings"

      "time"

 

      jwt "github.com/dgrijalva/jwt-go"

)

 

func CreateToken(user_id uint32) (string, error) {

      claims := jwt.MapClaims{}

      claims["authorized"] = true

      claims["user_id"] = user_id

      claims["exp"] = time.Now().Add(time.Hour * 1).Unix() //Token expires after 1 hour

      token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

      return token.SignedString([]byte(os.Getenv("API_SECRET")))

 

}

 

func TokenValid(r *http.Request) error {

      tokenString := ExtractToken(r)

      token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {

            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

                  return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])

            }

            return []byte(os.Getenv("API_SECRET")), nil

      })

      if err != nil {

            return err

      }

      if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {

            Pretty(claims)

      }

      return nil

}

 

func ExtractToken(r *http.Request) string {

      keys := r.URL.Query()

      token := keys.Get("token")

      if token != "" {

            return token

      }

      bearerToken := r.Header.Get("Authorization")

      if len(strings.Split(bearerToken, " ")) == 2 {

            return strings.Split(bearerToken, " ")[1]

      }

      return ""

}

 

func ExtractTokenID(r *http.Request) (uint32, error) {

 

      tokenString := ExtractToken(r)

      token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {

            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

                  return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])

            }

            return []byte(os.Getenv("API_SECRET")), nil

      })

      if err != nil {

            return 0, err

      }

      claims, ok := token.Claims.(jwt.MapClaims)

      if ok && token.Valid {

            uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32)

            if err != nil {

                  return 0, err

            }

            return uint32(uid), nil

      }

      return 0, nil

}

 

//Pretty display the claims licely in the terminal

func Pretty(data interface{}) {

      b, err := json.MarshalIndent(data, "", " ")

      if err != nil {

            log.Println(err)

            return

      }

 

      fmt.Println(string(b))

}

 

Observe from the token.go file that we assigned the import: "github.com/dgrijalva/jwt-go" to jwt.

Step 5. Create Middlewares

We will create two main middlewares:

  • SetMiddlewareJSON: This will format all responses to JSON
  • SetMiddlewareAuthentication: This will check for the validity of the authentication token provided.

create the middlewares package(directory) inside the api directory,

mkdir api/middlewares

Then create a middlewares.go file in the path api/middlewares

vi api/middlewares/middlewares.go

package middlewares

 

import (

        "errors"

        "net/http"

 

        "github.com/Sangwan70/skillpedia/api/auth"

        "github.com/Sangwan70/skillpedia/api/responses"

)

 

func SetMiddlewareJSON(next http.HandlerFunc) http.HandlerFunc {

        return func(w http.ResponseWriter, r *http.Request) {

                w.Header().Set("Content-Type", "application/json")

                next(w, r)

        }

}

 

func SetMiddlewareAuthentication(next http.HandlerFunc) http.HandlerFunc {

        return func(w http.ResponseWriter, r *http.Request) {

                err := auth.TokenValid(r)

                if err != nil {

                        responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))

                        return

                }

                next(w, r)

        }

}

Step 6. Create Custom Error Handling

To format some error messages in a more readable manner, we need to a create a package to help us achieve that.

Create a directory utils,and the package formaterror, inside this directory.

mkdir -p api/utils/formaterror

Then create a formaterror.go file in the path: api/utils/formaterror

vi api/utils/formaterror/formaterror.go

package formaterror

 

import (

        "errors"

        "strings"

)

 

func FormatError(err string) error {

 

        if strings.Contains(err, "nickname") {

                return errors.New("Nickname Already Taken")

        }

 

        if strings.Contains(err, "email") {

                return errors.New("Email Already Taken")

        }

 

        if strings.Contains(err, "title") {

                return errors.New("Title Already Taken")

        }

        if strings.Contains(err, "hashedPassword") {

                return errors.New("Incorrect Password")

        }

        return errors.New("Incorrect Details")

}

Step 7. Create Controllers

Now let's create the controllers package that will interact with our models package:

Create the controllers (directory) inside the api directory,

mkdir api/controllers

Create base.go  in api/controllers  for our database connection information, initialise our routes, and start our server

vi api/controllers/base.go

package controllers

 

import (

      "fmt"

      "log"

      "net/http"

 

      "github.com/gorilla/mux"

      "github.com/jinzhu/gorm"

        _ "github.com/badoux/checkmail"

        _ "github.com/jinzhu/gorm"

        _ "golang.org/x/crypto/bcrypt"

        _ "github.com/dgrijalva/jwt-go"

        _ "github.com/gorilla/mux"

        _ "github.com/joho/godotenv"

        _ "golang.org/x/text"

        _ "gopkg.in/go-playground/assert.v1"

      _ "github.com/jinzhu/gorm/dialects/mysql"   

      "github.com/Sangwan70/skillpedia/api/models"

)

 

type Server struct {

      DB     *gorm.DB

      Router *mux.Router

}

 

func (server *Server) Initialize(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) {

 

      var err error

 

      if Dbdriver == "mysql" {

            DBURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", DbUser, DbPassword, DbHost, DbPort, DbName)

            server.DB, err = gorm.Open(Dbdriver, DBURL)

            if err != nil {

                  fmt.Printf("Cannot connect to %s database", Dbdriver)

                  log.Fatal("This is the error:", err)

            } else {

                  fmt.Printf("We are connected to the %s database", Dbdriver)

            }

      }

      if Dbdriver == "postgres" {

            DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword)

            server.DB, err = gorm.Open(Dbdriver, DBURL)

            if err != nil {

                  fmt.Printf("Cannot connect to %s database", Dbdriver)

                  log.Fatal("This is the error:", err)

            } else {

                  fmt.Printf("We are connected to the %s database", Dbdriver)

            }

      }

 

      server.DB.Debug().AutoMigrate(&models.User{}, &models.Post{}) //database migration

 

      server.Router = mux.NewRouter()

 

      server.initializeRoutes()

}

 

func (server *Server) Run(addr string) {

      fmt.Println("Listening to port 8088")

      log.Fatal(http.ListenAndServe(addr, server.Router))

}

Note the underscore we used when we imported the mysql package and that we called the initializeRoutes() method. It will be defined in the routes.go file shortly.

Let’s create a home_controller.go file to welcome us to the API in api/controllers

vi api/controllers/home_controller.go

package controllers

 

import (

        "net/http"

 

        "github.com/Sangwan70/skillpedia/api/responses"

)

 

func (server *Server) Home(w http.ResponseWriter, r *http.Request) {

        responses.JSON(w, http.StatusOK, "Welcome To This Awesome API")

 

}

Create the users_controller.go file in api/controllers

vi api/controllers/users_controller.go

package controllers

 

import (

      "encoding/json"

      "errors"

      "fmt"

      "io/ioutil"

      "net/http"

      "strconv"

 

      "github.com/gorilla/mux"

      "github.com/Sangwan70/skillpedia/api/auth"

      "github.com/Sangwan70/skillpedia/api/models"

      "github.com/Sangwan70/skillpedia/api/responses"

      "github.com/Sangwan70/skillpedia/api/utils/formaterror"

)

 

func (server *Server) CreateUser(w http.ResponseWriter, r *http.Request) {

 

      body, err := ioutil.ReadAll(r.Body)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

      }

      user := models.User{}

      err = json.Unmarshal(body, &user)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      user.Prepare()

      err = user.Validate("")

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      userCreated, err := user.SaveUser(server.DB)

 

      if err != nil {

 

            formattedError := formaterror.FormatError(err.Error())

 

            responses.ERROR(w, http.StatusInternalServerError, formattedError)

            return

      }

      w.Header().Set("Location", fmt.Sprintf("%s%s/%d", r.Host, r.RequestURI, userCreated.ID))

      responses.JSON(w, http.StatusCreated, userCreated)

}

 

func (server *Server) GetUsers(w http.ResponseWriter, r *http.Request) {

 

      user := models.User{}

 

      users, err := user.FindAllUsers(server.DB)

      if err != nil {

            responses.ERROR(w, http.StatusInternalServerError, err)

            return

      }

      responses.JSON(w, http.StatusOK, users)

}

 

func (server *Server) GetUser(w http.ResponseWriter, r *http.Request) {

 

      vars := mux.Vars(r)

      uid, err := strconv.ParseUint(vars["id"], 10, 32)

      if err != nil {

            responses.ERROR(w, http.StatusBadRequest, err)

            return

      }

      user := models.User{}

      userGotten, err := user.FindUserByID(server.DB, uint32(uid))

      if err != nil {

            responses.ERROR(w, http.StatusBadRequest, err)

            return

      }

      responses.JSON(w, http.StatusOK, userGotten)

}

 

func (server *Server) UpdateUser(w http.ResponseWriter, r *http.Request) {

 

      vars := mux.Vars(r)

      uid, err := strconv.ParseUint(vars["id"], 10, 32)

      if err != nil {

            responses.ERROR(w, http.StatusBadRequest, err)

            return

      }

      body, err := ioutil.ReadAll(r.Body)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      user := models.User{}

      err = json.Unmarshal(body, &user)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      tokenID, err := auth.ExtractTokenID(r)

      if err != nil {

            responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))

            return

      }

      if tokenID != uint32(uid) {

            responses.ERROR(w, http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized)))

            return

      }

      user.Prepare()

      err = user.Validate("update")

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      updatedUser, err := user.UpdateAUser(server.DB, uint32(uid))

      if err != nil {

            formattedError := formaterror.FormatError(err.Error())

            responses.ERROR(w, http.StatusInternalServerError, formattedError)

            return

      }

      responses.JSON(w, http.StatusOK, updatedUser)

}

 

func (server *Server) DeleteUser(w http.ResponseWriter, r *http.Request) {

 

      vars := mux.Vars(r)

 

      user := models.User{}

 

      uid, err := strconv.ParseUint(vars["id"], 10, 32)

      if err != nil {

            responses.ERROR(w, http.StatusBadRequest, err)

            return

      }

      tokenID, err := auth.ExtractTokenID(r)

      if err != nil {

            responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))

            return

      }

      if tokenID != uint32(uid) {

            responses.ERROR(w, http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized)))

            return

      }

      _, err = user.DeleteAUser(server.DB, uint32(uid))

      if err != nil {

            responses.ERROR(w, http.StatusBadRequest, err)

            return

      }

      w.Header().Set("Entity", fmt.Sprintf("%d", uid))

      responses.JSON(w, http.StatusNoContent, "")

}

Create the posts_controller.go file in api/controllers

vi api/controllers/posts_controller.go

package controllers

 

import (

      "encoding/json"

      "errors"

      "fmt"

      "io/ioutil"

      "net/http"

      "strconv"

 

      "github.com/gorilla/mux"

      "github.com/Sangwan70/skillpedia/api/auth"

      "github.com/Sangwan70/skillpedia/api/models"

      "github.com/Sangwan70/skillpedia/api/responses"

      "github.com/Sangwan70/skillpedia/api/utils/formaterror"

)

 

func (server *Server) CreatePost(w http.ResponseWriter, r *http.Request) {

 

      body, err := ioutil.ReadAll(r.Body)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      post := models.Post{}

      err = json.Unmarshal(body, &post)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      post.Prepare()

      err = post.Validate()

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      uid, err := auth.ExtractTokenID(r)

      if err != nil {

            responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))

            return

      }

      if uid != post.AuthorID {

            responses.ERROR(w, http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized)))

            return

      }

      postCreated, err := post.SavePost(server.DB)

      if err != nil {

            formattedError := formaterror.FormatError(err.Error())

            responses.ERROR(w, http.StatusInternalServerError, formattedError)

            return

      }

      w.Header().Set("Lacation", fmt.Sprintf("%s%s/%d", r.Host, r.URL.Path, postCreated.ID))

      responses.JSON(w, http.StatusCreated, postCreated)

}

 

func (server *Server) GetPosts(w http.ResponseWriter, r *http.Request) {

 

      post := models.Post{}

 

      posts, err := post.FindAllPosts(server.DB)

      if err != nil {

            responses.ERROR(w, http.StatusInternalServerError, err)

            return

      }

      responses.JSON(w, http.StatusOK, posts)

}

 

func (server *Server) GetPost(w http.ResponseWriter, r *http.Request) {

 

      vars := mux.Vars(r)

      pid, err := strconv.ParseUint(vars["id"], 10, 64)

      if err != nil {

            responses.ERROR(w, http.StatusBadRequest, err)

            return

      }

      post := models.Post{}

 

      postReceived, err := post.FindPostByID(server.DB, pid)

      if err != nil {

            responses.ERROR(w, http.StatusInternalServerError, err)

            return

      }

      responses.JSON(w, http.StatusOK, postReceived)

}

 

func (server *Server) UpdatePost(w http.ResponseWriter, r *http.Request) {

 

      vars := mux.Vars(r)

      pid, err := strconv.ParseUint(vars["id"], 10, 64)

      if err != nil {

            responses.ERROR(w, http.StatusBadRequest, err)

            return

      }

      body, err := ioutil.ReadAll(r.Body)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      post := models.Post{}

      err = json.Unmarshal(body, &post)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      post.Prepare()

      err = post.Validate()

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      uid, err := auth.ExtractTokenID(r)

      if err != nil {

            responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))

            return

      }

      if uid != post.AuthorID {

            responses.ERROR(w, http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized)))

            return

      }

      postUpdated, err := post.UpdateAPost(server.DB, pid)

 

      if err != nil {

            formattedError := formaterror.FormatError(err.Error())

            responses.ERROR(w, http.StatusInternalServerError, formattedError)

            return

      }

      responses.JSON(w, http.StatusOK, postUpdated)

}

 

func (server *Server) DeletePost(w http.ResponseWriter, r *http.Request) {

 

      vars := mux.Vars(r)

 

      post := models.Post{}

 

      pid, err := strconv.ParseUint(vars["id"], 10, 64)

      if err != nil {

            responses.ERROR(w, http.StatusBadRequest, err)

            return

      }

      uid, err := auth.ExtractTokenID(r)

      if err != nil {

            responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))

            return

      }

      _, err = post.DeleteAPost(server.DB, pid, uid)

      if err != nil {

            responses.ERROR(w, http.StatusBadRequest, err)

            return

      }

      w.Header().Set("Entity", fmt.Sprintf("%d", pid))

      responses.JSON(w, http.StatusNoContent, "")

}

Create the login_controller.go file in api/controllers

vi api/controllers/login_controller.go

package controllers

 

import (

      "encoding/json"

      "io/ioutil"

      "net/http"

 

      "github.com/Sangwan70/skillpedia/api/auth"

      "github.com/Sangwan70/skillpedia/api/models"

      "github.com/Sangwan70/skillpedia/api/responses"

      "github.com/Sangwan70/skillpedia/api/utils/formaterror"

      "golang.org/x/crypto/bcrypt"

)

 

func (server *Server) Login(w http.ResponseWriter, r *http.Request) {

      body, err := ioutil.ReadAll(r.Body)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      user := models.User{}

      err = json.Unmarshal(body, &user)

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

 

      user.Prepare()

      err = user.Validate("login")

      if err != nil {

            responses.ERROR(w, http.StatusUnprocessableEntity, err)

            return

      }

      token, err := server.SignIn(user.Email, user.Password)

      if err != nil {

            formattedError := formaterror.FormatError(err.Error())

            responses.ERROR(w, http.StatusUnprocessableEntity, formattedError)

            return

      }

      responses.JSON(w, http.StatusOK, token)

}

 

func (server *Server) SignIn(email, password string) (string, error) {

 

      var err error

 

      user := models.User{}

 

      err = server.DB.Debug().Model(models.User{}).Where("email = ?", email).Take(&user).Error

      if err != nil {

            return "", err

      }

      err = models.VerifyPassword(user.Password, password)

      if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {

            return "", err

      }

      return auth.CreateToken(user.ID)

}

Light up the whole place up by creating the routes.go file in api/controllers

vi api/controllers/routes.go

package controllers

 

import "github.com/Sangwan70/skillpedia/api/middlewares"

 

func (s *Server) initializeRoutes() {

 

        // Home Route

        s.Router.HandleFunc("/", middlewares.SetMiddlewareJSON(s.Home)).Methods("GET")

 

        // Login Route

        s.Router.HandleFunc("/login", middlewares.SetMiddlewareJSON(s.Login)).Methods("POST")

 

        //Users routes

        s.Router.HandleFunc("/users", middlewares.SetMiddlewareJSON(s.CreateUser)).Methods("POST")

        s.Router.HandleFunc("/users", middlewares.SetMiddlewareJSON(s.GetUsers)).Methods("GET")

        s.Router.HandleFunc("/users/{id}", middlewares.SetMiddlewareJSON(s.GetUser)).Methods("GET")

        s.Router.HandleFunc("/users/{id}", middlewares.SetMiddlewareJSON(middlewares.SetMiddlewareAuthentication(s.UpdateUser))).Methods("PUT")

        s.Router.HandleFunc("/users/{id}", middlewares.SetMiddlewareAuthentication(s.DeleteUser)).Methods("DELETE")

 

        //Posts routes

        s.Router.HandleFunc("/posts", middlewares.SetMiddlewareJSON(s.CreatePost)).Methods("POST")

        s.Router.HandleFunc("/posts", middlewares.SetMiddlewareJSON(s.GetPosts)).Methods("GET")

        s.Router.HandleFunc("/posts/{id}", middlewares.SetMiddlewareJSON(s.GetPost)).Methods("GET")

        s.Router.HandleFunc("/posts/{id}", middlewares.SetMiddlewareJSON(middlewares.SetMiddlewareAuthentication(s.UpdatePost))).Methods("PUT")

        s.Router.HandleFunc("/posts/{id}", middlewares.SetMiddlewareAuthentication(s.DeletePost)).Methods("DELETE")

}

Step 8: Seeding the Database with Dummy Data

If you wish, you can add dummy data to the database before adding a real one.

Let’s create a seed package to achieve that in the path api/.

mkdir api/seed

Then create the seeder.go file:

vi api/seed/seeder.go

package seed

 

import (

      "log"

 

      "github.com/jinzhu/gorm"

      "github.com/Sangwan70/skillpedia/api/models"

)

 

var users = []models.User{

      models.User{

            Nickname: "Ram N Sangwan",

            Email:    "ramnsangwan@gmail.com",

            Password: "password",

      },

      models.User{

            Nickname: "Kapil Dev",

            Email:    "kapil@gmail.com",

            Password: "password",

      },

}

 

var posts = []models.Post{

      models.Post{

            Title:   "The SkillPedia Launched",

            Content: "A Great Portal to learn and Teach - www.theskillpedia.com",

      },

      models.Post{

            Title:   "Alliance Software Technology",

            Content: "Providing Consulting to IT Companies.",

      },

}

 

func Load(db *gorm.DB) {

 

      err := db.Debug().DropTableIfExists(&models.Post{}, &models.User{}).Error

      if err != nil {

            log.Fatalf("cannot drop table: %v", err)

      }

      err = db.Debug().AutoMigrate(&models.User{}, &models.Post{}).Error

      if err != nil {

            log.Fatalf("cannot migrate table: %v", err)

      }

 

      err = db.Debug().Model(&models.Post{}).AddForeignKey("author_id", "users(id)", "cascade", "cascade").Error

      if err != nil {

            log.Fatalf("attaching foreign key error: %v", err)

      }

 

      for i, _ := range users {

            err = db.Debug().Model(&models.User{}).Create(&users[i]).Error

            if err != nil {

                  log.Fatalf("cannot seed users table: %v", err)

            }

            posts[i].AuthorID = users[i].ID

 

            err = db.Debug().Model(&models.Post{}).Create(&posts[i]).Error

            if err != nil {

                  log.Fatalf("cannot seed posts table: %v", err)

            }

      }

}

Step 9. Create an Entry File to the API Directory

We need to call the initialize() method we defined in the base.go file and also the seeder we define above. We will create a file called server.go in the path api/

vi api/server.go

package api

 

import (

        "fmt"

        "log"

        "os"

 

        "github.com/joho/godotenv"

        "github.com/Sangwan70/skillpedia/api/controllers"

        "github.com/Sangwan70/skillpedia/api/seed"

)

 

var server = controllers.Server{}

 

func init() {

        // loads values from .env into the system

        if err := godotenv.Load(); err != nil {

                log.Print("sad .env file found")

        }

}

 

func Run() {

 

        var err error

        err = godotenv.Load()

        if err != nil {

                log.Fatalf("Error getting env, %v", err)

        } else {

                fmt.Println("We are getting the env values")

        }

 

        server.Initialize(os.Getenv("DB_DRIVER"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_PORT"), os.Getenv("DB_HOST"), os.Getenv("DB_NAME"))

 

        seed.Load(server.DB)

 

        server.Run(":8088")

 

}

Step 10: Create app entry file: main.go

Create the main.go file in the root of the skillpedia directory

vi main.go

package main

 

import (

        "github.com/Sangwan70/skillpedia/api"

)

 

func main() {

 

        api.Run()

 

}

This is the file that actually “start the engine”. We will call the Run method that was defined in server.go file

Before we run the app, let's confirm your directory structure:

Now let’s run the app to start up the app and run migrations.

go run main.go

Step 11: Testing the Endpoints in Postman

You can use Postman or any other testing tool, then in the next step, we will write test cases.

Lets test endpoints at random:

a. GetUsers (/users): send a GET request "http://localhost:8088/users"

Remember we seeded the users table

b. GetPosts (/posts): http://localhost:8088/posts

Remember we also seeded the posts table

c. Login (/login): http://localhost:8088/login

Lets Login User 1: We will use that token to update the post in the next test.

d. UpdatePost (/posts/{id}): http://localhost:8088/posts/{id}

To update a post, we will need the authentication token for a user and in the above, we generated a token when we logged in user 1. The token look like this:

"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2ODYwMjgxMjMsInVzZXJfaWQiOjF9.I5eFKSoSmGGLYc5406Z0mA3vzgNr62qszHn39nC24ss"

I will use this token and put it in Authorization: Bearer Token in Postman:

Then I will use JSON in the body to send data to update:

{

"title":"SkillPedia LMS",

"author_id": 1,

"content": "A Complete Learning Management System"

}

If user 2 tries to update user 1 post, the response will be authorized

e. DeleteUser (/users/{id}): http://localhost:8088/users/{id}

To delete an account, the user needs to be logged in first, so as to generate the JWT token. Let us login user 2 and delete him.

Grab the token, insert it in Authorization: Bearer Token. Then use a “DELETE” method in your Postman.

Note that Status Code is 204, which means No Content. And the response is 1, which means success.

f. UpdateUser (/users/{id}): http://localhost:8088/users/{id}

Remember to login the ‘user to update’ and use the Token. Let’s update User 1:

{

"nickname": "R N Sangwan",

"email": "admin@theskillpedia.com",

"password": "NewPassword"

}

 

g. CreateUser (/users):

We don’t need to be authenticated to create a user (signup). Let’s create a third user:

h. CreatePost (/posts):

Let the new user create a post. Remember again, he must log in and his token used in Authorization: Bearer Token

{

"email": "rnsangwan@gmail.com",

"password": "NewPassword"

}

 

{

"title":"Go Language is Fun",

"author_id": 3,

"content": "A Complete Development Environment"

}

 

You can test the remaining endpoints:

i. GetUser (/users/{id})— Get one user (No token required)

 

j. GetPost(/posts/{id}) — Get one post (No token required)

 

k. DeletePost(/posts/{id}) — An authenticated User deletes a post he created( Token required)

l. Home(/) — Get Home page (No token required)

Step 12: Writing Test Cases For Endpoints

We have proven that then endpoints are working. But that is not enough, we still need really test each of them to further proof.

Remember we defined a test database in our .env so, you can create a new database for the tests alone.

I am aware that we can use Interfaces and mock database calls, but I decided to be replicate the exact same process that happens in real life for testing.

At the start of this project, we created the tests directory.

Now, we will create two packages inside it: modeltests and controllertests

a. Models Tests

We are going to test the methods in the models package defined in step 3.

Create the modeltests package in tests:

mkdir tests/modeltests

cd into the package and create a file model_test.go

cd tests/modeltests

vi model_test.go

package modeltests

 

import (

      "fmt"

      "log"

      "os"

      "testing"

 

      "github.com/jinzhu/gorm"

      "github.com/joho/godotenv"

      "github.com/victorsteven/fullstack/api/controllers"

      "github.com/victorsteven/fullstack/api/models"

)

 

var server = controllers.Server{}

var userInstance = models.User{}

var postInstance = models.Post{}

 

func TestMain(m *testing.M) {

      var err error

      err = godotenv.Load(os.ExpandEnv("../../.env"))

      if err != nil {

            log.Fatalf("Error getting env %v\n", err)

      }

      Database()

 

      log.Printf("Before calling m.Run() !!!")

      ret := m.Run()

      log.Printf("After calling m.Run() !!!")

      //os.Exit(m.Run())

      os.Exit(ret)

}

 

func Database() {

 

      var err error

 

      TestDbDriver := os.Getenv("TestDbDriver")

 

      if TestDbDriver == "mysql" {

            DBURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", os.Getenv("TestDbUser"), os.Getenv("TestDbPassword"), os.Getenv("TestDbHost"), os.Getenv("TestDbPort"), os.Getenv("TestDbName"))

            server.DB, err = gorm.Open(TestDbDriver, DBURL)

            if err != nil {

                  fmt.Printf("Cannot connect to %s database\n", TestDbDriver)

                  log.Fatal("This is the error:", err)

            } else {

                  fmt.Printf("We are connected to the %s database\n", TestDbDriver)

            }

      }

      if TestDbDriver == "postgres" {

            DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", os.Getenv("TestDbHost"), os.Getenv("TestDbPort"), os.Getenv("TestDbUser"), os.Getenv("TestDbName"), os.Getenv("TestDbPassword"))

            server.DB, err = gorm.Open(TestDbDriver, DBURL)

            if err != nil {

                  fmt.Printf("Cannot connect to %s database\n", TestDbDriver)

                  log.Fatal("This is the error:", err)

            } else {

                  fmt.Printf("We are connected to the %s database\n", TestDbDriver)

            }

      }

      if TestDbDriver == "sqlite3" {

            //DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword)

            testDbName := os.Getenv("TestDbName")

            server.DB, err = gorm.Open(TestDbDriver, testDbName)

            if err != nil {

                  fmt.Printf("Cannot connect to %s database\n", TestDbDriver)

                  log.Fatal("This is the error:", err)

            } else {

                  fmt.Printf("We are connected to the %s database\n", TestDbDriver)

            }

            server.DB.Exec("PRAGMA foreign_keys = ON")

      }

 

}

 

func refreshUserTable() error {

      server.DB.Exec("SET foreign_key_checks=0")

      err := server.DB.Debug().DropTableIfExists(&models.User{}).Error

      if err != nil {

            return err

      }

      server.DB.Exec("SET foreign_key_checks=1")

      err = server.DB.Debug().AutoMigrate(&models.User{}).Error

      if err != nil {

            return err

      }

      log.Printf("Successfully refreshed table")

      log.Printf("refreshUserTable routine OK !!!")

      return nil

}

 

func seedOneUser() (models.User, error) {

 

      _ = refreshUserTable()

 

      user := models.User{

            Nickname: "Pet",

            Email:    "pet@gmail.com",

            Password: "password",

      }

 

      err := server.DB.Debug().Model(&models.User{}).Create(&user).Error

      if err != nil {

            log.Fatalf("cannot seed users table: %v", err)

      }

 

      log.Printf("seedOneUser routine OK !!!")

      return user, nil

}

 

func seedUsers() error {

 

      users := []models.User{

            models.User{

                  Nickname: "Steven victor",

                  Email:    "steven@gmail.com",

                  Password: "password",

            },

            models.User{

                  Nickname: "Kenny Morris",

                  Email:    "kenny@gmail.com",

                  Password: "password",

            },

      }

 

      for i := range users {

            err := server.DB.Debug().Model(&models.User{}).Create(&users[i]).Error

            if err != nil {

                  return err

            }

      }

 

      log.Printf("seedUsers routine OK !!!")

      return nil

}

 

func refreshUserAndPostTable() error {

 

      server.DB.Exec("SET foreign_key_checks=0")

      // NOTE: when deleting first delete Post as Post is depending on User table

      err := server.DB.Debug().DropTableIfExists(&models.Post{}, &models.User{}).Error

      if err != nil {

            return err

      }

      server.DB.Exec("SET foreign_key_checks=1")

      err = server.DB.Debug().AutoMigrate(&models.User{}, &models.Post{}).Error

      if err != nil {

            return err

      }

      log.Printf("Successfully refreshed tables")

      log.Printf("refreshUserAndPostTable routine OK !!!")

      return nil

}

 

func seedOneUserAndOnePost() (models.Post, error) {

 

      err := refreshUserAndPostTable()

      if err != nil {

            return models.Post{}, err

      }

      user := models.User{

            Nickname: "Sam Phil",

            Email:    "sam@gmail.com",

            Password: "password",

      }

      err = server.DB.Debug().Model(&models.User{}).Create(&user).Error

      if err != nil {

            return models.Post{}, err

      }

      post := models.Post{

            Title:    "This is the title sam",

            Content:  "This is the content sam",

            AuthorID: user.ID,

      }

      err = server.DB.Debug().Model(&models.Post{}).Create(&post).Error

      if err != nil {

            return models.Post{}, err

      }

 

      log.Printf("seedOneUserAndOnePost routine OK !!!")

      return post, nil

}

 

func seedUsersAndPosts() ([]models.User, []models.Post, error) {

 

      var err error

 

      if err != nil {

            return []models.User{}, []models.Post{}, err

      }

      var users = []models.User{

            models.User{

                  Nickname: "Steven victor",

                  Email:    "steven@gmail.com",

                  Password: "password",

            },

            models.User{

                  Nickname: "Magu Frank",

                  Email:    "magu@gmail.com",

                  Password: "password",

            },

      }

      var posts = []models.Post{

            models.Post{

                  Title:   "Title 1",

                  Content: "Hello world 1",

            },

            models.Post{

                  Title:   "Title 2",

                  Content: "Hello world 2",

            },

      }

 

      for i := range users {

            err = server.DB.Debug().Model(&models.User{}).Create(&users[i]).Error

            if err != nil {

                  log.Fatalf("cannot seed users table: %v", err)

            }

            posts[i].AuthorID = users[i].ID

 

            err = server.DB.Debug().Model(&models.Post{}).Create(&posts[i]).Error

            if err != nil {

                  log.Fatalf("cannot seed posts table: %v", err)

            }

      }

      log.Printf("seedUsersAndPosts routine OK !!!")

      return users, posts, nil

}

 

TestMain() does the necessary setup before testing. It loads the .env file and calls the Database() function to connect to the test database.

You can also see that we wrote other helper functions that we will use in the actual test, such as refreshing the test database and seeding it, this will be done for every test. So no test depends on another to pass.

i. user_model_test.go

Now, create tests for the methods in the User model. Create the file user_model_test.go in the path same as the model_test.go: tests/modeltests

vi user_model_test.go

package modeltests

 

import (

      "log"

      "testing"

 

      _ "github.com/jinzhu/gorm/dialects/mysql"    //mysql driver

      _ "github.com/jinzhu/gorm/dialects/postgres" //postgres driver

      "github.com/victorsteven/fullstack/api/models"

      "gopkg.in/go-playground/assert.v1"

)

 

func TestFindAllUsers(t *testing.T) {

 

      err := refreshUserTable()

      if err != nil {

            log.Fatalf("Error refreshing user table %v\n", err)

      }

 

      err = seedUsers()

      if err != nil {

            log.Fatalf("Error seeding user table %v\n", err)

      }

 

      users, err := userInstance.FindAllUsers(server.DB)

      if err != nil {

            t.Errorf("this is the error getting the users: %v\n", err)

            return

      }

      assert.Equal(t, len(*users), 2)

}

 

func TestSaveUser(t *testing.T) {

 

      err := refreshUserTable()

      if err != nil {

            log.Fatalf("Error user refreshing table %v\n", err)

      }

      newUser := models.User{

            ID:       1,

            Email:    "test@gmail.com",

            Nickname: "test",

            Password: "password",

      }

      savedUser, err := newUser.SaveUser(server.DB)

      if err != nil {

            t.Errorf("Error while saving a user: %v\n", err)

            return

      }

      assert.Equal(t, newUser.ID, savedUser.ID)

      assert.Equal(t, newUser.Email, savedUser.Email)

      assert.Equal(t, newUser.Nickname, savedUser.Nickname)

}

 

func TestGetUserByID(t *testing.T) {

 

      err := refreshUserTable()

      if err != nil {

            log.Fatalf("Error user refreshing table %v\n", err)

      }

 

      user, err := seedOneUser()

      if err != nil {

            log.Fatalf("cannot seed users table: %v", err)

      }

      foundUser, err := userInstance.FindUserByID(server.DB, user.ID)

      if err != nil {

            t.Errorf("this is the error getting one user: %v\n", err)

            return

      }

      assert.Equal(t, foundUser.ID, user.ID)

      assert.Equal(t, foundUser.Email, user.Email)

      assert.Equal(t, foundUser.Nickname, user.Nickname)

}

 

func TestUpdateAUser(t *testing.T) {

 

      err := refreshUserTable()

      if err != nil {

            log.Fatal(err)

      }

 

      user, err := seedOneUser()

      if err != nil {

            log.Fatalf("Cannot seed user: %v\n", err)

      }

 

      userUpdate := models.User{

            ID:       1,

            Nickname: "modiUpdate",

            Email:    "modiupdate@gmail.com",

            Password: "password",

      }

      updatedUser, err := userUpdate.UpdateAUser(server.DB, user.ID)

      if err != nil {

            t.Errorf("this is the error updating the user: %v\n", err)

            return

      }

      assert.Equal(t, updatedUser.ID, userUpdate.ID)

      assert.Equal(t, updatedUser.Email, userUpdate.Email)

      assert.Equal(t, updatedUser.Nickname, userUpdate.Nickname)

}

 

func TestDeleteAUser(t *testing.T) {

 

      err := refreshUserTable()

      if err != nil {

            log.Fatal(err)

      }

 

      user, err := seedOneUser()

 

      if err != nil {

            log.Fatalf("Cannot seed user: %v\n", err)

      }

 

      isDeleted, err := userInstance.DeleteAUser(server.DB, user.ID)

      if err != nil {

            t.Errorf("this is the error deleting the user: %v\n", err)

            return

      }

      //one shows that the record has been deleted or:

      // assert.Equal(t, int(isDeleted), 1)

 

      //Can be done this way too

      assert.Equal(t, isDeleted, int64(1))

}

We can run individual tests in user_model_test.go. To run TestFindAllUsers, simply do:

go test --run TestFindAllUsers

Get a more detailed output by attaching the -v flag:

go test -v --run TestFindAllUsers

ii. post_model_test.go

Let’s also create tests for the methods in the Post model. Create the file post_model_test.go in the path same as the model_test.go: tests/modeltests

This is the content:

Let's run one test:

go test -v --run TestUpdateAPost

Output:

Let’s make a test FAIL so that you can see what it looks like for a failing test. For the TestUpdateAPost test, I will change this assertion:

assert.Equal(t, updatedPost.ID, postUpdate.ID)

to

assert.Equal(t, updatedPost.ID, 544) //where 544 is invalid

Now, when we run this test, it will fail:

You can see that line:

post_model_test.go:100 1 does not equal 544

Running all tests in the modeltests package:

To run the test suite in the modeltests package, make sure in your terminal, you are in the path: tests/modeltests .

Then run:

go test -v

All tests in the modeltests package are run and all passed.

b. Controllers Tests

In the tests paths: /tests, we will create a package called controllertests, which is in the same directory path as modeltests.

mkdir controllertests

Next, will we create the controller_test.go file where we will define the TestMain() which loads the .env file, setup database connection, and call seeder functions.

In the path: tests/controllertests

vi controller_test.go

i. login_controller_test.go

We will use TABLE TEST here. The idea behind the table test is that all possible cases defined in a struct, which uses a loop to run each test case. This saves us a lot of time as we tend to test all cases for a particular functionality with just one test function.

In the same directory path as controller_test.go, we create a file

touch login_controller_test.go

With the content:

Running the TestLogin:

go test -v --run TestLogin

ii. user_controller_test.go

This test file extensively uses TABLE TEST to test all possible cases, rather creating separate test functions for each case, hence saving us a lot of time.

In the same path as controller_test.go, create the user_controller_test.go file

touch user_controller_test.go file

Let’s run one test function inside that file to demonstrate.

Running TestCreateUser

go test -v --run TestCreateUser

Observe this:

 

This is one of the cases in our table test:

{
  inputJSON: `{"nickname":"Pet", "email": "grand@gmail.com",  "password": "password"}`,statusCode:   500,errorMessage: "Nickname Already Taken",
},

Since this test hits the database and identified that the Nickname already exist and cannot insert that record, an error is returned, this is exactly what we want in that test. Hence the test passed. The same thing applies when the email is duplicated.

iii. post_controller_test.go

TABLE TEST is also extensively used in this file. In the same directory as controller_test.go, create the post_controller_test.go file

touch post_controller_test.go

With the content:

In this test file, we can run TestDeletePost function

go test -v --run TestDeletePost

Running all tests in the controllertests package

We can also run the test suite for the controllertests package.

In the path tests/controllertests run:

go test -v

All Tests passed. Feels Good!

c. Running the Entire Tests in the app.

At this point, your folder structure should look like this:

Wrapping up.

Frankly, I did a lot of research to put this article together. Because of writing test cases, I had to change the entire app structure. This was really a pain in the butt. I think is all worth it now, testing every single function/method ships a better software. Really, I feel good.

What Next?

  • Dockerizing the application(Part 2 of the article here)
  • Deploying on Kubernetes(Part 3 of the article here)
  • Integrate Travis, Code Climate and Coveralls
  • Deploy the application on AWS, Digital Ocean or Heroku.
  • Consuming the API with React/Vue.

I actually summed everything together in this article here. Where I built a Forum App using Gin Framework. React is used as the Frontend Stack.

You can follow me on medium to get updates. You can also follow me on twitter

Get the source code for this article on github