├── .gitignore ├── main.go ├── README.md ├── domain ├── message_schema.sql ├── message_dto.go ├── message_dao.go └── message_test.go ├── .env.example ├── go.mod ├── app ├── routes.go └── app.go ├── utils ├── error_formats │ └── error_formats.go └── error_utils │ └── error_utils.go ├── services ├── messages_service.go └── messages_service_test.go ├── controllers ├── messages_controller.go └── messages_controller_test.go ├── integration__tests ├── setup_test.go └── message_controller_integration_test.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.DS_Store 3 | .env -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "efficient-api/app" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | fmt.Println("Welcome to the app") 10 | app.StartApp() 11 | } 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unit-And-Integration-Testing 2 | A simple approach to understanding Unit and Integration testing in Golang 3 | 4 | Ensure to rename the ``.env.example`` file to ``.env`` when you clone the project and input your database details. -------------------------------------------------------------------------------- /domain/message_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `messages` ( 2 | `id` INT NOT NULL AUTO_INCREMENT, 3 | `title` VARCHAR(100) NULL, 4 | `body` VARCHAR(200) NULL, 5 | `created_at` TIMESTAMP NULL, 6 | PRIMARY KEY (`id`), 7 | UNIQUE INDEX `title_UNIQUE` (`title` ASC)); -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | USERNAME=root 2 | PASSWORD= 3 | DATABASE=efficient 4 | PORT=3306 5 | HOST=127.0.0.1 6 | DBDRIVER=mysql 7 | 8 | USERNAME_TEST=root 9 | PASSWORD_TEST= 10 | DATABASE_TEST=efficient_test 11 | PORT_TEST=3306 12 | HOST_TEST=127.0.0.1 13 | DBDRIVER_TEST=mysql -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module efficient-api 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.3.3 7 | github.com/gin-gonic/gin v1.5.0 8 | github.com/go-sql-driver/mysql v1.4.1 9 | github.com/joho/godotenv v1.3.0 10 | github.com/lib/pq v1.2.0 11 | github.com/stretchr/testify v1.4.0 12 | google.golang.org/appengine v1.6.5 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /app/routes.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "efficient-api/controllers" 4 | 5 | func routes() { 6 | router.GET("/messages/:message_id", controllers.GetMessage) 7 | router.GET("/messages", controllers.GetAllMessages) 8 | router.POST("/messages", controllers.CreateMessage) 9 | router.PUT("/messages/:message_id", controllers.UpdateMessage) 10 | router.DELETE("/messages/:message_id", controllers.DeleteMessage) 11 | } 12 | -------------------------------------------------------------------------------- /domain/message_dto.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "efficient-api/utils/error_utils" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type Message struct { 10 | Id int64 `json:"id"` 11 | Title string `json:"title"` 12 | Body string `json:"body"` 13 | CreatedAt time.Time `json:"created_at"` 14 | } 15 | 16 | func (m *Message) Validate() error_utils.MessageErr { 17 | m.Title = strings.TrimSpace(m.Title) 18 | m.Body = strings.TrimSpace(m.Body) 19 | if m.Title == "" { 20 | return error_utils.NewUnprocessibleEntityError("Please enter a valid title") 21 | } 22 | if m.Body == "" { 23 | return error_utils.NewUnprocessibleEntityError("Please enter a valid body") 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /utils/error_formats/error_formats.go: -------------------------------------------------------------------------------- 1 | package error_formats 2 | 3 | import ( 4 | "efficient-api/utils/error_utils" 5 | "fmt" 6 | "github.com/go-sql-driver/mysql" 7 | "strings" 8 | ) 9 | 10 | func ParseError(err error) error_utils.MessageErr { 11 | sqlErr, ok := err.(*mysql.MySQLError) 12 | if !ok { 13 | if strings.Contains(err.Error(), "no rows in result set") { 14 | return error_utils.NewNotFoundError("no record matching given id") 15 | } 16 | return error_utils.NewInternalServerError(fmt.Sprintf("error when trying to save message: %s", err.Error())) 17 | } 18 | switch sqlErr.Number { 19 | case 1062: 20 | return error_utils.NewInternalServerError("title already taken") 21 | } 22 | return error_utils.NewInternalServerError(fmt.Sprintf("error when processing request: %s", err.Error())) 23 | } -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "efficient-api/domain" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/joho/godotenv" 8 | "log" 9 | "os" 10 | ) 11 | 12 | var ( 13 | router = gin.Default() 14 | ) 15 | 16 | func init() { 17 | //loads values from .env into the system 18 | if err := godotenv.Load(); err != nil { 19 | log.Print("sad .env file found") 20 | } 21 | } 22 | 23 | func StartApp() { 24 | 25 | dbdriver := os.Getenv("DBDRIVER") 26 | username := os.Getenv("USERNAME") 27 | password := os.Getenv("PASSWORD") 28 | host := os.Getenv("HOST") 29 | database := os.Getenv("DATABASE") 30 | port := os.Getenv("PORT") 31 | 32 | domain.MessageRepo.Initialize(dbdriver, username, password, port, host, database) 33 | fmt.Println("DATABASE STARTED") 34 | 35 | routes() 36 | 37 | router.Run(":8080") 38 | } 39 | 40 | -------------------------------------------------------------------------------- /utils/error_utils/error_utils.go: -------------------------------------------------------------------------------- 1 | package error_utils 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type MessageErr interface { 9 | Message() string 10 | Status() int 11 | Error() string 12 | } 13 | 14 | type messageErr struct { 15 | ErrMessage string `json:"message"` 16 | ErrStatus int `json:"status"` 17 | ErrError string `json:"error"` 18 | } 19 | 20 | func (e *messageErr) Error() string { 21 | return e.ErrError 22 | } 23 | 24 | func (e *messageErr) Message() string { 25 | return e.ErrMessage 26 | } 27 | 28 | func (e *messageErr) Status() int { 29 | return e.ErrStatus 30 | } 31 | 32 | func NewNotFoundError(message string) MessageErr { 33 | return &messageErr{ 34 | ErrMessage: message, 35 | ErrStatus: http.StatusNotFound, 36 | ErrError: "not_found", 37 | } 38 | } 39 | 40 | func NewBadRequestError(message string) MessageErr { 41 | return &messageErr{ 42 | ErrMessage: message, 43 | ErrStatus: http.StatusBadRequest, 44 | ErrError: "bad_request", 45 | } 46 | } 47 | func NewUnprocessibleEntityError(message string) MessageErr { 48 | return &messageErr{ 49 | ErrMessage: message, 50 | ErrStatus: http.StatusUnprocessableEntity, 51 | ErrError: "invalid_request", 52 | } 53 | } 54 | 55 | func NewApiErrFromBytes(body []byte) (MessageErr, error) { 56 | var result messageErr 57 | if err := json.Unmarshal(body, &result); err != nil { 58 | return nil, err 59 | } 60 | return &result, nil 61 | } 62 | 63 | func NewInternalServerError(message string) MessageErr { 64 | return &messageErr{ 65 | ErrMessage: message, 66 | ErrStatus: http.StatusInternalServerError, 67 | ErrError: "server_error", 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /services/messages_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "efficient-api/domain" 5 | "efficient-api/utils/error_utils" 6 | "time" 7 | ) 8 | 9 | var ( 10 | MessagesService messageServiceInterface = &messagesService{} 11 | ) 12 | 13 | type messagesService struct{} 14 | 15 | type messageServiceInterface interface { 16 | GetMessage(int64) (*domain.Message, error_utils.MessageErr) 17 | CreateMessage(*domain.Message) (*domain.Message, error_utils.MessageErr) 18 | UpdateMessage(*domain.Message) (*domain.Message, error_utils.MessageErr) 19 | DeleteMessage(int64) error_utils.MessageErr 20 | GetAllMessages() ([]domain.Message, error_utils.MessageErr) 21 | } 22 | 23 | func (m *messagesService) GetMessage(msgId int64) (*domain.Message, error_utils.MessageErr) { 24 | message, err := domain.MessageRepo.Get(msgId) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return message, nil 29 | } 30 | 31 | func (m *messagesService) GetAllMessages() ([]domain.Message, error_utils.MessageErr) { 32 | messages, err := domain.MessageRepo.GetAll() 33 | if err != nil { 34 | return nil, err 35 | } 36 | return messages, nil 37 | } 38 | 39 | func (m *messagesService) CreateMessage(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 40 | if err := message.Validate(); err != nil { 41 | return nil, err 42 | } 43 | message.CreatedAt = time.Now() 44 | message, err := domain.MessageRepo.Create(message) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return message, nil 49 | } 50 | 51 | func (m *messagesService) UpdateMessage(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 52 | 53 | if err := message.Validate(); err != nil { 54 | return nil, err 55 | } 56 | current, err := domain.MessageRepo.Get(message.Id) 57 | if err != nil { 58 | return nil, err 59 | } 60 | current.Title = message.Title 61 | current.Body = message.Body 62 | 63 | updateMsg, err := domain.MessageRepo.Update(current) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return updateMsg, nil 68 | } 69 | 70 | func (m *messagesService) DeleteMessage(msgId int64) error_utils.MessageErr { 71 | msg, err := domain.MessageRepo.Get(msgId) 72 | if err != nil { 73 | return err 74 | } 75 | deleteErr := domain.MessageRepo.Delete(msg.Id) 76 | if deleteErr != nil { 77 | return deleteErr 78 | } 79 | return nil 80 | } 81 | 82 | -------------------------------------------------------------------------------- /controllers/messages_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "efficient-api/domain" 5 | "efficient-api/services" 6 | "efficient-api/utils/error_utils" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | //Since we are going for the message id more than we, we extracted this functionality to a function so we can have a DRY code. 13 | func getMessageId(msgIdParam string) (int64, error_utils.MessageErr) { 14 | msgId, msgErr := strconv.ParseInt(msgIdParam, 10, 64) 15 | if msgErr != nil { 16 | return 0, error_utils.NewBadRequestError("message id should be a number") 17 | } 18 | return msgId, nil 19 | } 20 | 21 | func GetMessage(c *gin.Context) { 22 | msgId, err := getMessageId(c.Param("message_id")) 23 | if err != nil { 24 | c.JSON(err.Status(), err) 25 | return 26 | } 27 | message, getErr := services.MessagesService.GetMessage(msgId) 28 | if getErr != nil { 29 | c.JSON(getErr.Status(), getErr) 30 | return 31 | } 32 | c.JSON(http.StatusOK, message) 33 | } 34 | 35 | func GetAllMessages(c *gin.Context) { 36 | messages, getErr := services.MessagesService.GetAllMessages() 37 | if getErr != nil { 38 | c.JSON(getErr.Status(), getErr) 39 | return 40 | } 41 | c.JSON(http.StatusOK, messages) 42 | } 43 | 44 | func CreateMessage(c *gin.Context) { 45 | var message domain.Message 46 | if err := c.ShouldBindJSON(&message); err != nil { 47 | theErr := error_utils.NewUnprocessibleEntityError("invalid json body") 48 | c.JSON(theErr.Status(), theErr) 49 | return 50 | } 51 | msg, err := services.MessagesService.CreateMessage(&message) 52 | if err != nil { 53 | c.JSON(err.Status(), err) 54 | return 55 | } 56 | c.JSON(http.StatusCreated, msg) 57 | } 58 | 59 | func UpdateMessage(c *gin.Context) { 60 | msgId, err := getMessageId(c.Param("message_id")) 61 | if err != nil { 62 | c.JSON(err.Status(), err) 63 | return 64 | } 65 | var message domain.Message 66 | if err := c.ShouldBindJSON(&message); err != nil { 67 | theErr := error_utils.NewUnprocessibleEntityError("invalid json body") 68 | c.JSON(theErr.Status(), theErr) 69 | return 70 | } 71 | message.Id = msgId 72 | msg, err := services.MessagesService.UpdateMessage(&message) 73 | if err != nil { 74 | c.JSON(err.Status(), err) 75 | return 76 | } 77 | c.JSON(http.StatusOK, msg) 78 | } 79 | 80 | func DeleteMessage(c *gin.Context) { 81 | msgId, err := getMessageId(c.Param("message_id")) 82 | if err != nil { 83 | c.JSON(err.Status(), err) 84 | return 85 | } 86 | if err := services.MessagesService.DeleteMessage(msgId); err != nil { 87 | c.JSON(err.Status(), err) 88 | return 89 | } 90 | c.JSON(http.StatusOK, map[string]string{"status": "deleted"}) 91 | } 92 | 93 | -------------------------------------------------------------------------------- /integration__tests/setup_test.go: -------------------------------------------------------------------------------- 1 | package integration__tests 2 | 3 | import ( 4 | "database/sql" 5 | "efficient-api/domain" 6 | _ "github.com/go-sql-driver/mysql" 7 | "github.com/joho/godotenv" 8 | "log" 9 | "os" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | const ( 15 | queryTruncateMessage = "TRUNCATE TABLE messages;" 16 | queryInsertMessage = "INSERT INTO messages(title, body, created_at) VALUES(?, ?, ?);" 17 | queryGetAllMessages = "SELECT id, title, body, created_at FROM messages;" 18 | ) 19 | var ( 20 | dbConn *sql.DB 21 | ) 22 | 23 | func TestMain(m *testing.M) { 24 | var err error 25 | err = godotenv.Load(os.ExpandEnv("./../.env")) 26 | if err != nil { 27 | log.Fatalf("Error getting env %v\n", err) 28 | } 29 | os.Exit(m.Run()) 30 | } 31 | 32 | func database() { 33 | dbDriver := os.Getenv("DBDRIVER_TEST") 34 | username := os.Getenv("USERNAME_TEST") 35 | password := os.Getenv("PASSWORD_TEST") 36 | host := os.Getenv("HOST_TEST") 37 | database := os.Getenv("DATABASE_TEST") 38 | port := os.Getenv("PORT_TEST") 39 | 40 | dbConn = domain.MessageRepo.Initialize(dbDriver, username, password, port, host, database) 41 | } 42 | 43 | func refreshMessagesTable() error { 44 | 45 | stmt, err := dbConn.Prepare(queryTruncateMessage) 46 | if err != nil { 47 | panic(err.Error()) 48 | } 49 | _, err = stmt.Exec() 50 | if err != nil { 51 | log.Fatalf("Error truncating messages table: %s", err) 52 | } 53 | return nil 54 | } 55 | 56 | func seedOneMessage() (domain.Message, error) { 57 | msg := domain.Message{ 58 | Title: "the title", 59 | Body: "the body", 60 | CreatedAt: time.Now(), 61 | } 62 | stmt, err := dbConn.Prepare(queryInsertMessage) 63 | if err != nil { 64 | panic(err.Error()) 65 | } 66 | insertResult, createErr := stmt.Exec(msg.Title, msg.Body, msg.CreatedAt) 67 | if createErr != nil { 68 | log.Fatalf("Error creating message: %s", createErr) 69 | } 70 | msgId, err := insertResult.LastInsertId() 71 | if err != nil { 72 | log.Fatalf("Error creating message: %s", createErr) 73 | } 74 | msg.Id = msgId 75 | return msg, nil 76 | } 77 | 78 | func seedMessages() ([]domain.Message, error) { 79 | msgs := []domain.Message{ 80 | { 81 | Title: "first title", 82 | Body: "first body", 83 | CreatedAt: time.Now(), 84 | }, 85 | { 86 | Title: "second title", 87 | Body: "second body", 88 | CreatedAt: time.Now(), 89 | }, 90 | } 91 | stmt, err := dbConn.Prepare(queryInsertMessage) 92 | if err != nil { 93 | panic(err.Error()) 94 | } 95 | for i, _ := range msgs { 96 | _, createErr := stmt.Exec(msgs[i].Title, msgs[i].Body, msgs[i].CreatedAt) 97 | if createErr != nil { 98 | return nil, createErr 99 | } 100 | } 101 | get_stmt, err := dbConn.Prepare(queryGetAllMessages) 102 | if err != nil { 103 | return nil, err 104 | } 105 | defer stmt.Close() 106 | 107 | rows, err := get_stmt.Query() 108 | if err != nil { 109 | return nil, err 110 | } 111 | defer rows.Close() 112 | 113 | results := make([]domain.Message, 0) 114 | 115 | for rows.Next() { 116 | var msg domain.Message 117 | if getError := rows.Scan(&msg.Id, &msg.Title, &msg.Body, &msg.CreatedAt); getError != nil { 118 | return nil, err 119 | } 120 | results = append(results, msg) 121 | } 122 | return results, nil 123 | } 124 | 125 | -------------------------------------------------------------------------------- /domain/message_dao.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "database/sql" 5 | "efficient-api/utils/error_formats" 6 | "efficient-api/utils/error_utils" 7 | "fmt" 8 | _ "github.com/go-sql-driver/mysql" 9 | "log" 10 | ) 11 | 12 | var ( 13 | MessageRepo messageRepoInterface = &messageRepo{} 14 | ) 15 | 16 | const ( 17 | queryGetMessage = "SELECT id, title, body, created_at FROM messages WHERE id=?;" 18 | queryInsertMessage = "INSERT INTO messages(title, body, created_at) VALUES(?, ?, ?);" 19 | queryUpdateMessage = "UPDATE messages SET title=?, body=? WHERE id=?;" 20 | queryDeleteMessage = "DELETE FROM messages WHERE id=?;" 21 | queryGetAllMessages = "SELECT id, title, body, created_at FROM messages;" 22 | ) 23 | 24 | type messageRepoInterface interface { 25 | Get(int64) (*Message, error_utils.MessageErr) 26 | Create(*Message) (*Message, error_utils.MessageErr) 27 | Update(*Message) (*Message, error_utils.MessageErr) 28 | Delete(int64) error_utils.MessageErr 29 | GetAll() ([]Message, error_utils.MessageErr) 30 | Initialize(string, string, string, string, string, string) *sql.DB 31 | } 32 | type messageRepo struct { 33 | db *sql.DB 34 | } 35 | 36 | func (mr *messageRepo) Initialize(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) *sql.DB { 37 | var err error 38 | DBURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", DbUser, DbPassword, DbHost, DbPort, DbName) 39 | 40 | mr.db, err = sql.Open(Dbdriver, DBURL) 41 | if err != nil { 42 | log.Fatal("This is the error connecting to the database:", err) 43 | } 44 | fmt.Printf("We are connected to the %s database", Dbdriver) 45 | 46 | return mr.db 47 | } 48 | 49 | func NewMessageRepository(db *sql.DB) messageRepoInterface { 50 | return &messageRepo{db: db} 51 | } 52 | 53 | func (mr *messageRepo) Get(messageId int64) (*Message, error_utils.MessageErr) { 54 | stmt, err := mr.db.Prepare(queryGetMessage) 55 | if err != nil { 56 | return nil, error_utils.NewInternalServerError(fmt.Sprintf("Error when trying to prepare message: %s", err.Error())) 57 | } 58 | defer stmt.Close() 59 | 60 | var msg Message 61 | result := stmt.QueryRow(messageId) 62 | if getError := result.Scan(&msg.Id, &msg.Title, &msg.Body, &msg.CreatedAt); getError != nil { 63 | fmt.Println("this is the error man: ", getError) 64 | return nil, error_formats.ParseError(getError) 65 | } 66 | return &msg, nil 67 | } 68 | 69 | func (mr *messageRepo) GetAll() ([]Message, error_utils.MessageErr) { 70 | stmt, err := mr.db.Prepare(queryGetAllMessages) 71 | if err != nil { 72 | return nil, error_utils.NewInternalServerError(fmt.Sprintf("Error when trying to prepare all messages: %s", err.Error())) 73 | } 74 | defer stmt.Close() 75 | 76 | rows, err := stmt.Query() 77 | if err != nil { 78 | return nil, error_formats.ParseError(err) 79 | } 80 | defer rows.Close() 81 | 82 | results := make([]Message, 0) 83 | 84 | for rows.Next() { 85 | var msg Message 86 | if getError := rows.Scan(&msg.Id, &msg.Title, &msg.Body, &msg.CreatedAt); getError != nil { 87 | return nil, error_utils.NewInternalServerError(fmt.Sprintf("Error when trying to get message: %s", getError.Error())) 88 | } 89 | results = append(results, msg) 90 | } 91 | if len(results) == 0 { 92 | return nil, error_utils.NewNotFoundError("no records found") 93 | } 94 | return results, nil 95 | } 96 | 97 | func (mr *messageRepo) Create(msg *Message) (*Message, error_utils.MessageErr) { 98 | fmt.Println("WE REACHED THE DOMAIN") 99 | stmt, err := mr.db.Prepare(queryInsertMessage) 100 | if err != nil { 101 | return nil, error_utils.NewInternalServerError(fmt.Sprintf("error when trying to prepare user to save: %s", err.Error())) 102 | } 103 | fmt.Println("WE DIDNT REACH HERE") 104 | 105 | defer stmt.Close() 106 | 107 | insertResult, createErr := stmt.Exec(msg.Title, msg.Body, msg.CreatedAt) 108 | if createErr != nil { 109 | return nil, error_formats.ParseError(createErr) 110 | } 111 | msgId, err := insertResult.LastInsertId() 112 | if err != nil { 113 | return nil, error_utils.NewInternalServerError(fmt.Sprintf("error when trying to save message: %s", err.Error())) 114 | } 115 | msg.Id = msgId 116 | 117 | return msg, nil 118 | } 119 | 120 | func (mr *messageRepo) Update(msg *Message) (*Message, error_utils.MessageErr) { 121 | stmt, err := mr.db.Prepare(queryUpdateMessage) 122 | if err != nil { 123 | return nil, error_utils.NewInternalServerError(fmt.Sprintf("error when trying to prepare user to update: %s", err.Error())) 124 | } 125 | defer stmt.Close() 126 | 127 | _, updateErr := stmt.Exec(msg.Title, msg.Body, msg.Id) 128 | if updateErr != nil { 129 | return nil, error_formats.ParseError(updateErr) 130 | } 131 | return msg, nil 132 | } 133 | 134 | func (mr *messageRepo) Delete(msgId int64) error_utils.MessageErr { 135 | stmt, err := mr.db.Prepare(queryDeleteMessage) 136 | if err != nil { 137 | return error_utils.NewInternalServerError(fmt.Sprintf("error when trying to delete message: %s", err.Error())) 138 | } 139 | defer stmt.Close() 140 | 141 | if _, err := stmt.Exec(msgId); err != nil { 142 | return error_utils.NewInternalServerError(fmt.Sprintf("error when trying to delete message %s", err.Error())) 143 | } 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= 2 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 7 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 8 | github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= 9 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 10 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= 11 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 12 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= 13 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 14 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 15 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 16 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 17 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 18 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 21 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 22 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 23 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 24 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 25 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 26 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 27 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 28 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 29 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 30 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 31 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 32 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 33 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 38 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 39 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 40 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 41 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 42 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 43 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 44 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 45 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 46 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 47 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 48 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 50 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 51 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 52 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 53 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 55 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 57 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 58 | gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= 59 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 60 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 61 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 62 | -------------------------------------------------------------------------------- /integration__tests/message_controller_integration_test.go: -------------------------------------------------------------------------------- 1 | package integration__tests 2 | 3 | import ( 4 | "bytes" 5 | "efficient-api/controllers" 6 | "efficient-api/domain" 7 | "encoding/json" 8 | "fmt" 9 | "github.com/gin-gonic/gin" 10 | "github.com/stretchr/testify/assert" 11 | "log" 12 | "net/http" 13 | "net/http/httptest" 14 | "strconv" 15 | "testing" 16 | ) 17 | 18 | func TestCreateMessage(t *testing.T) { 19 | 20 | database() 21 | 22 | gin.SetMode(gin.TestMode) 23 | 24 | err := refreshMessagesTable() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | samples := []struct { 29 | inputJSON string 30 | statusCode int 31 | title string 32 | body string 33 | errMessage string 34 | }{ 35 | { 36 | inputJSON: `{"title":"the title", "body": "the body"}`, 37 | statusCode: 201, 38 | title: "the title", 39 | body: "the body", 40 | errMessage: "", 41 | }, 42 | { 43 | inputJSON: `{"title":"the title", "body": "the body"}`, 44 | statusCode: 500, 45 | errMessage: "title already taken", 46 | }, 47 | { 48 | inputJSON: `{"title":"", "body": "the body"}`, 49 | statusCode: 422, 50 | errMessage: "Please enter a valid title", 51 | }, 52 | { 53 | inputJSON: `{"title":"the title", "body": ""}`, 54 | statusCode: 422, 55 | errMessage: "Please enter a valid body", 56 | }, 57 | { 58 | //when an integer is used like a string for title 59 | inputJSON: `{"title": 12345, "body": "the body"}`, 60 | statusCode: 422, 61 | errMessage: "invalid json body", 62 | }, 63 | { 64 | //when an integer is used like a string for body 65 | inputJSON: `{"title": "the title", "body": 123453 }`, 66 | statusCode: 422, 67 | errMessage: "invalid json body", 68 | }, 69 | } 70 | for _, v := range samples { 71 | r := gin.Default() 72 | r.POST("/messages", controllers.CreateMessage) 73 | req, err := http.NewRequest(http.MethodPost, "/messages", bytes.NewBufferString(v.inputJSON)) 74 | if err != nil { 75 | t.Errorf("this is the error: %v\n", err) 76 | } 77 | rr := httptest.NewRecorder() 78 | r.ServeHTTP(rr, req) 79 | 80 | responseMap := make(map[string]interface{}) 81 | err = json.Unmarshal(rr.Body.Bytes(), &responseMap) 82 | if err != nil { 83 | t.Errorf("Cannot convert to json: %v", err) 84 | } 85 | fmt.Println("this is the response data: ", responseMap) 86 | assert.Equal(t, rr.Code, v.statusCode) 87 | if v.statusCode == 201 { 88 | //casting the interface to map: 89 | assert.Equal(t, responseMap["title"], v.title) 90 | assert.Equal(t, responseMap["body"], v.body) 91 | } 92 | if v.statusCode == 400 || v.statusCode == 422 || v.statusCode == 500 && v.errMessage != "" { 93 | assert.Equal(t, responseMap["message"], v.errMessage) 94 | } 95 | } 96 | } 97 | 98 | func TestGetMessageByID(t *testing.T) { 99 | 100 | database() 101 | 102 | gin.SetMode(gin.TestMode) 103 | 104 | err := refreshMessagesTable() 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | message, err := seedOneMessage() 109 | if err != nil { 110 | t.Errorf("Error while seeding table: %s", err) 111 | } 112 | 113 | samples := []struct { 114 | id string 115 | statusCode int 116 | title string 117 | body string 118 | errMessage string 119 | }{ 120 | { 121 | id: strconv.Itoa(int(message.Id)), 122 | statusCode: 200, 123 | title: message.Title, 124 | body: message.Body, 125 | errMessage: "", 126 | }, 127 | { 128 | id: "unknwon", 129 | statusCode: 400, 130 | errMessage: "message id should be a number", 131 | }, 132 | { 133 | id: strconv.Itoa(12322), //an id that does not exist 134 | statusCode: 404, 135 | errMessage: "no record matching given id", 136 | }, 137 | } 138 | for _, v := range samples { 139 | r := gin.Default() 140 | r.GET("/messages/:message_id", controllers.GetMessage) 141 | req, err := http.NewRequest(http.MethodGet, "/messages/"+v.id, nil) 142 | if err != nil { 143 | t.Errorf("this is the error: %v\n", err) 144 | } 145 | rr := httptest.NewRecorder() 146 | r.ServeHTTP(rr, req) 147 | 148 | responseMap := make(map[string]interface{}) 149 | err = json.Unmarshal(rr.Body.Bytes(), &responseMap) 150 | if err != nil { 151 | t.Errorf("Cannot convert to json: %v", err) 152 | } 153 | assert.Equal(t, rr.Code, v.statusCode) 154 | 155 | if v.statusCode == 200 { 156 | //casting the interface to map: 157 | assert.Equal(t, responseMap["title"], v.title) 158 | assert.Equal(t, responseMap["body"], v.body) 159 | } 160 | if v.statusCode == 400 || v.statusCode == 422 && v.errMessage != "" { 161 | assert.Equal(t, responseMap["message"], v.errMessage) 162 | } 163 | } 164 | } 165 | 166 | func TestUpdateMessage(t *testing.T) { 167 | 168 | database() 169 | 170 | gin.SetMode(gin.TestMode) 171 | 172 | err := refreshMessagesTable() 173 | if err != nil { 174 | log.Fatal(err) 175 | } 176 | messages, err := seedMessages() 177 | if err != nil { 178 | t.Errorf("Error while seeding table: %s", err) 179 | } 180 | 181 | //Get only the first message id 182 | firstId := messages[0].Id 183 | 184 | samples := []struct { 185 | id string 186 | inputJSON string 187 | statusCode int 188 | title string 189 | body string 190 | errMessage string 191 | }{ 192 | { 193 | id: strconv.Itoa(int(firstId)), 194 | inputJSON: `{"title":"update title", "body": "update body"}`, 195 | statusCode: 200, 196 | title: "update title", 197 | body: "update body", 198 | errMessage: "", 199 | }, 200 | { 201 | // "second title" belongs to the second message so, the cannot be used for the first message 202 | id: strconv.Itoa(int(firstId)), 203 | inputJSON: `{"title":"second title", "body": "update body"}`, 204 | statusCode: 500, 205 | errMessage: "title already taken", 206 | }, 207 | { 208 | //Empty title 209 | id: strconv.Itoa(int(firstId)), 210 | inputJSON: `{"title":"", "body": "update body"}`, 211 | statusCode: 422, 212 | errMessage: "Please enter a valid title", 213 | }, 214 | { 215 | //Empty body 216 | id: strconv.Itoa(int(firstId)), 217 | inputJSON: `{"title":"the title", "body": ""}`, 218 | statusCode: 422, 219 | errMessage: "Please enter a valid body", 220 | }, 221 | { 222 | //when an integer is used like a string for title 223 | id: strconv.Itoa(int(firstId)), 224 | inputJSON: `{"title": 12345, "body": "the body"}`, 225 | statusCode: 422, 226 | errMessage: "invalid json body", 227 | }, 228 | { 229 | //when an integer is used like a string for body 230 | id: strconv.Itoa(int(firstId)), 231 | inputJSON: `{"title": "the title", "body": 123453 }`, 232 | statusCode: 422, 233 | errMessage: "invalid json body", 234 | }, 235 | { 236 | id: "unknwon", 237 | statusCode: 400, 238 | errMessage: "message id should be a number", 239 | }, 240 | { 241 | id: strconv.Itoa(12322), //an id that does not exist 242 | inputJSON: `{"title":"the title", "body": "the body"}`, 243 | statusCode: 404, 244 | errMessage: "no record matching given id", 245 | }, 246 | } 247 | for _, v := range samples { 248 | r := gin.Default() 249 | r.PUT("/messages/:message_id", controllers.UpdateMessage) 250 | req, err := http.NewRequest(http.MethodPut, "/messages/"+v.id, bytes.NewBufferString(v.inputJSON)) 251 | if err != nil { 252 | t.Errorf("this is the error: %v\n", err) 253 | } 254 | rr := httptest.NewRecorder() 255 | r.ServeHTTP(rr, req) 256 | 257 | responseMap := make(map[string]interface{}) 258 | err = json.Unmarshal(rr.Body.Bytes(), &responseMap) 259 | if err != nil { 260 | t.Errorf("Cannot convert to json: %v", err) 261 | } 262 | assert.Equal(t, rr.Code, v.statusCode) 263 | if v.statusCode == 200 { 264 | //casting the interface to map: 265 | assert.Equal(t, responseMap["title"], v.title) 266 | assert.Equal(t, responseMap["body"], v.body) 267 | } 268 | if v.statusCode == 400 || v.statusCode == 422 || v.statusCode == 500 && v.errMessage != "" { 269 | assert.Equal(t, responseMap["message"], v.errMessage) 270 | } 271 | } 272 | } 273 | 274 | func TestGetAllMessage(t *testing.T) { 275 | 276 | database() 277 | 278 | gin.SetMode(gin.TestMode) 279 | 280 | err := refreshMessagesTable() 281 | if err != nil { 282 | log.Fatal(err) 283 | } 284 | _, err = seedMessages() 285 | if err != nil { 286 | t.Errorf("Error while seeding table: %s", err) 287 | } 288 | r := gin.Default() 289 | r.GET("/messages", controllers.GetAllMessages) 290 | 291 | req, err := http.NewRequest(http.MethodGet, "/messages", nil) 292 | if err != nil { 293 | t.Errorf("this is the error: %v\n", err) 294 | } 295 | rr := httptest.NewRecorder() 296 | r.ServeHTTP(rr, req) 297 | 298 | var msgs []domain.Message 299 | 300 | err = json.Unmarshal(rr.Body.Bytes(), &msgs) 301 | if err != nil { 302 | log.Fatalf("Cannot convert to json: %v\n", err) 303 | } 304 | assert.Equal(t, rr.Code, http.StatusOK) 305 | assert.Equal(t, len(msgs), 2) 306 | } 307 | 308 | func TestDeleteMessage(t *testing.T) { 309 | 310 | database() 311 | 312 | gin.SetMode(gin.TestMode) 313 | 314 | err := refreshMessagesTable() 315 | if err != nil { 316 | log.Fatal(err) 317 | } 318 | message, err := seedOneMessage() 319 | if err != nil { 320 | t.Errorf("Error while seeding table: %s", err) 321 | } 322 | samples := []struct { 323 | id string 324 | statusCode int 325 | status string 326 | errMessage string 327 | }{ 328 | { 329 | id: strconv.Itoa(int(message.Id)), 330 | statusCode: 200, 331 | status: "deleted", 332 | errMessage: "", 333 | }, 334 | { 335 | id: "unknwon", 336 | statusCode: 400, 337 | errMessage: "message id should be a number", 338 | }, 339 | { 340 | id: strconv.Itoa(12322), //an id that does not exist 341 | statusCode: 404, 342 | errMessage: "no record matching given id", 343 | }, 344 | } 345 | for _, v := range samples { 346 | r := gin.Default() 347 | r.DELETE("/messages/:message_id", controllers.DeleteMessage) 348 | req, err := http.NewRequest(http.MethodDelete, "/messages/"+v.id, nil) 349 | if err != nil { 350 | t.Errorf("this is the error: %v\n", err) 351 | } 352 | rr := httptest.NewRecorder() 353 | r.ServeHTTP(rr, req) 354 | 355 | responseMap := make(map[string]interface{}) 356 | err = json.Unmarshal(rr.Body.Bytes(), &responseMap) 357 | if err != nil { 358 | t.Errorf("Cannot convert to json: %v", err) 359 | } 360 | assert.Equal(t, rr.Code, v.statusCode) 361 | 362 | if v.statusCode == 200 { 363 | //casting the interface to map: 364 | assert.Equal(t, responseMap["status"], v.status) 365 | } 366 | if v.statusCode == 400 || v.statusCode == 422 && v.errMessage != "" { 367 | assert.Equal(t, responseMap["message"], v.errMessage) 368 | } 369 | } 370 | } 371 | 372 | -------------------------------------------------------------------------------- /domain/message_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/DATA-DOG/go-sqlmock" 7 | "reflect" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var created_at = time.Now() 13 | 14 | func TestMessageRepo_Get(t *testing.T) { 15 | db, mock, err := sqlmock.New() 16 | if err != nil { 17 | t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 18 | } 19 | defer db.Close() 20 | s := NewMessageRepository(db) 21 | 22 | tests := []struct { 23 | name string 24 | s messageRepoInterface 25 | msgId int64 26 | mock func() 27 | want *Message 28 | wantErr bool 29 | }{ 30 | { 31 | //When everything works as expected 32 | name: "OK", 33 | s: s, 34 | msgId: 1, 35 | mock: func() { 36 | //We added one row 37 | rows := sqlmock.NewRows([]string{"Id", "Title", "Body", "CreatedAt"}).AddRow(1, "title", "body", created_at) 38 | mock.ExpectPrepare("SELECT (.+) FROM messages").ExpectQuery().WithArgs(1).WillReturnRows(rows) 39 | }, 40 | want: &Message{ 41 | Id: 1, 42 | Title: "title", 43 | Body: "body", 44 | CreatedAt: created_at, 45 | }, 46 | }, 47 | { 48 | //When the role tried to access is not found 49 | name: "Not Found", 50 | s: s, 51 | msgId: 1, 52 | mock: func() { 53 | rows := sqlmock.NewRows([]string{"Id", "Title", "Body", "CreatedAt"}) //observe that we didnt add any role here 54 | mock.ExpectPrepare("SELECT (.+) FROM messages").ExpectQuery().WithArgs(1).WillReturnRows(rows) 55 | }, 56 | wantErr: true, 57 | }, 58 | { 59 | //When invalid statement is provided, ie the SQL syntax is wrong(in this case, we provided a wrong database) 60 | name: "Invalid Prepare", 61 | s: s, 62 | msgId: 1, 63 | mock: func() { 64 | rows := sqlmock.NewRows([]string{"Id", "Title", "Body", "CreatedAt"}).AddRow(1, "title", "body", created_at) 65 | mock.ExpectPrepare("SELECT (.+) FROM wrong_table").ExpectQuery().WithArgs(1).WillReturnRows(rows) 66 | }, 67 | wantErr: true, 68 | }, 69 | } 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | tt.mock() 73 | got, err := tt.s.Get(tt.msgId) 74 | if (err != nil) != tt.wantErr { 75 | t.Errorf("Get() error new = %v, wantErr %v", err, tt.wantErr) 76 | return 77 | } 78 | if err == nil && !reflect.DeepEqual(got, tt.want) { 79 | t.Errorf("Get() = %v, want %v", got, tt.want) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestMessageRepo_Create(t *testing.T) { 86 | db, mock, err := sqlmock.New() 87 | if err != nil { 88 | t.Fatalf("an error '%s' was not expected when opening a stub database", err) 89 | } 90 | defer db.Close() 91 | s := NewMessageRepository(db) 92 | tm := time.Now() 93 | 94 | tests := []struct { 95 | name string 96 | s messageRepoInterface 97 | request *Message 98 | mock func() 99 | want *Message 100 | wantErr bool 101 | }{ 102 | { 103 | name: "OK", 104 | s: s, 105 | request: &Message{ 106 | Title: "title", 107 | Body: "body", 108 | CreatedAt: tm, 109 | }, 110 | mock: func() { 111 | mock.ExpectPrepare("INSERT INTO messages").ExpectExec().WithArgs("title", "body", tm).WillReturnResult(sqlmock.NewResult(1, 1)) 112 | }, 113 | want: &Message{ 114 | Id: 1, 115 | Title: "title", 116 | Body: "body", 117 | CreatedAt: tm, 118 | }, 119 | }, 120 | { 121 | name: "Empty title", 122 | s: s, 123 | request: &Message{ 124 | Title: "", 125 | Body: "body", 126 | CreatedAt: tm, 127 | }, 128 | mock: func(){ 129 | mock.ExpectPrepare("INSERT INTO messages").ExpectExec().WithArgs("title", "body", tm).WillReturnError(errors.New("empty title")) 130 | }, 131 | wantErr: true, 132 | }, 133 | { 134 | name: "Empty body", 135 | s: s, 136 | request: &Message{ 137 | Title: "title", 138 | Body: "", 139 | CreatedAt: tm, 140 | }, 141 | mock: func(){ 142 | mock.ExpectPrepare("INSERT INTO messages").ExpectExec().WithArgs("title", "body", tm).WillReturnError(errors.New("empty body")) 143 | }, 144 | wantErr: true, 145 | }, 146 | { 147 | name: "Invalid SQL query", 148 | s: s, 149 | request: &Message{ 150 | Title: "title", 151 | Body: "body", 152 | CreatedAt: tm, 153 | }, 154 | mock: func(){ 155 | //Instead of using "INSERT", we used "INSETER" 156 | mock.ExpectPrepare("INSERT INTO wrong_table").ExpectExec().WithArgs("title", "body", tm).WillReturnError( errors.New("invalid sql query")) 157 | }, 158 | wantErr: true, 159 | }, 160 | } 161 | for _, tt := range tests { 162 | t.Run(tt.name, func(t *testing.T) { 163 | tt.mock() 164 | got, err := tt.s.Create(tt.request) 165 | if (err != nil) != tt.wantErr { 166 | fmt.Println("this is the error message: ", err.Message()) 167 | t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) 168 | return 169 | } 170 | if err == nil && !reflect.DeepEqual(got, tt.want) { 171 | t.Errorf("Create() = %v, want %v", got, tt.want) 172 | } 173 | }) 174 | } 175 | } 176 | 177 | func TestMessageRepo_Update(t *testing.T) { 178 | db, mock, err := sqlmock.New() 179 | if err != nil { 180 | t.Fatalf("an error '%s' was not expected when opening a stub database", err) 181 | } 182 | defer db.Close() 183 | s := NewMessageRepository(db) 184 | 185 | tests := []struct { 186 | name string 187 | s messageRepoInterface 188 | request *Message 189 | mock func() 190 | want *Message 191 | wantErr bool 192 | }{ 193 | { 194 | name: "OK", 195 | s: s, 196 | request: &Message{ 197 | Id: 1, 198 | Title: "update title", 199 | Body: "update body", 200 | }, 201 | mock: func() { 202 | mock.ExpectPrepare("UPDATE messages").ExpectExec().WithArgs("update title", "update body", 1).WillReturnResult(sqlmock.NewResult(0, 1)) 203 | }, 204 | want: &Message{ 205 | Id: 1, 206 | Title: "update title", 207 | Body: "update body", 208 | }, 209 | }, 210 | { 211 | name: "Invalid SQL Query", 212 | s: s, 213 | request: &Message{ 214 | Id: 1, 215 | Title: "update title", 216 | Body: "update body", 217 | }, 218 | mock: func() { 219 | mock.ExpectPrepare("UPDATER messages").ExpectExec().WithArgs("update title", "update body", 1).WillReturnError(errors.New("error in sql query statement")) 220 | }, 221 | wantErr: true, 222 | }, 223 | { 224 | name: "Invalid Query Id", 225 | s: s, 226 | request: &Message{ 227 | Id: 0, 228 | Title: "update title", 229 | Body: "update body", 230 | }, 231 | mock: func() { 232 | mock.ExpectPrepare("UPDATE messages").ExpectExec().WithArgs("update title", "update body", 0).WillReturnError(errors.New("invalid update id")) 233 | }, 234 | wantErr: true, 235 | }, 236 | { 237 | name: "Empty Title", 238 | s: s, 239 | request: &Message{ 240 | Id: 1, 241 | Title: "", 242 | Body: "update body", 243 | }, 244 | mock: func() { 245 | mock.ExpectPrepare("UPDATE messages").ExpectExec().WithArgs("", "update body", 1).WillReturnError(errors.New("Please enter a valid title")) 246 | }, 247 | wantErr: true, 248 | }, 249 | { 250 | name: "Empty Body", 251 | s: s, 252 | request: &Message{ 253 | Id: 1, 254 | Title: "update title", 255 | Body: "", 256 | }, 257 | mock: func() { 258 | mock.ExpectPrepare("UPDATE messages").ExpectExec().WithArgs("update title", "", 1).WillReturnError(errors.New("Please enter a valid body")) 259 | }, 260 | wantErr: true, 261 | }, 262 | { 263 | name: "Update failed", 264 | s: s, 265 | request: &Message{ 266 | Id: 1, 267 | Title: "update title", 268 | Body: "update body", 269 | }, 270 | mock: func() { 271 | mock.ExpectPrepare("UPDATE messages").ExpectExec().WithArgs("update title", "update body", 1).WillReturnResult(sqlmock.NewErrorResult(errors.New("Update failed"))) 272 | }, 273 | wantErr: true, 274 | }, 275 | } 276 | for _, tt := range tests { 277 | t.Run(tt.name, func(t *testing.T) { 278 | tt.mock() 279 | got, err := tt.s.Update(tt.request) 280 | if (err != nil) != tt.wantErr { 281 | fmt.Println("this is the error message: ", err.Message()) 282 | t.Errorf("Update() error = %v, wantErr %v", err, tt.wantErr) 283 | return 284 | } 285 | if err == nil && !reflect.DeepEqual(got, tt.want) { 286 | t.Errorf("Update() = %v, want %v", got, tt.want) 287 | } 288 | }) 289 | } 290 | } 291 | 292 | func TestMessageRepo_GetAll(t *testing.T) { 293 | db, mock, err := sqlmock.New() 294 | if err != nil { 295 | t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 296 | } 297 | defer db.Close() 298 | s := NewMessageRepository(db) 299 | 300 | tests := []struct { 301 | name string 302 | s messageRepoInterface 303 | msgId int64 304 | mock func() 305 | want []Message 306 | wantErr bool 307 | }{ 308 | { 309 | //When everything works as expected 310 | name: "OK", 311 | s: s, 312 | mock: func() { 313 | //We added two rows 314 | rows := sqlmock.NewRows([]string{"Id", "Title", "Body", "CreatedAt"}).AddRow(1, "first title", "first body", created_at).AddRow(2, "second title", "second body", created_at) 315 | mock.ExpectPrepare("SELECT (.+) FROM messages").ExpectQuery().WillReturnRows(rows) 316 | }, 317 | want: []Message{ 318 | { 319 | Id: 1, 320 | Title: "first title", 321 | Body: "first body", 322 | CreatedAt: created_at, 323 | }, 324 | { 325 | Id: 2, 326 | Title: "second title", 327 | Body: "second body", 328 | CreatedAt: created_at, 329 | }, 330 | }, 331 | }, 332 | { 333 | name: "Invalid SQL Syntax", 334 | s: s, 335 | mock: func() { 336 | //We added two rows 337 | _ = sqlmock.NewRows([]string{"Id", "Title", "Body", "CreatedAt"}).AddRow(1, "first title", "first body", created_at).AddRow(2, "second title", "second body", created_at) 338 | //"SELECTS" is used instead of "SELECT" 339 | mock.ExpectPrepare("SELECTS (.+) FROM messages").ExpectQuery().WillReturnError(errors.New("Error when trying to prepare all messages")) 340 | }, 341 | wantErr: true, 342 | }, 343 | } 344 | for _, tt := range tests { 345 | t.Run(tt.name, func(t *testing.T) { 346 | tt.mock() 347 | got, err := tt.s.GetAll() 348 | if (err != nil) != tt.wantErr { 349 | t.Errorf("GetAll() error new = %v, wantErr %v", err, tt.wantErr) 350 | return 351 | } 352 | if err == nil && !reflect.DeepEqual(got, tt.want) { 353 | t.Errorf("GetAll() = %v, want %v", got, tt.want) 354 | } 355 | }) 356 | } 357 | } 358 | 359 | func TestMessageRepo_Delete(t *testing.T) { 360 | db, mock, err := sqlmock.New() 361 | if err != nil { 362 | t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 363 | } 364 | defer db.Close() 365 | s := NewMessageRepository(db) 366 | 367 | tests := []struct { 368 | name string 369 | s messageRepoInterface 370 | msgId int64 371 | mock func() 372 | want *Message 373 | wantErr bool 374 | }{ 375 | { 376 | //When everything works as expected 377 | name: "OK", 378 | s: s, 379 | msgId: 1, 380 | mock: func() { 381 | mock.ExpectPrepare("DELETE FROM messages").ExpectExec().WithArgs(1).WillReturnResult(sqlmock.NewResult(0, 1)) 382 | }, 383 | wantErr: false, 384 | }, 385 | { 386 | name: "Invalid Id/Not Found Id", 387 | s: s, 388 | msgId: 1, 389 | mock: func() { 390 | mock.ExpectPrepare("DELETE FROM messages").ExpectExec().WithArgs(100).WillReturnResult(sqlmock.NewResult(0, 0)) 391 | }, 392 | wantErr: true, 393 | }, 394 | { 395 | name: "Invalid SQL query", 396 | s: s, 397 | msgId: 1, 398 | mock: func() { 399 | mock.ExpectPrepare("DELETE FROMSSSS messages").ExpectExec().WithArgs(1).WillReturnResult(sqlmock.NewResult(0, 0)) 400 | }, 401 | wantErr: true, 402 | }, 403 | } 404 | for _, tt := range tests { 405 | t.Run(tt.name, func(t *testing.T) { 406 | tt.mock() 407 | err := tt.s.Delete(tt.msgId) 408 | if (err != nil) != tt.wantErr { 409 | t.Errorf("Delete() error new = %v, wantErr %v", err, tt.wantErr) 410 | return 411 | } 412 | }) 413 | } 414 | } 415 | 416 | //When the right number of arguments are passed 417 | //This test is just to improve coverage 418 | func TestMessageRepo_Initialize(t *testing.T) { 419 | dbdriver := "mysql" 420 | username := "username" 421 | password := "password" 422 | host := "host" 423 | database := "database" 424 | port := "port" 425 | dbConnect := MessageRepo.Initialize(dbdriver, username, password, port, host, database) 426 | fmt.Println("this is the pool: ", dbConnect) 427 | } -------------------------------------------------------------------------------- /services/messages_service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "database/sql" 5 | "efficient-api/domain" 6 | "efficient-api/utils/error_utils" 7 | "fmt" 8 | "github.com/stretchr/testify/assert" 9 | "net/http" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var ( 15 | tm = time.Now() 16 | getMessageDomain func(messageId int64) (*domain.Message, error_utils.MessageErr) 17 | createMessageDomain func(msg *domain.Message) (*domain.Message, error_utils.MessageErr) 18 | updateMessageDomain func(msg *domain.Message) (*domain.Message, error_utils.MessageErr) 19 | deleteMessageDomain func(messageId int64) error_utils.MessageErr 20 | getAllMessagesDomain func() ([]domain.Message, error_utils.MessageErr) 21 | ) 22 | 23 | type getDBMock struct {} 24 | 25 | func (m *getDBMock) Get(messageId int64) (*domain.Message, error_utils.MessageErr){ 26 | return getMessageDomain(messageId) 27 | } 28 | func (m *getDBMock) Create(msg *domain.Message) (*domain.Message, error_utils.MessageErr){ 29 | return createMessageDomain(msg) 30 | } 31 | func (m *getDBMock) Update(msg *domain.Message) (*domain.Message, error_utils.MessageErr){ 32 | return updateMessageDomain(msg) 33 | } 34 | func (m *getDBMock) Delete(messageId int64) error_utils.MessageErr { 35 | return deleteMessageDomain(messageId) 36 | } 37 | func (m *getDBMock) GetAll() ([]domain.Message, error_utils.MessageErr) { 38 | return getAllMessagesDomain() 39 | } 40 | func (m *getDBMock) Initialize(string, string, string, string, string, string) *sql.DB { 41 | return nil 42 | } 43 | 44 | 45 | /////////////////////////////////////////////////////////////// 46 | // Start of "GetMessage" test cases 47 | /////////////////////////////////////////////////////////////// 48 | func TestMessagesService_GetMessage_Success(t *testing.T) { 49 | domain.MessageRepo = &getDBMock{} //this is where we swapped the functionality 50 | getMessageDomain = func(messageId int64) (*domain.Message, error_utils.MessageErr) { 51 | return &domain.Message{ 52 | Id: 1, 53 | Title: "the title", 54 | Body: "the body", 55 | CreatedAt: tm, 56 | }, nil 57 | } 58 | msg, err := MessagesService.GetMessage(1) 59 | fmt.Println("this is the message: ", msg) 60 | assert.NotNil(t, msg) 61 | assert.Nil(t, err) 62 | assert.EqualValues(t, 1, msg.Id) 63 | assert.EqualValues(t, "the title", msg.Title) 64 | assert.EqualValues(t, "the body", msg.Body) 65 | assert.EqualValues(t, tm, msg.CreatedAt) 66 | } 67 | 68 | //Test the not found functionality 69 | func TestMessagesService_GetMessage_NotFoundID(t *testing.T) { 70 | domain.MessageRepo = &getDBMock{} 71 | //MessagesService = &serviceMock{} 72 | 73 | getMessageDomain = func(messageId int64) (*domain.Message, error_utils.MessageErr) { 74 | return nil, error_utils.NewNotFoundError("the id is not found") 75 | } 76 | msg, err := MessagesService.GetMessage(1) 77 | assert.Nil(t, msg) 78 | assert.NotNil(t, err) 79 | assert.EqualValues(t, http.StatusNotFound, err.Status()) 80 | assert.EqualValues(t, "the id is not found", err.Message()) 81 | assert.EqualValues(t, "not_found", err.Error()) 82 | } 83 | /////////////////////////////////////////////////////////////// 84 | // End of "GetMessage" test cases 85 | /////////////////////////////////////////////////////////////// 86 | 87 | 88 | /////////////////////////////////////////////////////////////// 89 | // Start of "CreateMessage" test cases 90 | /////////////////////////////////////////////////////////////// 91 | 92 | //Here we call the domain method, so we must mock it 93 | func TestMessagesService_CreateMessage_Success(t *testing.T) { 94 | domain.MessageRepo = &getDBMock{} 95 | createMessageDomain = func(msg *domain.Message) (*domain.Message, error_utils.MessageErr){ 96 | return &domain.Message{ 97 | Id: 1, 98 | Title: "the title", 99 | Body: "the body", 100 | CreatedAt: tm, 101 | }, nil 102 | } 103 | request := &domain.Message{ 104 | Title: "the title", 105 | Body: "the body", 106 | CreatedAt: tm, 107 | } 108 | msg, err := MessagesService.CreateMessage(request) 109 | fmt.Println("this is the message: ", msg) 110 | assert.NotNil(t, msg) 111 | assert.Nil(t, err) 112 | assert.EqualValues(t, 1, msg.Id) 113 | assert.EqualValues(t, "the title", msg.Title) 114 | assert.EqualValues(t, "the body", msg.Body) 115 | assert.EqualValues(t, tm, msg.CreatedAt) 116 | } 117 | 118 | //This is a table test that check both the title and the body 119 | //Since this will never call the domain "Get" method, no need to mock that method here 120 | func TestMessagesService_CreateMessage_Invalid_Request(t *testing.T) { 121 | tests := []struct { 122 | request *domain.Message 123 | statusCode int 124 | errMsg string 125 | errErr string 126 | }{ 127 | { 128 | request: &domain.Message{ 129 | Title: "", 130 | Body: "the body", 131 | CreatedAt: tm, 132 | }, 133 | statusCode: http.StatusUnprocessableEntity, 134 | errMsg: "Please enter a valid title", 135 | errErr: "invalid_request", 136 | }, 137 | { 138 | request: &domain.Message{ 139 | Title: "the title", 140 | Body: "", 141 | CreatedAt: tm, 142 | }, 143 | statusCode: http.StatusUnprocessableEntity, 144 | errMsg: "Please enter a valid body", 145 | errErr: "invalid_request", 146 | }, 147 | } 148 | for _, tt := range tests { 149 | msg, err := MessagesService.CreateMessage(tt.request) 150 | assert.Nil(t, msg) 151 | assert.NotNil(t, err) 152 | assert.EqualValues(t, tt.errMsg, err.Message()) 153 | assert.EqualValues(t, tt.statusCode, err.Status()) 154 | assert.EqualValues(t, tt.errErr, err.Error()) 155 | } 156 | } 157 | 158 | //We mock the "Get" method in the domain here. What could go wrong?, 159 | //Since the title of the message must be unique, an error must be thrown, 160 | //Of course you can also mock when the sql query is wrong, etc(these where covered in the domain integration__tests), 161 | //For now, we have 100% coverage on the "CreateMessage" method in the service 162 | func TestMessagesService_CreateMessage_Failure(t *testing.T) { 163 | domain.MessageRepo = &getDBMock{} 164 | createMessageDomain = func(msg *domain.Message) (*domain.Message, error_utils.MessageErr){ 165 | return nil, error_utils.NewInternalServerError("title already taken") 166 | } 167 | request := &domain.Message{ 168 | Title: "the title", 169 | Body: "the body", 170 | CreatedAt: tm, 171 | } 172 | msg, err := MessagesService.CreateMessage(request) 173 | assert.Nil(t, msg) 174 | assert.NotNil(t, err) 175 | assert.EqualValues(t, "title already taken", err.Message()) 176 | assert.EqualValues(t, http.StatusInternalServerError, err.Status()) 177 | assert.EqualValues(t, "server_error", err.Error()) 178 | } 179 | 180 | /////////////////////////////////////////////////////////////// 181 | // End of "CreateMessage" test cases 182 | /////////////////////////////////////////////////////////////// 183 | 184 | 185 | 186 | /////////////////////////////////////////////////////////////// 187 | // Start of "UpdateMessage" test cases 188 | /////////////////////////////////////////////////////////////// 189 | func TestMessagesService_UpdateMessage_Success(t *testing.T) { 190 | domain.MessageRepo = &getDBMock{} 191 | getMessageDomain = func(messageId int64) (*domain.Message, error_utils.MessageErr) { 192 | return &domain.Message{ 193 | Id: 1, 194 | Title: "former title", 195 | Body: "former body", 196 | }, nil 197 | } 198 | updateMessageDomain = func(msg *domain.Message) (*domain.Message, error_utils.MessageErr){ 199 | return &domain.Message{ 200 | Id: 1, 201 | Title: "the title update", 202 | Body: "the body update", 203 | }, nil 204 | } 205 | request := &domain.Message{ 206 | Title: "the title update", 207 | Body: "the body update", 208 | } 209 | msg, err := MessagesService.UpdateMessage(request) 210 | assert.NotNil(t, msg) 211 | assert.Nil(t, err) 212 | assert.EqualValues(t, 1, msg.Id) 213 | assert.EqualValues(t, "the title update", msg.Title) 214 | assert.EqualValues(t, "the body update", msg.Body) 215 | } 216 | 217 | //This is a validation test, it wont call the domain methods, so, we dont need to mock them. 218 | //It is also a table 219 | func TestMessagesService_UpdateMessage_Empty_Title_Or_Body(t *testing.T) { 220 | tests := []struct { 221 | request *domain.Message 222 | statusCode int 223 | errMsg string 224 | errErr string 225 | }{ 226 | { 227 | request: &domain.Message{ 228 | Title: "", 229 | Body: "the body", 230 | }, 231 | statusCode: http.StatusUnprocessableEntity, 232 | errMsg: "Please enter a valid title", 233 | errErr: "invalid_request", 234 | }, 235 | { 236 | request: &domain.Message{ 237 | Title: "the title", 238 | Body: "", 239 | }, 240 | statusCode: http.StatusUnprocessableEntity, 241 | errMsg: "Please enter a valid body", 242 | errErr: "invalid_request", 243 | }, 244 | } 245 | for _, tt := range tests { 246 | msg, err := MessagesService.UpdateMessage(tt.request) 247 | assert.Nil(t, msg) 248 | assert.NotNil(t, err) 249 | assert.EqualValues(t, tt.statusCode, err.Status()) 250 | assert.EqualValues(t, tt.errMsg, err.Message()) 251 | assert.EqualValues(t, tt.errErr, err.Error()) 252 | } 253 | } 254 | 255 | //An error can occur when trying to fetch the user to update, anything from a timeout error to a not found error. 256 | //We need to test for that. 257 | //Here we checked for 500 error, you can also check for others if you have time. 258 | func TestMessagesService_UpdateMessage_Failure_Getting_Former_Message(t *testing.T) { 259 | domain.MessageRepo = &getDBMock{} 260 | getMessageDomain = func(messageId int64) (*domain.Message, error_utils.MessageErr) { 261 | return nil, error_utils.NewInternalServerError("error getting message") 262 | } 263 | request := &domain.Message{ 264 | Title: "the title update", 265 | Body: "the body update", 266 | } 267 | msg, err := MessagesService.UpdateMessage(request) 268 | assert.Nil(t, msg) 269 | assert.NotNil(t, err) 270 | assert.EqualValues(t, "error getting message", err.Message()) 271 | assert.EqualValues(t, http.StatusInternalServerError, err.Status()) 272 | assert.EqualValues(t, "server_error", err.Error()) 273 | } 274 | 275 | //We can get the former message, but we might have issues updating it. Here also, we tested using 500, you can also assert other possible failure status 276 | func TestMessagesService_UpdateMessage_Failure_Updating_Message(t *testing.T) { 277 | domain.MessageRepo = &getDBMock{} 278 | 279 | getMessageDomain = func(messageId int64) (*domain.Message, error_utils.MessageErr) { 280 | return &domain.Message{ 281 | Id: 1, 282 | Title: "former title", 283 | Body: "former body", 284 | }, nil 285 | } 286 | updateMessageDomain = func(msg *domain.Message) (*domain.Message, error_utils.MessageErr){ 287 | return nil, error_utils.NewInternalServerError("error updating message") 288 | } 289 | request := &domain.Message{ 290 | Title: "the title update", 291 | Body: "the body update", 292 | } 293 | msg, err := MessagesService.UpdateMessage(request) 294 | assert.Nil(t, msg) 295 | assert.NotNil(t, err) 296 | assert.EqualValues(t, "error updating message", err.Message()) 297 | assert.EqualValues(t, http.StatusInternalServerError, err.Status()) 298 | assert.EqualValues(t, "server_error", err.Error()) 299 | } 300 | /////////////////////////////////////////////////////////////// 301 | // End of"UpdateMessage" test cases 302 | /////////////////////////////////////////////////////////////// 303 | 304 | 305 | /////////////////////////////////////////////////////////////// 306 | // Start of"DeleteMessage" test cases 307 | /////////////////////////////////////////////////////////////// 308 | func TestMessagesService_DeleteMessage_Success(t *testing.T) { 309 | domain.MessageRepo = &getDBMock{} 310 | getMessageDomain = func(messageId int64) (*domain.Message, error_utils.MessageErr) { 311 | return &domain.Message{ 312 | Id: 1, 313 | Title: "former title", 314 | Body: "former body", 315 | }, nil 316 | } 317 | deleteMessageDomain = func(messageId int64) error_utils.MessageErr { 318 | return nil 319 | } 320 | err := MessagesService.DeleteMessage(1) 321 | assert.Nil(t, err) 322 | } 323 | 324 | //It can range from a 500 error to a 404 error, we didnt mock deleting the message because we will not get there 325 | func TestMessagesService_DeleteMessage_Error_Getting_Message(t *testing.T) { 326 | domain.MessageRepo = &getDBMock{} 327 | getMessageDomain = func(messageId int64) (*domain.Message, error_utils.MessageErr) { 328 | return nil, error_utils.NewInternalServerError("Something went wrong getting message") 329 | } 330 | err := MessagesService.DeleteMessage(1) 331 | assert.NotNil(t, err) 332 | assert.EqualValues(t, "Something went wrong getting message", err.Message()) 333 | assert.EqualValues(t, http.StatusInternalServerError, err.Status()) 334 | assert.EqualValues(t, "server_error", err.Error()) 335 | } 336 | 337 | func TestMessagesService_DeleteMessage_Error_Deleting_Message(t *testing.T) { 338 | domain.MessageRepo = &getDBMock{} 339 | getMessageDomain = func(messageId int64) (*domain.Message, error_utils.MessageErr) { 340 | return &domain.Message{ 341 | Id: 1, 342 | Title: "former title", 343 | Body: "former body", 344 | }, nil 345 | } 346 | deleteMessageDomain = func(messageId int64) error_utils.MessageErr { 347 | return error_utils.NewInternalServerError("error deleting message") 348 | } 349 | err := MessagesService.DeleteMessage(1) 350 | assert.NotNil(t, err) 351 | assert.EqualValues(t, "error deleting message", err.Message()) 352 | assert.EqualValues(t, http.StatusInternalServerError, err.Status()) 353 | assert.EqualValues(t, "server_error", err.Error()) 354 | } 355 | /////////////////////////////////////////////////////////////// 356 | // End of "DeleteMessage" test cases 357 | /////////////////////////////////////////////////////////////// 358 | 359 | 360 | 361 | /////////////////////////////////////////////////////////////// 362 | // Start of "GetAllMessage" test cases 363 | /////////////////////////////////////////////////////////////// 364 | func TestMessagesService_GetAllMessages(t *testing.T) { 365 | domain.MessageRepo = &getDBMock{} 366 | getAllMessagesDomain = func() ([]domain.Message, error_utils.MessageErr) { 367 | return []domain.Message{ 368 | { 369 | Id: 1, 370 | Title: "first title", 371 | Body: "first body", 372 | }, 373 | { 374 | Id: 2, 375 | Title: "second title", 376 | Body: "second body", 377 | }, 378 | }, nil 379 | } 380 | messages, err := MessagesService.GetAllMessages() 381 | assert.Nil(t, err) 382 | assert.NotNil(t, messages) 383 | assert.EqualValues(t, messages[0].Id, 1) 384 | assert.EqualValues(t, messages[0].Title, "first title") 385 | assert.EqualValues(t, messages[0].Body, "first body") 386 | assert.EqualValues(t, messages[1].Id, 2) 387 | assert.EqualValues(t, messages[1].Title, "second title") 388 | assert.EqualValues(t, messages[1].Body, "second body") 389 | } 390 | 391 | func TestMessagesService_GetAllMessages_Error_Getting_Messages(t *testing.T) { 392 | domain.MessageRepo = &getDBMock{} 393 | getAllMessagesDomain = func() ([]domain.Message, error_utils.MessageErr) { 394 | return nil, error_utils.NewInternalServerError("error getting messages") 395 | } 396 | messages, err := MessagesService.GetAllMessages() 397 | assert.NotNil(t, err) 398 | assert.Nil(t, messages) 399 | assert.EqualValues(t, http.StatusInternalServerError, err.Status()) 400 | assert.EqualValues(t, "error getting messages", err.Message()) 401 | assert.EqualValues(t, "server_error", err.Error()) 402 | } 403 | /////////////////////////////////////////////////////////////// 404 | // End of "GetAllMessage" test cases 405 | /////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /controllers/messages_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "bytes" 5 | "efficient-api/domain" 6 | "efficient-api/services" 7 | "efficient-api/utils/error_utils" 8 | "encoding/json" 9 | "github.com/gin-gonic/gin" 10 | "github.com/stretchr/testify/assert" 11 | "net/http" 12 | "net/http/httptest" 13 | "testing" 14 | ) 15 | 16 | var ( 17 | getMessageService func(msgId int64) (*domain.Message, error_utils.MessageErr) 18 | createMessageService func(message *domain.Message) (*domain.Message, error_utils.MessageErr) 19 | updateMessageService func(message *domain.Message) (*domain.Message, error_utils.MessageErr) 20 | deleteMessageService func(msgId int64) error_utils.MessageErr 21 | getAllMessageService func() ([]domain.Message, error_utils.MessageErr) 22 | ) 23 | 24 | type serviceMock struct {} 25 | 26 | func (sm *serviceMock) GetMessage(msgId int64) (*domain.Message, error_utils.MessageErr) { 27 | return getMessageService(msgId) 28 | } 29 | func (sm *serviceMock) CreateMessage(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 30 | return createMessageService(message) 31 | } 32 | func (sm *serviceMock) UpdateMessage(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 33 | return updateMessageService(message) 34 | } 35 | func (sm *serviceMock) DeleteMessage(msgId int64) error_utils.MessageErr { 36 | return deleteMessageService(msgId) 37 | } 38 | func (sm *serviceMock) GetAllMessages() ([]domain.Message, error_utils.MessageErr) { 39 | return getAllMessageService() 40 | } 41 | 42 | /////////////////////////////////////////////////////////////// 43 | // Start of "GetMessage" test cases 44 | /////////////////////////////////////////////////////////////// 45 | func TestGetMessage_Success(t *testing.T) { 46 | services.MessagesService = &serviceMock{} 47 | getMessageService = func(msgId int64) (*domain.Message, error_utils.MessageErr) { 48 | return &domain.Message{ 49 | Id: 1, 50 | Title: "the title", 51 | Body: "the body", 52 | }, nil 53 | } 54 | msgId := "1" //this has to be a string, because is passed through the url 55 | r := gin.Default() 56 | req, _ := http.NewRequest(http.MethodGet, "/messages/"+msgId, nil) 57 | rr := httptest.NewRecorder() 58 | r.GET("/messages/:message_id", GetMessage) 59 | r.ServeHTTP(rr, req) 60 | 61 | var message domain.Message 62 | err := json.Unmarshal(rr.Body.Bytes(), &message) 63 | assert.Nil(t, err) 64 | assert.NotNil(t, message) 65 | assert.EqualValues(t, http.StatusOK, rr.Code) 66 | assert.EqualValues(t, 1, message.Id) 67 | assert.EqualValues(t, "the title", message.Title) 68 | assert.EqualValues(t, "the body", message.Body) 69 | } 70 | 71 | //When an invalid id id passed. No need to mock the service here because we will never call it 72 | func TestGetMessage_Invalid_Id(t *testing.T) { 73 | msgId := "abc" //this has to be a string, because is passed through the url 74 | r := gin.Default() 75 | req, _ := http.NewRequest(http.MethodGet, "/messages/"+msgId, nil) 76 | rr := httptest.NewRecorder() 77 | r.GET("/messages/:message_id", GetMessage) 78 | r.ServeHTTP(rr, req) 79 | 80 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 81 | assert.Nil(t, err) 82 | assert.NotNil(t, apiErr) 83 | assert.EqualValues(t, http.StatusBadRequest, apiErr.Status()) 84 | assert.EqualValues(t, "message id should be a number", apiErr.Message()) 85 | assert.EqualValues(t, "bad_request", apiErr.Error()) 86 | } 87 | 88 | //We will call the service method here, so we need to mock it 89 | func TestGetMessage_Message_Not_Found(t *testing.T) { 90 | services.MessagesService = &serviceMock{} 91 | getMessageService = func(msgId int64) (*domain.Message, error_utils.MessageErr) { 92 | return nil, error_utils.NewNotFoundError("message not found") 93 | } 94 | msgId := "1" //valid id 95 | r := gin.Default() 96 | req, _ := http.NewRequest(http.MethodGet, "/messages/"+msgId, nil) 97 | rr := httptest.NewRecorder() 98 | r.GET("/messages/:message_id", GetMessage) 99 | r.ServeHTTP(rr, req) 100 | 101 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 102 | assert.Nil(t, err) 103 | assert.NotNil(t, apiErr) 104 | assert.EqualValues(t, http.StatusNotFound, apiErr.Status()) 105 | assert.EqualValues(t, "message not found", apiErr.Message()) 106 | assert.EqualValues(t, "not_found", apiErr.Error()) 107 | } 108 | 109 | //We will call the service method here, so we need to mock it 110 | //If for any reason, we could not get the message 111 | func TestGetMessage_Message_Database_Error(t *testing.T) { 112 | services.MessagesService = &serviceMock{} 113 | getMessageService = func(msgId int64) (*domain.Message, error_utils.MessageErr) { 114 | return nil, error_utils.NewInternalServerError("database error") 115 | } 116 | msgId := "1" //valid id 117 | r := gin.Default() 118 | req, _ := http.NewRequest(http.MethodGet, "/messages/"+msgId, nil) 119 | rr := httptest.NewRecorder() 120 | r.GET("/messages/:message_id", GetMessage) 121 | r.ServeHTTP(rr, req) 122 | 123 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 124 | assert.Nil(t, err) 125 | assert.NotNil(t, apiErr) 126 | assert.EqualValues(t, http.StatusInternalServerError, apiErr.Status()) 127 | assert.EqualValues(t, "database error", apiErr.Message()) 128 | assert.EqualValues(t, "server_error", apiErr.Error()) 129 | } 130 | /////////////////////////////////////////////////////////////// 131 | // End of "GetMessage" test cases 132 | /////////////////////////////////////////////////////////////// 133 | 134 | 135 | 136 | /////////////////////////////////////////////////////////////// 137 | // Start of "CreateMessage" test cases 138 | /////////////////////////////////////////////////////////////// 139 | func TestCreateMessage_Success(t *testing.T) { 140 | services.MessagesService = &serviceMock{} 141 | createMessageService = func(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 142 | return &domain.Message{ 143 | Id: 1, 144 | Title: "the title", 145 | Body: "the body", 146 | }, nil 147 | } 148 | jsonBody := `{"title": "the title", "body": "the body"}` 149 | r := gin.Default() 150 | req, err := http.NewRequest(http.MethodPost, "/messages", bytes.NewBufferString(jsonBody)) 151 | if err != nil { 152 | t.Errorf("this is the error: %v\n", err) 153 | } 154 | rr := httptest.NewRecorder() 155 | r.POST("/messages", CreateMessage) 156 | r.ServeHTTP(rr, req) 157 | 158 | var message domain.Message 159 | err = json.Unmarshal(rr.Body.Bytes(), &message) 160 | assert.Nil(t, err) 161 | assert.NotNil(t, message) 162 | assert.EqualValues(t, http.StatusCreated, rr.Code) 163 | assert.EqualValues(t, 1, message.Id) 164 | assert.EqualValues(t, "the title", message.Title) 165 | assert.EqualValues(t, "the body", message.Body) 166 | } 167 | 168 | func TestCreateMessage_Invalid_Json(t *testing.T) { 169 | inputJson := `{"title": 1234, "body": "the body"}` 170 | r := gin.Default() 171 | req, err := http.NewRequest(http.MethodPost, "/messages", bytes.NewBufferString(inputJson)) 172 | if err != nil { 173 | t.Errorf("this is the error: %v\n", err) 174 | } 175 | rr := httptest.NewRecorder() 176 | r.POST("/messages", CreateMessage) 177 | r.ServeHTTP(rr, req) 178 | 179 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 180 | 181 | assert.Nil(t, err) 182 | assert.NotNil(t, apiErr) 183 | assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status()) 184 | assert.EqualValues(t, "invalid json body", apiErr.Message()) 185 | assert.EqualValues(t, "invalid_request", apiErr.Error()) 186 | } 187 | 188 | //This test is not really necessary here, because it has been handled in the service test 189 | func TestCreateMessage_Empty_Body(t *testing.T) { 190 | services.MessagesService = &serviceMock{} 191 | createMessageService = func(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 192 | return nil, error_utils.NewUnprocessibleEntityError("Please enter a valid body") 193 | } 194 | inputJson := `{"title": "the title", "body": ""}` 195 | r := gin.Default() 196 | req, err := http.NewRequest(http.MethodPost, "/messages", bytes.NewBufferString(inputJson)) 197 | if err != nil { 198 | t.Errorf("this is the error: %v\n", err) 199 | } 200 | rr := httptest.NewRecorder() 201 | r.POST("/messages", CreateMessage) 202 | r.ServeHTTP(rr, req) 203 | 204 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 205 | 206 | assert.Nil(t, err) 207 | assert.NotNil(t, apiErr) 208 | assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status()) 209 | assert.EqualValues(t, "Please enter a valid body", apiErr.Message()) 210 | assert.EqualValues(t, "invalid_request", apiErr.Error()) 211 | } 212 | //This test is not really necessary here, because it has been handled in the service test 213 | func TestCreateMessage_Empty_Title(t *testing.T) { 214 | services.MessagesService = &serviceMock{} 215 | createMessageService = func(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 216 | return nil, error_utils.NewUnprocessibleEntityError("Please enter a valid title") 217 | } 218 | inputJson := `{"title": "", "body": "the body"}` 219 | r := gin.Default() 220 | req, err := http.NewRequest(http.MethodPost, "/messages", bytes.NewBufferString(inputJson)) 221 | if err != nil { 222 | t.Errorf("this is the error: %v\n", err) 223 | } 224 | rr := httptest.NewRecorder() 225 | r.POST("/messages", CreateMessage) 226 | r.ServeHTTP(rr, req) 227 | 228 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 229 | 230 | assert.Nil(t, err) 231 | assert.NotNil(t, apiErr) 232 | assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status()) 233 | assert.EqualValues(t, "Please enter a valid title", apiErr.Message()) 234 | assert.EqualValues(t, "invalid_request", apiErr.Error()) 235 | } 236 | /////////////////////////////////////////////////////////////// 237 | // End of "CreateMessage" test cases 238 | /////////////////////////////////////////////////////////////// 239 | 240 | 241 | 242 | /////////////////////////////////////////////////////////////// 243 | // Start of "UpdateMessage" test cases 244 | /////////////////////////////////////////////////////////////// 245 | func TestUpdateMessage_Success(t *testing.T) { 246 | services.MessagesService = &serviceMock{} 247 | updateMessageService = func(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 248 | return &domain.Message{ 249 | Id: 1, 250 | Title: "update title", 251 | Body: "update body", 252 | }, nil 253 | } 254 | jsonBody := `{"title": "update title", "body": "update body"}` 255 | r := gin.Default() 256 | id := "1" 257 | req, err := http.NewRequest(http.MethodPut, "/messages/"+id, bytes.NewBufferString(jsonBody)) 258 | if err != nil { 259 | t.Errorf("this is the error: %v\n", err) 260 | } 261 | rr := httptest.NewRecorder() 262 | r.PUT("/messages/:message_id", UpdateMessage) 263 | r.ServeHTTP(rr, req) 264 | 265 | var message domain.Message 266 | err = json.Unmarshal(rr.Body.Bytes(), &message) 267 | assert.Nil(t, err) 268 | assert.NotNil(t, message) 269 | assert.EqualValues(t, http.StatusOK, rr.Code) 270 | assert.EqualValues(t, 1, message.Id) 271 | assert.EqualValues(t, "update title", message.Title) 272 | assert.EqualValues(t, "update body", message.Body) 273 | } 274 | 275 | //We dont need to mock the service method here, because we wont call it 276 | func TestUpdateMessage_Invalid_Id(t *testing.T) { 277 | jsonBody := `{"title": "update title", "body": "update body"}` 278 | r := gin.Default() 279 | id := "abc" 280 | req, err := http.NewRequest(http.MethodPut, "/messages/"+id, bytes.NewBufferString(jsonBody)) 281 | if err != nil { 282 | t.Errorf("this is the error: %v\n", err) 283 | } 284 | rr := httptest.NewRecorder() 285 | r.PUT("/messages/:message_id", UpdateMessage) 286 | r.ServeHTTP(rr, req) 287 | 288 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 289 | assert.Nil(t, err) 290 | assert.NotNil(t, apiErr) 291 | assert.EqualValues(t, http.StatusBadRequest, apiErr.Status()) 292 | assert.EqualValues(t, "message id should be a number", apiErr.Message()) 293 | assert.EqualValues(t, "bad_request", apiErr.Error()) 294 | } 295 | 296 | //When for instance an integer is provided instead of a string 297 | func TestUpdateMessage_Invalid_Json(t *testing.T) { 298 | inputJson := `{"title": 1234, "body": "the body"}` 299 | r := gin.Default() 300 | id := "1" 301 | req, err := http.NewRequest(http.MethodPut, "/messages/"+id, bytes.NewBufferString(inputJson)) 302 | if err != nil { 303 | t.Errorf("this is the error: %v\n", err) 304 | } 305 | rr := httptest.NewRecorder() 306 | r.PUT("/messages/:message_id", UpdateMessage) 307 | r.ServeHTTP(rr, req) 308 | 309 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 310 | 311 | assert.Nil(t, err) 312 | assert.NotNil(t, apiErr) 313 | assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status()) 314 | assert.EqualValues(t, "invalid json body", apiErr.Message()) 315 | assert.EqualValues(t, "invalid_request", apiErr.Error()) 316 | } 317 | 318 | 319 | //This test is not really necessary here, because it has been handled in the service test 320 | func TestUpdateMessage_Empty_Body(t *testing.T) { 321 | services.MessagesService = &serviceMock{} 322 | updateMessageService = func(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 323 | return nil, error_utils.NewUnprocessibleEntityError("Please enter a valid body") 324 | } 325 | inputJson := `{"title": "the title", "body": ""}` 326 | id := "1" 327 | r := gin.Default() 328 | req, err := http.NewRequest(http.MethodPut, "/messages/"+id, bytes.NewBufferString(inputJson)) 329 | if err != nil { 330 | t.Errorf("this is the error: %v\n", err) 331 | } 332 | rr := httptest.NewRecorder() 333 | r.PUT("/messages/:message_id", UpdateMessage) 334 | r.ServeHTTP(rr, req) 335 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 336 | assert.Nil(t, err) 337 | assert.NotNil(t, apiErr) 338 | assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status()) 339 | assert.EqualValues(t, "Please enter a valid body", apiErr.Message()) 340 | assert.EqualValues(t, "invalid_request", apiErr.Error()) 341 | } 342 | //This test is not really necessary here, because it has been handled in the service test 343 | func TestUpdateMessage_Empty_Title(t *testing.T) { 344 | services.MessagesService = &serviceMock{} 345 | updateMessageService = func(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 346 | return nil, error_utils.NewUnprocessibleEntityError("Please enter a valid title") 347 | } 348 | inputJson := `{"title": "", "body": "the body"}` 349 | id := "1" 350 | r := gin.Default() 351 | req, err := http.NewRequest(http.MethodPut, "/messages/"+id, bytes.NewBufferString(inputJson)) 352 | if err != nil { 353 | t.Errorf("this is the error: %v\n", err) 354 | } 355 | rr := httptest.NewRecorder() 356 | r.PUT("/messages/:message_id", UpdateMessage) 357 | r.ServeHTTP(rr, req) 358 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 359 | assert.Nil(t, err) 360 | assert.NotNil(t, apiErr) 361 | assert.EqualValues(t, http.StatusUnprocessableEntity, apiErr.Status()) 362 | assert.EqualValues(t, "Please enter a valid title", apiErr.Message()) 363 | assert.EqualValues(t, "invalid_request", apiErr.Error()) 364 | } 365 | 366 | //Other errors can happen when we try to update the message 367 | func TestUpdateMessage_Error_Updating(t *testing.T) { 368 | services.MessagesService = &serviceMock{} 369 | updateMessageService = func(message *domain.Message) (*domain.Message, error_utils.MessageErr) { 370 | return nil, error_utils.NewInternalServerError("error when updating message") 371 | } 372 | jsonBody := `{"title": "update title", "body": "update body"}` 373 | r := gin.Default() 374 | id := "1" 375 | req, err := http.NewRequest(http.MethodPut, "/messages/"+id, bytes.NewBufferString(jsonBody)) 376 | if err != nil { 377 | t.Errorf("this is the error: %v\n", err) 378 | } 379 | rr := httptest.NewRecorder() 380 | r.PUT("/messages/:message_id", UpdateMessage) 381 | r.ServeHTTP(rr, req) 382 | 383 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 384 | assert.Nil(t, err) 385 | assert.NotNil(t, apiErr) 386 | 387 | assert.EqualValues(t, http.StatusInternalServerError, apiErr.Status()) 388 | assert.EqualValues(t, "error when updating message", apiErr.Message()) 389 | assert.EqualValues(t, "server_error", apiErr.Error()) 390 | } 391 | /////////////////////////////////////////////////////////////// 392 | // End of "UpdateMessage" test cases 393 | /////////////////////////////////////////////////////////////// 394 | 395 | 396 | 397 | /////////////////////////////////////////////////////////////// 398 | // Start of "DeleteMessage" test cases 399 | /////////////////////////////////////////////////////////////// 400 | func TestDeleteMessage_Success(t *testing.T) { 401 | services.MessagesService = &serviceMock{} 402 | deleteMessageService = func(msg int64) error_utils.MessageErr { 403 | return nil 404 | } 405 | r := gin.Default() 406 | id := "1" 407 | req, err := http.NewRequest(http.MethodDelete, "/messages/"+id, nil) 408 | if err != nil { 409 | t.Errorf("this is the error: %v\n", err) 410 | } 411 | rr := httptest.NewRecorder() 412 | r.DELETE("/messages/:message_id", DeleteMessage) 413 | r.ServeHTTP(rr, req) 414 | 415 | var response = make(map[string]string) 416 | theErr := json.Unmarshal(rr.Body.Bytes(), &response) 417 | if theErr != nil { 418 | t.Errorf("this is the error: %v\n", err) 419 | } 420 | assert.EqualValues(t, http.StatusOK, rr.Code) 421 | assert.EqualValues(t, response["status"], "deleted") 422 | } 423 | 424 | //We wont call the service Delete method here, so no need to mock it 425 | func TestDeleteMessage_Invalid_Id(t *testing.T) { 426 | 427 | r := gin.Default() 428 | id := "abc" 429 | req, err := http.NewRequest(http.MethodDelete, "/messages/"+id, nil) 430 | if err != nil { 431 | t.Errorf("this is the error: %v\n", err) 432 | } 433 | rr := httptest.NewRecorder() 434 | r.DELETE("/messages/:message_id", DeleteMessage) 435 | r.ServeHTTP(rr, req) 436 | 437 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 438 | if err != nil { 439 | t.Errorf("this is the error: %v\n", err) 440 | } 441 | assert.Nil(t, err) 442 | assert.NotNil(t, apiErr) 443 | assert.EqualValues(t, http.StatusBadRequest, apiErr.Status()) 444 | assert.EqualValues(t, "message id should be a number", apiErr.Message()) 445 | assert.EqualValues(t, "bad_request", apiErr.Error()) 446 | } 447 | 448 | //If for any reason, our update didnt happen(e.g server error, etc), This is an error from the service, but the controller conditions where met. 449 | //Maybe the message does not exist, or the server timeout 450 | func TestDeleteMessage_Failure(t *testing.T) { 451 | services.MessagesService = &serviceMock{} 452 | deleteMessageService = func(msg int64) error_utils.MessageErr { 453 | return error_utils.NewInternalServerError("error deleting message") 454 | } 455 | r := gin.Default() 456 | id := "1" 457 | req, err := http.NewRequest(http.MethodDelete, "/messages/"+id, nil) 458 | if err != nil { 459 | t.Errorf("this is the error: %v\n", err) 460 | } 461 | rr := httptest.NewRecorder() 462 | r.DELETE("/messages/:message_id", DeleteMessage) 463 | r.ServeHTTP(rr, req) 464 | 465 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 466 | if err != nil { 467 | t.Errorf("this is the error: %v\n", err) 468 | } 469 | assert.Nil(t, err) 470 | assert.NotNil(t, apiErr) 471 | assert.EqualValues(t, http.StatusInternalServerError, apiErr.Status()) 472 | assert.EqualValues(t, "error deleting message", apiErr.Message()) 473 | assert.EqualValues(t, "server_error", apiErr.Error()) 474 | } 475 | /////////////////////////////////////////////////////////////// 476 | // End of "DeleteMessage" test cases 477 | /////////////////////////////////////////////////////////////// 478 | 479 | 480 | /////////////////////////////////////////////////////////////// 481 | // Start of "GetAllMessages" test cases 482 | /////////////////////////////////////////////////////////////// 483 | func TestGetAllMessages_Success(t *testing.T) { 484 | services.MessagesService = &serviceMock{} 485 | getAllMessageService = func() ([]domain.Message, error_utils.MessageErr) { 486 | return []domain.Message{ 487 | { 488 | Id: 1, 489 | Title: "first title", 490 | Body: "first body", 491 | }, 492 | { 493 | Id: 2, 494 | Title: "second title", 495 | Body: "second body", 496 | }, 497 | }, nil 498 | } 499 | r := gin.Default() 500 | req, err := http.NewRequest(http.MethodGet, "/messages", nil) 501 | if err != nil { 502 | t.Errorf("this is the error: %v\n", err) 503 | } 504 | rr := httptest.NewRecorder() 505 | r.GET("/messages", GetAllMessages) 506 | r.ServeHTTP(rr, req) 507 | 508 | var messages []domain.Message 509 | theErr := json.Unmarshal(rr.Body.Bytes(), &messages) 510 | if theErr != nil { 511 | t.Errorf("this is the error: %v\n", err) 512 | } 513 | assert.Nil(t, err) 514 | assert.NotNil(t, messages) 515 | assert.EqualValues(t, messages[0].Id, 1) 516 | assert.EqualValues(t, messages[0].Title, "first title") 517 | assert.EqualValues(t, messages[0].Body, "first body") 518 | assert.EqualValues(t, messages[1].Id, 2) 519 | assert.EqualValues(t, messages[1].Title, "second title") 520 | assert.EqualValues(t, messages[1].Body, "second body") 521 | } 522 | 523 | //For any reason we could not get the messages 524 | func TestGetAllMessages_Failure(t *testing.T) { 525 | services.MessagesService = &serviceMock{} 526 | getAllMessageService = func() ([]domain.Message, error_utils.MessageErr) { 527 | return nil, error_utils.NewInternalServerError("error getting messages") 528 | } 529 | r := gin.Default() 530 | req, err := http.NewRequest(http.MethodGet, "/messages", nil) 531 | if err != nil { 532 | t.Errorf("this is the error: %v\n", err) 533 | } 534 | rr := httptest.NewRecorder() 535 | r.GET("/messages", GetAllMessages) 536 | r.ServeHTTP(rr, req) 537 | 538 | apiErr, err := error_utils.NewApiErrFromBytes(rr.Body.Bytes()) 539 | assert.Nil(t, err) 540 | assert.NotNil(t, apiErr) 541 | assert.EqualValues(t, "error getting messages", apiErr.Message()) 542 | assert.EqualValues(t, "server_error", apiErr.Error()) 543 | assert.EqualValues(t, http.StatusInternalServerError, apiErr.Status()) 544 | } 545 | --------------------------------------------------------------------------------