├── .circleci └── config.yml ├── .env ├── .gitignore ├── .idea ├── .gitignore ├── fullstack.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── Dockerfile ├── README.md ├── api ├── auth │ └── token.go ├── controllers │ ├── base.go │ ├── home_controller.go │ ├── login_controller.go │ ├── posts_controller.go │ ├── routes.go │ └── users_controller.go ├── middlewares │ └── middlewares.go ├── models │ ├── Post.go │ └── User.go ├── responses │ └── json.go ├── seed │ └── seeder.go ├── server.go └── utils │ └── formaterror │ └── formaterror.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go └── tests ├── controllertests ├── controller_test.go ├── login_controller_test.go ├── post_controller_test.go └── user_controller_test.go └── modeltests ├── model_test.go ├── post_model_test.go └── user_model_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # use CircleCI 2.0 2 | jobs: # basic units of work in a run 3 | build: # runs not using Workflows must have a `build` job as entry point 4 | docker: # run the steps with Docker 5 | - image: circleci/golang:1.12 6 | - image: circleci/postgres:9.6-alpine 7 | environment: # environment variables for primary container 8 | POSTGRES_USER: steven 9 | POSTGRES_DB: fullstack_db_test 10 | 11 | environment: # environment variables for the build itself 12 | GO111MODULE: "on" #we don't rely on GOPATH 13 | 14 | working_directory: ~/usr/src/app # Go module is used, so we dont need to worry about GOPATH 15 | 16 | steps: # steps that comprise the `build` job 17 | - checkout # check out source code to working directory 18 | - run: 19 | name: "Fetch dependencies" 20 | command: go mod download 21 | 22 | # Wait for Postgres to be ready before proceeding 23 | - run: 24 | name: Waiting for Postgres to be ready 25 | command: dockerize -wait tcp://localhost:5432 -timeout 1m 26 | 27 | - run: 28 | name: Run unit tests 29 | environment: # environment variables for the database url and path to migration files 30 | FORUM_DB_URL: "postgres://steven@localhost:5432/fullstack_db_test?sslmode=disable" 31 | command: go test -v ./tests/... # our test is inside the "tests" folder, so target only that 32 | 33 | workflows: 34 | version: 2 35 | build-workflow: 36 | jobs: 37 | - build 38 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Postgres Live 2 | # DB_HOST=127.0.0.1 3 | # DB_DRIVER=postgres 4 | # API_SECRET=98hbun98h #Used for creating a JWT. Can be anything 5 | # DB_USER=steven 6 | # DB_PASSWORD= 7 | # DB_NAME=fullstack_api 8 | # DB_PORT=5432 9 | 10 | # Postgres Test 11 | # TestDbHost=127.0.0.1 12 | # TestDbDriver=postgres 13 | # TestApiSecret=98hbun98h 14 | # TestDbUser=steven 15 | # TestDbPassword= 16 | # TestDbName=fullstack_api_test 17 | # TestDbPort=5432 18 | 19 | # Mysql Live 20 | # DB_HOST=127.0.0.1 21 | # DB_DRIVER=mysql 22 | # API_SECRET=98hbun98h #Used for creating a JWT. Can be anything 23 | # DB_USER=steven 24 | # DB_PASSWORD=here 25 | # DB_NAME=fullstack_mysql 26 | # DB_PORT=3306 #Default mysql port 27 | 28 | # # Mysql Test 29 | # TestDbHost=127.0.0.1 30 | # TestDbDriver=mysql 31 | # TestApiSecret=98hbun98h 32 | # TestDbUser=steven 33 | # TestDbPassword=here 34 | # TestDbName=fullstack_api_test 35 | # TestDbPort=3306 36 | 37 | # sqlite live 38 | DB_DRIVER=sqlite3 39 | API_SECRET=98hbun98h #Used for creating a JWT. Can be anything 40 | DB_NAME=fullstack.sqlite 41 | 42 | # sqlite test 43 | TestDbDriver=sqlite3 44 | TestApiSecret=98hbun98h 45 | TestDbName=fullstack_test.sqlite 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | *.idea/* 3 | *.idea 4 | *.idea/.* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Default ignored files 3 | /workspace.xml -------------------------------------------------------------------------------- /.idea/fullstack.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from golang base image 2 | FROM golang:alpine as builder 3 | 4 | # ENV GO111MODULE=on 5 | 6 | # Add Maintainer info 7 | LABEL maintainer="Steven Victor " 8 | 9 | # Install git. 10 | # Git is required for fetching the dependencies. 11 | RUN apk update && apk add --no-cache git 12 | 13 | # Set the current working directory inside the container 14 | WORKDIR /app 15 | 16 | # Copy go mod and sum files 17 | COPY go.mod go.sum ./ 18 | 19 | # RUN go mod tidy 20 | 21 | # Download all dependencies. Dependencies will be cached if the go.mod and the go.sum files are not changed 22 | RUN go mod download 23 | 24 | # Copy the source from the current directory to the working Directory inside the container 25 | 26 | COPY . . 27 | # Build the Go app 28 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . 29 | 30 | # Start a new stage from scratch 31 | FROM alpine:latest 32 | RUN apk --no-cache add ca-certificates 33 | 34 | WORKDIR /root/ 35 | 36 | # Copy the Pre-built binary file from the previous stage 37 | COPY --from=builder /app/main . 38 | COPY --from=builder /app/.env . 39 | 40 | # Expose port 8080 to the outside world 41 | EXPOSE 8080 42 | 43 | #Command to run the executable 44 | CMD ["./main"] 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/victorsteven/Go-JWT-Postgres-Mysql-Restful-API.svg?style=svg)](https://circleci.com/gh/victorsteven/Go-JWT-Postgres-Mysql-Restful-API) 2 | 3 | # Go-JWT-Postgres-Mysql-Restful-API 4 | This is an application built with golang, jwt, gorm, postgresql, mysql. 5 | 6 | You can follow the guide here: 7 | https://levelup.gitconnected.com/crud-restful-api-with-go-gorm-jwt-postgres-mysql-and-testing-460a85ab7121 8 | 9 | ### Dockerizing the API 10 | The dockerized API can be found here: 11 | https://github.com/victorsteven/Dockerized-Golang-Postgres-Mysql-API 12 | -------------------------------------------------------------------------------- /api/auth/token.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | jwt "github.com/dgrijalva/jwt-go" 14 | ) 15 | 16 | func CreateToken(user_id uint32) (string, error) { 17 | claims := jwt.MapClaims{} 18 | claims["authorized"] = true 19 | claims["user_id"] = user_id 20 | claims["exp"] = time.Now().Add(time.Hour * 1).Unix() //Token expires after 1 hour 21 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 22 | return token.SignedString([]byte(os.Getenv("API_SECRET"))) 23 | 24 | } 25 | 26 | func TokenValid(r *http.Request) error { 27 | tokenString := ExtractToken(r) 28 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 29 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 30 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 31 | } 32 | return []byte(os.Getenv("API_SECRET")), nil 33 | }) 34 | if err != nil { 35 | return err 36 | } 37 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 38 | Pretty(claims) 39 | } 40 | return nil 41 | } 42 | 43 | func ExtractToken(r *http.Request) string { 44 | keys := r.URL.Query() 45 | token := keys.Get("token") 46 | if token != "" { 47 | return token 48 | } 49 | bearerToken := r.Header.Get("Authorization") 50 | if len(strings.Split(bearerToken, " ")) == 2 { 51 | return strings.Split(bearerToken, " ")[1] 52 | } 53 | return "" 54 | } 55 | 56 | func ExtractTokenID(r *http.Request) (uint32, error) { 57 | 58 | tokenString := ExtractToken(r) 59 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 60 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 61 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 62 | } 63 | return []byte(os.Getenv("API_SECRET")), nil 64 | }) 65 | if err != nil { 66 | return 0, err 67 | } 68 | claims, ok := token.Claims.(jwt.MapClaims) 69 | if ok && token.Valid { 70 | uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32) 71 | if err != nil { 72 | return 0, err 73 | } 74 | return uint32(uid), nil 75 | } 76 | return 0, nil 77 | } 78 | 79 | //Pretty display the claims licely in the terminal 80 | func Pretty(data interface{}) { 81 | b, err := json.MarshalIndent(data, "", " ") 82 | if err != nil { 83 | log.Println(err) 84 | return 85 | } 86 | 87 | fmt.Println(string(b)) 88 | } 89 | -------------------------------------------------------------------------------- /api/controllers/base.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/jinzhu/gorm" 10 | 11 | _ "github.com/jinzhu/gorm/dialects/mysql" //mysql database driver 12 | _ "github.com/jinzhu/gorm/dialects/postgres" //postgres database driver 13 | _ "github.com/jinzhu/gorm/dialects/sqlite" // sqlite database driver 14 | "github.com/victorsteven/fullstack/api/models" 15 | ) 16 | 17 | type Server struct { 18 | DB *gorm.DB 19 | Router *mux.Router 20 | } 21 | 22 | func (server *Server) Initialize(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) { 23 | 24 | var err error 25 | 26 | if Dbdriver == "mysql" { 27 | DBURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", DbUser, DbPassword, DbHost, DbPort, DbName) 28 | server.DB, err = gorm.Open(Dbdriver, DBURL) 29 | if err != nil { 30 | fmt.Printf("Cannot connect to %s database", Dbdriver) 31 | log.Fatal("This is the error:", err) 32 | } else { 33 | fmt.Printf("We are connected to the %s database", Dbdriver) 34 | } 35 | } 36 | if Dbdriver == "postgres" { 37 | DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword) 38 | server.DB, err = gorm.Open(Dbdriver, DBURL) 39 | if err != nil { 40 | fmt.Printf("Cannot connect to %s database", Dbdriver) 41 | log.Fatal("This is the error:", err) 42 | } else { 43 | fmt.Printf("We are connected to the %s database", Dbdriver) 44 | } 45 | } 46 | if Dbdriver == "sqlite3" { 47 | //DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword) 48 | server.DB, err = gorm.Open(Dbdriver, DbName) 49 | if err != nil { 50 | fmt.Printf("Cannot connect to %s database\n", Dbdriver) 51 | log.Fatal("This is the error:", err) 52 | } else { 53 | fmt.Printf("We are connected to the %s database\n", Dbdriver) 54 | } 55 | server.DB.Exec("PRAGMA foreign_keys = ON") 56 | } 57 | 58 | server.DB.Debug().AutoMigrate(&models.User{}, &models.Post{}) //database migration 59 | 60 | server.Router = mux.NewRouter() 61 | 62 | server.initializeRoutes() 63 | } 64 | 65 | func (server *Server) Run(addr string) { 66 | fmt.Println("Listening to port 8080") 67 | log.Fatal(http.ListenAndServe(addr, server.Router)) 68 | } 69 | -------------------------------------------------------------------------------- /api/controllers/home_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/victorsteven/fullstack/api/responses" 7 | ) 8 | 9 | func (server *Server) Home(w http.ResponseWriter, r *http.Request) { 10 | responses.JSON(w, http.StatusOK, "Welcome To This Awesome API") 11 | 12 | } 13 | -------------------------------------------------------------------------------- /api/controllers/login_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/victorsteven/fullstack/api/auth" 9 | "github.com/victorsteven/fullstack/api/models" 10 | "github.com/victorsteven/fullstack/api/responses" 11 | "github.com/victorsteven/fullstack/api/utils/formaterror" 12 | "golang.org/x/crypto/bcrypt" 13 | ) 14 | 15 | func (server *Server) Login(w http.ResponseWriter, r *http.Request) { 16 | body, err := ioutil.ReadAll(r.Body) 17 | if err != nil { 18 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 19 | return 20 | } 21 | user := models.User{} 22 | err = json.Unmarshal(body, &user) 23 | if err != nil { 24 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 25 | return 26 | } 27 | 28 | user.Prepare() 29 | err = user.Validate("login") 30 | if err != nil { 31 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 32 | return 33 | } 34 | token, err := server.SignIn(user.Email, user.Password) 35 | if err != nil { 36 | formattedError := formaterror.FormatError(err.Error()) 37 | responses.ERROR(w, http.StatusUnprocessableEntity, formattedError) 38 | return 39 | } 40 | responses.JSON(w, http.StatusOK, token) 41 | } 42 | 43 | func (server *Server) SignIn(email, password string) (string, error) { 44 | 45 | var err error 46 | 47 | user := models.User{} 48 | 49 | err = server.DB.Debug().Model(models.User{}).Where("email = ?", email).Take(&user).Error 50 | if err != nil { 51 | return "", err 52 | } 53 | err = models.VerifyPassword(user.Password, password) 54 | if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { 55 | return "", err 56 | } 57 | return auth.CreateToken(user.ID) 58 | } 59 | -------------------------------------------------------------------------------- /api/controllers/posts_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "strconv" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/victorsteven/fullstack/api/auth" 13 | "github.com/victorsteven/fullstack/api/models" 14 | "github.com/victorsteven/fullstack/api/responses" 15 | "github.com/victorsteven/fullstack/api/utils/formaterror" 16 | ) 17 | 18 | func (server *Server) CreatePost(w http.ResponseWriter, r *http.Request) { 19 | 20 | body, err := ioutil.ReadAll(r.Body) 21 | if err != nil { 22 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 23 | return 24 | } 25 | post := models.Post{} 26 | err = json.Unmarshal(body, &post) 27 | if err != nil { 28 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 29 | return 30 | } 31 | post.Prepare() 32 | err = post.Validate() 33 | if err != nil { 34 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 35 | return 36 | } 37 | uid, err := auth.ExtractTokenID(r) 38 | if err != nil { 39 | responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized")) 40 | return 41 | } 42 | if uid != post.AuthorID { 43 | responses.ERROR(w, http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized))) 44 | return 45 | } 46 | postCreated, err := post.SavePost(server.DB) 47 | if err != nil { 48 | formattedError := formaterror.FormatError(err.Error()) 49 | responses.ERROR(w, http.StatusInternalServerError, formattedError) 50 | return 51 | } 52 | w.Header().Set("Location", fmt.Sprintf("%s%s/%d", r.Host, r.URL.Path, postCreated.ID)) 53 | responses.JSON(w, http.StatusCreated, postCreated) 54 | } 55 | 56 | func (server *Server) GetPosts(w http.ResponseWriter, r *http.Request) { 57 | 58 | post := models.Post{} 59 | 60 | posts, err := post.FindAllPosts(server.DB) 61 | if err != nil { 62 | responses.ERROR(w, http.StatusInternalServerError, err) 63 | return 64 | } 65 | responses.JSON(w, http.StatusOK, posts) 66 | } 67 | 68 | func (server *Server) GetPost(w http.ResponseWriter, r *http.Request) { 69 | 70 | vars := mux.Vars(r) 71 | pid, err := strconv.ParseUint(vars["id"], 10, 64) 72 | if err != nil { 73 | responses.ERROR(w, http.StatusBadRequest, err) 74 | return 75 | } 76 | post := models.Post{} 77 | 78 | postReceived, err := post.FindPostByID(server.DB, pid) 79 | if err != nil { 80 | responses.ERROR(w, http.StatusInternalServerError, err) 81 | return 82 | } 83 | responses.JSON(w, http.StatusOK, postReceived) 84 | } 85 | 86 | func (server *Server) UpdatePost(w http.ResponseWriter, r *http.Request) { 87 | 88 | vars := mux.Vars(r) 89 | 90 | // Check if the post id is valid 91 | pid, err := strconv.ParseUint(vars["id"], 10, 64) 92 | if err != nil { 93 | responses.ERROR(w, http.StatusBadRequest, err) 94 | return 95 | } 96 | 97 | //CHeck if the auth token is valid and get the user id from it 98 | uid, err := auth.ExtractTokenID(r) 99 | if err != nil { 100 | responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized")) 101 | return 102 | } 103 | 104 | // Check if the post exist 105 | post := models.Post{} 106 | err = server.DB.Debug().Model(models.Post{}).Where("id = ?", pid).Take(&post).Error 107 | if err != nil { 108 | responses.ERROR(w, http.StatusNotFound, errors.New("Post not found")) 109 | return 110 | } 111 | 112 | // If a user attempt to update a post not belonging to him 113 | if uid != post.AuthorID { 114 | responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized")) 115 | return 116 | } 117 | // Read the data posted 118 | body, err := ioutil.ReadAll(r.Body) 119 | if err != nil { 120 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 121 | return 122 | } 123 | 124 | // Start processing the request data 125 | postUpdate := models.Post{} 126 | err = json.Unmarshal(body, &postUpdate) 127 | if err != nil { 128 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 129 | return 130 | } 131 | 132 | //Also check if the request user id is equal to the one gotten from token 133 | if uid != postUpdate.AuthorID { 134 | responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized")) 135 | return 136 | } 137 | 138 | postUpdate.Prepare() 139 | err = postUpdate.Validate() 140 | if err != nil { 141 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 142 | return 143 | } 144 | 145 | postUpdate.ID = post.ID //this is important to tell the model the post id to update, the other update field are set above 146 | 147 | postUpdated, err := postUpdate.UpdateAPost(server.DB) 148 | 149 | if err != nil { 150 | formattedError := formaterror.FormatError(err.Error()) 151 | responses.ERROR(w, http.StatusInternalServerError, formattedError) 152 | return 153 | } 154 | responses.JSON(w, http.StatusOK, postUpdated) 155 | } 156 | 157 | func (server *Server) DeletePost(w http.ResponseWriter, r *http.Request) { 158 | 159 | vars := mux.Vars(r) 160 | 161 | // Is a valid post id given to us? 162 | pid, err := strconv.ParseUint(vars["id"], 10, 64) 163 | if err != nil { 164 | responses.ERROR(w, http.StatusBadRequest, err) 165 | return 166 | } 167 | 168 | // Is this user authenticated? 169 | uid, err := auth.ExtractTokenID(r) 170 | if err != nil { 171 | responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized")) 172 | return 173 | } 174 | 175 | // Check if the post exist 176 | post := models.Post{} 177 | err = server.DB.Debug().Model(models.Post{}).Where("id = ?", pid).Take(&post).Error 178 | if err != nil { 179 | responses.ERROR(w, http.StatusNotFound, errors.New("Unauthorized")) 180 | return 181 | } 182 | 183 | // Is the authenticated user, the owner of this post? 184 | if uid != post.AuthorID { 185 | responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized")) 186 | return 187 | } 188 | _, err = post.DeleteAPost(server.DB, pid, uid) 189 | if err != nil { 190 | responses.ERROR(w, http.StatusBadRequest, err) 191 | return 192 | } 193 | w.Header().Set("Entity", fmt.Sprintf("%d", pid)) 194 | responses.JSON(w, http.StatusNoContent, "") 195 | } 196 | -------------------------------------------------------------------------------- /api/controllers/routes.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import "github.com/victorsteven/fullstack/api/middlewares" 4 | 5 | func (s *Server) initializeRoutes() { 6 | 7 | // Home Route 8 | s.Router.HandleFunc("/", middlewares.SetMiddlewareJSON(s.Home)).Methods("GET") 9 | 10 | // Login Route 11 | s.Router.HandleFunc("/login", middlewares.SetMiddlewareJSON(s.Login)).Methods("POST") 12 | 13 | //Users routes 14 | s.Router.HandleFunc("/users", middlewares.SetMiddlewareJSON(s.CreateUser)).Methods("POST") 15 | s.Router.HandleFunc("/users", middlewares.SetMiddlewareJSON(s.GetUsers)).Methods("GET") 16 | s.Router.HandleFunc("/users/{id}", middlewares.SetMiddlewareJSON(s.GetUser)).Methods("GET") 17 | s.Router.HandleFunc("/users/{id}", middlewares.SetMiddlewareJSON(middlewares.SetMiddlewareAuthentication(s.UpdateUser))).Methods("PUT") 18 | s.Router.HandleFunc("/users/{id}", middlewares.SetMiddlewareAuthentication(s.DeleteUser)).Methods("DELETE") 19 | 20 | //Posts routes 21 | s.Router.HandleFunc("/posts", middlewares.SetMiddlewareJSON(s.CreatePost)).Methods("POST") 22 | s.Router.HandleFunc("/posts", middlewares.SetMiddlewareJSON(s.GetPosts)).Methods("GET") 23 | s.Router.HandleFunc("/posts/{id}", middlewares.SetMiddlewareJSON(s.GetPost)).Methods("GET") 24 | s.Router.HandleFunc("/posts/{id}", middlewares.SetMiddlewareJSON(middlewares.SetMiddlewareAuthentication(s.UpdatePost))).Methods("PUT") 25 | s.Router.HandleFunc("/posts/{id}", middlewares.SetMiddlewareAuthentication(s.DeletePost)).Methods("DELETE") 26 | } 27 | -------------------------------------------------------------------------------- /api/controllers/users_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "strconv" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/victorsteven/fullstack/api/auth" 13 | "github.com/victorsteven/fullstack/api/models" 14 | "github.com/victorsteven/fullstack/api/responses" 15 | "github.com/victorsteven/fullstack/api/utils/formaterror" 16 | ) 17 | 18 | func (server *Server) CreateUser(w http.ResponseWriter, r *http.Request) { 19 | 20 | body, err := ioutil.ReadAll(r.Body) 21 | if err != nil { 22 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 23 | } 24 | user := models.User{} 25 | err = json.Unmarshal(body, &user) 26 | if err != nil { 27 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 28 | return 29 | } 30 | user.Prepare() 31 | err = user.Validate("") 32 | if err != nil { 33 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 34 | return 35 | } 36 | userCreated, err := user.SaveUser(server.DB) 37 | 38 | if err != nil { 39 | 40 | formattedError := formaterror.FormatError(err.Error()) 41 | 42 | responses.ERROR(w, http.StatusInternalServerError, formattedError) 43 | return 44 | } 45 | w.Header().Set("Location", fmt.Sprintf("%s%s/%d", r.Host, r.RequestURI, userCreated.ID)) 46 | responses.JSON(w, http.StatusCreated, userCreated) 47 | } 48 | 49 | func (server *Server) GetUsers(w http.ResponseWriter, r *http.Request) { 50 | 51 | user := models.User{} 52 | 53 | users, err := user.FindAllUsers(server.DB) 54 | if err != nil { 55 | responses.ERROR(w, http.StatusInternalServerError, err) 56 | return 57 | } 58 | responses.JSON(w, http.StatusOK, users) 59 | } 60 | 61 | func (server *Server) GetUser(w http.ResponseWriter, r *http.Request) { 62 | 63 | vars := mux.Vars(r) 64 | uid, err := strconv.ParseUint(vars["id"], 10, 32) 65 | if err != nil { 66 | responses.ERROR(w, http.StatusBadRequest, err) 67 | return 68 | } 69 | user := models.User{} 70 | userGotten, err := user.FindUserByID(server.DB, uint32(uid)) 71 | if err != nil { 72 | responses.ERROR(w, http.StatusBadRequest, err) 73 | return 74 | } 75 | responses.JSON(w, http.StatusOK, userGotten) 76 | } 77 | 78 | func (server *Server) UpdateUser(w http.ResponseWriter, r *http.Request) { 79 | 80 | vars := mux.Vars(r) 81 | uid, err := strconv.ParseUint(vars["id"], 10, 32) 82 | if err != nil { 83 | responses.ERROR(w, http.StatusBadRequest, err) 84 | return 85 | } 86 | body, err := ioutil.ReadAll(r.Body) 87 | if err != nil { 88 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 89 | return 90 | } 91 | user := models.User{} 92 | err = json.Unmarshal(body, &user) 93 | if err != nil { 94 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 95 | return 96 | } 97 | tokenID, err := auth.ExtractTokenID(r) 98 | if err != nil { 99 | responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized")) 100 | return 101 | } 102 | if tokenID != uint32(uid) { 103 | responses.ERROR(w, http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized))) 104 | return 105 | } 106 | user.Prepare() 107 | err = user.Validate("update") 108 | if err != nil { 109 | responses.ERROR(w, http.StatusUnprocessableEntity, err) 110 | return 111 | } 112 | updatedUser, err := user.UpdateAUser(server.DB, uint32(uid)) 113 | if err != nil { 114 | formattedError := formaterror.FormatError(err.Error()) 115 | responses.ERROR(w, http.StatusInternalServerError, formattedError) 116 | return 117 | } 118 | responses.JSON(w, http.StatusOK, updatedUser) 119 | } 120 | 121 | func (server *Server) DeleteUser(w http.ResponseWriter, r *http.Request) { 122 | 123 | vars := mux.Vars(r) 124 | 125 | user := models.User{} 126 | 127 | uid, err := strconv.ParseUint(vars["id"], 10, 32) 128 | if err != nil { 129 | responses.ERROR(w, http.StatusBadRequest, err) 130 | return 131 | } 132 | tokenID, err := auth.ExtractTokenID(r) 133 | if err != nil { 134 | responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized")) 135 | return 136 | } 137 | if tokenID != 0 && tokenID != uint32(uid) { 138 | responses.ERROR(w, http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized))) 139 | return 140 | } 141 | _, err = user.DeleteAUser(server.DB, uint32(uid)) 142 | if err != nil { 143 | responses.ERROR(w, http.StatusInternalServerError, err) 144 | return 145 | } 146 | w.Header().Set("Entity", fmt.Sprintf("%d", uid)) 147 | responses.JSON(w, http.StatusNoContent, "") 148 | } 149 | -------------------------------------------------------------------------------- /api/middlewares/middlewares.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/victorsteven/fullstack/api/auth" 8 | "github.com/victorsteven/fullstack/api/responses" 9 | ) 10 | 11 | func SetMiddlewareJSON(next http.HandlerFunc) http.HandlerFunc { 12 | return func(w http.ResponseWriter, r *http.Request) { 13 | w.Header().Set("Content-Type", "application/json") 14 | next(w, r) 15 | } 16 | } 17 | 18 | func SetMiddlewareAuthentication(next http.HandlerFunc) http.HandlerFunc { 19 | return func(w http.ResponseWriter, r *http.Request) { 20 | err := auth.TokenValid(r) 21 | if err != nil { 22 | responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized")) 23 | return 24 | } 25 | next(w, r) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/models/Post.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "html" 6 | "strings" 7 | "time" 8 | 9 | "github.com/jinzhu/gorm" 10 | ) 11 | 12 | type Post struct { 13 | ID uint64 `gorm:"primary_key;auto_increment" json:"id"` 14 | Title string `gorm:"size:255;not null;unique" json:"title"` 15 | Content string `gorm:"size:255;not null;" json:"content"` 16 | Author User `json:"author"` 17 | AuthorID uint32 `sql:"type:int REFERENCES users(id)" json:"author_id"` 18 | CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` 19 | UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` 20 | } 21 | 22 | func (p *Post) Prepare() { 23 | p.ID = 0 24 | p.Title = html.EscapeString(strings.TrimSpace(p.Title)) 25 | p.Content = html.EscapeString(strings.TrimSpace(p.Content)) 26 | p.Author = User{} 27 | p.CreatedAt = time.Now() 28 | p.UpdatedAt = time.Now() 29 | } 30 | 31 | func (p *Post) Validate() error { 32 | 33 | if p.Title == "" { 34 | return errors.New("Required Title") 35 | } 36 | if p.Content == "" { 37 | return errors.New("Required Content") 38 | } 39 | if p.AuthorID < 1 { 40 | return errors.New("Required Author") 41 | } 42 | return nil 43 | } 44 | 45 | func (p *Post) SavePost(db *gorm.DB) (*Post, error) { 46 | var err error 47 | err = db.Debug().Model(&Post{}).Create(&p).Error 48 | if err != nil { 49 | return &Post{}, err 50 | } 51 | if p.ID != 0 { 52 | err = db.Debug().Model(&User{}).Where("id = ?", p.AuthorID).Take(&p.Author).Error 53 | if err != nil { 54 | return &Post{}, err 55 | } 56 | } 57 | return p, nil 58 | } 59 | 60 | func (p *Post) FindAllPosts(db *gorm.DB) (*[]Post, error) { 61 | var err error 62 | posts := []Post{} 63 | err = db.Debug().Model(&Post{}).Limit(100).Find(&posts).Error 64 | if err != nil { 65 | return &[]Post{}, err 66 | } 67 | if len(posts) > 0 { 68 | for i, _ := range posts { 69 | err := db.Debug().Model(&User{}).Where("id = ?", posts[i].AuthorID).Take(&posts[i].Author).Error 70 | if err != nil { 71 | return &[]Post{}, err 72 | } 73 | } 74 | } 75 | return &posts, nil 76 | } 77 | 78 | func (p *Post) FindPostByID(db *gorm.DB, pid uint64) (*Post, error) { 79 | var err error 80 | err = db.Debug().Model(&Post{}).Where("id = ?", pid).Take(&p).Error 81 | if err != nil { 82 | return &Post{}, err 83 | } 84 | if p.ID != 0 { 85 | err = db.Debug().Model(&User{}).Where("id = ?", p.AuthorID).Take(&p.Author).Error 86 | if err != nil { 87 | return &Post{}, err 88 | } 89 | } 90 | return p, nil 91 | } 92 | 93 | func (p *Post) UpdateAPost(db *gorm.DB) (*Post, error) { 94 | 95 | var err error 96 | // db = db.Debug().Model(&Post{}).Where("id = ?", pid).Take(&Post{}).UpdateColumns( 97 | // map[string]interface{}{ 98 | // "title": p.Title, 99 | // "content": p.Content, 100 | // "updated_at": time.Now(), 101 | // }, 102 | // ) 103 | // err = db.Debug().Model(&Post{}).Where("id = ?", pid).Take(&p).Error 104 | // if err != nil { 105 | // return &Post{}, err 106 | // } 107 | // if p.ID != 0 { 108 | // err = db.Debug().Model(&User{}).Where("id = ?", p.AuthorID).Take(&p.Author).Error 109 | // if err != nil { 110 | // return &Post{}, err 111 | // } 112 | // } 113 | err = db.Debug().Model(&Post{}).Where("id = ?", p.ID).Updates(Post{Title: p.Title, Content: p.Content, UpdatedAt: time.Now()}).Error 114 | if err != nil { 115 | return &Post{}, err 116 | } 117 | if p.ID != 0 { 118 | err = db.Debug().Model(&User{}).Where("id = ?", p.AuthorID).Take(&p.Author).Error 119 | if err != nil { 120 | return &Post{}, err 121 | } 122 | } 123 | return p, nil 124 | } 125 | 126 | func (p *Post) DeleteAPost(db *gorm.DB, pid uint64, uid uint32) (int64, error) { 127 | 128 | db = db.Debug().Model(&Post{}).Where("id = ? and author_id = ?", pid, uid).Take(&Post{}).Delete(&Post{}) 129 | 130 | if db.Error != nil { 131 | if gorm.IsRecordNotFoundError(db.Error) { 132 | return 0, errors.New("Post not found") 133 | } 134 | return 0, db.Error 135 | } 136 | return db.RowsAffected, nil 137 | } 138 | -------------------------------------------------------------------------------- /api/models/User.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "html" 6 | "log" 7 | "strings" 8 | "time" 9 | 10 | "github.com/badoux/checkmail" 11 | "github.com/jinzhu/gorm" 12 | "golang.org/x/crypto/bcrypt" 13 | ) 14 | 15 | type User struct { 16 | ID uint32 `gorm:"primary_key;auto_increment" json:"id"` 17 | Nickname string `gorm:"size:255;not null;unique" json:"nickname"` 18 | Email string `gorm:"size:100;not null;unique" json:"email"` 19 | Password string `gorm:"size:100;not null;" json:"password"` 20 | CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` 21 | UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` 22 | } 23 | 24 | func Hash(password string) ([]byte, error) { 25 | return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 26 | } 27 | 28 | func VerifyPassword(hashedPassword, password string) error { 29 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 30 | } 31 | 32 | func (u *User) BeforeSave() error { 33 | hashedPassword, err := Hash(u.Password) 34 | if err != nil { 35 | return err 36 | } 37 | u.Password = string(hashedPassword) 38 | return nil 39 | } 40 | 41 | func (u *User) Prepare() { 42 | u.ID = 0 43 | u.Nickname = html.EscapeString(strings.TrimSpace(u.Nickname)) 44 | u.Email = html.EscapeString(strings.TrimSpace(u.Email)) 45 | u.CreatedAt = time.Now() 46 | u.UpdatedAt = time.Now() 47 | } 48 | 49 | func (u *User) Validate(action string) error { 50 | switch strings.ToLower(action) { 51 | case "update": 52 | if u.Nickname == "" { 53 | return errors.New("Required Nickname") 54 | } 55 | if u.Password == "" { 56 | return errors.New("Required Password") 57 | } 58 | if u.Email == "" { 59 | return errors.New("Required Email") 60 | } 61 | if err := checkmail.ValidateFormat(u.Email); err != nil { 62 | return errors.New("Invalid Email") 63 | } 64 | 65 | return nil 66 | case "login": 67 | if u.Password == "" { 68 | return errors.New("Required Password") 69 | } 70 | if u.Email == "" { 71 | return errors.New("Required Email") 72 | } 73 | if err := checkmail.ValidateFormat(u.Email); err != nil { 74 | return errors.New("Invalid Email") 75 | } 76 | return nil 77 | 78 | default: 79 | if u.Nickname == "" { 80 | return errors.New("Required Nickname") 81 | } 82 | if u.Password == "" { 83 | return errors.New("Required Password") 84 | } 85 | if u.Email == "" { 86 | return errors.New("Required Email") 87 | } 88 | if err := checkmail.ValidateFormat(u.Email); err != nil { 89 | return errors.New("Invalid Email") 90 | } 91 | return nil 92 | } 93 | } 94 | 95 | func (u *User) SaveUser(db *gorm.DB) (*User, error) { 96 | 97 | var err error 98 | err = db.Debug().Create(&u).Error 99 | if err != nil { 100 | return &User{}, err 101 | } 102 | return u, nil 103 | } 104 | 105 | func (u *User) FindAllUsers(db *gorm.DB) (*[]User, error) { 106 | var err error 107 | users := []User{} 108 | err = db.Debug().Model(&User{}).Limit(100).Find(&users).Error 109 | if err != nil { 110 | return &[]User{}, err 111 | } 112 | return &users, err 113 | } 114 | 115 | func (u *User) FindUserByID(db *gorm.DB, uid uint32) (*User, error) { 116 | var err error 117 | err = db.Debug().Model(User{}).Where("id = ?", uid).Take(&u).Error 118 | if err != nil { 119 | return &User{}, err 120 | } 121 | if gorm.IsRecordNotFoundError(err) { 122 | return &User{}, errors.New("User Not Found") 123 | } 124 | return u, err 125 | } 126 | 127 | func (u *User) UpdateAUser(db *gorm.DB, uid uint32) (*User, error) { 128 | 129 | // To hash the password 130 | err := u.BeforeSave() 131 | if err != nil { 132 | log.Fatal(err) 133 | } 134 | db = db.Debug().Model(&User{}).Where("id = ?", uid).Take(&User{}).UpdateColumns( 135 | map[string]interface{}{ 136 | "password": u.Password, 137 | "nickname": u.Nickname, 138 | "email": u.Email, 139 | "updated_at": time.Now(), 140 | }, 141 | ) 142 | if db.Error != nil { 143 | return &User{}, db.Error 144 | } 145 | // This is the display the updated user 146 | err = db.Debug().Model(&User{}).Where("id = ?", uid).Take(&u).Error 147 | if err != nil { 148 | return &User{}, err 149 | } 150 | return u, nil 151 | } 152 | 153 | func (u *User) DeleteAUser(db *gorm.DB, uid uint32) (int64, error) { 154 | 155 | db = db.Debug().Model(&User{}).Where("id = ?", uid).Take(&User{}).Delete(&User{}) 156 | 157 | if db.Error != nil { 158 | return 0, db.Error 159 | } 160 | return db.RowsAffected, nil 161 | } 162 | -------------------------------------------------------------------------------- /api/responses/json.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | func JSON(w http.ResponseWriter, statusCode int, data interface{}) { 10 | w.WriteHeader(statusCode) 11 | err := json.NewEncoder(w).Encode(data) 12 | if err != nil { 13 | fmt.Fprintf(w, "%s", err.Error()) 14 | } 15 | } 16 | 17 | func ERROR(w http.ResponseWriter, statusCode int, err error) { 18 | if err != nil { 19 | JSON(w, statusCode, struct { 20 | Error string `json:"error"` 21 | }{ 22 | Error: err.Error(), 23 | }) 24 | return 25 | } 26 | JSON(w, http.StatusBadRequest, nil) 27 | } 28 | -------------------------------------------------------------------------------- /api/seed/seeder.go: -------------------------------------------------------------------------------- 1 | package seed 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/jinzhu/gorm" 7 | "github.com/victorsteven/fullstack/api/models" 8 | ) 9 | 10 | var users = []models.User{ 11 | models.User{ 12 | Nickname: "Steven victor", 13 | Email: "steven@gmail.com", 14 | Password: "password", 15 | }, 16 | models.User{ 17 | Nickname: "Martin Luther", 18 | Email: "luther@gmail.com", 19 | Password: "password", 20 | }, 21 | } 22 | 23 | var posts = []models.Post{ 24 | models.Post{ 25 | Title: "Title 1", 26 | Content: "Hello world 1", 27 | }, 28 | models.Post{ 29 | Title: "Title 2", 30 | Content: "Hello world 2", 31 | }, 32 | } 33 | 34 | func Load(db *gorm.DB) { 35 | 36 | err := db.Debug().DropTableIfExists(&models.Post{}, &models.User{}).Error 37 | if err != nil { 38 | log.Fatalf("cannot drop table: %v", err) 39 | } 40 | err = db.Debug().AutoMigrate(&models.User{}, &models.Post{}).Error 41 | if err != nil { 42 | log.Fatalf("cannot migrate table: %v", err) 43 | } 44 | 45 | /* 46 | err = db.Debug().Model(&models.Post{}).AddForeignKey("author_id", "users(id)", "cascade", "cascade").Error 47 | if err != nil { 48 | log.Fatalf("attaching foreign key error: %v", err) 49 | } 50 | */ 51 | 52 | for i, _ := range users { 53 | err = db.Debug().Model(&models.User{}).Create(&users[i]).Error 54 | if err != nil { 55 | log.Fatalf("cannot seed users table: %v", err) 56 | } 57 | posts[i].AuthorID = users[i].ID 58 | 59 | err = db.Debug().Model(&models.Post{}).Create(&posts[i]).Error 60 | if err != nil { 61 | log.Fatalf("cannot seed posts table: %v", err) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/joho/godotenv" 9 | "github.com/victorsteven/fullstack/api/controllers" 10 | "github.com/victorsteven/fullstack/api/seed" 11 | ) 12 | 13 | var server = controllers.Server{} 14 | 15 | func init() { 16 | // loads values from .env into the system 17 | if err := godotenv.Load(); err != nil { 18 | log.Print("sad .env file found") 19 | } 20 | } 21 | 22 | func Run() { 23 | 24 | var err error 25 | err = godotenv.Load() 26 | if err != nil { 27 | log.Fatalf("Error getting env, %v", err) 28 | } else { 29 | fmt.Println("We are getting the env values") 30 | } 31 | 32 | 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")) 33 | 34 | seed.Load(server.DB) 35 | 36 | server.Run(":8080") 37 | 38 | } 39 | -------------------------------------------------------------------------------- /api/utils/formaterror/formaterror.go: -------------------------------------------------------------------------------- 1 | package formaterror 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | func FormatError(err string) error { 9 | 10 | if strings.Contains(err, "nickname") { 11 | return errors.New("Nickname Already Taken") 12 | } 13 | 14 | if strings.Contains(err, "email") { 15 | return errors.New("Email Already Taken") 16 | } 17 | 18 | if strings.Contains(err, "title") { 19 | return errors.New("Title Already Taken") 20 | } 21 | if strings.Contains(err, "hashedPassword") { 22 | return errors.New("Incorrect Password") 23 | } 24 | return errors.New("Incorrect Details") 25 | } 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | container_name: full_app 5 | build: . 6 | ports: 7 | - 8080:8080 # Forward the exposed port 8080 on the container to port 8080 on the host machine 8 | restart: on-failure 9 | volumes: 10 | - api:/usr/src/app/ 11 | depends_on: 12 | - postgres # This service depends on postgres. Start that first. 13 | # - mysql # This service depends on mysql. Start that first. 14 | networks: 15 | - fullstack 16 | 17 | postgres: 18 | image: postgres:latest 19 | container_name: full_db 20 | environment: 21 | - POSTGRES_USER=${DB_USER} 22 | - POSTGRES_PASSWORD=${DB_PASSWORD} 23 | - POSTGRES_DB=${DB_NAME} 24 | - DATABASE_HOST=${DB_HOST} 25 | ports: 26 | - '5432:5432' 27 | volumes: 28 | - database_postgres:/var/lib/postgresql/data 29 | networks: 30 | - fullstack 31 | 32 | # mysql: 33 | # image: mysql:5.7 34 | # ports: 35 | # - 3306:3306 36 | # environment: 37 | # - MYSQL_DATABASE=${DB_NAME} 38 | # - MYSQL_USER=${DB_USER} 39 | # - MYSQL_PASSWORD=${DB_PASSWORD} 40 | # - MYSQL_ROOT_PASSWORD=${DB_PASSWORD} 41 | # - DATABASE_HOST=${DB_HOST} 42 | # volumes: 43 | # - database_mysql:/var/lib/postgresql/data 44 | # networks: 45 | # - fullstack 46 | 47 | volumes: 48 | api: 49 | database_postgres: 50 | 51 | # Networks to be created to facilitate communication between containers 52 | networks: 53 | fullstack: 54 | 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/victorsteven/fullstack 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.3.3 7 | github.com/badoux/checkmail v0.0.0-20181210160741-9661bd69e9ad 8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 9 | github.com/fatih/structs v1.1.0 10 | github.com/gin-gonic/gin v1.4.0 11 | github.com/go-sql-driver/mysql v1.4.1 12 | github.com/go-test/deep v1.0.2 13 | github.com/gorilla/handlers v1.4.2 14 | github.com/gorilla/mux v1.6.2 15 | github.com/jinzhu/gorm v1.9.10 16 | github.com/joho/godotenv v1.3.0 17 | github.com/kr/pretty v0.1.0 // indirect 18 | github.com/satori/go.uuid v1.2.0 19 | github.com/stretchr/testify v1.3.0 20 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 21 | gopkg.in/go-playground/assert.v1 v1.2.1 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= 4 | cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= 7 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 8 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 9 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 10 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 12 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 13 | github.com/badoux/checkmail v0.0.0-20181210160741-9661bd69e9ad h1:kXfVkP8xPSJXzicomzjECcw6tv1Wl9h1lNenWBfNKdg= 14 | github.com/badoux/checkmail v0.0.0-20181210160741-9661bd69e9ad/go.mod h1:r5ZalvRl3tXevRNJkwIB6DC4DD3DMjIlY9NEU1XGoaQ= 15 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 16 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= 21 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= 22 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 23 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 24 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 25 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 26 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 27 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 28 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 29 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 30 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 31 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 32 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= 33 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 34 | github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= 35 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 36 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 37 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 38 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 39 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 40 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 41 | github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= 42 | github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 43 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 44 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 45 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 46 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 47 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 48 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 49 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 50 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 51 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 52 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 53 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 54 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 55 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 56 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 57 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 58 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 59 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 60 | github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= 61 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 62 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 63 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 64 | github.com/jinzhu/gorm v1.9.10 h1:HvrsqdhCW78xpJF67g1hMxS6eCToo9PZH4LDB8WKPac= 65 | github.com/jinzhu/gorm v1.9.10/go.mod h1:Kh6hTsSGffh4ui079FHrR5Gg+5D0hgihqDcsDN2BBJY= 66 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 67 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 68 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 69 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 70 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 71 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 72 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 73 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 74 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 75 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 76 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 77 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 78 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 79 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 80 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 81 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 82 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 83 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 84 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 85 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= 86 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 87 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= 88 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 89 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 90 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 91 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 92 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 93 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 94 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 95 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 96 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 97 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 98 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 99 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 100 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 101 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 102 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 103 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 104 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 105 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 106 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 107 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 108 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 109 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 110 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 111 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 112 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 113 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 115 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 116 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 117 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 118 | github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= 119 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 120 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 121 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 122 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 123 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 124 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 125 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 126 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 127 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 128 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 129 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 130 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 131 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 132 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 133 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 134 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 135 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 136 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 137 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 138 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 139 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= 140 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 141 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 142 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 143 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 144 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 145 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 146 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 147 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 148 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 149 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 150 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 151 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 152 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 153 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 154 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 155 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 156 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 157 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 158 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 159 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 160 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 161 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 162 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 163 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 164 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 165 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 166 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 167 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 168 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 169 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 170 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 171 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 172 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 173 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 174 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 175 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 176 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 177 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= 178 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 179 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 180 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 181 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 182 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 183 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 184 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 185 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 186 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/victorsteven/fullstack/api" 5 | ) 6 | 7 | func main() { 8 | 9 | api.Run() 10 | 11 | } 12 | -------------------------------------------------------------------------------- /tests/controllertests/controller_test.go: -------------------------------------------------------------------------------- 1 | package controllertests 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/jinzhu/gorm" 10 | "github.com/joho/godotenv" 11 | "github.com/victorsteven/fullstack/api/controllers" 12 | "github.com/victorsteven/fullstack/api/models" 13 | ) 14 | 15 | var server = controllers.Server{} 16 | var userInstance = models.User{} 17 | var postInstance = models.Post{} 18 | 19 | func TestMain(m *testing.M) { 20 | err := godotenv.Load(os.ExpandEnv("../../.env")) 21 | if err != nil { 22 | log.Fatalf("Error getting env %v\n", err) 23 | } 24 | Database() 25 | 26 | os.Exit(m.Run()) 27 | 28 | } 29 | 30 | func Database() { 31 | 32 | var err error 33 | 34 | TestDbDriver := os.Getenv("TestDbDriver") 35 | 36 | if TestDbDriver == "mysql" { 37 | 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")) 38 | server.DB, err = gorm.Open(TestDbDriver, DBURL) 39 | if err != nil { 40 | fmt.Printf("Cannot connect to %s database\n", TestDbDriver) 41 | log.Fatal("This is the error:", err) 42 | } else { 43 | fmt.Printf("We are connected to the %s database\n", TestDbDriver) 44 | } 45 | } 46 | if TestDbDriver == "postgres" { 47 | 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")) 48 | server.DB, err = gorm.Open(TestDbDriver, DBURL) 49 | if err != nil { 50 | fmt.Printf("Cannot connect to %s database\n", TestDbDriver) 51 | log.Fatal("This is the error:", err) 52 | } else { 53 | fmt.Printf("We are connected to the %s database\n", TestDbDriver) 54 | } 55 | } 56 | if TestDbDriver == "sqlite3" { 57 | //DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword) 58 | testDbName := os.Getenv("TestDbName") 59 | server.DB, err = gorm.Open(TestDbDriver, testDbName) 60 | if err != nil { 61 | fmt.Printf("Cannot connect to %s database\n", TestDbDriver) 62 | log.Fatal("This is the error:", err) 63 | } else { 64 | fmt.Printf("We are connected to the %s database\n", TestDbDriver) 65 | } 66 | server.DB.Exec("PRAGMA foreign_keys = ON") 67 | } 68 | } 69 | 70 | func refreshUserTable() error { 71 | /* 72 | err := server.DB.DropTableIfExists(&models.User{}).Error 73 | if err != nil { 74 | return err 75 | } 76 | err = server.DB.AutoMigrate(&models.User{}).Error 77 | if err != nil { 78 | return err 79 | } 80 | */ 81 | err := server.DB.DropTableIfExists(&models.Post{}, &models.User{}).Error 82 | if err != nil { 83 | return err 84 | } 85 | err = server.DB.AutoMigrate(&models.User{}, &models.Post{}).Error 86 | if err != nil { 87 | return err 88 | } 89 | 90 | log.Printf("Successfully refreshed table(s)") 91 | return nil 92 | } 93 | 94 | func seedOneUser() (models.User, error) { 95 | 96 | err := refreshUserTable() 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | user := models.User{ 102 | Nickname: "Pet", 103 | Email: "pet@gmail.com", 104 | Password: "password", 105 | } 106 | 107 | err = server.DB.Model(&models.User{}).Create(&user).Error 108 | if err != nil { 109 | return models.User{}, err 110 | } 111 | return user, nil 112 | } 113 | 114 | func seedUsers() ([]models.User, error) { 115 | 116 | var err error 117 | if err != nil { 118 | return nil, err 119 | } 120 | users := []models.User{ 121 | models.User{ 122 | Nickname: "Steven victor", 123 | Email: "steven@gmail.com", 124 | Password: "password", 125 | }, 126 | models.User{ 127 | Nickname: "Kenny Morris", 128 | Email: "kenny@gmail.com", 129 | Password: "password", 130 | }, 131 | } 132 | for i, _ := range users { 133 | err := server.DB.Model(&models.User{}).Create(&users[i]).Error 134 | if err != nil { 135 | return []models.User{}, err 136 | } 137 | } 138 | return users, nil 139 | } 140 | 141 | func refreshUserAndPostTable() error { 142 | 143 | err := server.DB.DropTableIfExists(&models.Post{}, &models.User{}).Error 144 | if err != nil { 145 | return err 146 | } 147 | err = server.DB.AutoMigrate(&models.User{}, &models.Post{}).Error 148 | if err != nil { 149 | return err 150 | } 151 | log.Printf("Successfully refreshed tables") 152 | return nil 153 | } 154 | 155 | func seedOneUserAndOnePost() (models.Post, error) { 156 | 157 | err := refreshUserAndPostTable() 158 | if err != nil { 159 | return models.Post{}, err 160 | } 161 | user := models.User{ 162 | Nickname: "Sam Phil", 163 | Email: "sam@gmail.com", 164 | Password: "password", 165 | } 166 | err = server.DB.Model(&models.User{}).Create(&user).Error 167 | if err != nil { 168 | return models.Post{}, err 169 | } 170 | post := models.Post{ 171 | Title: "This is the title sam", 172 | Content: "This is the content sam", 173 | AuthorID: user.ID, 174 | } 175 | err = server.DB.Model(&models.Post{}).Create(&post).Error 176 | if err != nil { 177 | return models.Post{}, err 178 | } 179 | return post, nil 180 | } 181 | 182 | func seedUsersAndPosts() ([]models.User, []models.Post, error) { 183 | 184 | var err error 185 | 186 | if err != nil { 187 | return []models.User{}, []models.Post{}, err 188 | } 189 | var users = []models.User{ 190 | models.User{ 191 | Nickname: "Steven victor", 192 | Email: "steven@gmail.com", 193 | Password: "password", 194 | }, 195 | models.User{ 196 | Nickname: "Magu Frank", 197 | Email: "magu@gmail.com", 198 | Password: "password", 199 | }, 200 | } 201 | var posts = []models.Post{ 202 | models.Post{ 203 | Title: "Title 1", 204 | Content: "Hello world 1", 205 | }, 206 | models.Post{ 207 | Title: "Title 2", 208 | Content: "Hello world 2", 209 | }, 210 | } 211 | 212 | for i, _ := range users { 213 | err = server.DB.Model(&models.User{}).Create(&users[i]).Error 214 | if err != nil { 215 | log.Fatalf("cannot seed users table: %v", err) 216 | } 217 | posts[i].AuthorID = users[i].ID 218 | 219 | err = server.DB.Model(&models.Post{}).Create(&posts[i]).Error 220 | if err != nil { 221 | log.Fatalf("cannot seed posts table: %v", err) 222 | } 223 | } 224 | return users, posts, nil 225 | } 226 | -------------------------------------------------------------------------------- /tests/controllertests/login_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllertests 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | 13 | "gopkg.in/go-playground/assert.v1" 14 | ) 15 | 16 | func TestSignIn(t *testing.T) { 17 | 18 | err := refreshUserTable() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | user, err := seedOneUser() 23 | if err != nil { 24 | fmt.Printf("This is the error %v\n", err) 25 | } 26 | 27 | samples := []struct { 28 | email string 29 | password string 30 | errorMessage string 31 | }{ 32 | { 33 | email: user.Email, 34 | password: "password", //Note the password has to be this, not the hashed one from the database 35 | errorMessage: "", 36 | }, 37 | { 38 | email: user.Email, 39 | password: "Wrong password", 40 | errorMessage: "crypto/bcrypt: hashedPassword is not the hash of the given password", 41 | }, 42 | { 43 | email: "Wrong email", 44 | password: "password", 45 | errorMessage: "record not found", 46 | }, 47 | } 48 | 49 | for _, v := range samples { 50 | 51 | token, err := server.SignIn(v.email, v.password) 52 | if err != nil { 53 | assert.Equal(t, err, errors.New(v.errorMessage)) 54 | } else { 55 | assert.NotEqual(t, token, "") 56 | } 57 | } 58 | } 59 | 60 | func TestLogin(t *testing.T) { 61 | 62 | refreshUserTable() 63 | 64 | _, err := seedOneUser() 65 | if err != nil { 66 | fmt.Printf("This is the error %v\n", err) 67 | } 68 | samples := []struct { 69 | inputJSON string 70 | statusCode int 71 | email string 72 | password string 73 | errorMessage string 74 | }{ 75 | { 76 | inputJSON: `{"email": "pet@gmail.com", "password": "password"}`, 77 | statusCode: 200, 78 | errorMessage: "", 79 | }, 80 | { 81 | inputJSON: `{"email": "pet@gmail.com", "password": "wrong password"}`, 82 | statusCode: 422, 83 | errorMessage: "Incorrect Password", 84 | }, 85 | { 86 | inputJSON: `{"email": "frank@gmail.com", "password": "password"}`, 87 | statusCode: 422, 88 | errorMessage: "Incorrect Details", 89 | }, 90 | { 91 | inputJSON: `{"email": "kangmail.com", "password": "password"}`, 92 | statusCode: 422, 93 | errorMessage: "Invalid Email", 94 | }, 95 | { 96 | inputJSON: `{"email": "", "password": "password"}`, 97 | statusCode: 422, 98 | errorMessage: "Required Email", 99 | }, 100 | { 101 | inputJSON: `{"email": "kan@gmail.com", "password": ""}`, 102 | statusCode: 422, 103 | errorMessage: "Required Password", 104 | }, 105 | { 106 | inputJSON: `{"email": "", "password": "password"}`, 107 | statusCode: 422, 108 | errorMessage: "Required Email", 109 | }, 110 | } 111 | 112 | for _, v := range samples { 113 | 114 | req, err := http.NewRequest("POST", "/login", bytes.NewBufferString(v.inputJSON)) 115 | if err != nil { 116 | t.Errorf("this is the error: %v", err) 117 | } 118 | rr := httptest.NewRecorder() 119 | handler := http.HandlerFunc(server.Login) 120 | handler.ServeHTTP(rr, req) 121 | 122 | assert.Equal(t, rr.Code, v.statusCode) 123 | if v.statusCode == 200 { 124 | assert.NotEqual(t, rr.Body.String(), "") 125 | } 126 | 127 | if v.statusCode == 422 && v.errorMessage != "" { 128 | responseMap := make(map[string]interface{}) 129 | err = json.Unmarshal([]byte(rr.Body.String()), &responseMap) 130 | if err != nil { 131 | t.Errorf("Cannot convert to json: %v", err) 132 | } 133 | assert.Equal(t, responseMap["error"], v.errorMessage) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/controllertests/post_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllertests 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/http/httptest" 10 | "strconv" 11 | "testing" 12 | 13 | "github.com/gorilla/mux" 14 | "github.com/victorsteven/fullstack/api/models" 15 | "gopkg.in/go-playground/assert.v1" 16 | ) 17 | 18 | func TestCreatePost(t *testing.T) { 19 | 20 | err := refreshUserAndPostTable() 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | user, err := seedOneUser() 25 | if err != nil { 26 | log.Fatalf("Cannot seed user %v\n", err) 27 | } 28 | token, err := server.SignIn(user.Email, "password") //Note the password in the database is already hashed, we want unhashed 29 | if err != nil { 30 | log.Fatalf("cannot login: %v\n", err) 31 | } 32 | tokenString := fmt.Sprintf("Bearer %v", token) 33 | 34 | samples := []struct { 35 | inputJSON string 36 | statusCode int 37 | title string 38 | content string 39 | author_id uint32 40 | tokenGiven string 41 | errorMessage string 42 | }{ 43 | { 44 | inputJSON: `{"title":"The title", "content": "the content", "author_id": 1}`, 45 | statusCode: 201, 46 | tokenGiven: tokenString, 47 | title: "The title", 48 | content: "the content", 49 | author_id: user.ID, 50 | errorMessage: "", 51 | }, 52 | { 53 | inputJSON: `{"title":"The title", "content": "the content", "author_id": 1}`, 54 | statusCode: 500, 55 | tokenGiven: tokenString, 56 | errorMessage: "Title Already Taken", 57 | }, 58 | { 59 | // When no token is passed 60 | inputJSON: `{"title":"When no token is passed", "content": "the content", "author_id": 1}`, 61 | statusCode: 401, 62 | tokenGiven: "", 63 | errorMessage: "Unauthorized", 64 | }, 65 | { 66 | // When incorrect token is passed 67 | inputJSON: `{"title":"When incorrect token is passed", "content": "the content", "author_id": 1}`, 68 | statusCode: 401, 69 | tokenGiven: "This is an incorrect token", 70 | errorMessage: "Unauthorized", 71 | }, 72 | { 73 | inputJSON: `{"title": "", "content": "The content", "author_id": 1}`, 74 | statusCode: 422, 75 | tokenGiven: tokenString, 76 | errorMessage: "Required Title", 77 | }, 78 | { 79 | inputJSON: `{"title": "This is a title", "content": "", "author_id": 1}`, 80 | statusCode: 422, 81 | tokenGiven: tokenString, 82 | errorMessage: "Required Content", 83 | }, 84 | { 85 | inputJSON: `{"title": "This is an awesome title", "content": "the content"}`, 86 | statusCode: 422, 87 | tokenGiven: tokenString, 88 | errorMessage: "Required Author", 89 | }, 90 | { 91 | // When user 2 uses user 1 token 92 | inputJSON: `{"title": "This is an awesome title", "content": "the content", "author_id": 2}`, 93 | statusCode: 401, 94 | tokenGiven: tokenString, 95 | errorMessage: "Unauthorized", 96 | }, 97 | } 98 | for _, v := range samples { 99 | 100 | req, err := http.NewRequest("POST", "/posts", bytes.NewBufferString(v.inputJSON)) 101 | if err != nil { 102 | t.Errorf("this is the error: %v\n", err) 103 | } 104 | rr := httptest.NewRecorder() 105 | handler := http.HandlerFunc(server.CreatePost) 106 | 107 | req.Header.Set("Authorization", v.tokenGiven) 108 | handler.ServeHTTP(rr, req) 109 | 110 | responseMap := make(map[string]interface{}) 111 | err = json.Unmarshal([]byte(rr.Body.String()), &responseMap) 112 | if err != nil { 113 | fmt.Printf("Cannot convert to json: %v", err) 114 | } 115 | assert.Equal(t, rr.Code, v.statusCode) 116 | if v.statusCode == 201 { 117 | assert.Equal(t, responseMap["title"], v.title) 118 | assert.Equal(t, responseMap["content"], v.content) 119 | assert.Equal(t, responseMap["author_id"], float64(v.author_id)) //just for both ids to have the same type 120 | } 121 | if v.statusCode == 401 || v.statusCode == 422 || v.statusCode == 500 && v.errorMessage != "" { 122 | assert.Equal(t, responseMap["error"], v.errorMessage) 123 | } 124 | } 125 | } 126 | 127 | func TestGetPosts(t *testing.T) { 128 | 129 | err := refreshUserAndPostTable() 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | _, _, err = seedUsersAndPosts() 134 | if err != nil { 135 | log.Fatal(err) 136 | } 137 | 138 | req, err := http.NewRequest("GET", "/posts", nil) 139 | if err != nil { 140 | t.Errorf("this is the error: %v\n", err) 141 | } 142 | rr := httptest.NewRecorder() 143 | handler := http.HandlerFunc(server.GetPosts) 144 | handler.ServeHTTP(rr, req) 145 | 146 | var posts []models.Post 147 | err = json.Unmarshal([]byte(rr.Body.String()), &posts) 148 | 149 | assert.Equal(t, rr.Code, http.StatusOK) 150 | assert.Equal(t, len(posts), 2) 151 | } 152 | func TestGetPostByID(t *testing.T) { 153 | 154 | err := refreshUserAndPostTable() 155 | if err != nil { 156 | log.Fatal(err) 157 | } 158 | post, err := seedOneUserAndOnePost() 159 | if err != nil { 160 | log.Fatal(err) 161 | } 162 | postSample := []struct { 163 | id string 164 | statusCode int 165 | title string 166 | content string 167 | author_id uint32 168 | errorMessage string 169 | }{ 170 | { 171 | id: strconv.Itoa(int(post.ID)), 172 | statusCode: 200, 173 | title: post.Title, 174 | content: post.Content, 175 | author_id: post.AuthorID, 176 | }, 177 | { 178 | id: "unknwon", 179 | statusCode: 400, 180 | }, 181 | } 182 | for _, v := range postSample { 183 | 184 | req, err := http.NewRequest("GET", "/posts", nil) 185 | if err != nil { 186 | t.Errorf("this is the error: %v\n", err) 187 | } 188 | req = mux.SetURLVars(req, map[string]string{"id": v.id}) 189 | 190 | rr := httptest.NewRecorder() 191 | handler := http.HandlerFunc(server.GetPost) 192 | handler.ServeHTTP(rr, req) 193 | 194 | responseMap := make(map[string]interface{}) 195 | err = json.Unmarshal([]byte(rr.Body.String()), &responseMap) 196 | if err != nil { 197 | log.Fatalf("Cannot convert to json: %v", err) 198 | } 199 | assert.Equal(t, rr.Code, v.statusCode) 200 | 201 | if v.statusCode == 200 { 202 | assert.Equal(t, post.Title, responseMap["title"]) 203 | assert.Equal(t, post.Content, responseMap["content"]) 204 | assert.Equal(t, float64(post.AuthorID), responseMap["author_id"]) //the response author id is float64 205 | } 206 | } 207 | } 208 | 209 | func TestUpdatePost(t *testing.T) { 210 | 211 | var PostUserEmail, PostUserPassword string 212 | var AuthPostAuthorID uint32 213 | var AuthPostID uint64 214 | 215 | err := refreshUserAndPostTable() 216 | if err != nil { 217 | log.Fatal(err) 218 | } 219 | users, posts, err := seedUsersAndPosts() 220 | if err != nil { 221 | log.Fatal(err) 222 | } 223 | // Get only the first user 224 | for _, user := range users { 225 | if user.ID == 2 { 226 | continue 227 | } 228 | PostUserEmail = user.Email 229 | PostUserPassword = "password" //Note the password in the database is already hashed, we want unhashed 230 | } 231 | //Login the user and get the authentication token 232 | token, err := server.SignIn(PostUserEmail, PostUserPassword) 233 | if err != nil { 234 | log.Fatalf("cannot login: %v\n", err) 235 | } 236 | tokenString := fmt.Sprintf("Bearer %v", token) 237 | 238 | // Get only the first post 239 | for _, post := range posts { 240 | if post.ID == 2 { 241 | continue 242 | } 243 | AuthPostID = post.ID 244 | AuthPostAuthorID = post.AuthorID 245 | } 246 | // fmt.Printf("this is the auth post: %v\n", AuthPostID) 247 | 248 | samples := []struct { 249 | id string 250 | updateJSON string 251 | statusCode int 252 | title string 253 | content string 254 | author_id uint32 255 | tokenGiven string 256 | errorMessage string 257 | }{ 258 | { 259 | // Convert int64 to int first before converting to string 260 | id: strconv.Itoa(int(AuthPostID)), 261 | updateJSON: `{"title":"The updated post", "content": "This is the updated content", "author_id": 1}`, 262 | statusCode: 200, 263 | title: "The updated post", 264 | content: "This is the updated content", 265 | author_id: AuthPostAuthorID, 266 | tokenGiven: tokenString, 267 | errorMessage: "", 268 | }, 269 | { 270 | // When no token is provided 271 | id: strconv.Itoa(int(AuthPostID)), 272 | updateJSON: `{"title":"This is still another title", "content": "This is the updated content", "author_id": 1}`, 273 | tokenGiven: "", 274 | statusCode: 401, 275 | errorMessage: "Unauthorized", 276 | }, 277 | { 278 | // When incorrect token is provided 279 | id: strconv.Itoa(int(AuthPostID)), 280 | updateJSON: `{"title":"This is still another title", "content": "This is the updated content", "author_id": 1}`, 281 | tokenGiven: "this is an incorrect token", 282 | statusCode: 401, 283 | errorMessage: "Unauthorized", 284 | }, 285 | { 286 | //Note: "Title 2" belongs to post 2, and title must be unique 287 | id: strconv.Itoa(int(AuthPostID)), 288 | updateJSON: `{"title":"Title 2", "content": "This is the updated content", "author_id": 1}`, 289 | statusCode: 500, 290 | tokenGiven: tokenString, 291 | errorMessage: "Title Already Taken", 292 | }, 293 | { 294 | id: strconv.Itoa(int(AuthPostID)), 295 | updateJSON: `{"title":"", "content": "This is the updated content", "author_id": 1}`, 296 | statusCode: 422, 297 | tokenGiven: tokenString, 298 | errorMessage: "Required Title", 299 | }, 300 | { 301 | id: strconv.Itoa(int(AuthPostID)), 302 | updateJSON: `{"title":"Awesome title", "content": "", "author_id": 1}`, 303 | statusCode: 422, 304 | tokenGiven: tokenString, 305 | errorMessage: "Required Content", 306 | }, 307 | { 308 | id: strconv.Itoa(int(AuthPostID)), 309 | updateJSON: `{"title":"This is another title", "content": "This is the updated content"}`, 310 | statusCode: 401, 311 | tokenGiven: tokenString, 312 | errorMessage: "Unauthorized", 313 | }, 314 | { 315 | id: "unknwon", 316 | statusCode: 400, 317 | }, 318 | { 319 | id: strconv.Itoa(int(AuthPostID)), 320 | updateJSON: `{"title":"This is still another title", "content": "This is the updated content", "author_id": 2}`, 321 | tokenGiven: tokenString, 322 | statusCode: 401, 323 | errorMessage: "Unauthorized", 324 | }, 325 | } 326 | 327 | for _, v := range samples { 328 | 329 | req, err := http.NewRequest("POST", "/posts", bytes.NewBufferString(v.updateJSON)) 330 | if err != nil { 331 | t.Errorf("this is the error: %v\n", err) 332 | } 333 | req = mux.SetURLVars(req, map[string]string{"id": v.id}) 334 | rr := httptest.NewRecorder() 335 | handler := http.HandlerFunc(server.UpdatePost) 336 | 337 | req.Header.Set("Authorization", v.tokenGiven) 338 | 339 | handler.ServeHTTP(rr, req) 340 | 341 | responseMap := make(map[string]interface{}) 342 | err = json.Unmarshal([]byte(rr.Body.String()), &responseMap) 343 | if err != nil { 344 | t.Errorf("Cannot convert to json: %v", err) 345 | } 346 | assert.Equal(t, rr.Code, v.statusCode) 347 | if v.statusCode == 200 { 348 | assert.Equal(t, responseMap["title"], v.title) 349 | assert.Equal(t, responseMap["content"], v.content) 350 | assert.Equal(t, responseMap["author_id"], float64(v.author_id)) //just to match the type of the json we receive thats why we used float64 351 | } 352 | if v.statusCode == 401 || v.statusCode == 422 || v.statusCode == 500 && v.errorMessage != "" { 353 | assert.Equal(t, responseMap["error"], v.errorMessage) 354 | } 355 | } 356 | } 357 | 358 | func TestDeletePost(t *testing.T) { 359 | 360 | var PostUserEmail, PostUserPassword string 361 | var PostUserID uint32 362 | var AuthPostID uint64 363 | 364 | err := refreshUserAndPostTable() 365 | if err != nil { 366 | log.Fatal(err) 367 | } 368 | users, posts, err := seedUsersAndPosts() 369 | if err != nil { 370 | log.Fatal(err) 371 | } 372 | //Let's get only the Second user 373 | for _, user := range users { 374 | if user.ID == 1 { 375 | continue 376 | } 377 | PostUserEmail = user.Email 378 | PostUserPassword = "password" //Note the password in the database is already hashed, we want unhashed 379 | } 380 | //Login the user and get the authentication token 381 | token, err := server.SignIn(PostUserEmail, PostUserPassword) 382 | if err != nil { 383 | log.Fatalf("cannot login: %v\n", err) 384 | } 385 | tokenString := fmt.Sprintf("Bearer %v", token) 386 | 387 | // Get only the second post 388 | for _, post := range posts { 389 | if post.ID == 1 { 390 | continue 391 | } 392 | AuthPostID = post.ID 393 | PostUserID = post.AuthorID 394 | } 395 | postSample := []struct { 396 | id string 397 | author_id uint32 398 | tokenGiven string 399 | statusCode int 400 | errorMessage string 401 | }{ 402 | { 403 | // Convert int64 to int first before converting to string 404 | id: strconv.Itoa(int(AuthPostID)), 405 | author_id: PostUserID, 406 | tokenGiven: tokenString, 407 | statusCode: 204, 408 | errorMessage: "", 409 | }, 410 | { 411 | // When empty token is passed 412 | id: strconv.Itoa(int(AuthPostID)), 413 | author_id: PostUserID, 414 | tokenGiven: "", 415 | statusCode: 401, 416 | errorMessage: "Unauthorized", 417 | }, 418 | { 419 | // When incorrect token is passed 420 | id: strconv.Itoa(int(AuthPostID)), 421 | author_id: PostUserID, 422 | tokenGiven: "This is an incorrect token", 423 | statusCode: 401, 424 | errorMessage: "Unauthorized", 425 | }, 426 | { 427 | id: "unknwon", 428 | tokenGiven: tokenString, 429 | statusCode: 400, 430 | }, 431 | { 432 | id: strconv.Itoa(int(1)), 433 | author_id: 1, 434 | statusCode: 401, 435 | errorMessage: "Unauthorized", 436 | }, 437 | } 438 | for _, v := range postSample { 439 | 440 | req, _ := http.NewRequest("GET", "/posts", nil) 441 | req = mux.SetURLVars(req, map[string]string{"id": v.id}) 442 | 443 | rr := httptest.NewRecorder() 444 | handler := http.HandlerFunc(server.DeletePost) 445 | 446 | req.Header.Set("Authorization", v.tokenGiven) 447 | 448 | handler.ServeHTTP(rr, req) 449 | 450 | assert.Equal(t, rr.Code, v.statusCode) 451 | 452 | if v.statusCode == 401 && v.errorMessage != "" { 453 | 454 | responseMap := make(map[string]interface{}) 455 | err = json.Unmarshal([]byte(rr.Body.String()), &responseMap) 456 | if err != nil { 457 | t.Errorf("Cannot convert to json: %v", err) 458 | } 459 | assert.Equal(t, responseMap["error"], v.errorMessage) 460 | } 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /tests/controllertests/user_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllertests 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/http/httptest" 10 | "strconv" 11 | "testing" 12 | 13 | "github.com/gorilla/mux" 14 | "github.com/victorsteven/fullstack/api/models" 15 | "gopkg.in/go-playground/assert.v1" 16 | ) 17 | 18 | func TestCreateUser(t *testing.T) { 19 | 20 | err := refreshUserTable() 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | samples := []struct { 25 | inputJSON string 26 | statusCode int 27 | nickname string 28 | email string 29 | errorMessage string 30 | }{ 31 | { 32 | inputJSON: `{"nickname":"Pet", "email": "pet@gmail.com", "password": "password"}`, 33 | statusCode: 201, 34 | nickname: "Pet", 35 | email: "pet@gmail.com", 36 | errorMessage: "", 37 | }, 38 | { 39 | inputJSON: `{"nickname":"Frank", "email": "pet@gmail.com", "password": "password"}`, 40 | statusCode: 500, 41 | errorMessage: "Email Already Taken", 42 | }, 43 | { 44 | inputJSON: `{"nickname":"Pet", "email": "grand@gmail.com", "password": "password"}`, 45 | statusCode: 500, 46 | errorMessage: "Nickname Already Taken", 47 | }, 48 | { 49 | inputJSON: `{"nickname":"Kan", "email": "kangmail.com", "password": "password"}`, 50 | statusCode: 422, 51 | errorMessage: "Invalid Email", 52 | }, 53 | { 54 | inputJSON: `{"nickname": "", "email": "kan@gmail.com", "password": "password"}`, 55 | statusCode: 422, 56 | errorMessage: "Required Nickname", 57 | }, 58 | { 59 | inputJSON: `{"nickname": "Kan", "email": "", "password": "password"}`, 60 | statusCode: 422, 61 | errorMessage: "Required Email", 62 | }, 63 | { 64 | inputJSON: `{"nickname": "Kan", "email": "kan@gmail.com", "password": ""}`, 65 | statusCode: 422, 66 | errorMessage: "Required Password", 67 | }, 68 | } 69 | 70 | for _, v := range samples { 71 | 72 | req, err := http.NewRequest("POST", "/users", bytes.NewBufferString(v.inputJSON)) 73 | if err != nil { 74 | t.Errorf("this is the error: %v", err) 75 | } 76 | rr := httptest.NewRecorder() 77 | handler := http.HandlerFunc(server.CreateUser) 78 | handler.ServeHTTP(rr, req) 79 | 80 | responseMap := make(map[string]interface{}) 81 | err = json.Unmarshal([]byte(rr.Body.String()), &responseMap) 82 | if err != nil { 83 | fmt.Printf("Cannot convert to json: %v", err) 84 | } 85 | assert.Equal(t, rr.Code, v.statusCode) 86 | if v.statusCode == 201 { 87 | assert.Equal(t, responseMap["nickname"], v.nickname) 88 | assert.Equal(t, responseMap["email"], v.email) 89 | } 90 | if v.statusCode == 422 || v.statusCode == 500 && v.errorMessage != "" { 91 | assert.Equal(t, responseMap["error"], v.errorMessage) 92 | } 93 | } 94 | } 95 | 96 | func TestGetUsers(t *testing.T) { 97 | 98 | err := refreshUserTable() 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | _, err = seedUsers() 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | req, err := http.NewRequest("GET", "/users", nil) 107 | if err != nil { 108 | t.Errorf("this is the error: %v\n", err) 109 | } 110 | rr := httptest.NewRecorder() 111 | handler := http.HandlerFunc(server.GetUsers) 112 | handler.ServeHTTP(rr, req) 113 | 114 | var users []models.User 115 | err = json.Unmarshal([]byte(rr.Body.String()), &users) 116 | if err != nil { 117 | log.Fatalf("Cannot convert to json: %v\n", err) 118 | } 119 | assert.Equal(t, rr.Code, http.StatusOK) 120 | assert.Equal(t, len(users), 2) 121 | } 122 | 123 | func TestGetUserByID(t *testing.T) { 124 | 125 | err := refreshUserTable() 126 | if err != nil { 127 | log.Fatal(err) 128 | } 129 | user, err := seedOneUser() 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | userSample := []struct { 134 | id string 135 | statusCode int 136 | nickname string 137 | email string 138 | errorMessage string 139 | }{ 140 | { 141 | id: strconv.Itoa(int(user.ID)), 142 | statusCode: 200, 143 | nickname: user.Nickname, 144 | email: user.Email, 145 | }, 146 | { 147 | id: "unknwon", 148 | statusCode: 400, 149 | }, 150 | } 151 | for _, v := range userSample { 152 | 153 | req, err := http.NewRequest("GET", "/users", nil) 154 | if err != nil { 155 | t.Errorf("This is the error: %v\n", err) 156 | } 157 | req = mux.SetURLVars(req, map[string]string{"id": v.id}) 158 | rr := httptest.NewRecorder() 159 | handler := http.HandlerFunc(server.GetUser) 160 | handler.ServeHTTP(rr, req) 161 | 162 | responseMap := make(map[string]interface{}) 163 | err = json.Unmarshal([]byte(rr.Body.String()), &responseMap) 164 | if err != nil { 165 | log.Fatalf("Cannot convert to json: %v", err) 166 | } 167 | 168 | assert.Equal(t, rr.Code, v.statusCode) 169 | 170 | if v.statusCode == 200 { 171 | assert.Equal(t, user.Nickname, responseMap["nickname"]) 172 | assert.Equal(t, user.Email, responseMap["email"]) 173 | } 174 | } 175 | } 176 | 177 | func TestUpdateUser(t *testing.T) { 178 | 179 | var AuthEmail, AuthPassword string 180 | var AuthID uint32 181 | 182 | err := refreshUserTable() 183 | if err != nil { 184 | log.Fatal(err) 185 | } 186 | users, err := seedUsers() //we need atleast two users to properly check the update 187 | if err != nil { 188 | log.Fatalf("Error seeding user: %v\n", err) 189 | } 190 | // Get only the first user 191 | for _, user := range users { 192 | if user.ID == 2 { 193 | continue 194 | } 195 | AuthID = user.ID 196 | AuthEmail = user.Email 197 | AuthPassword = "password" //Note the password in the database is already hashed, we want unhashed 198 | } 199 | //Login the user and get the authentication token 200 | token, err := server.SignIn(AuthEmail, AuthPassword) 201 | if err != nil { 202 | log.Fatalf("cannot login: %v\n", err) 203 | } 204 | tokenString := fmt.Sprintf("Bearer %v", token) 205 | 206 | samples := []struct { 207 | id string 208 | updateJSON string 209 | statusCode int 210 | updateNickname string 211 | updateEmail string 212 | tokenGiven string 213 | errorMessage string 214 | }{ 215 | { 216 | // Convert int32 to int first before converting to string 217 | id: strconv.Itoa(int(AuthID)), 218 | updateJSON: `{"nickname":"Grand", "email": "grand@gmail.com", "password": "password"}`, 219 | statusCode: 200, 220 | updateNickname: "Grand", 221 | updateEmail: "grand@gmail.com", 222 | tokenGiven: tokenString, 223 | errorMessage: "", 224 | }, 225 | { 226 | // When password field is empty 227 | id: strconv.Itoa(int(AuthID)), 228 | updateJSON: `{"nickname":"Woman", "email": "woman@gmail.com", "password": ""}`, 229 | statusCode: 422, 230 | tokenGiven: tokenString, 231 | errorMessage: "Required Password", 232 | }, 233 | { 234 | // When no token was passed 235 | id: strconv.Itoa(int(AuthID)), 236 | updateJSON: `{"nickname":"Man", "email": "man@gmail.com", "password": "password"}`, 237 | statusCode: 401, 238 | tokenGiven: "", 239 | errorMessage: "Unauthorized", 240 | }, 241 | { 242 | // When incorrect token was passed 243 | id: strconv.Itoa(int(AuthID)), 244 | updateJSON: `{"nickname":"Woman", "email": "woman@gmail.com", "password": "password"}`, 245 | statusCode: 401, 246 | tokenGiven: "This is incorrect token", 247 | errorMessage: "Unauthorized", 248 | }, 249 | { 250 | // Remember "kenny@gmail.com" belongs to user 2 251 | id: strconv.Itoa(int(AuthID)), 252 | updateJSON: `{"nickname":"Frank", "email": "kenny@gmail.com", "password": "password"}`, 253 | statusCode: 500, 254 | tokenGiven: tokenString, 255 | errorMessage: "Email Already Taken", 256 | }, 257 | { 258 | // Remember "Kenny Morris" belongs to user 2 259 | id: strconv.Itoa(int(AuthID)), 260 | updateJSON: `{"nickname":"Kenny Morris", "email": "grand@gmail.com", "password": "password"}`, 261 | statusCode: 500, 262 | tokenGiven: tokenString, 263 | errorMessage: "Nickname Already Taken", 264 | }, 265 | { 266 | id: strconv.Itoa(int(AuthID)), 267 | updateJSON: `{"nickname":"Kan", "email": "kangmail.com", "password": "password"}`, 268 | statusCode: 422, 269 | tokenGiven: tokenString, 270 | errorMessage: "Invalid Email", 271 | }, 272 | { 273 | id: strconv.Itoa(int(AuthID)), 274 | updateJSON: `{"nickname": "", "email": "kan@gmail.com", "password": "password"}`, 275 | statusCode: 422, 276 | tokenGiven: tokenString, 277 | errorMessage: "Required Nickname", 278 | }, 279 | { 280 | id: strconv.Itoa(int(AuthID)), 281 | updateJSON: `{"nickname": "Kan", "email": "", "password": "password"}`, 282 | statusCode: 422, 283 | tokenGiven: tokenString, 284 | errorMessage: "Required Email", 285 | }, 286 | { 287 | id: "unknwon", 288 | tokenGiven: tokenString, 289 | statusCode: 400, 290 | }, 291 | { 292 | // When user 2 is using user 1 token 293 | id: strconv.Itoa(int(2)), 294 | updateJSON: `{"nickname": "Mike", "email": "mike@gmail.com", "password": "password"}`, 295 | tokenGiven: tokenString, 296 | statusCode: 401, 297 | errorMessage: "Unauthorized", 298 | }, 299 | } 300 | 301 | for _, v := range samples { 302 | 303 | req, err := http.NewRequest("POST", "/users", bytes.NewBufferString(v.updateJSON)) 304 | if err != nil { 305 | t.Errorf("This is the error: %v\n", err) 306 | } 307 | req = mux.SetURLVars(req, map[string]string{"id": v.id}) 308 | 309 | rr := httptest.NewRecorder() 310 | handler := http.HandlerFunc(server.UpdateUser) 311 | 312 | req.Header.Set("Authorization", v.tokenGiven) 313 | 314 | handler.ServeHTTP(rr, req) 315 | 316 | responseMap := make(map[string]interface{}) 317 | err = json.Unmarshal([]byte(rr.Body.String()), &responseMap) 318 | if err != nil { 319 | t.Errorf("Cannot convert to json: %v", err) 320 | } 321 | assert.Equal(t, rr.Code, v.statusCode) 322 | if v.statusCode == 200 { 323 | assert.Equal(t, responseMap["nickname"], v.updateNickname) 324 | assert.Equal(t, responseMap["email"], v.updateEmail) 325 | } 326 | if v.statusCode == 401 || v.statusCode == 422 || v.statusCode == 500 && v.errorMessage != "" { 327 | assert.Equal(t, responseMap["error"], v.errorMessage) 328 | } 329 | } 330 | } 331 | 332 | func TestDeleteUser(t *testing.T) { 333 | 334 | var AuthEmail, AuthPassword string 335 | var AuthID uint32 336 | 337 | err := refreshUserTable() 338 | if err != nil { 339 | log.Fatal(err) 340 | } 341 | 342 | users, err := seedUsers() //we need atleast two users to properly check the update 343 | if err != nil { 344 | log.Fatalf("Error seeding user: %v\n", err) 345 | } 346 | // Get only the first and log him in 347 | for _, user := range users { 348 | if user.ID == 2 { 349 | continue 350 | } 351 | AuthID = user.ID 352 | AuthEmail = user.Email 353 | AuthPassword = "password" ////Note the password in the database is already hashed, we want unhashed 354 | } 355 | //Login the user and get the authentication token 356 | token, err := server.SignIn(AuthEmail, AuthPassword) 357 | if err != nil { 358 | log.Fatalf("cannot login: %v\n", err) 359 | } 360 | tokenString := fmt.Sprintf("Bearer %v", token) 361 | 362 | userSample := []struct { 363 | id string 364 | tokenGiven string 365 | statusCode int 366 | errorMessage string 367 | }{ 368 | { 369 | // Convert int32 to int first before converting to string 370 | id: strconv.Itoa(int(AuthID)), 371 | tokenGiven: tokenString, 372 | statusCode: 204, 373 | errorMessage: "", 374 | }, 375 | { 376 | // When no token is given 377 | id: strconv.Itoa(int(AuthID)), 378 | tokenGiven: "", 379 | statusCode: 401, 380 | errorMessage: "Unauthorized", 381 | }, 382 | { 383 | // When incorrect token is given 384 | id: strconv.Itoa(int(AuthID)), 385 | tokenGiven: "This is an incorrect token", 386 | statusCode: 401, 387 | errorMessage: "Unauthorized", 388 | }, 389 | { 390 | id: "unknwon", 391 | tokenGiven: tokenString, 392 | statusCode: 400, 393 | }, 394 | { 395 | // User 2 trying to use User 1 token 396 | id: strconv.Itoa(int(2)), 397 | tokenGiven: tokenString, 398 | statusCode: 401, 399 | errorMessage: "Unauthorized", 400 | }, 401 | } 402 | for _, v := range userSample { 403 | 404 | req, err := http.NewRequest("GET", "/users", nil) 405 | if err != nil { 406 | t.Errorf("This is the error: %v\n", err) 407 | } 408 | req = mux.SetURLVars(req, map[string]string{"id": v.id}) 409 | rr := httptest.NewRecorder() 410 | handler := http.HandlerFunc(server.DeleteUser) 411 | 412 | req.Header.Set("Authorization", v.tokenGiven) 413 | 414 | handler.ServeHTTP(rr, req) 415 | assert.Equal(t, rr.Code, v.statusCode) 416 | 417 | if v.statusCode == 401 && v.errorMessage != "" { 418 | responseMap := make(map[string]interface{}) 419 | err = json.Unmarshal([]byte(rr.Body.String()), &responseMap) 420 | if err != nil { 421 | t.Errorf("Cannot convert to json: %v", err) 422 | } 423 | assert.Equal(t, responseMap["error"], v.errorMessage) 424 | } 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /tests/modeltests/model_test.go: -------------------------------------------------------------------------------- 1 | package modeltests 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/jinzhu/gorm" 10 | "github.com/joho/godotenv" 11 | "github.com/victorsteven/fullstack/api/controllers" 12 | "github.com/victorsteven/fullstack/api/models" 13 | ) 14 | 15 | var server = controllers.Server{} 16 | var userInstance = models.User{} 17 | var postInstance = models.Post{} 18 | 19 | func TestMain(m *testing.M) { 20 | var err error 21 | err = godotenv.Load(os.ExpandEnv("../../.env")) 22 | if err != nil { 23 | log.Fatalf("Error getting env %v\n", err) 24 | } 25 | Database() 26 | 27 | log.Printf("Before calling m.Run() !!!") 28 | ret := m.Run() 29 | log.Printf("After calling m.Run() !!!") 30 | //os.Exit(m.Run()) 31 | os.Exit(ret) 32 | } 33 | 34 | func Database() { 35 | 36 | var err error 37 | 38 | TestDbDriver := os.Getenv("TestDbDriver") 39 | 40 | if TestDbDriver == "mysql" { 41 | 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")) 42 | server.DB, err = gorm.Open(TestDbDriver, DBURL) 43 | if err != nil { 44 | fmt.Printf("Cannot connect to %s database\n", TestDbDriver) 45 | log.Fatal("This is the error:", err) 46 | } else { 47 | fmt.Printf("We are connected to the %s database\n", TestDbDriver) 48 | } 49 | } 50 | if TestDbDriver == "postgres" { 51 | 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")) 52 | server.DB, err = gorm.Open(TestDbDriver, DBURL) 53 | if err != nil { 54 | fmt.Printf("Cannot connect to %s database\n", TestDbDriver) 55 | log.Fatal("This is the error:", err) 56 | } else { 57 | fmt.Printf("We are connected to the %s database\n", TestDbDriver) 58 | } 59 | } 60 | if TestDbDriver == "sqlite3" { 61 | //DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword) 62 | testDbName := os.Getenv("TestDbName") 63 | server.DB, err = gorm.Open(TestDbDriver, testDbName) 64 | if err != nil { 65 | fmt.Printf("Cannot connect to %s database\n", TestDbDriver) 66 | log.Fatal("This is the error:", err) 67 | } else { 68 | fmt.Printf("We are connected to the %s database\n", TestDbDriver) 69 | } 70 | server.DB.Exec("PRAGMA foreign_keys = ON") 71 | } 72 | 73 | } 74 | 75 | func refreshUserTable() error { 76 | server.DB.Exec("SET foreign_key_checks=0") 77 | err := server.DB.Debug().DropTableIfExists(&models.User{}).Error 78 | if err != nil { 79 | return err 80 | } 81 | server.DB.Exec("SET foreign_key_checks=1") 82 | err = server.DB.Debug().AutoMigrate(&models.User{}).Error 83 | if err != nil { 84 | return err 85 | } 86 | log.Printf("Successfully refreshed table") 87 | log.Printf("refreshUserTable routine OK !!!") 88 | return nil 89 | } 90 | 91 | func seedOneUser() (models.User, error) { 92 | 93 | _ = refreshUserTable() 94 | 95 | user := models.User{ 96 | Nickname: "Pet", 97 | Email: "pet@gmail.com", 98 | Password: "password", 99 | } 100 | 101 | err := server.DB.Debug().Model(&models.User{}).Create(&user).Error 102 | if err != nil { 103 | log.Fatalf("cannot seed users table: %v", err) 104 | } 105 | 106 | log.Printf("seedOneUser routine OK !!!") 107 | return user, nil 108 | } 109 | 110 | func seedUsers() error { 111 | 112 | users := []models.User{ 113 | models.User{ 114 | Nickname: "Steven victor", 115 | Email: "steven@gmail.com", 116 | Password: "password", 117 | }, 118 | models.User{ 119 | Nickname: "Kenny Morris", 120 | Email: "kenny@gmail.com", 121 | Password: "password", 122 | }, 123 | } 124 | 125 | for i := range users { 126 | err := server.DB.Debug().Model(&models.User{}).Create(&users[i]).Error 127 | if err != nil { 128 | return err 129 | } 130 | } 131 | 132 | log.Printf("seedUsers routine OK !!!") 133 | return nil 134 | } 135 | 136 | func refreshUserAndPostTable() error { 137 | 138 | server.DB.Exec("SET foreign_key_checks=0") 139 | // NOTE: when deleting first delete Post as Post is depending on User table 140 | err := server.DB.Debug().DropTableIfExists(&models.Post{}, &models.User{}).Error 141 | if err != nil { 142 | return err 143 | } 144 | server.DB.Exec("SET foreign_key_checks=1") 145 | err = server.DB.Debug().AutoMigrate(&models.User{}, &models.Post{}).Error 146 | if err != nil { 147 | return err 148 | } 149 | log.Printf("Successfully refreshed tables") 150 | log.Printf("refreshUserAndPostTable routine OK !!!") 151 | return nil 152 | } 153 | 154 | func seedOneUserAndOnePost() (models.Post, error) { 155 | 156 | err := refreshUserAndPostTable() 157 | if err != nil { 158 | return models.Post{}, err 159 | } 160 | user := models.User{ 161 | Nickname: "Sam Phil", 162 | Email: "sam@gmail.com", 163 | Password: "password", 164 | } 165 | err = server.DB.Debug().Model(&models.User{}).Create(&user).Error 166 | if err != nil { 167 | return models.Post{}, err 168 | } 169 | post := models.Post{ 170 | Title: "This is the title sam", 171 | Content: "This is the content sam", 172 | AuthorID: user.ID, 173 | } 174 | err = server.DB.Debug().Model(&models.Post{}).Create(&post).Error 175 | if err != nil { 176 | return models.Post{}, err 177 | } 178 | 179 | log.Printf("seedOneUserAndOnePost routine OK !!!") 180 | return post, nil 181 | } 182 | 183 | func seedUsersAndPosts() ([]models.User, []models.Post, error) { 184 | 185 | var err error 186 | 187 | if err != nil { 188 | return []models.User{}, []models.Post{}, err 189 | } 190 | var users = []models.User{ 191 | models.User{ 192 | Nickname: "Steven victor", 193 | Email: "steven@gmail.com", 194 | Password: "password", 195 | }, 196 | models.User{ 197 | Nickname: "Magu Frank", 198 | Email: "magu@gmail.com", 199 | Password: "password", 200 | }, 201 | } 202 | var posts = []models.Post{ 203 | models.Post{ 204 | Title: "Title 1", 205 | Content: "Hello world 1", 206 | }, 207 | models.Post{ 208 | Title: "Title 2", 209 | Content: "Hello world 2", 210 | }, 211 | } 212 | 213 | for i := range users { 214 | err = server.DB.Debug().Model(&models.User{}).Create(&users[i]).Error 215 | if err != nil { 216 | log.Fatalf("cannot seed users table: %v", err) 217 | } 218 | posts[i].AuthorID = users[i].ID 219 | 220 | err = server.DB.Debug().Model(&models.Post{}).Create(&posts[i]).Error 221 | if err != nil { 222 | log.Fatalf("cannot seed posts table: %v", err) 223 | } 224 | } 225 | log.Printf("seedUsersAndPosts routine OK !!!") 226 | return users, posts, nil 227 | } 228 | -------------------------------------------------------------------------------- /tests/modeltests/post_model_test.go: -------------------------------------------------------------------------------- 1 | package modeltests 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | _ "github.com/jinzhu/gorm/dialects/mysql" 8 | "github.com/victorsteven/fullstack/api/models" 9 | "gopkg.in/go-playground/assert.v1" 10 | ) 11 | 12 | func TestFindAllPosts(t *testing.T) { 13 | 14 | err := refreshUserAndPostTable() 15 | if err != nil { 16 | log.Fatalf("Error refreshing user and post table %v\n", err) 17 | } 18 | _, _, err = seedUsersAndPosts() 19 | if err != nil { 20 | log.Fatalf("Error seeding user and post table %v\n", err) 21 | } 22 | posts, err := postInstance.FindAllPosts(server.DB) 23 | if err != nil { 24 | t.Errorf("this is the error getting the posts: %v\n", err) 25 | return 26 | } 27 | assert.Equal(t, len(*posts), 2) 28 | } 29 | 30 | func TestSavePost(t *testing.T) { 31 | 32 | err := refreshUserAndPostTable() 33 | if err != nil { 34 | log.Fatalf("Error user and post refreshing table %v\n", err) 35 | } 36 | 37 | user, err := seedOneUser() 38 | if err != nil { 39 | log.Fatalf("Cannot seed user %v\n", err) 40 | } 41 | 42 | newPost := models.Post{ 43 | ID: 1, 44 | Title: "This is the title", 45 | Content: "This is the content", 46 | AuthorID: user.ID, 47 | } 48 | savedPost, err := newPost.SavePost(server.DB) 49 | if err != nil { 50 | t.Errorf("this is the error getting the post: %v\n", err) 51 | return 52 | } 53 | assert.Equal(t, newPost.ID, savedPost.ID) 54 | assert.Equal(t, newPost.Title, savedPost.Title) 55 | assert.Equal(t, newPost.Content, savedPost.Content) 56 | assert.Equal(t, newPost.AuthorID, savedPost.AuthorID) 57 | 58 | } 59 | 60 | func TestGetPostByID(t *testing.T) { 61 | 62 | err := refreshUserAndPostTable() 63 | if err != nil { 64 | log.Fatalf("Error refreshing user and post table: %v\n", err) 65 | } 66 | post, err := seedOneUserAndOnePost() 67 | if err != nil { 68 | log.Fatalf("Error Seeding User and post table") 69 | } 70 | foundPost, err := postInstance.FindPostByID(server.DB, post.ID) 71 | if err != nil { 72 | t.Errorf("this is the error getting one user: %v\n", err) 73 | return 74 | } 75 | assert.Equal(t, foundPost.ID, post.ID) 76 | assert.Equal(t, foundPost.Title, post.Title) 77 | assert.Equal(t, foundPost.Content, post.Content) 78 | } 79 | 80 | func TestUpdateAPost(t *testing.T) { 81 | 82 | err := refreshUserAndPostTable() 83 | if err != nil { 84 | log.Fatalf("Error refreshing user and post table: %v\n", err) 85 | } 86 | post, err := seedOneUserAndOnePost() 87 | if err != nil { 88 | log.Fatalf("Error Seeding table") 89 | } 90 | postUpdate := models.Post{ 91 | ID: 1, 92 | Title: "modiUpdate", 93 | Content: "modiupdate@gmail.com", 94 | AuthorID: post.AuthorID, 95 | } 96 | updatedPost, err := postUpdate.UpdateAPost(server.DB) 97 | if err != nil { 98 | t.Errorf("this is the error updating the user: %v\n", err) 99 | return 100 | } 101 | assert.Equal(t, updatedPost.ID, postUpdate.ID) 102 | assert.Equal(t, updatedPost.Title, postUpdate.Title) 103 | assert.Equal(t, updatedPost.Content, postUpdate.Content) 104 | assert.Equal(t, updatedPost.AuthorID, postUpdate.AuthorID) 105 | } 106 | 107 | func TestDeleteAPost(t *testing.T) { 108 | 109 | err := refreshUserAndPostTable() 110 | if err != nil { 111 | log.Fatalf("Error refreshing user and post table: %v\n", err) 112 | } 113 | post, err := seedOneUserAndOnePost() 114 | if err != nil { 115 | log.Fatalf("Error Seeding tables") 116 | } 117 | isDeleted, err := postInstance.DeleteAPost(server.DB, post.ID, post.AuthorID) 118 | if err != nil { 119 | t.Errorf("this is the error deleting the user: %v\n", err) 120 | return 121 | } 122 | //one shows that the record has been deleted or: 123 | // assert.Equal(t, int(isDeleted), 1) 124 | 125 | //Can be done this way too 126 | assert.Equal(t, isDeleted, int64(1)) 127 | } 128 | -------------------------------------------------------------------------------- /tests/modeltests/user_model_test.go: -------------------------------------------------------------------------------- 1 | package modeltests 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | _ "github.com/jinzhu/gorm/dialects/mysql" //mysql driver 8 | _ "github.com/jinzhu/gorm/dialects/postgres" //postgres driver 9 | "github.com/victorsteven/fullstack/api/models" 10 | "gopkg.in/go-playground/assert.v1" 11 | ) 12 | 13 | func TestFindAllUsers(t *testing.T) { 14 | 15 | err := refreshUserTable() 16 | if err != nil { 17 | log.Fatalf("Error refreshing user table %v\n", err) 18 | } 19 | 20 | err = seedUsers() 21 | if err != nil { 22 | log.Fatalf("Error seeding user table %v\n", err) 23 | } 24 | 25 | users, err := userInstance.FindAllUsers(server.DB) 26 | if err != nil { 27 | t.Errorf("this is the error getting the users: %v\n", err) 28 | return 29 | } 30 | assert.Equal(t, len(*users), 2) 31 | } 32 | 33 | func TestSaveUser(t *testing.T) { 34 | 35 | err := refreshUserTable() 36 | if err != nil { 37 | log.Fatalf("Error user refreshing table %v\n", err) 38 | } 39 | newUser := models.User{ 40 | ID: 1, 41 | Email: "test@gmail.com", 42 | Nickname: "test", 43 | Password: "password", 44 | } 45 | savedUser, err := newUser.SaveUser(server.DB) 46 | if err != nil { 47 | t.Errorf("Error while saving a user: %v\n", err) 48 | return 49 | } 50 | assert.Equal(t, newUser.ID, savedUser.ID) 51 | assert.Equal(t, newUser.Email, savedUser.Email) 52 | assert.Equal(t, newUser.Nickname, savedUser.Nickname) 53 | } 54 | 55 | func TestGetUserByID(t *testing.T) { 56 | 57 | err := refreshUserTable() 58 | if err != nil { 59 | log.Fatalf("Error user refreshing table %v\n", err) 60 | } 61 | 62 | user, err := seedOneUser() 63 | if err != nil { 64 | log.Fatalf("cannot seed users table: %v", err) 65 | } 66 | foundUser, err := userInstance.FindUserByID(server.DB, user.ID) 67 | if err != nil { 68 | t.Errorf("this is the error getting one user: %v\n", err) 69 | return 70 | } 71 | assert.Equal(t, foundUser.ID, user.ID) 72 | assert.Equal(t, foundUser.Email, user.Email) 73 | assert.Equal(t, foundUser.Nickname, user.Nickname) 74 | } 75 | 76 | func TestUpdateAUser(t *testing.T) { 77 | 78 | err := refreshUserTable() 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | user, err := seedOneUser() 84 | if err != nil { 85 | log.Fatalf("Cannot seed user: %v\n", err) 86 | } 87 | 88 | userUpdate := models.User{ 89 | ID: 1, 90 | Nickname: "modiUpdate", 91 | Email: "modiupdate@gmail.com", 92 | Password: "password", 93 | } 94 | updatedUser, err := userUpdate.UpdateAUser(server.DB, user.ID) 95 | if err != nil { 96 | t.Errorf("this is the error updating the user: %v\n", err) 97 | return 98 | } 99 | assert.Equal(t, updatedUser.ID, userUpdate.ID) 100 | assert.Equal(t, updatedUser.Email, userUpdate.Email) 101 | assert.Equal(t, updatedUser.Nickname, userUpdate.Nickname) 102 | } 103 | 104 | func TestDeleteAUser(t *testing.T) { 105 | 106 | err := refreshUserTable() 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | 111 | user, err := seedOneUser() 112 | 113 | if err != nil { 114 | log.Fatalf("Cannot seed user: %v\n", err) 115 | } 116 | 117 | isDeleted, err := userInstance.DeleteAUser(server.DB, user.ID) 118 | if err != nil { 119 | t.Errorf("this is the error deleting the user: %v\n", err) 120 | return 121 | } 122 | //one shows that the record has been deleted or: 123 | // assert.Equal(t, int(isDeleted), 1) 124 | 125 | //Can be done this way too 126 | assert.Equal(t, isDeleted, int64(1)) 127 | } 128 | --------------------------------------------------------------------------------