├── .gitignore ├── docker ├── Dockerfile └── .dockerignore ├── .golangci.yml ├── .example.env ├── pkg ├── internal │ ├── user.go │ ├── usecases │ │ ├── usecases.go │ │ └── usecases_test.go │ └── repository │ │ ├── repository.go │ │ └── repository_test.go ├── adapter │ ├── controller │ │ ├── models.go │ │ ├── controller.go │ │ └── controller_test.go │ └── routes │ │ └── routes.go ├── infrastructure │ ├── database │ │ ├── db.go │ │ └── db_test.go │ └── middleware │ │ └── middleware.go └── server │ └── server.go ├── main.go ├── .vscode └── launch.json ├── utils ├── uuid_mock.go ├── uuid.go ├── jwt_mock.go ├── validations.go ├── uuid_test.go ├── logger_mock.go ├── validations_test.go ├── logger.go ├── logger_test.go ├── jwt.go └── jwt_test.go ├── LICENCE ├── cmd ├── tools │ ├── flags.go │ └── flags_test.go └── app.go ├── README.md ├── config ├── config_test.go └── config.go ├── .github └── workflows │ └── docker_build.yml ├── scripts └── db_go-cleanapi.sql ├── docs ├── swagger.yaml ├── swagger.json └── docs.go └── go.mod /.gitignore: -------------------------------------------------------------------------------- 1 | /logs/*.log 2 | # 3 | !.example.env 4 | # 5 | .env -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | WORKDIR /app 3 | 4 | # manage dependencies 5 | COPY go.mod . 6 | COPY go.sum . 7 | RUN go mod download 8 | 9 | COPY . . 10 | 11 | ENV PORT 8080 12 | 13 | RUN go build 14 | 15 | CMD [ "./go-cleanapi" ] -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | 2 | run: 3 | skip-dirs-use-default: false 4 | 5 | linters: 6 | enable: 7 | - gofmt 8 | - revive 9 | - gocyclo 10 | - misspell 11 | - bodyclose 12 | 13 | gocyclo: 14 | min-complexity: 15 15 | 16 | issues: 17 | exclude-use-default: false 18 | 19 | -------------------------------------------------------------------------------- /.example.env: -------------------------------------------------------------------------------- 1 | USER_DB="root" 2 | PASSWORD_DB="password" 3 | HOST_DB="localhost" 4 | PORT_DB="3306" 5 | NAME_DB="clean" 6 | SECRET_JWT="mysecret" 7 | STAGE="DEV" 8 | COOKIE_ENCRYPTION="0123456789abcdef0123456789abcdef" 9 | API_KEY="0123456789abcdef0123456789abcdef" 10 | 11 | // GENERATE YOUR OWN .ENV FILE -------------------------------------------------------------------------------- /pkg/internal/user.go: -------------------------------------------------------------------------------- 1 | // Package internal contains bussines rules 2 | package internal 3 | 4 | // User struct is the model of the actor called user 5 | type User struct { 6 | ID string 7 | Email string 8 | Phone string 9 | Password string 10 | } 11 | 12 | // Users is an array type of User 13 | type Users []*User 14 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main runs main thread of the app 2 | package main 3 | 4 | import ( 5 | "dall06/go-cleanapi/cmd" 6 | ) 7 | 8 | // @title go-cleanapi 9 | // @description Golang REST Api based on Uncle's Bob Clean Arch 10 | // @version 1.0.0 11 | // @host localhost:8080 12 | // @BasePath /go-cleanapi/api/v1 13 | func main() { 14 | app := cmd.NewApp() 15 | 16 | err := app.Main() 17 | if err != nil { 18 | panic(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docker/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/bin 15 | **/charts 16 | **/docker-compose* 17 | **/compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | README.md 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /utils/uuid_mock.go: -------------------------------------------------------------------------------- 1 | //go:build !coverage 2 | // +build !coverage 3 | 4 | // Package utils is a package that provides general method for the api usage 5 | package utils 6 | 7 | import "github.com/google/uuid" 8 | 9 | type uuidMock struct{} 10 | 11 | // NewUUIDMock is a contructor for a mock UUIDRepository 12 | func NewUUIDMock() UUID { 13 | return &uuidMock{} 14 | } 15 | 16 | func (r *uuidMock) NewUUID() uuid.UUID { 17 | return uuid.New() 18 | } 19 | 20 | func (r *uuidMock) NewString() string { 21 | return uuid.NewString() 22 | } 23 | -------------------------------------------------------------------------------- /utils/uuid.go: -------------------------------------------------------------------------------- 1 | // Package utils is a package that provides general method for the api usage 2 | package utils 3 | 4 | import "github.com/google/uuid" 5 | 6 | // UUID is an interface for uuidGen 7 | type UUID interface { 8 | NewString() string 9 | NewUUID() uuid.UUID 10 | } 11 | 12 | var _ UUID = (*uuidGen)(nil) 13 | 14 | type uuidGen struct{} 15 | 16 | // NewUUIDGenerator is a constructir for uidGen 17 | func NewUUIDGenerator() UUID { 18 | return &uuidGen{} 19 | } 20 | 21 | func (g *uuidGen) NewString() string { 22 | return uuid.NewString() 23 | } 24 | 25 | func (g *uuidGen) NewUUID() uuid.UUID { 26 | return uuid.New() 27 | } 28 | -------------------------------------------------------------------------------- /utils/jwt_mock.go: -------------------------------------------------------------------------------- 1 | //go:build !coverage 2 | // +build !coverage 3 | 4 | // Package utils is a package that provides general method for the api usage 5 | package utils 6 | 7 | type jwtMock struct{} 8 | 9 | // NewJWTMock is a mock for jwt 10 | func NewJWTMock() JWT { 11 | return &jwtMock{} 12 | } 13 | 14 | func (j *jwtMock) CreateUserJWT(uid string) (string, error) { 15 | return uid, nil 16 | } 17 | 18 | func (j *jwtMock) CheckUserJwt(_ string) (bool, error) { 19 | return true, nil 20 | } 21 | 22 | func (j *jwtMock) CreateAPIJWT() (string, error) { 23 | return "", nil 24 | } 25 | 26 | func (j *jwtMock) CheckAPIJWT(_ string) (bool, error) { 27 | return true, nil 28 | } 29 | -------------------------------------------------------------------------------- /utils/validations.go: -------------------------------------------------------------------------------- 1 | // Package utils is a package that provides general method for the api usage 2 | package utils 3 | 4 | import "regexp" 5 | 6 | const ( 7 | phoneRegexStr = `^\+[0-9]{10,}$` 8 | emailRegexStr = `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` 9 | ) 10 | 11 | // Validations is an interface that extends validations 12 | type Validations interface { 13 | IsPhone(string) bool 14 | IsEmail(string) bool 15 | } 16 | 17 | type validations struct{} 18 | 19 | var _ Validations = (*validations)(nil) 20 | 21 | // NewValidations is a constructor for validations 22 | func NewValidations() Validations { 23 | return &validations{} 24 | } 25 | 26 | func (*validations) IsPhone(value string) bool { 27 | // use regex to check if it's a valid phone number 28 | phoneRegex := regexp.MustCompile(phoneRegexStr) 29 | r := phoneRegex.MatchString(value) 30 | return r 31 | } 32 | 33 | func (*validations) IsEmail(value string) bool { 34 | // use regex to check if it's a valid email address 35 | emailRegex := regexp.MustCompile(emailRegexStr) 36 | r := emailRegex.MatchString(value) 37 | return r 38 | } 39 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Diego Alberto León López 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /utils/uuid_test.go: -------------------------------------------------------------------------------- 1 | // Package utils_test is a test package for utils 2 | package utils_test 3 | 4 | import ( 5 | "dall06/go-cleanapi/utils" 6 | "testing" 7 | 8 | "github.com/google/uuid" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewString(test *testing.T) { 13 | successfulCases := []struct { 14 | name string 15 | }{ 16 | { 17 | name: "it should generate an string", 18 | }, 19 | } 20 | 21 | for _, tc := range successfulCases { 22 | tc := tc 23 | 24 | test.Run(tc.name, func(t *testing.T) { 25 | t.Parallel() 26 | 27 | u := utils.NewUUIDGenerator() 28 | s := u.NewString() 29 | 30 | assert.NotEmpty(t, s, "expected string not to be empty") 31 | }) 32 | } 33 | } 34 | 35 | func TestNewUUID(test *testing.T) { 36 | successfulCases := []struct { 37 | name string 38 | }{ 39 | { 40 | name: "it should generate an uuid", 41 | }, 42 | } 43 | 44 | for _, tc := range successfulCases { 45 | tc := tc 46 | 47 | test.Run(tc.name, func(t *testing.T) { 48 | t.Parallel() 49 | 50 | u := utils.NewUUIDGenerator() 51 | s := u.NewUUID() 52 | 53 | assert.IsType(t, uuid.UUID{}, s, "expecting an uuid but got other") 54 | assert.NotEmpty(t, s, "expected an uuid not to be empty") 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cmd/tools/flags.go: -------------------------------------------------------------------------------- 1 | // Package tools define the main entry point of an application, as well as any command-line utilities or tools that are part of the application. 2 | package tools 3 | 4 | import ( 5 | "flag" 6 | "os" 7 | ) 8 | 9 | // FlagValues means the values obtained as flag parameters of cli 10 | type FlagValues struct { 11 | Port string 12 | Version string 13 | } 14 | 15 | // Flags is an interface that extend tools 16 | type Flags interface { 17 | GetFlags() (*FlagValues, error) 18 | } 19 | 20 | type flags struct { 21 | flagSet *flag.FlagSet 22 | } 23 | 24 | var _ Flags = (*flags)(nil) 25 | 26 | // NewFlags is a constructor for tools 27 | func NewFlags() Flags { 28 | return &flags{ 29 | flagSet: flag.NewFlagSet(os.Args[0], flag.ExitOnError), 30 | } 31 | } 32 | 33 | func (f *flags) GetFlags() (*FlagValues, error) { 34 | // run app 35 | var ( 36 | port string 37 | version string 38 | ) 39 | f.flagSet.StringVar(&port, "p", "8080", "port for http server") 40 | f.flagSet.StringVar(&version, "v", "0.0.0", "version for http server") 41 | 42 | if err := f.flagSet.Parse(os.Args[1:]); err != nil { 43 | return nil, err 44 | } 45 | 46 | fv := &FlagValues{ 47 | Port: port, 48 | Version: version, 49 | } 50 | 51 | return fv, nil 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-cleanapi 2 | 3 | This is a REST API template project, based on Uncle's Bob Clean Architecture principles, using go fiber as rest server package alongside another complementary packages for middleware and utilities. 4 | 5 | The project contain different packages based on the layers of the consulted literature. 6 | 7 | Principal unit tests can be found, as well as CI implementation with docker and GitHub actions. 8 | 9 | ## Run 10 | 11 | Use the go command [go](https://go.dev/) to run the project locally. 12 | 13 | ```bash 14 | go run main.go -p -v -p 15 | ``` 16 | 17 | ## Principal commands 18 | 19 | ```bash 20 | # test with coverage and save in file 21 | go test ./... -coverprofile=coverage.out -coverpkg=./... && go tool cover -func=coverage.out 22 | 23 | # export go path env 24 | export PATH=$PATH:$(go env GOPATH)/bin 25 | 26 | # runs golangci linter 27 | golangci-lint run 28 | 29 | # runs swagger for the endpoints 30 | swag init 31 | ``` 32 | 33 | ## Contributing 34 | 35 | Pull requests are welcome. For major changes, please open an issue first 36 | to discuss what you would like to change. 37 | 38 | Please make sure to update tests as appropriate. 39 | 40 | ## License 41 | 42 | [MIT](https://choosealicense.com/licenses/mit/) 43 | -------------------------------------------------------------------------------- /utils/logger_mock.go: -------------------------------------------------------------------------------- 1 | //go:build !coverage 2 | // +build !coverage 3 | 4 | // Package utils is a package that provides general method for the api usage 5 | package utils 6 | 7 | import "fmt" 8 | 9 | type loggerMock struct { 10 | WarnCalled bool 11 | WarnMsg string 12 | WarnArgs []interface{} 13 | 14 | InfoCalled bool 15 | InfoMsg string 16 | InfoArgs []interface{} 17 | 18 | ErrorCalled bool 19 | ErrorMsg string 20 | ErrorArgs []interface{} 21 | } 22 | 23 | // NewLoggerMock is a consturcotr to genrate a logger mock 24 | func NewLoggerMock() Logger { 25 | return &loggerMock{} 26 | } 27 | 28 | func (l loggerMock) Initialize() error { 29 | return nil 30 | } 31 | 32 | func (l loggerMock) Warn(message string, args ...interface{}) { 33 | l.WarnCalled = true 34 | l.WarnMsg = message 35 | l.WarnArgs = args 36 | fmt.Println(l.WarnCalled, l.WarnMsg, l.WarnArgs) 37 | } 38 | 39 | func (l loggerMock) Info(message string, args ...interface{}) { 40 | l.InfoCalled = true 41 | l.InfoMsg = message 42 | l.InfoArgs = args 43 | fmt.Println(l.InfoCalled, l.InfoMsg, l.InfoArgs) 44 | } 45 | 46 | func (l loggerMock) Error(message string, args ...interface{}) { 47 | l.ErrorCalled = true 48 | l.ErrorMsg = message 49 | l.ErrorArgs = args 50 | fmt.Println(l.ErrorCalled, l.ErrorMsg, l.ErrorArgs) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/adapter/controller/models.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | // User is a struct model for users interaction in controller layer 4 | type User struct { 5 | ID string `json:"uid"` 6 | Email string `json:"email"` 7 | Phone string `json:"phone"` 8 | Password string `json:"password"` 9 | } 10 | 11 | // Users is a struct model for a slice of users interaction in controller layer 12 | type Users []User 13 | 14 | // AuthRequest is a struct model for auth post requests in controller layer 15 | type AuthRequest struct { 16 | UserName string `json:"user" validate:"required"` 17 | Password string `json:"password" validate:"required"` 18 | } 19 | 20 | // PostRequest is a struct model for post requests in controller layer 21 | type PostRequest struct { 22 | Email string `json:"email" validate:"required,email"` 23 | Phone string `json:"phone" validate:"omitempty"` 24 | Password string `json:"password" validate:"required"` 25 | } 26 | 27 | // PutRequest is a struct model for put requests in controller layer 28 | type PutRequest struct { 29 | Email string `json:"email" validate:"omitempty"` 30 | Phone string `json:"phone" validate:"omitempty"` 31 | Password string `json:"password" validate:"required"` 32 | } 33 | 34 | // DeleteRequest is a struct model for delete requests in controller layer 35 | type DeleteRequest struct { 36 | Password string `json:"password" validate:"required"` 37 | } 38 | -------------------------------------------------------------------------------- /cmd/tools/flags_test.go: -------------------------------------------------------------------------------- 1 | package tools_test 2 | 3 | import ( 4 | "dall06/go-cleanapi/cmd/tools" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestFlags(t *testing.T) { 13 | // define test cases 14 | testCases := []struct { 15 | name string 16 | inputArgs []string 17 | expected tools.FlagValues 18 | defaulValues bool 19 | }{ 20 | { 21 | name: "it should get default flags", 22 | inputArgs: []string{}, 23 | expected: tools.FlagValues{ 24 | Port: "8080", 25 | Version: "0.0.0", 26 | }, 27 | defaulValues: true, 28 | }, 29 | { 30 | name: "it should get custom flags", 31 | inputArgs: []string{"-p", "9000", "-v", "0.0.1"}, 32 | expected: tools.FlagValues{ 33 | Port: "9000", 34 | Version: "0.0.1", 35 | }, 36 | defaulValues: false, 37 | }, 38 | } 39 | i := 0 40 | // run tests 41 | for _, tc := range testCases { 42 | tc := tc 43 | 44 | t.Run(tc.name, func(t *testing.T) { 45 | t.Parallel() 46 | // save original flag values 47 | originalArgs := os.Args 48 | args := tc.inputArgs 49 | 50 | os.Args = append([]string{originalArgs[0]}, args...) 51 | 52 | flags := tools.NewFlags() 53 | f, err := flags.GetFlags() 54 | assert.NoError(t, err) 55 | 56 | // Check the results 57 | assert.Equal(t, tc.expected.Port, f.Port) 58 | assert.Equal(t, tc.expected.Version, f.Version) 59 | 60 | // Clean up 61 | os.Args = originalArgs 62 | }) 63 | 64 | i++ 65 | fmt.Println(i) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/adapter/routes/routes.go: -------------------------------------------------------------------------------- 1 | //go:build !coverage 2 | // +build !coverage 3 | 4 | // Package routes contains the routes per version 5 | // this package should not be tested 6 | // it is just an implementation and integration fo methos that are goning to be tested on integration 7 | package routes 8 | 9 | import ( 10 | "dall06/go-cleanapi/config" 11 | "dall06/go-cleanapi/pkg/adapter/controller" 12 | "fmt" 13 | 14 | "github.com/gofiber/fiber/v2" 15 | "github.com/gofiber/swagger" // swagger handler 16 | 17 | // docs are generated by Swag CLI, you have to import them. 18 | _ "dall06/go-cleanapi/docs" 19 | ) 20 | 21 | // Routes is an interface that extends routes 22 | type Routes interface { 23 | Set() 24 | } 25 | 26 | var _ Routes = (*routes)(nil) 27 | 28 | type routes struct { 29 | app *fiber.App 30 | config config.Vars 31 | controller controller.Controller 32 | } 33 | 34 | // NewRoutes is a constructor for routes generator 35 | func NewRoutes(app *fiber.App, vars config.Vars, ctrl controller.Controller) Routes { 36 | return &routes{ 37 | app: app, 38 | config: vars, 39 | controller: ctrl, 40 | } 41 | } 42 | 43 | func (routes *routes) Set() { 44 | basePath := routes.config.APIBasePath 45 | 46 | swaggerPath := fmt.Sprintf("%s/swagger/*", basePath) 47 | routes.app.Get(swaggerPath, swagger.HandlerDefault) 48 | 49 | usersPath := fmt.Sprintf("%s/users", basePath) 50 | usersGroup := routes.app.Group(usersPath) 51 | usersGroup.Get("/hello", func(c *fiber.Ctx) error { 52 | return c.SendString("welcome to go-cleanapi user path ...") 53 | }) 54 | usersGroup.Post("/auth", routes.controller.Auth) 55 | usersGroup.Post("/signup", routes.controller.Post) 56 | usersGroup.Get("/:id", routes.controller.Get) 57 | usersGroup.Get("/all", routes.controller.GetAll) 58 | usersGroup.Put("/modify/:id", routes.controller.Put) 59 | usersGroup.Delete("/delete/:id", routes.controller.Delete) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/infrastructure/database/db.go: -------------------------------------------------------------------------------- 1 | // Package database is the package that contains the sql driver 2 | package database 3 | 4 | import ( 5 | "dall06/go-cleanapi/config" 6 | "dall06/go-cleanapi/utils" 7 | "database/sql" 8 | "errors" 9 | "time" 10 | 11 | _ "github.com/go-sql-driver/mysql" // this package registers the mysql driver with sql 12 | ) 13 | 14 | // DB is an interface that extend dbConn 15 | type DB interface { 16 | Open() (*sql.DB, error) 17 | Close(db *sql.DB) error 18 | } 19 | 20 | const ( 21 | emptyConnectionString = "empty connection string" 22 | emptydbConn = "empty connection" 23 | dbEngine = "mysql" 24 | maxLifeTime = time.Minute * 3 25 | maxOpenConns = 10 26 | idleConns = 10 27 | ) 28 | 29 | var _ DB = (*dbConn)(nil) 30 | 31 | type dbConn struct { 32 | logger utils.Logger 33 | config config.Vars 34 | } 35 | 36 | // NewDBConn is a constructor for dbConn 37 | func NewDBConn(l utils.Logger, v config.Vars) DB { 38 | return &dbConn{ 39 | logger: l, 40 | config: v, 41 | } 42 | } 43 | 44 | func (c *dbConn) Open() (*sql.DB, error) { 45 | if c.config.DBConnString == "" { 46 | c.logger.Warn("db connection failed: %v", emptyConnectionString) 47 | return nil, errors.New(emptyConnectionString) 48 | } 49 | 50 | db, err := sql.Open(dbEngine, c.config.DBConnString) 51 | if err != nil { 52 | c.logger.Error("db connection failed: %v", err) 53 | return nil, err 54 | } 55 | 56 | db.SetConnMaxLifetime(maxLifeTime) 57 | db.SetMaxOpenConns(maxOpenConns) 58 | db.SetMaxIdleConns(idleConns) 59 | 60 | c.logger.Info("db connection opened") 61 | return db, nil 62 | } 63 | 64 | func (c *dbConn) Close(db *sql.DB) error { 65 | if db == nil { 66 | c.logger.Warn("db connection close failed: %s", emptyConnectionString) 67 | return errors.New(emptydbConn) 68 | } 69 | 70 | err := db.Close() 71 | if err != nil { 72 | c.logger.Error("db connection close failed: %v", err) 73 | return err 74 | } 75 | 76 | //c.logger.Info("db connection closed", nil) 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /cmd/app.go: -------------------------------------------------------------------------------- 1 | //go:build !coverage 2 | // +build !coverage 3 | 4 | // Package cmd define the main entry point of an application, as well as any command-line utilities or tools that are part of the application. 5 | // it is just an implementation, the test of the app ocurrs on integration testing in server layer 6 | package cmd 7 | 8 | import ( 9 | "dall06/go-cleanapi/cmd/tools" 10 | "dall06/go-cleanapi/config" 11 | "dall06/go-cleanapi/pkg/server" 12 | "dall06/go-cleanapi/utils" 13 | "errors" 14 | "fmt" 15 | 16 | "github.com/go-playground/validator/v10" 17 | ) 18 | 19 | // App is an interface that extends app 20 | type App interface { 21 | Main() error 22 | } 23 | 24 | var _ App = (*app)(nil) 25 | 26 | type app struct { 27 | } 28 | 29 | // NewApp is a constructor for app 30 | func NewApp() App { 31 | return &app{} 32 | } 33 | 34 | // Main app configuration such as servers, cache and utils 35 | func (a *app) Main() error { 36 | flags := tools.NewFlags() 37 | flagValues, err := flags.GetFlags() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | prt := flagValues.Port 43 | ver := flagValues.Version 44 | 45 | conf := config.NewConfig(prt, ver) 46 | v, err := conf.SetConfig() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | jwt := utils.NewJWT(*v) 52 | if jwt == nil { 53 | return errors.New("empty jwt repo") 54 | } 55 | 56 | l := utils.NewLogger(*v) 57 | if l == nil { 58 | return errors.New("empty logger repo") 59 | } 60 | err = l.Initialize() 61 | if err != nil { 62 | return fmt.Errorf("error when init logger %v: ", err) 63 | } 64 | 65 | u := utils.NewUUIDGenerator() 66 | if u == nil { 67 | return errors.New("empty uid generator repo") 68 | } 69 | 70 | vals := utils.NewValidations() 71 | if u == nil { 72 | return errors.New("empty uid generator repo") 73 | } 74 | 75 | val := validator.New() 76 | if val == nil { 77 | return errors.New("empty validator repo") 78 | } 79 | 80 | s := server.NewServer(*v, l, jwt, u, vals, *val) 81 | if err := s.Start(); err != nil { 82 | return fmt.Errorf("error when starting the server %v: ", err) 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // Package config_test is a package to test the config variables 2 | package config_test 3 | 4 | import ( 5 | "dall06/go-cleanapi/config" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestConfigVars(test *testing.T) { 12 | successfulCases := []struct { 13 | name string 14 | port string 15 | version string 16 | }{ 17 | { 18 | name: "it should load all variables", 19 | port: "8080", 20 | version: "0", 21 | }, 22 | } 23 | 24 | failedCases := []struct { 25 | name string 26 | port string 27 | version string 28 | }{ 29 | { 30 | name: "it should not load all variables, empty port", 31 | port: "", 32 | version: "0", 33 | }, 34 | { 35 | name: "it should not load all variables, empty version", 36 | port: "8080", 37 | version: "", 38 | }, 39 | } 40 | 41 | for _, tc := range successfulCases { 42 | tc := tc 43 | test.Run(tc.name, func(t *testing.T) { 44 | t.Parallel() 45 | 46 | cfg := config.NewConfig(tc.port, tc.version) 47 | vars, err := cfg.SetConfig() 48 | assert.NoError(t, err) 49 | 50 | assert.NotEmpty(t, vars, "expected vars, but got nil") 51 | 52 | assert.NotEmpty(t, vars.APIBasePath, "expected api base path, but got empty") 53 | assert.NotEmpty(t, vars.APIPort, "expected api port, but got empty") 54 | assert.NotEmpty(t, vars.APIKey, "expected api key, but got empty") 55 | assert.NotEmpty(t, vars.APIKeyHash, "expected api hash key, but got empty") 56 | assert.NotEmpty(t, vars.DBConnString, "expected db conn str, but got empty") 57 | assert.NotEmpty(t, vars.JWTSecret, "expected jwt secret, but got empty") 58 | assert.NotEmpty(t, vars.ProyectName, "expected proyect name, but got empty") 59 | assert.NotEmpty(t, vars.Stage, "expected stage, but got empty") 60 | assert.NotEmpty(t, vars.ProyectPath, "expected proyect path, but got empty") 61 | assert.NotEmpty(t, vars.CookieSecret, "expected cookie secret, but got empty") 62 | assert.NotEmpty(t, vars.APIVersion, "expected api version, but got empty") 63 | assert.NotEmpty(t, vars.AppName, "expected app name, but got empty") 64 | }) 65 | } 66 | 67 | for _, tc := range failedCases { 68 | tc := tc 69 | test.Run(tc.name, func(t *testing.T) { 70 | t.Parallel() 71 | 72 | cfg := config.NewConfig(tc.port, tc.version) 73 | vars, err := cfg.SetConfig() 74 | assert.Error(t, err) 75 | assert.Empty(t, vars, "expected nil, but got vars") 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pkg/infrastructure/database/db_test.go: -------------------------------------------------------------------------------- 1 | // Package database_test is a test for db driver 2 | package database_test 3 | 4 | import ( 5 | "dall06/go-cleanapi/config" 6 | "dall06/go-cleanapi/pkg/infrastructure/database" 7 | "dall06/go-cleanapi/utils" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestDBConn(test *testing.T) { 14 | cfg := config.NewConfig("8080", "0.0.0") 15 | vars, err := cfg.SetConfig() 16 | if err != nil { 17 | test.Fatal("expected no error, but got:", err) 18 | } 19 | 20 | varsEmptyDBConn := *vars 21 | varsEmptyDBConn.DBConnString = "" 22 | 23 | logger := utils.NewLoggerMock() 24 | err = logger.Initialize() 25 | if err != nil { 26 | test.Fatal("expected no error, but got:", err) 27 | } 28 | 29 | successfulCases := []struct { 30 | name string 31 | vars *config.Vars 32 | logger utils.Logger 33 | }{ 34 | { 35 | name: "it should run and close a db conn", 36 | vars: vars, 37 | logger: logger, 38 | }, 39 | } 40 | 41 | failedCases := []struct { 42 | name string 43 | vars *config.Vars 44 | logger utils.Logger 45 | }{ 46 | { 47 | name: "it should not run a db conn, empty db conn string", 48 | vars: &varsEmptyDBConn, 49 | logger: logger, 50 | }, 51 | } 52 | 53 | failedCasesClose := []struct { 54 | name string 55 | vars config.Vars 56 | logger utils.Logger 57 | }{ 58 | { 59 | name: "it should not run a db conn, empty db conn string", 60 | vars: *vars, 61 | logger: logger, 62 | }, 63 | } 64 | 65 | for _, tc := range successfulCases { 66 | tc := tc 67 | 68 | test.Run(tc.name, func(t *testing.T) { 69 | t.Parallel() 70 | 71 | db := database.NewDBConn(tc.logger, *tc.vars) 72 | conn, err := db.Open() 73 | assert.NoError(t, err) 74 | assert.NotEmpty(t, conn, "connection should not be empty") 75 | 76 | err = db.Close(conn) 77 | assert.NoError(t, err) 78 | }) 79 | } 80 | 81 | for _, tc := range failedCases { 82 | tc := tc 83 | 84 | test.Run(tc.name, func(t *testing.T) { 85 | t.Parallel() 86 | 87 | db := database.NewDBConn(tc.logger, *tc.vars) 88 | conn, err := db.Open() 89 | assert.NotEmpty(t, err, "expected error, but got nil") 90 | assert.Empty(t, conn, "connection should be empty") 91 | 92 | err = db.Close(conn) 93 | assert.Error(t, err) 94 | }) 95 | } 96 | 97 | for _, tc := range failedCasesClose { 98 | tc := tc 99 | 100 | test.Run(tc.name, func(t *testing.T) { 101 | t.Parallel() 102 | 103 | db := database.NewDBConn(tc.logger, tc.vars) 104 | 105 | err = db.Close(nil) 106 | assert.Error(t, err) 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /utils/validations_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "dall06/go-cleanapi/utils" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIsEmail(test *testing.T) { 11 | successfulCases := []struct { 12 | name string 13 | input string 14 | expected bool 15 | }{ 16 | { 17 | name: "it should check ok, is an email", 18 | input: "test@test.com", 19 | expected: true, 20 | }, 21 | } 22 | 23 | failedCases := []struct { 24 | name string 25 | input string 26 | expected bool 27 | }{ 28 | { 29 | name: "it should not check ok, is not an email", 30 | input: "testtest.com", 31 | expected: false, 32 | }, 33 | { 34 | name: "it should not check ok, is empty", 35 | input: "", 36 | expected: false, 37 | }, 38 | { 39 | name: "it should not check ok, is an phone", 40 | input: "+991234567890", 41 | expected: false, 42 | }, 43 | } 44 | 45 | for _, tc := range successfulCases { 46 | tc := tc 47 | 48 | test.Run(tc.name, func(t *testing.T) { 49 | v := utils.NewValidations() 50 | res := v.IsEmail(tc.input) 51 | assert.Equal(t, tc.expected, res) 52 | }) 53 | } 54 | 55 | for _, tc := range failedCases { 56 | tc := tc 57 | 58 | test.Run(tc.name, func(t *testing.T) { 59 | v := utils.NewValidations() 60 | res := v.IsEmail(tc.input) 61 | assert.Equal(t, tc.expected, res) 62 | }) 63 | } 64 | } 65 | 66 | func TestIsPhone(test *testing.T) { 67 | successfulCases := []struct { 68 | name string 69 | input string 70 | expected bool 71 | }{ 72 | { 73 | name: "it should check ok, is a phone", 74 | input: "+991234567890", 75 | expected: true, 76 | }, 77 | } 78 | 79 | failedCases := []struct { 80 | name string 81 | input string 82 | expected bool 83 | }{ 84 | { 85 | name: "it should not check ok, is not a random string", 86 | input: "dawedawedawe", 87 | expected: false, 88 | }, 89 | { 90 | name: "it should not check ok, is empty", 91 | input: "", 92 | expected: false, 93 | }, 94 | 95 | { 96 | name: "it should not check ok, is an email", 97 | input: "test@test.com", 98 | expected: false, 99 | }, 100 | } 101 | 102 | for _, tc := range successfulCases { 103 | tc := tc 104 | 105 | test.Run(tc.name, func(t *testing.T) { 106 | v := utils.NewValidations() 107 | res := v.IsPhone(tc.input) 108 | assert.Equal(t, tc.expected, res) 109 | }) 110 | } 111 | 112 | for _, tc := range failedCases { 113 | tc := tc 114 | 115 | test.Run(tc.name, func(t *testing.T) { 116 | v := utils.NewValidations() 117 | res := v.IsPhone(tc.input) 118 | assert.Equal(t, tc.expected, res) 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.github/workflows/docker_build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - 'main' 10 | 11 | jobs: 12 | dotenv: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v3 18 | - 19 | name: Generate dotenv for go 20 | shell: bash 21 | run: | 22 | { 23 | echo USER_DB="${{ secrets.USER_DB }}" 24 | echo PASSWORD_DB="${{ secrets.PASSWORD_DB }}" 25 | echo HOST_DB="${{ secrets.HOST_DB }}" 26 | echo PORT_DB="${{ secrets.PORT_DB }}" 27 | echo NAME_DB="${{ secrets.NAME_DB }} " 28 | echo SECRET_JWT="${{ secrets.SECRET_JWT }}" 29 | echo STAGE="${{ secrets.STAGE }}" 30 | echo COOKIE_ENCRYPTION="${{ secrets.COOKIE_ENCRYPTION }}" 31 | echo API_KEY="${{ secrets.API_KEY }}" 32 | } > .env 33 | - 34 | name: Check the content 35 | run: | 36 | cat .env 37 | - 38 | name: Upload dotenv result 39 | uses: actions/upload-artifact@v3 40 | with: 41 | name: dotenv 42 | path: .env 43 | 44 | test: 45 | needs: [dotenv] 46 | runs-on: ubuntu-latest 47 | steps: 48 | - 49 | uses: actions/checkout@v3 50 | - 51 | name: Download dotenv result 52 | uses: actions/download-artifact@v3 53 | with: 54 | name: dotenv 55 | - 56 | uses: actions/setup-go@v4 57 | with: 58 | go-version: '1.20' 59 | check-latest: true 60 | - 61 | run: go test ./... -coverprofile=coverage.out -coverpkg=./... && go tool cover -func=coverage.out 62 | 63 | build: 64 | needs: [test] 65 | runs-on: ubuntu-latest 66 | steps: 67 | - 68 | name: Checkout 69 | uses: actions/checkout@v3 70 | - 71 | name: Login to Docker Hub 72 | uses: docker/login-action@v2 73 | with: 74 | username: ${{ secrets.DOCKERHUB_USERNAME }} 75 | password: ${{ secrets.DOCKERHUB_TOKEN }} 76 | # Add support for more platforms with QEMU (optional) 77 | # https://github.com/docker/setup-qemu-action 78 | - 79 | name: Set up QEMU 80 | uses: docker/setup-qemu-action@v2 81 | - 82 | name: Set up Docker Buildx 83 | uses: docker/setup-buildx-action@v2 84 | - 85 | name: Build and push 86 | uses: docker/build-push-action@v4 87 | with: 88 | context: . 89 | file: docker/Dockerfile 90 | push: true 91 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/go_cleanapi_img:latest -------------------------------------------------------------------------------- /utils/logger.go: -------------------------------------------------------------------------------- 1 | // Package utils is a package that provides general method for the api usage 2 | package utils 3 | 4 | import ( 5 | "dall06/go-cleanapi/config" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "go.uber.org/zap" 12 | "go.uber.org/zap/zapcore" 13 | ) 14 | 15 | // Logger refers to the repository as interface of the logger 16 | type Logger interface { 17 | Initialize() error 18 | Warn(message string, args ...interface{}) 19 | Info(message string, args ...interface{}) 20 | Error(message string, args ...interface{}) 21 | } 22 | 23 | var _ Logger = (*logger)(nil) 24 | 25 | type logger struct { 26 | loggers map[zapcore.Level]*zap.SugaredLogger 27 | config config.Vars 28 | } 29 | 30 | // NewLogger is a function constructor for Logger 31 | func NewLogger(v config.Vars) Logger { 32 | return logger{ 33 | loggers: make(map[zapcore.Level]*zap.SugaredLogger), 34 | config: v, 35 | } 36 | } 37 | 38 | func (l logger) Initialize() error { 39 | encoderConfig := zap.NewProductionEncoderConfig() 40 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 41 | encoderConfig.MessageKey = "message" 42 | encoderConfig.LevelKey = "severity" 43 | 44 | for _, level := range []zapcore.Level{zap.WarnLevel, zap.InfoLevel, zap.ErrorLevel} { 45 | logFilePath, err := l.getLogFilePath(l.config.Stage, level.String()) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | fmt.Printf("logFilePath: %s, level: %s\n", logFilePath, level.String()) 51 | 52 | cfg := zap.Config{ 53 | Level: zap.NewAtomicLevelAt(level), 54 | Development: false, 55 | DisableStacktrace: true, 56 | Encoding: "json", 57 | EncoderConfig: encoderConfig, 58 | OutputPaths: []string{"stdout", logFilePath}, 59 | ErrorOutputPaths: []string{"stderr"}, 60 | } 61 | 62 | zl, err := cfg.Build() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | logger := zl.Sugar() 68 | 69 | fmt.Printf("logger: %+v\n", logger) 70 | 71 | l.loggers[level] = logger 72 | } 73 | 74 | if len(l.loggers) < 3 { 75 | return errors.New("no loggers configured") 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func (l logger) Warn(message string, args ...interface{}) { 82 | l.loggers[zapcore.WarnLevel].Warnf(message, args...) 83 | } 84 | 85 | func (l logger) Info(message string, args ...interface{}) { 86 | l.loggers[zapcore.InfoLevel].Infof(message, args...) 87 | } 88 | 89 | func (l logger) Error(message string, args ...interface{}) { 90 | l.loggers[zapcore.ErrorLevel].Errorf(message, args...) 91 | } 92 | 93 | func (l logger) getLogFilePath(stage string, level string) (string, error) { 94 | dirName := "logs" 95 | dir := filepath.Join(l.config.ProyectPath, dirName) 96 | if err := os.MkdirAll(dir, os.ModePerm); err != nil { 97 | return "", err 98 | } 99 | return dir + "/" + stage + "_" + level + ".log", nil 100 | } 101 | -------------------------------------------------------------------------------- /utils/logger_test.go: -------------------------------------------------------------------------------- 1 | // Package utils_test is a test package for utils 2 | package utils_test 3 | 4 | import ( 5 | "dall06/go-cleanapi/config" 6 | "dall06/go-cleanapi/utils" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestInitializeLogger(test *testing.T) { 13 | cfg := config.NewConfig("8080", "0.0.0") 14 | vars, err := cfg.SetConfig() 15 | if err != nil { 16 | test.Fatal("expected no error, but got:", err) 17 | } 18 | 19 | varsEmptyStage := *vars 20 | varsEmptyStage.Stage = "" 21 | 22 | successfulCases := []struct { 23 | name string 24 | config *config.Vars 25 | }{ 26 | { 27 | config: vars, 28 | name: "it should initialize logger", 29 | }, 30 | } 31 | 32 | for _, tc := range successfulCases { 33 | tc := tc 34 | 35 | test.Run(tc.name, func(t *testing.T) { 36 | t.Parallel() 37 | 38 | logger := utils.NewLogger(*tc.config) 39 | err := logger.Initialize() 40 | assert.NoError(t, err) 41 | }) 42 | } 43 | } 44 | 45 | func TestLoggerWarn(test *testing.T) { 46 | cfg := config.NewConfig("8080", "0.0.0") 47 | vars, err := cfg.SetConfig() 48 | if err != nil { 49 | test.Fatal("expected no error, but got:", err) 50 | } 51 | 52 | successfulCases := []struct { 53 | name string 54 | config *config.Vars 55 | }{ 56 | { 57 | config: vars, 58 | name: "it should log a warn", 59 | }, 60 | } 61 | 62 | for _, tc := range successfulCases { 63 | tc := tc 64 | 65 | test.Run(tc.name, func(t *testing.T) { 66 | t.Parallel() 67 | 68 | logger := utils.NewLogger(*tc.config) 69 | err := logger.Initialize() 70 | assert.NoError(t, err) 71 | 72 | logger.Warn("im a format %s", "im a string") 73 | }) 74 | } 75 | } 76 | 77 | func TestLoggerInfo(test *testing.T) { 78 | cfg := config.NewConfig("8080", "0.0.0") 79 | vars, err := cfg.SetConfig() 80 | if err != nil { 81 | test.Fatal("expected no error, but got:", err) 82 | } 83 | 84 | successfulCases := []struct { 85 | name string 86 | config *config.Vars 87 | }{ 88 | { 89 | config: vars, 90 | name: "it should log info", 91 | }, 92 | } 93 | 94 | for _, tc := range successfulCases { 95 | tc := tc 96 | 97 | test.Run(tc.name, func(t *testing.T) { 98 | t.Parallel() 99 | 100 | logger := utils.NewLogger(*tc.config) 101 | err := logger.Initialize() 102 | assert.NoError(t, err) 103 | 104 | logger.Info("im a format %s", "im a string") 105 | }) 106 | } 107 | } 108 | 109 | func TestLoggerError(test *testing.T) { 110 | cfg := config.NewConfig("8080", "0.0.0") 111 | vars, err := cfg.SetConfig() 112 | if err != nil { 113 | test.Fatal("expected no error, but got:", err) 114 | } 115 | 116 | successfulCases := []struct { 117 | name string 118 | config *config.Vars 119 | }{ 120 | { 121 | config: vars, 122 | name: "it should log a error", 123 | }, 124 | } 125 | 126 | for _, tc := range successfulCases { 127 | tc := tc 128 | 129 | test.Run(tc.name, func(t *testing.T) { 130 | t.Parallel() 131 | 132 | logger := utils.NewLogger(*tc.config) 133 | err := logger.Initialize() 134 | assert.NoError(t, err) 135 | 136 | logger.Error("im a format %s", "im a string") 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /scripts/db_go-cleanapi.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS db_go_cleanapi; 2 | CREATE DATABASE db_go_cleanapi; 3 | 4 | USE db_go_cleanapi; 5 | 6 | CREATE TABLE users ( 7 | id_user VARCHAR(64) NOT NULL UNIQUE, 8 | user_email VARCHAR(128) NOT NULL, 9 | user_phone VARCHAR(16), 10 | user_password VARCHAR(64) NOT NULL 11 | ); 12 | 13 | DELIMITER $$ 14 | CREATE DEFINER=`root`@`localhost` FUNCTION `fn_validate_user`( 15 | in_u_id VARCHAR(36), 16 | in_u_pass VARCHAR(128) 17 | ) RETURNS BOOL 18 | READS SQL DATA 19 | DETERMINISTIC 20 | BEGIN 21 | RETURN IF( EXISTS( 22 | SELECT * FROM `users` WHERE `user_id` = in_u_id AND `user_pass` = SHA2(in_u_pass, 512)), 1, 0); 23 | END$$ 24 | DELIMITER ; 25 | 26 | DELIMITER $$ 27 | CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_login_user`( 28 | p_user_email VARCHAR(128), 29 | p_user_phone VARCHAR(16), 30 | p_user_password VARCHAR(64) 31 | ) 32 | BEGIN 33 | IF p_user_phone = '' THEN 34 | SELECT * FROM `users` WHERE `p_user_email` = user_email AND `user_pass` = SHA2(in_u_pass, 512); 35 | ELSEIF p_user_email = '' THEN 36 | SELECT * FROM `users` WHERE `p_user_email` = user_email AND `user_pass` = SHA2(in_u_pass, 512); 37 | END IF; 38 | END$$ 39 | DELIMITER ; 40 | 41 | DELIMITER $$ 42 | CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_read_user`( 43 | p_id_user VARCHAR(64) 44 | ) 45 | BEGIN 46 | SELECT (`id_user`, 47 | `user_email`, 48 | `user_phone`) FROM users WHERE id_user = p_id_user; 49 | END$$ 50 | DELIMITER ; 51 | 52 | DELIMITER $$ 53 | CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_read_users`() 54 | BEGIN 55 | SELECT 56 | (`id_user`, 57 | `user_email`, 58 | `user_phone`) 59 | FROM users; 60 | END$$ 61 | DELIMITER ; 62 | 63 | DELIMITER $$ 64 | CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_create_user`( 65 | p_id_user VARCHAR(64), 66 | p_user_email VARCHAR(128), 67 | p_user_phone VARCHAR(16), 68 | p_user_password VARCHAR(64) 69 | ) 70 | BEGIN 71 | INSERT INTO `db_go_cleanapi`.`users` 72 | (`id_user`, 73 | `user_email`, 74 | `user_phone`, 75 | `user_password`) 76 | VALUES 77 | (p_id_user, 78 | p_user_email, 79 | p_user_phone, 80 | SHA2(p_user_password, 512)); 81 | END$$ 82 | DELIMITER ; 83 | 84 | DELIMITER $$ 85 | CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_update_user`( 86 | p_id_user VARCHAR(64), 87 | p_user_email VARCHAR(128), 88 | p_user_phone VARCHAR(16), 89 | p_user_password VARCHAR(64) 90 | ) 91 | BEGIN 92 | UPDATE `db_go_cleanapi`.`users` 93 | SET 94 | `user_email` = p_user_email, 95 | `user_phone` = p_user_phone, 96 | `user_password` = p_user_password 97 | WHERE `id_user` = p_id_user; 98 | END$$ 99 | DELIMITER ; 100 | 101 | DELIMITER $$ 102 | CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_delete_user`( 103 | p_id_user VARCHAR(64), 104 | p_user_password VARCHAR(64) 105 | ) 106 | BEGIN 107 | DECLARE is_auth TINYINT; 108 | 109 | SELECT `db_go_cleanapi`.`fn_validate_user`(p_id_user, p_user_password) INTO is_auth; 110 | IF is_auth = FALSE THEN 111 | SIGNAL SQLSTATE '40400' SET MESSAGE_TEXT = 'not authorized (worng credentials)'; 112 | END IF; 113 | 114 | DELETE FROM `db_go_cleanapi`.users WHERE id_user = p_id_user AND user_password = SHA2(p_user_password, 512); 115 | END$$ 116 | DELIMITER ; -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | //go:build !coverage 2 | // +build !coverage 3 | 4 | // Package server runs the server configuration and initialization 5 | // 6 | //go:generate swag init -g server.go 7 | package server 8 | 9 | import ( 10 | "dall06/go-cleanapi/config" 11 | "dall06/go-cleanapi/pkg/adapter/controller" 12 | "dall06/go-cleanapi/pkg/adapter/routes" 13 | "dall06/go-cleanapi/pkg/infrastructure/database" 14 | "dall06/go-cleanapi/pkg/infrastructure/middleware" 15 | "dall06/go-cleanapi/pkg/internal/repository" 16 | "dall06/go-cleanapi/pkg/internal/usecases" 17 | "dall06/go-cleanapi/utils" 18 | "fmt" 19 | "os" 20 | "os/signal" 21 | "time" 22 | 23 | "github.com/go-playground/validator/v10" 24 | "github.com/gofiber/fiber/v2" 25 | "github.com/patrickmn/go-cache" 26 | ) 27 | 28 | // Server is an interface for server 29 | type Server interface { 30 | Start() error 31 | } 32 | 33 | type server struct { 34 | config config.Vars 35 | logger utils.Logger 36 | jwt utils.JWT 37 | uids utils.UUID 38 | validations utils.Validations 39 | validation validator.Validate 40 | } 41 | 42 | var _ Server = (*server)(nil) 43 | 44 | // NewServer is a constructor for server 45 | func NewServer( 46 | vars config.Vars, 47 | l utils.Logger, 48 | j utils.JWT, 49 | u utils.UUID, 50 | vs utils.Validations, 51 | v validator.Validate) Server { 52 | return server{ 53 | config: vars, 54 | logger: l, 55 | jwt: j, 56 | uids: u, 57 | validations: vs, 58 | validation: v, 59 | } 60 | } 61 | 62 | func (s server) Start() error { 63 | // init database 64 | dbConn := database.NewDBConn(s.logger, s.config) 65 | conn, err := dbConn.Open() 66 | if err != nil { 67 | s.logger.Error("Failed to open database connection", err) 68 | return err 69 | } 70 | 71 | // generate caches, depending on the needs of each dependency 72 | ctrlCache := cache.New(5*time.Minute, 10*time.Minute) 73 | 74 | // generate internal controllers 75 | // user 76 | repo := repository.NewRepository(conn) 77 | usecases := usecases.NewUseCases(repo, s.uids) 78 | ctrl := controller.NewController(usecases, s.validation, s.logger, s.jwt, s.validations, *ctrlCache) 79 | 80 | // init server 81 | cfg := fiber.Config{ 82 | Prefork: false, 83 | CaseSensitive: true, 84 | ServerHeader: "go-cleanapi", 85 | AppName: s.config.AppName, 86 | } 87 | 88 | app := fiber.New(cfg) 89 | // init middleware 90 | mw := middleware.NewMiddleware(s.config, s.jwt) 91 | app.Use(mw.CORS()) 92 | app.Use(mw.Compress()) 93 | app.Use(mw.Helmet()) 94 | app.Use(mw.EncryptCookie()) 95 | app.Use(mw.ETag()) 96 | app.Use(mw.Recover()) 97 | app.Use(mw.JwtWare()) 98 | app.Use(mw.KeyAuth()) 99 | app.Use(mw.CRSF()) 100 | app.Use(mw.Idempotency()) 101 | 102 | // generate routing 103 | rts := routes.NewRoutes(app, s.config, ctrl) 104 | rts.Set() 105 | 106 | // run gracefully 107 | go func() { 108 | if err := app.Listen(fmt.Sprintf(":%s", s.config.APIPort)); err != nil { 109 | s.logger.Error("Failed to listen on port", err) 110 | } 111 | }() 112 | 113 | s.logger.Info("Running api server version %s in port %s, with base path %s", 114 | s.config.APIVersion, s.config.APIPort, s.config.APIBasePath) 115 | 116 | // Gracefully shutdown 117 | c := make(chan os.Signal, 1) 118 | signal.Notify(c, os.Interrupt) 119 | <-c 120 | 121 | // Clean up tasks 122 | s.logger.Info("Shutting down server...") 123 | err = app.Shutdown() 124 | if err != nil { 125 | s.logger.Error("Failed to shutdown", err) 126 | return err 127 | } 128 | err = dbConn.Close(conn) 129 | if err != nil { 130 | s.logger.Error("Failed to close db connection") 131 | return err 132 | } 133 | 134 | // Before close 135 | s.logger.Info("Successfully shutdow nof the server") 136 | fmt.Println("shuted down...") 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /pkg/internal/usecases/usecases.go: -------------------------------------------------------------------------------- 1 | // Package usecases contains bussines logig cases interface 2 | package usecases 3 | 4 | import ( 5 | "dall06/go-cleanapi/pkg/internal" 6 | "dall06/go-cleanapi/pkg/internal/repository" 7 | "dall06/go-cleanapi/utils" 8 | "database/sql" 9 | "fmt" 10 | 11 | "github.com/mitchellh/mapstructure" 12 | ) 13 | 14 | // UseCases is an interface that extend the cases 15 | type UseCases interface { 16 | RegisterUser(req interface{}) error 17 | AuthUser(req interface{}) (*internal.User, error) 18 | IndexUserByID(req interface{}) (*internal.User, error) 19 | IndexUsers() (internal.Users, error) 20 | ModifyUser(req interface{}) error 21 | DestroyUser(req interface{}) error 22 | } 23 | 24 | var _ UseCases = (*cases)(nil) 25 | 26 | type cases struct { 27 | repository repository.Repository 28 | uuid utils.UUID 29 | } 30 | 31 | // NewUseCases is a construcotr for the cases 32 | func NewUseCases(r repository.Repository, uid utils.UUID) UseCases { 33 | return &cases{ 34 | repository: r, 35 | uuid: uid, 36 | } 37 | } 38 | 39 | func (s *cases) AuthUser(req interface{}) (*internal.User, error) { 40 | user := &internal.User{} 41 | 42 | if req == nil { 43 | return nil, fmt.Errorf("empty request") 44 | } 45 | 46 | err := mapstructure.Decode(req, &user) 47 | if err != nil { 48 | return nil, fmt.Errorf("failed to decode user details: %v", err) 49 | } 50 | 51 | // add uuidGenerator to the user 52 | res, err := s.repository.Login(user) 53 | if err == sql.ErrNoRows { 54 | empty := &internal.User{} 55 | return empty, fmt.Errorf("failed to fetch user details: %v", "user not found") 56 | } 57 | if err != nil { 58 | return nil, fmt.Errorf("failed to auth user details: %v", err) 59 | } 60 | 61 | return res, nil 62 | } 63 | 64 | func (s *cases) RegisterUser(req interface{}) error { 65 | user := &internal.User{} 66 | 67 | if req == nil { 68 | return fmt.Errorf("empty request") 69 | } 70 | 71 | err := mapstructure.Decode(req, &user) 72 | if err != nil { 73 | return fmt.Errorf("failed to decode user details: %v", err) 74 | } 75 | 76 | // add uuidGenerator to the user 77 | user.ID = s.uuid.NewString() 78 | err = s.repository.Create(user) 79 | if err != nil { 80 | return fmt.Errorf("failed to fetch user details: %v", err) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func (s *cases) IndexUserByID(req interface{}) (*internal.User, error) { 87 | user := &internal.User{} 88 | 89 | if req == nil { 90 | return nil, fmt.Errorf("empty request") 91 | } 92 | 93 | err := mapstructure.Decode(req, &user) 94 | if err != nil { 95 | return nil, fmt.Errorf("failed to decode user details: %v", err) 96 | } 97 | 98 | res, err := s.repository.Read(user) 99 | if err == sql.ErrNoRows { 100 | empty := &internal.User{} 101 | return empty, nil 102 | } 103 | if err != nil { 104 | return nil, fmt.Errorf("failed to fetch user details: %v", err) 105 | } 106 | 107 | return res, nil 108 | } 109 | 110 | func (s *cases) IndexUsers() (internal.Users, error) { 111 | users, err := s.repository.ReadAll() 112 | if err != nil { 113 | return nil, fmt.Errorf("failed to fetch user details: %v", err) 114 | } 115 | return users, nil 116 | } 117 | 118 | func (s *cases) ModifyUser(req interface{}) error { 119 | user := &internal.User{} 120 | 121 | if req == nil { 122 | return fmt.Errorf("empty request") 123 | } 124 | 125 | err := mapstructure.Decode(req, &user) 126 | if err != nil { 127 | return fmt.Errorf("failed to decode user details: %v", err) 128 | } 129 | 130 | err = s.repository.Update(user) 131 | if err != nil { 132 | return fmt.Errorf("failed to update user: %v", err) 133 | } 134 | 135 | return nil 136 | } 137 | 138 | func (s *cases) DestroyUser(req interface{}) error { 139 | user := &internal.User{} 140 | 141 | if req == nil { 142 | return fmt.Errorf("empty request") 143 | } 144 | 145 | err := mapstructure.Decode(req, &user) 146 | if err != nil { 147 | return fmt.Errorf("failed to decode user details: %v", err) 148 | } 149 | 150 | err = s.repository.Delete(user) 151 | if err != nil { 152 | return fmt.Errorf("failed to fetch user details: %v", err) 153 | } 154 | 155 | return nil 156 | } 157 | -------------------------------------------------------------------------------- /utils/jwt.go: -------------------------------------------------------------------------------- 1 | // Package utils is a package that provides general method for the api usage 2 | package utils 3 | 4 | import ( 5 | "crypto/sha512" 6 | "dall06/go-cleanapi/config" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/golang-jwt/jwt/v4" 13 | ) 14 | 15 | const jwtExpirationTime = 72 * time.Hour 16 | 17 | type userClaims struct { 18 | UID string `json:"uid"` 19 | jwt.RegisteredClaims 20 | } 21 | 22 | type apiClaims struct { 23 | Hash string `json:"hash"` 24 | jwt.RegisteredClaims 25 | } 26 | 27 | // JWT is an interface for jwt util 28 | type JWT interface { 29 | CreateUserJWT(uid string) (string, error) 30 | CheckUserJwt(requestToken string) (bool, error) 31 | CreateAPIJWT() (string, error) 32 | CheckAPIJWT(requestToken string) (bool, error) 33 | } 34 | 35 | var _ JWT = (*myJwt)(nil) 36 | 37 | type myJwt struct { 38 | config config.Vars 39 | } 40 | 41 | // NewJWT returns a pointer to a JwtUtil struct. 42 | func NewJWT(c config.Vars) JWT { 43 | return &myJwt{ 44 | config: c, 45 | } 46 | } 47 | 48 | func (ju *myJwt) CreateUserJWT(id string) (string, error) { 49 | if id == "" { 50 | return "", errors.New("id cannot be empty") 51 | } 52 | 53 | expiresAt := time.Now().Add(jwtExpirationTime) 54 | userClaims := userClaims{ 55 | UID: id, 56 | RegisteredClaims: jwt.RegisteredClaims{ 57 | ExpiresAt: jwt.NewNumericDate(expiresAt), 58 | }, 59 | } 60 | 61 | // Embed User information to `token` 62 | token := jwt.NewWithClaims(jwt.SigningMethodHS512, userClaims) 63 | 64 | // token -> string. Only server knows the secret. 65 | s := ju.config.JWTSecret 66 | signedToken, err := token.SignedString(s) 67 | if err != nil { 68 | return "", fmt.Errorf("failed to sign token: %w", err) 69 | } 70 | 71 | return signedToken, nil 72 | } 73 | 74 | func (ju *myJwt) CheckUserJwt(requestToken string) (bool, error) { 75 | if requestToken == "" { 76 | return false, errors.New("token cannot be empty") 77 | } 78 | 79 | claims := &userClaims{} 80 | token, err := jwt.ParseWithClaims(requestToken, claims, func(token *jwt.Token) (interface{}, error) { 81 | // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") 82 | return ju.config.JWTSecret, nil 83 | }) 84 | if err != nil { 85 | return false, fmt.Errorf("failed to parse token: %w", err) 86 | } 87 | 88 | if !token.Valid { 89 | return false, errors.New("invalid token") 90 | } 91 | 92 | return true, nil 93 | } 94 | 95 | func (ju *myJwt) CreateAPIJWT() (string, error) { 96 | apiKey := ju.config.APIKey 97 | 98 | if apiKey == "" { 99 | return "", errors.New("api key cannot be empty") 100 | } 101 | 102 | sha := sha512.Sum512_256([]byte(apiKey)) 103 | hexString := hex.EncodeToString(sha[:]) 104 | 105 | expiresAt := time.Now().Add(jwtExpirationTime) 106 | 107 | apiClaims := apiClaims{ 108 | Hash: hexString, 109 | RegisteredClaims: jwt.RegisteredClaims{ 110 | ExpiresAt: jwt.NewNumericDate(expiresAt), 111 | }, 112 | } 113 | 114 | // Embed User information to `token` 115 | token := jwt.NewWithClaims(jwt.SigningMethodHS512, apiClaims) 116 | 117 | // token -> string. Only server knows the secret. 118 | signedToken, err := token.SignedString(ju.config.JWTSecret) 119 | if err != nil { 120 | return "", fmt.Errorf("failed to sign token: %w", err) 121 | } 122 | 123 | return signedToken, nil 124 | } 125 | 126 | func (ju *myJwt) CheckAPIJWT(requestToken string) (bool, error) { 127 | apiKeyHash := ju.config.APIKeyHash 128 | if apiKeyHash == "" { 129 | return false, errors.New("id cannot be empty") 130 | } 131 | 132 | if requestToken == "" { 133 | return false, errors.New("token cannot be empty") 134 | } 135 | 136 | claims := &apiClaims{} 137 | token, err := jwt.ParseWithClaims(requestToken, claims, func(token *jwt.Token) (interface{}, error) { 138 | // Don't forget to validate the alg is what you expect: 139 | // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") 140 | return ju.config.JWTSecret, nil 141 | }) 142 | if err != nil { 143 | return false, fmt.Errorf("failed to parse token: %w", err) 144 | } 145 | 146 | if !token.Valid { 147 | return false, errors.New("invalid token") 148 | } 149 | 150 | fmt.Println("///////") 151 | fmt.Println(token.Claims) 152 | 153 | c, ok := token.Claims.(*apiClaims) 154 | if !ok { 155 | return false, fmt.Errorf("unexpected claims error: %v", token.Claims) 156 | } 157 | hashToCompare := c.Hash 158 | 159 | if apiKeyHash != hashToCompare { 160 | return false, fmt.Errorf("different hash error: %v", c) 161 | } 162 | 163 | return true, nil 164 | } 165 | -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /go-cleanapi/api/v1 2 | definitions: 3 | controller.DeleteRequest: 4 | properties: 5 | password: 6 | type: string 7 | required: 8 | - password 9 | type: object 10 | controller.PostRequest: 11 | properties: 12 | email: 13 | type: string 14 | password: 15 | type: string 16 | phone: 17 | type: string 18 | required: 19 | - email 20 | - password 21 | type: object 22 | controller.PutRequest: 23 | properties: 24 | email: 25 | type: string 26 | password: 27 | type: string 28 | phone: 29 | type: string 30 | required: 31 | - password 32 | type: object 33 | controller.User: 34 | properties: 35 | email: 36 | type: string 37 | password: 38 | type: string 39 | phone: 40 | type: string 41 | uid: 42 | type: string 43 | type: object 44 | host: localhost:8080 45 | info: 46 | contact: {} 47 | description: Golang REST Api based on Uncle's Bob Clean Arch 48 | title: go-cleanapi 49 | version: 1.0.0 50 | paths: 51 | /users: 52 | get: 53 | description: Retrieve all users 54 | produces: 55 | - application/json 56 | responses: 57 | "200": 58 | description: OK 59 | schema: 60 | items: 61 | $ref: '#/definitions/controller.User' 62 | type: array 63 | security: 64 | - ApiKeyAuth: [] 65 | - JwtTokenAuth: [] 66 | summary: Get all users 67 | post: 68 | consumes: 69 | - application/json 70 | description: Create a new user 71 | parameters: 72 | - description: PostRequest object 73 | in: body 74 | name: user 75 | required: true 76 | schema: 77 | $ref: '#/definitions/controller.PostRequest' 78 | produces: 79 | - application/json 80 | responses: 81 | "201": 82 | description: Created 83 | schema: 84 | type: string 85 | security: 86 | - ApiKeyAuth: [] 87 | summary: Create a user 88 | /users/{id}: 89 | delete: 90 | description: Delete a user with a given ID 91 | parameters: 92 | - description: User ID 93 | in: path 94 | name: id 95 | required: true 96 | type: integer 97 | - description: DeleteRequest object 98 | in: body 99 | name: user 100 | required: true 101 | schema: 102 | $ref: '#/definitions/controller.DeleteRequest' 103 | responses: 104 | "204": 105 | description: No Content 106 | security: 107 | - ApiKeyAuth: [] 108 | - JwtTokenAuth: [] 109 | summary: Delete a user 110 | get: 111 | description: Retrieve a single user by ID 112 | parameters: 113 | - description: User ID 114 | in: path 115 | name: id 116 | required: true 117 | type: integer 118 | produces: 119 | - application/json 120 | responses: 121 | "200": 122 | description: OK 123 | schema: 124 | $ref: '#/definitions/controller.User' 125 | security: 126 | - ApiKeyAuth: [] 127 | - JwtTokenAuth: [] 128 | summary: Get a user by ID 129 | put: 130 | consumes: 131 | - application/json 132 | description: Update a user with a given ID 133 | parameters: 134 | - description: User ID 135 | in: path 136 | name: id 137 | required: true 138 | type: integer 139 | - description: PutRequest object 140 | in: body 141 | name: user 142 | required: true 143 | schema: 144 | $ref: '#/definitions/controller.PutRequest' 145 | produces: 146 | - application/json 147 | responses: 148 | "200": 149 | description: OK 150 | schema: 151 | type: string 152 | security: 153 | - ApiKeyAuth: [] 154 | - JwtTokenAuth: [] 155 | summary: Update a user 156 | /users/auth: 157 | post: 158 | consumes: 159 | - application/json 160 | description: auth a as user with phone or mail 161 | parameters: 162 | - description: PostRequest object 163 | in: body 164 | name: user 165 | required: true 166 | schema: 167 | $ref: '#/definitions/controller.PostRequest' 168 | produces: 169 | - application/json 170 | responses: 171 | "200": 172 | description: OK 173 | schema: 174 | type: string 175 | security: 176 | - ApiKeyAuth: [] 177 | summary: Auth as user 178 | swagger: "2.0" 179 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // Package config contains the method to load config variables that will be used in the app 2 | package config 3 | 4 | import ( 5 | "crypto/sha512" 6 | "encoding/hex" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/joho/godotenv" 14 | ) 15 | 16 | // Vars are config variables 17 | type Vars struct { 18 | // APIBasePath makes reference to api basepath 19 | APIBasePath string 20 | // APIPort makes reference to api port 21 | APIPort string 22 | // APIKey makes reference to api key string 23 | APIKey string 24 | // APIKeyHash makes reference to api key string in hash 25 | APIKeyHash string 26 | // DBConnString is the connection string 27 | DBConnString string 28 | // JWTSecret is the secret to generate the jwts 29 | JWTSecret []byte 30 | // ProyectName means the proyect name 31 | ProyectName string 32 | // Stage is the stage in which the app runs 33 | Stage string 34 | // ProyectPath means the absolute path of th proyect 35 | ProyectPath string 36 | // CookieSecret is the secret to encode the cookies 37 | CookieSecret string 38 | // APIVersion indicates the version of the api 39 | APIVersion string 40 | // AppName contains the name of the server including version 41 | AppName string 42 | } 43 | 44 | const ( 45 | file = ".env" 46 | 47 | envUserDB = "USER_DB" 48 | envPasswordDB = "PASSWORD_DB" 49 | envHostDB = "HOST_DB" 50 | envPortDB = "PORT_DB" 51 | envNameDB = "NAME_DB" 52 | envSecretJWT = "SECRET_JWT" 53 | envStage = "STAGE" 54 | envCookieEncryption = "COOKIE_ENCRYPTION" 55 | envAPIKey = "API_KEY" 56 | ) 57 | 58 | // Config is an interface that extends config 59 | type Config interface { 60 | SetConfig() (*Vars, error) 61 | } 62 | 63 | type config struct { 64 | port string 65 | version string 66 | Vars Vars 67 | } 68 | 69 | var _ Config = (*config)(nil) 70 | 71 | // NewConfig is a constructor for config 72 | func NewConfig(p string, v string) Config { 73 | return &config{ 74 | port: p, 75 | version: v, 76 | } 77 | } 78 | 79 | func (c *config) SetConfig() (*Vars, error) { 80 | if c.port == "" { 81 | return nil, fmt.Errorf("empty port parameter") 82 | } 83 | if c.version == "" { 84 | return nil, fmt.Errorf("empty version parameter") 85 | } 86 | 87 | if err := c.loadEnv(); err != nil { 88 | return nil, err 89 | } 90 | 91 | c.Vars.DBConnString = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", //":@tcp(127.0.0.1:3306)/" 92 | os.Getenv(envUserDB), os.Getenv(envPasswordDB), os.Getenv(envHostDB), os.Getenv(envPortDB), os.Getenv(envNameDB)) 93 | c.Vars.JWTSecret = []byte(fmt.Sprint(os.Getenv(envSecretJWT))) 94 | c.Vars.Stage = strings.ToLower(os.Getenv(envStage)) 95 | 96 | proyectName, err := c.loadName() 97 | if err != nil { 98 | return nil, err 99 | } 100 | c.Vars.ProyectName = proyectName 101 | c.Vars.APIPort = c.port 102 | c.Vars.APIVersion = c.version 103 | 104 | c.Vars.CookieSecret = os.Getenv(envCookieEncryption) 105 | c.Vars.APIBasePath = fmt.Sprintf("/%s/api/v%s", c.Vars.ProyectName, c.version) 106 | c.Vars.AppName = fmt.Sprintf("%s v%s", proyectName, c.Vars.APIVersion) 107 | 108 | ak := os.Getenv(envAPIKey) 109 | c.Vars.APIKey = ak 110 | sha := sha512.Sum512_256([]byte(ak)) 111 | c.Vars.APIKeyHash = hex.EncodeToString(sha[:]) 112 | 113 | return &c.Vars, nil 114 | } 115 | 116 | func (c *config) loadEnv() error { 117 | projectPath, err := c.getProjectPath() 118 | if err != nil { 119 | return err 120 | } 121 | c.Vars.ProyectPath = projectPath 122 | 123 | filePath := filepath.Join(projectPath, file) 124 | 125 | err = godotenv.Load(filePath) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func (c *config) getProjectPath() (string, error) { 134 | cwd, err := os.Getwd() 135 | if err != nil { 136 | return "", fmt.Errorf("failed to get current working directory: %w", err) 137 | } 138 | 139 | for dir := cwd; dir != string(filepath.Separator); dir = filepath.Dir(dir) { 140 | _, err := os.Stat(filepath.Join(dir, "go.mod")) 141 | if err == nil { 142 | return dir, nil 143 | } 144 | 145 | if !os.IsNotExist(err) { 146 | return "", fmt.Errorf("failed to check directory: %w", err) 147 | } 148 | } 149 | 150 | return "", fmt.Errorf("failed to find project root directory") 151 | } 152 | 153 | func (c *config) loadName() (string, error) { 154 | cmd := exec.Command("go", "list", "-m") 155 | out, err := cmd.Output() 156 | if err != nil { 157 | return "", err 158 | } 159 | // The output of the `go list -m` command will contain the module name 160 | // and version, separated by a space. We only want the module name. 161 | module := strings.Split(string(out), " ")[0] 162 | module = strings.Split(string(module), "/")[1] 163 | module = strings.ReplaceAll(module, "\n", "") 164 | return module, nil 165 | } 166 | -------------------------------------------------------------------------------- /pkg/infrastructure/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | //go:build !coverage 2 | // +build !coverage 3 | 4 | // Package middleware Contains middleware implementation 5 | // individual packages from middleware for fiber are already tested in their own github repository 6 | package middleware 7 | 8 | import ( 9 | "dall06/go-cleanapi/config" 10 | "dall06/go-cleanapi/utils" 11 | "fmt" 12 | "strings" 13 | "time" 14 | 15 | "github.com/gofiber/fiber/v2" 16 | "github.com/gofiber/fiber/v2/middleware/compress" 17 | "github.com/gofiber/fiber/v2/middleware/cors" 18 | "github.com/gofiber/fiber/v2/middleware/csrf" 19 | "github.com/gofiber/fiber/v2/middleware/encryptcookie" 20 | "github.com/gofiber/fiber/v2/middleware/etag" 21 | "github.com/gofiber/fiber/v2/middleware/idempotency" 22 | "github.com/gofiber/fiber/v2/middleware/recover" 23 | "github.com/gofiber/helmet/v2" 24 | jwtware "github.com/gofiber/jwt/v3" 25 | "github.com/gofiber/keyauth/v2" 26 | ) 27 | 28 | // Middleware is an interface that extends middleware 29 | type Middleware interface { 30 | CORS() fiber.Handler 31 | Helmet() fiber.Handler 32 | Compress() fiber.Handler 33 | EncryptCookie() fiber.Handler 34 | ETag() fiber.Handler 35 | Recover() fiber.Handler 36 | JwtWare() fiber.Handler 37 | KeyAuth() fiber.Handler 38 | CRSF() fiber.Handler 39 | Idempotency() fiber.Handler 40 | } 41 | 42 | var _ Middleware = (*middleware)(nil) 43 | 44 | type middleware struct { 45 | jwt utils.JWT 46 | config config.Vars 47 | } 48 | 49 | // NewMiddleware is a constructor for middleware 50 | func NewMiddleware(vars config.Vars, jr utils.JWT) Middleware { 51 | return &middleware{ 52 | jwt: jr, 53 | config: vars, 54 | } 55 | } 56 | 57 | func (*middleware) CORS() fiber.Handler { 58 | cfg := &cors.Config{ 59 | AllowOrigins: "*", 60 | AllowHeaders: "Origin,Content-Type,Accept,X-Session-Token,X-Application-Key", 61 | AllowMethods: "GET,POST,PUT,DELETE", 62 | ExposeHeaders: "Content-Length,Authorization", 63 | MaxAge: 5600, 64 | } 65 | return cors.New(*cfg) 66 | } 67 | 68 | func (*middleware) Helmet() fiber.Handler { 69 | cfg := helmet.Config{ 70 | CSPReportOnly: true, 71 | } 72 | 73 | return helmet.New(cfg) 74 | } 75 | 76 | func (*middleware) Compress() fiber.Handler { 77 | cfg := compress.Config{ 78 | Next: func(c *fiber.Ctx) bool { 79 | return c.Method() != fiber.MethodGet 80 | }, 81 | Level: compress.LevelBestSpeed, // 1 82 | } 83 | return compress.New(cfg) 84 | } 85 | 86 | func (m *middleware) EncryptCookie() fiber.Handler { 87 | cfg := encryptcookie.Config{ 88 | Key: m.config.CookieSecret, 89 | } 90 | return encryptcookie.New(cfg) 91 | } 92 | 93 | func (*middleware) ETag() fiber.Handler { 94 | cfg := etag.Config{ 95 | Next: func(c *fiber.Ctx) bool { 96 | return c.Method() != fiber.MethodGet 97 | }, 98 | Weak: true, 99 | } 100 | return etag.New(cfg) 101 | } 102 | 103 | func (*middleware) Recover() fiber.Handler { 104 | return recover.New() 105 | } 106 | 107 | func (m *middleware) JwtWare() fiber.Handler { 108 | cfg := jwtware.Config{ 109 | SigningKey: m.config.JWTSecret, 110 | TokenLookup: "cookie:session_id", 111 | Filter: func(c *fiber.Ctx) bool { 112 | basePath := m.config.APIBasePath 113 | 114 | swaggerPath := fmt.Sprintf("%s/swagger/", basePath) 115 | 116 | usersPath := fmt.Sprintf("%s/users", basePath) 117 | authPath := fmt.Sprintf("%s/auth", usersPath) 118 | signupPath := fmt.Sprintf("%s/signup", usersPath) 119 | 120 | if c.Path() == swaggerPath { 121 | return true 122 | } 123 | if c.Path() == authPath { 124 | return true 125 | } 126 | if c.Path() == signupPath { 127 | return true 128 | } 129 | // Exclude all subroutes of /swagger 130 | if strings.HasPrefix(c.Path(), swaggerPath) { 131 | return true 132 | } 133 | 134 | return false 135 | }, 136 | } 137 | 138 | return jwtware.New(cfg) 139 | } 140 | 141 | func (m *middleware) KeyAuth() fiber.Handler { 142 | cfg := keyauth.Config{ 143 | KeyLookup: "header:x-access-token", 144 | Validator: func(c *fiber.Ctx, jwts string) (bool, error) { 145 | return m.jwt.CheckAPIJWT(jwts) 146 | }, 147 | Filter: func(c *fiber.Ctx) bool { 148 | basePath := m.config.APIBasePath 149 | 150 | swaggerPath := fmt.Sprintf("%s/swagger/", basePath) 151 | 152 | usersPath := fmt.Sprintf("%s/users", basePath) 153 | authPath := fmt.Sprintf("%s/auth", usersPath) 154 | signupPath := fmt.Sprintf("%s/signup", usersPath) 155 | 156 | if c.Path() == swaggerPath { 157 | return true 158 | } 159 | if c.Path() == authPath { 160 | return true 161 | } 162 | if c.Path() == signupPath { 163 | return true 164 | } 165 | // Exclude all subroutes of /swagger 166 | if strings.HasPrefix(c.Path(), swaggerPath) { 167 | return true 168 | } 169 | 170 | return false 171 | }, 172 | } 173 | return keyauth.New(cfg) 174 | } 175 | 176 | func (*middleware) CRSF() fiber.Handler { 177 | cfg := csrf.Config{ 178 | Expiration: 15 * time.Minute, 179 | } 180 | // Or extend your config for customization 181 | return csrf.New(cfg) 182 | } 183 | 184 | func (*middleware) Idempotency() fiber.Handler { 185 | return idempotency.New() 186 | } 187 | -------------------------------------------------------------------------------- /pkg/internal/repository/repository.go: -------------------------------------------------------------------------------- 1 | // Package repository provides the methods that intercat with data source 2 | package repository 3 | 4 | import ( 5 | "dall06/go-cleanapi/pkg/internal" 6 | "database/sql" 7 | "fmt" 8 | ) 9 | 10 | const ( 11 | spCreate = "CALL `go_cleanapi`.`sp_create_user`(?, ?, ?, ?);" 12 | spRead = "CALL `go_cleanapi`.`sp_read_user`(?);" 13 | spReadAll = "CALL `go_cleanapi`.`sp_read_users`();" 14 | spUpdate = "CALL `go_cleanapi`.`sp_update_user`(?, ?, ?, ?);" 15 | spDelete = "CALL `go_cleanapi`.`sp_delete_user`(?, ?);" 16 | spLogin = "CALL `go_cleanapi`.`sp_login_user`(?, ?, ?);" 17 | ) 18 | 19 | // Repository is an interface that extends the repository 20 | type Repository interface { 21 | Create(user *internal.User) error 22 | Read(user *internal.User) (*internal.User, error) 23 | ReadAll() (internal.Users, error) 24 | Update(user *internal.User) error 25 | Delete(user *internal.User) error 26 | Login(user *internal.User) (*internal.User, error) 27 | } 28 | 29 | var _ Repository = (*repository)(nil) 30 | 31 | type repository struct { 32 | dbConn *sql.DB 33 | } 34 | 35 | // NewRepository is a constructor for a repository 36 | func NewRepository(db *sql.DB) Repository { 37 | return &repository{ 38 | dbConn: db, 39 | } 40 | } 41 | 42 | func (r *repository) Login(user *internal.User) (*internal.User, error) { 43 | if user == nil { 44 | return nil, fmt.Errorf("user is required") 45 | } 46 | if user.Email == "" && user.Phone == "" { 47 | return nil, fmt.Errorf("data is required") 48 | } 49 | if user.Email != "" && user.Phone != "" { 50 | return nil, fmt.Errorf("only one parameter is required") 51 | } 52 | if user.Password == "" { 53 | return nil, fmt.Errorf("password is required") 54 | } 55 | // Add more validation checks as needed. 56 | row := r.dbConn.QueryRow(spLogin, 57 | user.Email, 58 | user.Phone, 59 | user.Password) 60 | if row == nil { 61 | empty := &internal.User{} 62 | return empty, nil 63 | } 64 | 65 | u := &internal.User{} 66 | 67 | err := row.Scan(&u.ID) 68 | if err == sql.ErrNoRows { 69 | return nil, sql.ErrNoRows 70 | } 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return u, nil 76 | } 77 | 78 | func (r *repository) Create(user *internal.User) error { 79 | if user == nil { 80 | return fmt.Errorf("user is empty") 81 | } 82 | if user.ID == "" { 83 | return fmt.Errorf("ID is required") 84 | } 85 | if user.Email == "" { 86 | return fmt.Errorf("email is required") 87 | } 88 | if user.Password == "" { 89 | return fmt.Errorf("password is required") 90 | } 91 | // Add more validation checks as needed. 92 | 93 | _, err := r.dbConn.Exec(spCreate, 94 | user.ID, 95 | user.Email, 96 | user.Phone, 97 | user.Password) 98 | if err != nil { 99 | return fmt.Errorf("failed to execute SQL statement: %v", err) 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (r repository) Read(user *internal.User) (*internal.User, error) { 106 | if user == nil { 107 | return nil, fmt.Errorf("user is required") 108 | } 109 | if user.ID == "" { 110 | return nil, fmt.Errorf("ID is required") 111 | } 112 | 113 | u := &internal.User{} 114 | 115 | row := r.dbConn.QueryRow(spRead, user.ID) 116 | if row == nil { 117 | empty := &internal.User{} 118 | return empty, nil 119 | } 120 | 121 | err := row.Scan( 122 | &u.ID, 123 | &u.Email, 124 | &u.Phone) 125 | if err == sql.ErrNoRows { 126 | return nil, sql.ErrNoRows 127 | } 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | return u, nil 133 | } 134 | 135 | func (r *repository) ReadAll() (internal.Users, error) { 136 | rows, err := r.dbConn.Query(spReadAll) 137 | if err != nil { 138 | return nil, err 139 | } 140 | defer func() { 141 | if cerr := rows.Close(); cerr != nil { 142 | err = cerr 143 | } 144 | }() 145 | 146 | users := make(internal.Users, 0) // allocate slice 147 | 148 | for rows.Next() { 149 | user := &internal.User{} 150 | err := rows.Scan( 151 | &user.ID, 152 | &user.Email, 153 | &user.Phone, 154 | ) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | users = append(users, user) 160 | } 161 | 162 | if err := rows.Err(); err != nil { 163 | return nil, err 164 | } 165 | 166 | if err := rows.Close(); err != nil { 167 | return nil, err 168 | } 169 | 170 | return users, nil 171 | } 172 | 173 | func (r repository) Update(user *internal.User) error { 174 | if user == nil { 175 | return fmt.Errorf("user is required") 176 | } 177 | if user.ID == "" { 178 | return fmt.Errorf("ID is resquired") 179 | } 180 | if user.Password == "" { 181 | return fmt.Errorf("password is required") 182 | } 183 | if user.Email == "" && user.Phone == "" { 184 | return fmt.Errorf("user data is required") 185 | } 186 | 187 | res, err := r.dbConn.Exec(spUpdate, 188 | user.ID, 189 | user.Email, 190 | user.Phone, 191 | user.Password) 192 | if err != nil { 193 | return err 194 | } 195 | 196 | affected, err := res.RowsAffected() 197 | if err != nil { 198 | return fmt.Errorf("failed to obtain rows affected: %v", err) 199 | } 200 | 201 | if affected == 0 { 202 | return fmt.Errorf("user not updated") 203 | } 204 | 205 | return nil 206 | } 207 | 208 | func (r repository) Delete(user *internal.User) error { 209 | if user == nil { 210 | return fmt.Errorf("user is required") 211 | } 212 | if user.ID == "" { 213 | return fmt.Errorf("ID is required") 214 | } 215 | if user.Password == "" { 216 | return fmt.Errorf("password is required") 217 | } 218 | 219 | res, err := r.dbConn.Exec(spDelete, user.ID, user.Password) 220 | if err != nil { 221 | return err 222 | } 223 | 224 | affected, err := res.RowsAffected() 225 | if err != nil { 226 | return fmt.Errorf("failed to obtain rows affected: %v", err) 227 | } 228 | 229 | if affected == 0 { 230 | return fmt.Errorf("user not deleted") 231 | } 232 | 233 | return nil 234 | } 235 | -------------------------------------------------------------------------------- /utils/jwt_test.go: -------------------------------------------------------------------------------- 1 | // Package utils_test is a test package for utils 2 | package utils_test 3 | 4 | import ( 5 | "dall06/go-cleanapi/config" 6 | "dall06/go-cleanapi/utils" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCreateUserJWT(test *testing.T) { 13 | cfg := config.NewConfig("8080", "0.0.0") 14 | vars, err := cfg.SetConfig() 15 | if err != nil { 16 | test.Fatal("expected no error, but got:", err) 17 | } 18 | 19 | successfulCases := []struct { 20 | name string 21 | input string 22 | config *config.Vars 23 | }{ 24 | { 25 | config: vars, 26 | name: "it should create a jwt user based", 27 | input: "im an id", 28 | }, 29 | } 30 | 31 | failedCases := []struct { 32 | name string 33 | input string 34 | config *config.Vars 35 | }{ 36 | { 37 | config: vars, 38 | name: "it should fail create a jwt user based, empty string", 39 | input: "", 40 | }, 41 | } 42 | 43 | for _, tc := range successfulCases { 44 | tc := tc 45 | 46 | test.Run(tc.name, func(t *testing.T) { 47 | t.Parallel() 48 | 49 | jwt := utils.NewJWT(*tc.config) 50 | 51 | r, err := jwt.CreateUserJWT(tc.input) 52 | 53 | assert.NoError(t, err) // it should be empty 54 | assert.NotEmpty(t, r, "expected a non-empty string, but got empty") // it should not be empty 55 | }) 56 | } 57 | 58 | for _, tc := range failedCases { 59 | tc := tc 60 | 61 | test.Run(tc.name, func(t *testing.T) { 62 | t.Parallel() 63 | 64 | jwt := utils.NewJWT(*tc.config) 65 | 66 | r, err := jwt.CreateUserJWT(tc.input) 67 | 68 | assert.Error(t, err) 69 | assert.Empty(t, r, "expected an empty string, but got something on it") 70 | }) 71 | } 72 | } 73 | 74 | func TestCheckUserJWT(test *testing.T) { 75 | cfg := config.NewConfig("8080", "0.0.0") 76 | vars, err := cfg.SetConfig() 77 | if err != nil { 78 | test.Fatal("expected no error, but got:", err) 79 | } 80 | 81 | successfulCases := []struct { 82 | name string 83 | input string 84 | config *config.Vars 85 | }{ 86 | { 87 | config: vars, 88 | name: "it should check a jwt user based", 89 | input: "im an id", 90 | }, 91 | } 92 | 93 | failedCases := []struct { 94 | name string 95 | input string 96 | config *config.Vars 97 | }{ 98 | { 99 | config: vars, 100 | name: "it should fail check a jwt user based, empty string", 101 | input: "", 102 | }, 103 | } 104 | 105 | for _, tc := range successfulCases { 106 | tc := tc 107 | 108 | test.Run(tc.name, func(t *testing.T) { 109 | t.Parallel() 110 | 111 | jwt := utils.NewJWT(*tc.config) 112 | 113 | r, err := jwt.CreateUserJWT(tc.input) 114 | 115 | if err != nil { 116 | t.Error("expected no error, but got:", err) 117 | } 118 | if r == "" { 119 | t.Error("expected a non-empty string, but got empty") 120 | } 121 | 122 | ok, err := jwt.CheckUserJwt(r) 123 | assert.NoError(t, err) 124 | assert.Equal(t, true, ok, "expected an ok, but got false") 125 | }) 126 | } 127 | 128 | for _, tc := range failedCases { 129 | tc := tc 130 | 131 | test.Run(tc.name, func(t *testing.T) { 132 | t.Parallel() 133 | 134 | jwt := utils.NewJWT(*tc.config) 135 | 136 | ok, err := jwt.CheckUserJwt("") 137 | assert.Error(t, err) 138 | assert.Equal(t, false, ok, "expected false, but got true") 139 | }) 140 | } 141 | } 142 | 143 | func TestCreateAPIJWT(test *testing.T) { 144 | cfg := config.NewConfig("8080", "0.0.0") 145 | vars, err := cfg.SetConfig() 146 | if err != nil { 147 | test.Fatal("expected no error, but got:", err) 148 | } 149 | 150 | varsEmptyAPIKey := *vars 151 | if err != nil { 152 | test.Fatal("expected no error, but got:", err) 153 | } 154 | varsEmptyAPIKey.APIKey = "" 155 | 156 | successfulCases := []struct { 157 | name string 158 | input string 159 | config *config.Vars 160 | }{ 161 | { 162 | config: vars, 163 | name: "it should create a jwt api based", 164 | input: "", 165 | }, 166 | } 167 | 168 | failedCases := []struct { 169 | name string 170 | input string 171 | config *config.Vars 172 | }{ 173 | { 174 | config: vars, 175 | name: "it should fail create a jwt api based, empty string", 176 | input: "", 177 | }, 178 | { 179 | config: &varsEmptyAPIKey, 180 | name: "it should fail create a jwt api based, empty string", 181 | input: "", 182 | }, 183 | } 184 | 185 | for _, tc := range successfulCases { 186 | tc := tc 187 | 188 | test.Run(tc.name, func(t *testing.T) { 189 | t.Parallel() 190 | 191 | jwt := utils.NewJWT(*tc.config) 192 | 193 | r, err := jwt.CreateAPIJWT() 194 | 195 | assert.NoError(t, err) 196 | assert.NotEmpty(t, r, "expected a non-empty string, but got empty") 197 | }) 198 | } 199 | 200 | for _, tc := range failedCases { 201 | tc := tc 202 | 203 | test.Run(tc.name, func(t *testing.T) { 204 | t.Parallel() 205 | 206 | jwt := utils.NewJWT(*tc.config) 207 | 208 | r, err := jwt.CreateUserJWT(tc.input) 209 | 210 | assert.Error(t, err) 211 | assert.Empty(t, r, "expected a non-empty string, but got empty") 212 | }) 213 | } 214 | } 215 | 216 | func TestCheckAPIJWT(test *testing.T) { 217 | cfg := config.NewConfig("8080", "0.0.0") 218 | vars, err := cfg.SetConfig() 219 | if err != nil { 220 | test.Fatal("expected no error, but got:", err) 221 | } 222 | 223 | varsEmptyAPIKey := *vars 224 | if err != nil { 225 | test.Fatal("expected no error, but got:", err) 226 | } 227 | varsEmptyAPIKey.APIKey = "" 228 | 229 | successfulCases := []struct { 230 | name string 231 | input string 232 | config *config.Vars 233 | }{ 234 | { 235 | config: vars, 236 | name: "it should check a jwt user based", 237 | input: "im an id", 238 | }, 239 | } 240 | 241 | failedCases := []struct { 242 | name string 243 | input string 244 | config *config.Vars 245 | }{ 246 | { 247 | config: vars, 248 | name: "it should fail check a jwt user based, empty string", 249 | input: "", 250 | }, 251 | { 252 | config: &varsEmptyAPIKey, 253 | name: "it should fail create a jwt api based, empty string", 254 | input: "", 255 | }, 256 | } 257 | 258 | for _, tc := range successfulCases { 259 | tc := tc 260 | 261 | test.Run(tc.name, func(t *testing.T) { 262 | t.Parallel() 263 | 264 | jwt := utils.NewJWT(*tc.config) 265 | 266 | r, err := jwt.CreateAPIJWT() 267 | 268 | if err != nil { 269 | t.Error("expected no error, but got:", err) 270 | } 271 | if r == "" { 272 | t.Error("expected a non-empty string, but got empty") 273 | } 274 | 275 | ok, err := jwt.CheckUserJwt(r) 276 | assert.NoError(t, err) 277 | assert.Equal(t, true, ok, "expected an ok, but got false") 278 | }) 279 | } 280 | 281 | for _, tc := range failedCases { 282 | tc := tc 283 | 284 | test.Run(tc.name, func(t *testing.T) { 285 | t.Parallel() 286 | 287 | jwt := utils.NewJWT(*tc.config) 288 | 289 | ok, err := jwt.CheckAPIJWT("") 290 | assert.Error(t, err) 291 | assert.Equal(t, false, ok, "expected an ok, but got false") 292 | }) 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "Golang REST Api based on Uncle's Bob Clean Arch", 5 | "title": "go-cleanapi", 6 | "contact": {}, 7 | "version": "1.0.0" 8 | }, 9 | "host": "localhost:8080", 10 | "basePath": "/go-cleanapi/api/v1", 11 | "paths": { 12 | "/users": { 13 | "get": { 14 | "security": [ 15 | { 16 | "ApiKeyAuth": [] 17 | }, 18 | { 19 | "JwtTokenAuth": [] 20 | } 21 | ], 22 | "description": "Retrieve all users", 23 | "produces": [ 24 | "application/json" 25 | ], 26 | "summary": "Get all users", 27 | "responses": { 28 | "200": { 29 | "description": "OK", 30 | "schema": { 31 | "type": "array", 32 | "items": { 33 | "$ref": "#/definitions/controller.User" 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "post": { 40 | "security": [ 41 | { 42 | "ApiKeyAuth": [] 43 | } 44 | ], 45 | "description": "Create a new user", 46 | "consumes": [ 47 | "application/json" 48 | ], 49 | "produces": [ 50 | "application/json" 51 | ], 52 | "summary": "Create a user", 53 | "parameters": [ 54 | { 55 | "description": "PostRequest object", 56 | "name": "user", 57 | "in": "body", 58 | "required": true, 59 | "schema": { 60 | "$ref": "#/definitions/controller.PostRequest" 61 | } 62 | } 63 | ], 64 | "responses": { 65 | "201": { 66 | "description": "Created", 67 | "schema": { 68 | "type": "string" 69 | } 70 | } 71 | } 72 | } 73 | }, 74 | "/users/auth": { 75 | "post": { 76 | "security": [ 77 | { 78 | "ApiKeyAuth": [] 79 | } 80 | ], 81 | "description": "auth a as user with phone or mail", 82 | "consumes": [ 83 | "application/json" 84 | ], 85 | "produces": [ 86 | "application/json" 87 | ], 88 | "summary": "Auth as user", 89 | "parameters": [ 90 | { 91 | "description": "PostRequest object", 92 | "name": "user", 93 | "in": "body", 94 | "required": true, 95 | "schema": { 96 | "$ref": "#/definitions/controller.PostRequest" 97 | } 98 | } 99 | ], 100 | "responses": { 101 | "200": { 102 | "description": "OK", 103 | "schema": { 104 | "type": "string" 105 | } 106 | } 107 | } 108 | } 109 | }, 110 | "/users/{id}": { 111 | "get": { 112 | "security": [ 113 | { 114 | "ApiKeyAuth": [] 115 | }, 116 | { 117 | "JwtTokenAuth": [] 118 | } 119 | ], 120 | "description": "Retrieve a single user by ID", 121 | "produces": [ 122 | "application/json" 123 | ], 124 | "summary": "Get a user by ID", 125 | "parameters": [ 126 | { 127 | "type": "integer", 128 | "description": "User ID", 129 | "name": "id", 130 | "in": "path", 131 | "required": true 132 | } 133 | ], 134 | "responses": { 135 | "200": { 136 | "description": "OK", 137 | "schema": { 138 | "$ref": "#/definitions/controller.User" 139 | } 140 | } 141 | } 142 | }, 143 | "put": { 144 | "security": [ 145 | { 146 | "ApiKeyAuth": [] 147 | }, 148 | { 149 | "JwtTokenAuth": [] 150 | } 151 | ], 152 | "description": "Update a user with a given ID", 153 | "consumes": [ 154 | "application/json" 155 | ], 156 | "produces": [ 157 | "application/json" 158 | ], 159 | "summary": "Update a user", 160 | "parameters": [ 161 | { 162 | "type": "integer", 163 | "description": "User ID", 164 | "name": "id", 165 | "in": "path", 166 | "required": true 167 | }, 168 | { 169 | "description": "PutRequest object", 170 | "name": "user", 171 | "in": "body", 172 | "required": true, 173 | "schema": { 174 | "$ref": "#/definitions/controller.PutRequest" 175 | } 176 | } 177 | ], 178 | "responses": { 179 | "200": { 180 | "description": "OK", 181 | "schema": { 182 | "type": "string" 183 | } 184 | } 185 | } 186 | }, 187 | "delete": { 188 | "security": [ 189 | { 190 | "ApiKeyAuth": [] 191 | }, 192 | { 193 | "JwtTokenAuth": [] 194 | } 195 | ], 196 | "description": "Delete a user with a given ID", 197 | "summary": "Delete a user", 198 | "parameters": [ 199 | { 200 | "type": "integer", 201 | "description": "User ID", 202 | "name": "id", 203 | "in": "path", 204 | "required": true 205 | }, 206 | { 207 | "description": "DeleteRequest object", 208 | "name": "user", 209 | "in": "body", 210 | "required": true, 211 | "schema": { 212 | "$ref": "#/definitions/controller.DeleteRequest" 213 | } 214 | } 215 | ], 216 | "responses": { 217 | "204": { 218 | "description": "No Content" 219 | } 220 | } 221 | } 222 | } 223 | }, 224 | "definitions": { 225 | "controller.DeleteRequest": { 226 | "type": "object", 227 | "required": [ 228 | "password" 229 | ], 230 | "properties": { 231 | "password": { 232 | "type": "string" 233 | } 234 | } 235 | }, 236 | "controller.PostRequest": { 237 | "type": "object", 238 | "required": [ 239 | "email", 240 | "password" 241 | ], 242 | "properties": { 243 | "email": { 244 | "type": "string" 245 | }, 246 | "password": { 247 | "type": "string" 248 | }, 249 | "phone": { 250 | "type": "string" 251 | } 252 | } 253 | }, 254 | "controller.PutRequest": { 255 | "type": "object", 256 | "required": [ 257 | "password" 258 | ], 259 | "properties": { 260 | "email": { 261 | "type": "string" 262 | }, 263 | "password": { 264 | "type": "string" 265 | }, 266 | "phone": { 267 | "type": "string" 268 | } 269 | } 270 | }, 271 | "controller.User": { 272 | "type": "object", 273 | "properties": { 274 | "email": { 275 | "type": "string" 276 | }, 277 | "password": { 278 | "type": "string" 279 | }, 280 | "phone": { 281 | "type": "string" 282 | }, 283 | "uid": { 284 | "type": "string" 285 | } 286 | } 287 | } 288 | } 289 | } -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Code generated by swaggo/swag. DO NOT EDIT. 2 | 3 | package docs 4 | 5 | import "github.com/swaggo/swag" 6 | 7 | const docTemplate = `{ 8 | "schemes": {{ marshal .Schemes }}, 9 | "swagger": "2.0", 10 | "info": { 11 | "description": "{{escape .Description}}", 12 | "title": "{{.Title}}", 13 | "contact": {}, 14 | "version": "{{.Version}}" 15 | }, 16 | "host": "{{.Host}}", 17 | "basePath": "{{.BasePath}}", 18 | "paths": { 19 | "/users": { 20 | "get": { 21 | "security": [ 22 | { 23 | "ApiKeyAuth": [] 24 | }, 25 | { 26 | "JwtTokenAuth": [] 27 | } 28 | ], 29 | "description": "Retrieve all users", 30 | "produces": [ 31 | "application/json" 32 | ], 33 | "summary": "Get all users", 34 | "responses": { 35 | "200": { 36 | "description": "OK", 37 | "schema": { 38 | "type": "array", 39 | "items": { 40 | "$ref": "#/definitions/controller.User" 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | "post": { 47 | "security": [ 48 | { 49 | "ApiKeyAuth": [] 50 | } 51 | ], 52 | "description": "Create a new user", 53 | "consumes": [ 54 | "application/json" 55 | ], 56 | "produces": [ 57 | "application/json" 58 | ], 59 | "summary": "Create a user", 60 | "parameters": [ 61 | { 62 | "description": "PostRequest object", 63 | "name": "user", 64 | "in": "body", 65 | "required": true, 66 | "schema": { 67 | "$ref": "#/definitions/controller.PostRequest" 68 | } 69 | } 70 | ], 71 | "responses": { 72 | "201": { 73 | "description": "Created", 74 | "schema": { 75 | "type": "string" 76 | } 77 | } 78 | } 79 | } 80 | }, 81 | "/users/auth": { 82 | "post": { 83 | "security": [ 84 | { 85 | "ApiKeyAuth": [] 86 | } 87 | ], 88 | "description": "auth a as user with phone or mail", 89 | "consumes": [ 90 | "application/json" 91 | ], 92 | "produces": [ 93 | "application/json" 94 | ], 95 | "summary": "Auth as user", 96 | "parameters": [ 97 | { 98 | "description": "PostRequest object", 99 | "name": "user", 100 | "in": "body", 101 | "required": true, 102 | "schema": { 103 | "$ref": "#/definitions/controller.PostRequest" 104 | } 105 | } 106 | ], 107 | "responses": { 108 | "200": { 109 | "description": "OK", 110 | "schema": { 111 | "type": "string" 112 | } 113 | } 114 | } 115 | } 116 | }, 117 | "/users/{id}": { 118 | "get": { 119 | "security": [ 120 | { 121 | "ApiKeyAuth": [] 122 | }, 123 | { 124 | "JwtTokenAuth": [] 125 | } 126 | ], 127 | "description": "Retrieve a single user by ID", 128 | "produces": [ 129 | "application/json" 130 | ], 131 | "summary": "Get a user by ID", 132 | "parameters": [ 133 | { 134 | "type": "integer", 135 | "description": "User ID", 136 | "name": "id", 137 | "in": "path", 138 | "required": true 139 | } 140 | ], 141 | "responses": { 142 | "200": { 143 | "description": "OK", 144 | "schema": { 145 | "$ref": "#/definitions/controller.User" 146 | } 147 | } 148 | } 149 | }, 150 | "put": { 151 | "security": [ 152 | { 153 | "ApiKeyAuth": [] 154 | }, 155 | { 156 | "JwtTokenAuth": [] 157 | } 158 | ], 159 | "description": "Update a user with a given ID", 160 | "consumes": [ 161 | "application/json" 162 | ], 163 | "produces": [ 164 | "application/json" 165 | ], 166 | "summary": "Update a user", 167 | "parameters": [ 168 | { 169 | "type": "integer", 170 | "description": "User ID", 171 | "name": "id", 172 | "in": "path", 173 | "required": true 174 | }, 175 | { 176 | "description": "PutRequest object", 177 | "name": "user", 178 | "in": "body", 179 | "required": true, 180 | "schema": { 181 | "$ref": "#/definitions/controller.PutRequest" 182 | } 183 | } 184 | ], 185 | "responses": { 186 | "200": { 187 | "description": "OK", 188 | "schema": { 189 | "type": "string" 190 | } 191 | } 192 | } 193 | }, 194 | "delete": { 195 | "security": [ 196 | { 197 | "ApiKeyAuth": [] 198 | }, 199 | { 200 | "JwtTokenAuth": [] 201 | } 202 | ], 203 | "description": "Delete a user with a given ID", 204 | "summary": "Delete a user", 205 | "parameters": [ 206 | { 207 | "type": "integer", 208 | "description": "User ID", 209 | "name": "id", 210 | "in": "path", 211 | "required": true 212 | }, 213 | { 214 | "description": "DeleteRequest object", 215 | "name": "user", 216 | "in": "body", 217 | "required": true, 218 | "schema": { 219 | "$ref": "#/definitions/controller.DeleteRequest" 220 | } 221 | } 222 | ], 223 | "responses": { 224 | "204": { 225 | "description": "No Content" 226 | } 227 | } 228 | } 229 | } 230 | }, 231 | "definitions": { 232 | "controller.DeleteRequest": { 233 | "type": "object", 234 | "required": [ 235 | "password" 236 | ], 237 | "properties": { 238 | "password": { 239 | "type": "string" 240 | } 241 | } 242 | }, 243 | "controller.PostRequest": { 244 | "type": "object", 245 | "required": [ 246 | "email", 247 | "password" 248 | ], 249 | "properties": { 250 | "email": { 251 | "type": "string" 252 | }, 253 | "password": { 254 | "type": "string" 255 | }, 256 | "phone": { 257 | "type": "string" 258 | } 259 | } 260 | }, 261 | "controller.PutRequest": { 262 | "type": "object", 263 | "required": [ 264 | "password" 265 | ], 266 | "properties": { 267 | "email": { 268 | "type": "string" 269 | }, 270 | "password": { 271 | "type": "string" 272 | }, 273 | "phone": { 274 | "type": "string" 275 | } 276 | } 277 | }, 278 | "controller.User": { 279 | "type": "object", 280 | "properties": { 281 | "email": { 282 | "type": "string" 283 | }, 284 | "password": { 285 | "type": "string" 286 | }, 287 | "phone": { 288 | "type": "string" 289 | }, 290 | "uid": { 291 | "type": "string" 292 | } 293 | } 294 | } 295 | } 296 | }` 297 | 298 | // SwaggerInfo holds exported Swagger Info so clients can modify it 299 | var SwaggerInfo = &swag.Spec{ 300 | Version: "1.0.0", 301 | Host: "localhost:8080", 302 | BasePath: "/go-cleanapi/api/v1", 303 | Schemes: []string{}, 304 | Title: "go-cleanapi", 305 | Description: "Golang REST Api based on Uncle's Bob Clean Arch", 306 | InfoInstanceName: "swagger", 307 | SwaggerTemplate: docTemplate, 308 | } 309 | 310 | func init() { 311 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 312 | } 313 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module dall06/go-cleanapi 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gofiber/helmet/v2 v2.2.26 7 | github.com/gofiber/jwt/v3 v3.3.9 8 | github.com/gofiber/keyauth/v2 v2.2.1 9 | github.com/gofiber/swagger v0.1.11 10 | github.com/google/uuid v1.3.0 11 | github.com/swaggo/swag v1.16.1 12 | ) 13 | 14 | require ( 15 | 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 16 | 4d63.com/gochecknoglobals v0.2.1 // indirect 17 | github.com/Abirdcfly/dupword v0.0.11 // indirect 18 | github.com/Antonboom/errname v0.1.9 // indirect 19 | github.com/Antonboom/nilnil v0.1.4 // indirect 20 | github.com/BurntSushi/toml v1.2.1 // indirect 21 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 22 | github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 // indirect 23 | github.com/KyleBanks/depth v1.2.1 // indirect 24 | github.com/Masterminds/semver v1.5.0 // indirect 25 | github.com/OpenPeeDeeP/depguard v1.1.1 // indirect 26 | github.com/alexkohler/prealloc v1.0.0 // indirect 27 | github.com/alingse/asasalint v0.0.11 // indirect 28 | github.com/ashanbrown/forbidigo v1.5.1 // indirect 29 | github.com/ashanbrown/makezero v1.1.1 // indirect 30 | github.com/beorn7/perks v1.0.1 // indirect 31 | github.com/bkielbasa/cyclop v1.2.0 // indirect 32 | github.com/blizzy78/varnamelen v0.8.0 // indirect 33 | github.com/bombsimon/wsl/v3 v3.4.0 // indirect 34 | github.com/breml/bidichk v0.2.4 // indirect 35 | github.com/breml/errchkjson v0.3.1 // indirect 36 | github.com/butuzov/ireturn v0.1.1 // indirect 37 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 38 | github.com/charithe/durationcheck v0.0.10 // indirect 39 | github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 // indirect 40 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 41 | github.com/curioswitch/go-reassign v0.2.0 // indirect 42 | github.com/daixiang0/gci v0.10.1 // indirect 43 | github.com/davecgh/go-spew v1.1.1 // indirect 44 | github.com/denis-tingaikin/go-header v0.4.3 // indirect 45 | github.com/esimonov/ifshort v1.0.4 // indirect 46 | github.com/ettle/strcase v0.1.1 // indirect 47 | github.com/fatih/color v1.15.0 // indirect 48 | github.com/fatih/structtag v1.2.0 // indirect 49 | github.com/firefart/nonamedreturns v1.0.4 // indirect 50 | github.com/fsnotify/fsnotify v1.6.0 // indirect 51 | github.com/fzipp/gocyclo v0.6.0 // indirect 52 | github.com/ghodss/yaml v1.0.0 // indirect 53 | github.com/go-critic/go-critic v0.7.0 // indirect 54 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 55 | github.com/go-openapi/jsonreference v0.20.2 // indirect 56 | github.com/go-openapi/spec v0.20.9 // indirect 57 | github.com/go-openapi/swag v0.22.3 // indirect 58 | github.com/go-playground/locales v0.14.1 // indirect 59 | github.com/go-playground/universal-translator v0.18.1 // indirect 60 | github.com/go-toolsmith/astcast v1.1.0 // indirect 61 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 62 | github.com/go-toolsmith/astequal v1.1.0 // indirect 63 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 64 | github.com/go-toolsmith/astp v1.1.0 // indirect 65 | github.com/go-toolsmith/strparse v1.1.0 // indirect 66 | github.com/go-toolsmith/typep v1.1.0 // indirect 67 | github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect 68 | github.com/gobwas/glob v0.2.3 // indirect 69 | github.com/gofrs/flock v0.8.1 // indirect 70 | github.com/golang/protobuf v1.5.3 // indirect 71 | github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect 72 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 73 | github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect 74 | github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 // indirect 75 | github.com/golangci/golangci-lint v1.52.2 // indirect 76 | github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect 77 | github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect 78 | github.com/golangci/misspell v0.4.0 // indirect 79 | github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect 80 | github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect 81 | github.com/google/go-cmp v0.5.9 // indirect 82 | github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // indirect 83 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 84 | github.com/gostaticanalysis/comment v1.4.2 // indirect 85 | github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect 86 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 87 | github.com/hashicorp/errwrap v1.1.0 // indirect 88 | github.com/hashicorp/go-multierror v1.1.1 // indirect 89 | github.com/hashicorp/go-version v1.6.0 // indirect 90 | github.com/hashicorp/hcl v1.0.0 // indirect 91 | github.com/hexops/gotextdiff v1.0.3 // indirect 92 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 93 | github.com/jgautheron/goconst v1.5.1 // indirect 94 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 95 | github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect 96 | github.com/josharian/intern v1.0.0 // indirect 97 | github.com/julz/importas v0.1.0 // indirect 98 | github.com/junk1tm/musttag v0.5.0 // indirect 99 | github.com/kisielk/errcheck v1.6.3 // indirect 100 | github.com/kisielk/gotool v1.0.0 // indirect 101 | github.com/kkHAIKE/contextcheck v1.1.4 // indirect 102 | github.com/kulti/thelper v0.6.3 // indirect 103 | github.com/kunwardeep/paralleltest v1.0.6 // indirect 104 | github.com/kyoh86/exportloopref v0.1.11 // indirect 105 | github.com/ldez/gomoddirectives v0.2.3 // indirect 106 | github.com/ldez/tagliatelle v0.4.0 // indirect 107 | github.com/leodido/go-urn v1.2.3 // indirect 108 | github.com/leonklingele/grouper v1.1.1 // indirect 109 | github.com/lufeee/execinquery v1.2.1 // indirect 110 | github.com/magiconair/properties v1.8.7 // indirect 111 | github.com/mailru/easyjson v0.7.7 // indirect 112 | github.com/maratori/testableexamples v1.0.0 // indirect 113 | github.com/maratori/testpackage v1.1.1 // indirect 114 | github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect 115 | github.com/mattn/go-colorable v0.1.13 // indirect 116 | github.com/mattn/go-isatty v0.0.18 // indirect 117 | github.com/mattn/go-runewidth v0.0.14 // indirect 118 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 119 | github.com/mbilski/exhaustivestruct v1.2.0 // indirect 120 | github.com/mgechev/revive v1.3.1 // indirect 121 | github.com/mitchellh/go-homedir v1.1.0 // indirect 122 | github.com/moricho/tparallel v0.3.1 // indirect 123 | github.com/nakabonne/nestif v0.3.1 // indirect 124 | github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect 125 | github.com/nishanths/exhaustive v0.10.0 // indirect 126 | github.com/nishanths/predeclared v0.2.2 // indirect 127 | github.com/nunnatsa/ginkgolinter v0.11.0 // indirect 128 | github.com/olekukonko/tablewriter v0.0.5 // indirect 129 | github.com/pelletier/go-toml v1.9.5 // indirect 130 | github.com/pelletier/go-toml/v2 v2.0.7 // indirect 131 | github.com/philhofer/fwd v1.1.2 // indirect 132 | github.com/pmezard/go-difflib v1.0.0 // indirect 133 | github.com/polyfloyd/go-errorlint v1.4.0 // indirect 134 | github.com/prometheus/client_golang v1.14.0 // indirect 135 | github.com/prometheus/client_model v0.3.0 // indirect 136 | github.com/prometheus/common v0.42.0 // indirect 137 | github.com/prometheus/procfs v0.9.0 // indirect 138 | github.com/quasilyte/go-ruleguard v0.3.19 // indirect 139 | github.com/quasilyte/gogrep v0.5.0 // indirect 140 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 141 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 142 | github.com/rivo/uniseg v0.4.4 // indirect 143 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 144 | github.com/ryancurrah/gomodguard v1.3.0 // indirect 145 | github.com/ryanrolds/sqlclosecheck v0.4.0 // indirect 146 | github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect 147 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 148 | github.com/sashamelentyev/usestdlibvars v1.23.0 // indirect 149 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect 150 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect 151 | github.com/securego/gosec/v2 v2.15.0 // indirect 152 | github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect 153 | github.com/sirupsen/logrus v1.9.0 // indirect 154 | github.com/sivchari/containedctx v1.0.3 // indirect 155 | github.com/sivchari/nosnakecase v1.7.0 // indirect 156 | github.com/sivchari/tenv v1.7.1 // indirect 157 | github.com/sonatard/noctx v0.0.2 // indirect 158 | github.com/sourcegraph/go-diff v0.7.0 // indirect 159 | github.com/spf13/afero v1.9.5 // indirect 160 | github.com/spf13/cast v1.5.0 // indirect 161 | github.com/spf13/cobra v1.7.0 // indirect 162 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 163 | github.com/spf13/pflag v1.0.5 // indirect 164 | github.com/spf13/viper v1.15.0 // indirect 165 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 166 | github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect 167 | github.com/stretchr/objx v0.5.0 // indirect 168 | github.com/subosito/gotenv v1.4.2 // indirect 169 | github.com/swaggo/fiber-swagger v1.3.0 // indirect 170 | github.com/swaggo/files v1.0.1 // indirect 171 | github.com/swaggo/files/v2 v2.0.0 // indirect 172 | github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect 173 | github.com/tdakkota/asciicheck v0.2.0 // indirect 174 | github.com/tetafro/godot v1.4.11 // indirect 175 | github.com/timakin/bodyclose v0.0.0-20221125081123-e39cf3fc478e // indirect 176 | github.com/timonwong/loggercheck v0.9.4 // indirect 177 | github.com/tinylib/msgp v1.1.8 // indirect 178 | github.com/tomarrell/wrapcheck/v2 v2.8.1 // indirect 179 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 180 | github.com/ultraware/funlen v0.0.3 // indirect 181 | github.com/ultraware/whitespace v0.0.5 // indirect 182 | github.com/urfave/cli/v2 v2.25.1 // indirect 183 | github.com/uudashr/gocognit v1.0.6 // indirect 184 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 185 | github.com/yagipy/maintidx v1.0.0 // indirect 186 | github.com/yeya24/promlinter v0.2.0 // indirect 187 | gitlab.com/bosi/decorder v0.2.3 // indirect 188 | go.uber.org/atomic v1.10.0 // indirect 189 | go.uber.org/multierr v1.11.0 // indirect 190 | golang.org/x/crypto v0.8.0 // indirect 191 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect 192 | golang.org/x/exp/typeparams v0.0.0-20230321023759-10a507213a29 // indirect 193 | golang.org/x/mod v0.10.0 // indirect 194 | golang.org/x/net v0.9.0 // indirect 195 | golang.org/x/sync v0.1.0 // indirect 196 | golang.org/x/text v0.9.0 // indirect 197 | golang.org/x/tools v0.8.0 // indirect 198 | google.golang.org/protobuf v1.30.0 // indirect 199 | gopkg.in/ini.v1 v1.67.0 // indirect 200 | gopkg.in/yaml.v2 v2.4.0 // indirect 201 | gopkg.in/yaml.v3 v3.0.1 // indirect 202 | honnef.co/go/tools v0.4.3 // indirect 203 | mvdan.cc/gofumpt v0.5.0 // indirect 204 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect 205 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect 206 | mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 // indirect 207 | ) 208 | 209 | require ( 210 | github.com/DATA-DOG/go-sqlmock v1.5.0 211 | github.com/andybalholm/brotli v1.0.5 // indirect 212 | github.com/go-playground/validator/v10 v10.12.0 213 | github.com/go-sql-driver/mysql v1.7.1 214 | github.com/gofiber/fiber/v2 v2.44.0 // direct 215 | github.com/gofiber/template v1.8.0 216 | github.com/golang-jwt/jwt/v4 v4.5.0 217 | github.com/joho/godotenv v1.5.1 218 | github.com/klauspost/compress v1.16.5 // indirect 219 | github.com/mitchellh/mapstructure v1.5.0 220 | github.com/patrickmn/go-cache v2.1.0+incompatible 221 | github.com/stretchr/testify v1.8.2 222 | github.com/valyala/bytebufferpool v1.0.0 // indirect 223 | github.com/valyala/fasthttp v1.46.0 // indirect 224 | github.com/valyala/tcplisten v1.0.0 // indirect 225 | go.uber.org/zap v1.24.0 226 | golang.org/x/sys v0.7.0 // indirect 227 | ) 228 | -------------------------------------------------------------------------------- /pkg/adapter/controller/controller.go: -------------------------------------------------------------------------------- 1 | //go:build !exclude_Permision 2 | // +build !exclude_Permision 3 | 4 | // Package controller package is a package that provides handlers of an http to intercat with a data source 5 | package controller 6 | 7 | import ( 8 | "dall06/go-cleanapi/pkg/internal/usecases" 9 | "dall06/go-cleanapi/utils" 10 | "database/sql" 11 | "fmt" 12 | "time" 13 | 14 | "github.com/go-playground/validator/v10" 15 | "github.com/gofiber/fiber/v2" 16 | "github.com/mitchellh/mapstructure" 17 | "github.com/patrickmn/go-cache" 18 | ) 19 | 20 | const ( 21 | statusOK = fiber.StatusOK 22 | statusCreated = fiber.StatusCreated 23 | statusBadRequest = fiber.StatusBadRequest 24 | statusNotFound = fiber.StatusNotFound 25 | statusInternalServerError = fiber.StatusInternalServerError 26 | 27 | requestError = "request error" 28 | internalError = "internal error" 29 | notFound = "not Found error" 30 | missingID = "missing id parameter" 31 | userIsNil = "user is null" 32 | usersAreNil = "users are null" 33 | registered = "account registered successfully" 34 | modified = "account modified successfully" 35 | deleted = "account deleted successfully" 36 | 37 | processed = "request processed" 38 | ) 39 | 40 | // Controller is an interface for controller 41 | type Controller interface { 42 | Auth(context *fiber.Ctx) error 43 | Post(context *fiber.Ctx) error 44 | Get(context *fiber.Ctx) error 45 | GetAll(context *fiber.Ctx) error 46 | Put(context *fiber.Ctx) error 47 | Delete(context *fiber.Ctx) error 48 | } 49 | 50 | type controller struct { 51 | usecases usecases.UseCases 52 | validate validator.Validate 53 | logger utils.Logger 54 | jwt utils.JWT 55 | validations utils.Validations 56 | cache *cache.Cache 57 | } 58 | 59 | var _ Controller = (*controller)(nil) 60 | 61 | // NewController is a Constructor for controller 62 | func NewController( 63 | uc usecases.UseCases, 64 | v validator.Validate, 65 | l utils.Logger, 66 | j utils.JWT, 67 | val utils.Validations, 68 | c cache.Cache, 69 | ) Controller { 70 | return &controller{ 71 | usecases: uc, 72 | validate: v, 73 | logger: l, 74 | jwt: j, 75 | validations: val, 76 | cache: &c, 77 | } 78 | } 79 | 80 | // @Summary Auth as user 81 | // @Description auth a as user with phone or mail 82 | // @Accept json 83 | // @Produce json 84 | // @Param user body PostRequest true "PostRequest object" 85 | // @Success 200 {string} Accepted 86 | // @Security ApiKeyAuth 87 | // @Router /users/auth [post] 88 | func (c *controller) Auth(ctx *fiber.Ctx) error { 89 | req := &AuthRequest{ 90 | UserName: ctx.FormValue("user"), 91 | Password: ctx.FormValue("password"), 92 | } 93 | userInput := &User{} 94 | 95 | if err := c.validate.Struct(req); err != nil { 96 | c.logger.Error("%s: %s", requestError, err) 97 | return fiber.NewError(statusBadRequest, fmt.Sprintf("%s: %s", requestError, err)) 98 | } 99 | 100 | userName := req.UserName 101 | switch { 102 | case c.validations.IsEmail(userName): 103 | userInput.Email = userName 104 | case c.validations.IsPhone(userName): 105 | userInput.Phone = userName 106 | default: 107 | return fiber.NewError(statusBadRequest, fmt.Sprintf("%s: %s", requestError, "invalid user format")) 108 | } 109 | userInput.Password = req.Password 110 | 111 | res, err := c.usecases.AuthUser(userInput) 112 | if err != nil { 113 | // Return an error response if the use case returns an error 114 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 115 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 116 | } 117 | if res.ID == "" { 118 | // Return an error response if the use case returns an error 119 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), requestError, missingID) 120 | return fiber.NewError(statusBadRequest, fmt.Sprintf("%s: %s", requestError, missingID)) 121 | } 122 | 123 | accessToken, err := c.jwt.CreateUserJWT(res.ID) 124 | if err != nil { 125 | // Return an error response if the use case returns an error 126 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, userIsNil) 127 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, userIsNil)) 128 | } 129 | 130 | // Create cookie 131 | cookie := new(fiber.Cookie) 132 | cookie.Name = "session_id" 133 | cookie.Value = accessToken 134 | cookie.Expires = time.Now().Add(15 * time.Hour) 135 | 136 | c.logger.Info("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), processed, ctx.BaseURL()) 137 | ctx.Cookie(cookie) 138 | return ctx.Status(fiber.StatusAccepted).JSON(fiber.Map{"msg": registered}) 139 | } 140 | 141 | // @Summary Create a user 142 | // @Description Create a new user 143 | // @Accept json 144 | // @Produce json 145 | // @Param user body PostRequest true "PostRequest object" 146 | // @Success 201 {string} Created 147 | // @Security ApiKeyAuth 148 | // @Router /users [post] 149 | func (c *controller) Post(ctx *fiber.Ctx) error { 150 | req := &PostRequest{} 151 | 152 | if err := ctx.BodyParser(&req); err != nil { 153 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 154 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 155 | } 156 | 157 | if err := c.validate.Struct(req); err != nil { 158 | c.logger.Error("%s: %s", requestError, err) 159 | return fiber.NewError(statusBadRequest, fmt.Sprintf("%s: %s", requestError, err)) 160 | } 161 | 162 | userInput := &User{ 163 | Email: req.Email, 164 | Phone: req.Phone, 165 | Password: req.Password, 166 | } 167 | 168 | err := c.usecases.RegisterUser(userInput) 169 | if err != nil { 170 | // Return an error response if the use case returns an error 171 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 172 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 173 | } 174 | 175 | c.logger.Info("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), processed, ctx.BaseURL()) 176 | return ctx.Status(fiber.StatusCreated).JSON(fiber.Map{"msg": registered}) 177 | } 178 | 179 | // @Summary Get a user by ID 180 | // @Description Retrieve a single user by ID 181 | // @Produce json 182 | // @Param id path int true "User ID" 183 | // @Success 200 {object} User 184 | // @Security ApiKeyAuth 185 | // @Security JwtTokenAuth 186 | // @Router /users/{id} [get] 187 | func (c *controller) Get(ctx *fiber.Ctx) error { 188 | // Get the id parameter from the request context 189 | id := ctx.Params("id") 190 | if id == "" { 191 | // Return an error response if the id parameter is missing 192 | c.logger.Error("%s: %s", requestError, missingID) 193 | return fiber.NewError(statusBadRequest, fmt.Sprintf("%s: %s", requestError, missingID)) 194 | } 195 | 196 | // Call the use case to retrieve the user by id 197 | userInput := &User{ID: id} 198 | empty := &User{} 199 | userData, err := c.usecases.IndexUserByID(userInput) 200 | if err != nil { 201 | // Return an error response if the use case returns an error 202 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 203 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 204 | } 205 | if userData == nil { 206 | c.logger.Error("%s: %s", statusNotFound, userIsNil) 207 | return fiber.NewError(statusNotFound, fmt.Sprintf("%s: %s", internalError, userIsNil)) 208 | } 209 | 210 | // Convert the user data to the output format 211 | userOutput := &User{} 212 | err = mapstructure.Decode(userData, &userOutput) 213 | if err != nil { 214 | // Return an error response if the user data cannot be converted 215 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 216 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 217 | } 218 | if err == sql.ErrNoRows { 219 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), notFound, userIsNil) 220 | return fiber.NewError(statusNotFound, fmt.Sprintf("%s: %s", notFound, err)) 221 | } 222 | if userOutput == nil { 223 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), notFound, userIsNil) 224 | return fiber.NewError(statusNotFound, fmt.Sprintf("%s: %s", notFound, err)) 225 | } 226 | if userOutput == empty { 227 | c.logger.Info("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), notFound, userIsNil) 228 | return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"data": empty, "msg": notFound}) 229 | } 230 | 231 | // Return a success response with the user data 232 | c.logger.Info("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), processed, ctx.BaseURL()) 233 | return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"data": userOutput}) 234 | } 235 | 236 | // @Summary Get all users 237 | // @Description Retrieve all users 238 | // @Produce json 239 | // @Success 200 {array} User 240 | // @Security ApiKeyAuth 241 | // @Security JwtTokenAuth 242 | // @Router /users [get] 243 | func (c *controller) GetAll(ctx *fiber.Ctx) error { 244 | // check if exists in cache, if yes returns value, if not, continues 245 | cachedUsers, found := c.cache.Get("users") 246 | if found { 247 | usersOutput := cachedUsers.(*Users) 248 | return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"data": usersOutput}) 249 | } 250 | 251 | users, err := c.usecases.IndexUsers() 252 | if err != nil { 253 | // Return an error response if the use case returns an error 254 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 255 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 256 | } 257 | if users == nil { 258 | c.logger.Error("%s: %s", notFound, usersAreNil) 259 | return fiber.NewError(statusNotFound, fmt.Sprintf("%s: %s", notFound, usersAreNil)) 260 | } 261 | 262 | // Convert the user data to the output format 263 | usersOutput := &Users{} 264 | err = mapstructure.Decode(users, &usersOutput) 265 | if err != nil { 266 | // Return an error response if the user data cannot be converted 267 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 268 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 269 | } 270 | if usersOutput == nil { 271 | c.logger.Error("%s: %s", notFound, usersAreNil) 272 | return fiber.NewError(statusNotFound, fmt.Sprintf("%s: %s", notFound, usersAreNil)) 273 | } 274 | if len(*usersOutput) == 0 { 275 | c.logger.Info("%s: %s", notFound, usersAreNil) 276 | return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"data": usersOutput}) 277 | } 278 | 279 | // Set new cache 280 | c.cache.Set("users", usersOutput, cache.DefaultExpiration) 281 | 282 | // Return a success response with the user data 283 | c.logger.Info("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), processed, ctx.BaseURL()) 284 | return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"data": usersOutput}) 285 | } 286 | 287 | // @Summary Update a user 288 | // @Description Update a user with a given ID 289 | // @Accept json 290 | // @Produce json 291 | // @Param id path int true "User ID" 292 | // @Param user body PutRequest true "PutRequest object" 293 | // @Success 200 {string} Updated 294 | // @Security ApiKeyAuth 295 | // @Security JwtTokenAuth 296 | // @Router /users/{id} [put] 297 | func (c *controller) Put(ctx *fiber.Ctx) error { 298 | id := ctx.Params("id") 299 | if id == "" { 300 | // Return an error response if the id parameter is missing 301 | c.logger.Error("%s: %s", statusBadRequest, missingID) 302 | return fiber.NewError(statusBadRequest, fmt.Sprintf("%s: %s", requestError, missingID)) 303 | } 304 | 305 | req := &PutRequest{} 306 | if err := ctx.BodyParser(req); err != nil { 307 | c.logger.Error("%s: %s", statusInternalServerError, err) 308 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 309 | } 310 | 311 | if err := c.validate.Struct(req); err != nil { 312 | c.logger.Error("%s: %s", statusBadRequest, err) 313 | return fiber.NewError(statusBadRequest, fmt.Sprintf("%s: %s", requestError, err)) 314 | } 315 | 316 | userInput := &User{ 317 | ID: id, 318 | Email: req.Email, 319 | Phone: req.Phone, 320 | Password: req.Password, 321 | } 322 | err := c.usecases.ModifyUser(userInput) 323 | if err != nil { 324 | // Return an error response if the use case returns an error 325 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 326 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 327 | } 328 | 329 | c.logger.Info("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), processed, ctx.BaseURL()) 330 | return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"msg": modified}) 331 | } 332 | 333 | // @Summary Delete a user 334 | // @Description Delete a user with a given ID 335 | // @Param id path int true "User ID" 336 | // @Param user body DeleteRequest true "DeleteRequest object" 337 | // @Success 204 338 | // @Security ApiKeyAuth 339 | // @Security JwtTokenAuth 340 | // @Router /users/{id} [delete] 341 | func (c *controller) Delete(ctx *fiber.Ctx) error { 342 | id := ctx.Params("id") 343 | if id == "" { 344 | // Return an error response if the id parameter is missing 345 | c.logger.Error("%s: %s", requestError, missingID) 346 | return fiber.NewError(statusBadRequest, fmt.Sprintf("%s: %s", requestError, missingID)) 347 | } 348 | 349 | req := &DeleteRequest{} 350 | if err := ctx.BodyParser(req); err != nil { 351 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 352 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 353 | } 354 | if err := c.validate.Struct(req); err != nil { 355 | c.logger.Error("%s: %s", requestError, missingID) 356 | return fiber.NewError(statusBadRequest, fmt.Sprintf("%s: %s", requestError, err)) 357 | } 358 | 359 | userInput := &User{ 360 | ID: id, 361 | Password: req.Password, 362 | } 363 | 364 | err := c.usecases.DestroyUser(userInput) 365 | if err != nil { 366 | c.logger.Error("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), internalError, err) 367 | return fiber.NewError(statusInternalServerError, fmt.Sprintf("%s: %s", internalError, err)) 368 | } 369 | 370 | c.logger.Info("%s path[%s] -> %s: %s", ctx.Method(), ctx.Path(), processed, ctx.BaseURL()) 371 | return ctx.Status(fiber.StatusNoContent).JSON(fiber.Map{"msg": deleted}) 372 | } 373 | -------------------------------------------------------------------------------- /pkg/internal/repository/repository_test.go: -------------------------------------------------------------------------------- 1 | // Package repository_test contains test for repository 2 | package repository_test 3 | 4 | import ( 5 | "dall06/go-cleanapi/pkg/internal" 6 | "dall06/go-cleanapi/pkg/internal/repository" 7 | "database/sql" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/DATA-DOG/go-sqlmock" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | const ( 16 | spCreate = "CALL `go_cleanapi`.`sp_create_user`(?, ?, ?, ?);" 17 | spRead = "CALL `go_cleanapi`.`sp_read_user`(?);" 18 | spReadAll = "CALL `go_cleanapi`.`sp_read_users`();" 19 | spUpdate = "CALL `go_cleanapi`.`sp_update_user`(?, ?, ?, ?);" 20 | spDelete = "CALL `go_cleanapi`.`sp_delete_user`(?, ?);" 21 | spLogin = "CALL `go_cleanapi`.`sp_login_user`(?, ?, ?);" 22 | ) 23 | 24 | func TestLogin(test *testing.T) { 25 | dbUserOne := &internal.User{ 26 | ID: "im an id", 27 | Email: "test@test.com", 28 | Phone: "+7812324524", 29 | Password: "12345pAsSWORd*", 30 | } 31 | 32 | dbUserTwo := &internal.User{ 33 | Email: "test@test.com", 34 | Phone: "", 35 | Password: "12345pAsSWORd*", 36 | } 37 | dbUserThree := &internal.User{ 38 | Email: "", 39 | Phone: "+7812324524", 40 | Password: "12345pAsSWORd*", 41 | } 42 | 43 | rowsSetOne := sqlmock.NewRows([]string{ 44 | "id_user", 45 | }).AddRow( 46 | &dbUserOne.ID, 47 | ) 48 | 49 | rowsSetOneTwo := sqlmock.NewRows([]string{ 50 | "id_user", 51 | }).AddRow( 52 | &dbUserOne.ID, 53 | ) 54 | 55 | rowsSetTwo := sqlmock.NewRows([]string{ 56 | "id_user", 57 | }) 58 | 59 | inputUserOne := &internal.User{ 60 | Email: "test@test.com", 61 | Phone: "", 62 | Password: "12345pAsSWORd*", 63 | } 64 | 65 | inputUserTwo := &internal.User{ 66 | Email: "", 67 | Phone: "+7812324524", 68 | Password: "12345pAsSWORd*", 69 | } 70 | 71 | inputUserThree := &internal.User{ 72 | ID: "im an id", 73 | Email: "test@test.com", 74 | Phone: "+7812324524", 75 | Password: "12345pAsSWORd*", 76 | } 77 | 78 | inputUserFour := &internal.User{ 79 | Email: "test2@test.com", 80 | Password: "12345pAsSWORd*", 81 | } 82 | 83 | inputUserFive := &internal.User{ 84 | Phone: "+8812324524", 85 | Password: "12345pAsSWORd*", 86 | } 87 | 88 | expectedOne := &internal.User{ 89 | ID: "im an id", 90 | } 91 | 92 | successfulCases := []struct { 93 | name string 94 | input *internal.User 95 | rows *sqlmock.Rows 96 | dbUser *internal.User 97 | expected *internal.User 98 | }{ 99 | { 100 | name: "it should login (mocked), whit email", 101 | input: inputUserOne, 102 | rows: rowsSetOne, 103 | dbUser: dbUserTwo, 104 | expected: expectedOne, 105 | }, 106 | { 107 | name: "it should login (mocked), with phone", 108 | input: inputUserTwo, 109 | rows: rowsSetOneTwo, 110 | dbUser: dbUserThree, 111 | expected: expectedOne, 112 | }, 113 | } 114 | 115 | failedCases := []struct { 116 | name string 117 | input *internal.User 118 | rows *sqlmock.Rows 119 | dbUser *internal.User 120 | expected *internal.User 121 | }{ 122 | { 123 | name: "it should not login (mocked), empty user", 124 | input: nil, 125 | rows: rowsSetOne, 126 | dbUser: dbUserOne, 127 | expected: expectedOne, 128 | }, 129 | { 130 | name: "it should not login (mocked), empty db", 131 | input: inputUserTwo, 132 | rows: rowsSetTwo, 133 | dbUser: dbUserOne, 134 | expected: expectedOne, 135 | }, 136 | { 137 | name: "it should not login (mocked), no id found", 138 | input: inputUserThree, 139 | rows: rowsSetOne, 140 | dbUser: dbUserOne, 141 | expected: expectedOne, 142 | }, 143 | { 144 | name: "it should not login (mocked), no password found", 145 | input: inputUserThree, 146 | rows: rowsSetOne, 147 | dbUser: dbUserOne, 148 | expected: expectedOne, 149 | }, 150 | { 151 | name: "it should not login (mocked), no email or phone", 152 | input: inputUserThree, 153 | rows: rowsSetOne, 154 | dbUser: dbUserOne, 155 | expected: expectedOne, 156 | }, 157 | { 158 | name: "it should not login (mocked), both params found", 159 | input: inputUserThree, 160 | rows: rowsSetOne, 161 | dbUser: dbUserOne, 162 | expected: expectedOne, 163 | }, 164 | { 165 | name: "it should not login (mocked), email not found", 166 | input: inputUserFour, 167 | rows: rowsSetOne, 168 | dbUser: dbUserOne, 169 | expected: expectedOne, 170 | }, 171 | { 172 | name: "it should not login (mocked), phone not found", 173 | input: inputUserFive, 174 | rows: rowsSetOne, 175 | dbUser: dbUserOne, 176 | expected: expectedOne, 177 | }, 178 | } 179 | 180 | for _, tc := range successfulCases { 181 | tc := tc 182 | 183 | test.Run(tc.name, func(t *testing.T) { 184 | t.Parallel() 185 | 186 | db, m, err := sqlmock.New() 187 | if err != nil { 188 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 189 | } 190 | 191 | assert.NoError(t, err) 192 | 193 | m.ExpectQuery(regexp.QuoteMeta(spLogin)).WithArgs( 194 | tc.dbUser.Email, 195 | tc.dbUser.Phone, 196 | tc.dbUser.Password, 197 | ).WillReturnRows(tc.rows) 198 | 199 | assert.NoError(t, err) 200 | 201 | r := repository.NewRepository(db) 202 | res, err := r.Login(tc.input) 203 | assert.NoError(t, err) 204 | assert.Equal(t, tc.expected, res) 205 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 206 | 207 | }) 208 | } 209 | 210 | for _, tc := range failedCases { 211 | tc := tc 212 | 213 | test.Run(tc.name, func(t *testing.T) { 214 | t.Parallel() 215 | db, m, err := sqlmock.New() 216 | if err != nil { 217 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 218 | } 219 | 220 | assert.NoError(t, err) 221 | 222 | m.ExpectQuery(regexp.QuoteMeta(spLogin)).WithArgs( 223 | tc.dbUser.Email, 224 | tc.dbUser.Phone, 225 | tc.dbUser.Password, 226 | ).WillReturnRows(tc.rows) 227 | assert.NoError(t, err) 228 | 229 | r := repository.NewRepository(db) 230 | res, err := r.Login(tc.input) 231 | assert.Error(t, err) 232 | assert.NotEqual(t, tc.expected, res) 233 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 234 | 235 | }) 236 | } 237 | } 238 | 239 | func TestCreate(test *testing.T) { 240 | dbUserOne := &internal.User{ 241 | ID: "im an id", 242 | Email: "test@test.com", 243 | Phone: "+7812324524", 244 | Password: "12345pAsSWORd*", 245 | } 246 | 247 | dbUserTwo := &internal.User{ 248 | ID: "im an id", 249 | Email: "test@test.com", 250 | Phone: "", 251 | Password: "12345pAsSWORd*", 252 | } 253 | 254 | inputUserOne := &internal.User{ 255 | ID: "im an id", 256 | Email: "test@test.com", 257 | Phone: "+7812324524", 258 | Password: "12345pAsSWORd*", 259 | } 260 | 261 | inputUserTwo := &internal.User{ 262 | ID: "", 263 | Email: "test@test.com", 264 | Phone: "+7812324524", 265 | Password: "12345pAsSWORd*", 266 | } 267 | 268 | inputUserThree := &internal.User{ 269 | ID: "im an id", 270 | Email: "", 271 | Phone: "+7812324524", 272 | Password: "12345pAsSWORd*", 273 | } 274 | 275 | inputUserFour := &internal.User{ 276 | ID: "im an id", 277 | Email: "test@test.com", 278 | Phone: "", 279 | Password: "12345pAsSWORd*", 280 | } 281 | 282 | inputUserFive := &internal.User{ 283 | ID: "im an id", 284 | Email: "test@test.com", 285 | Phone: "+7812324524", 286 | Password: "", 287 | } 288 | 289 | inputUserSix := &internal.User{ 290 | ID: "im an id but wrong", 291 | Email: "test@test.com", 292 | Phone: "+7812324524", 293 | Password: "12345pAsSWORd*", 294 | } 295 | 296 | successfulCases := []struct { 297 | name string 298 | input *internal.User 299 | dbUser *internal.User 300 | }{ 301 | { 302 | name: "it should create an user (mocked)", 303 | input: inputUserOne, 304 | dbUser: dbUserOne, 305 | }, 306 | { 307 | name: "it should create an user (mocked) besides empty phone", 308 | input: inputUserFour, 309 | dbUser: dbUserTwo, 310 | }, 311 | } 312 | 313 | failedCases := []struct { 314 | name string 315 | input *internal.User 316 | dbUser *internal.User 317 | }{ 318 | { 319 | name: "it should not create an user (mocked), empty id", 320 | input: inputUserTwo, 321 | dbUser: dbUserOne, 322 | }, 323 | { 324 | name: "it should not create an user (mocked), empty email", 325 | input: inputUserThree, 326 | dbUser: dbUserOne, 327 | }, 328 | { 329 | name: "it should not create an user (mocked), empty password", 330 | input: inputUserFive, 331 | dbUser: dbUserOne, 332 | }, 333 | { 334 | name: "it should not create an user (mocked), nil user", 335 | input: nil, 336 | dbUser: dbUserOne, 337 | }, 338 | { 339 | name: "it should not create an user (mocked), internal error", 340 | input: inputUserSix, 341 | dbUser: dbUserOne, 342 | }, 343 | } 344 | 345 | for _, tc := range successfulCases { 346 | tc := tc 347 | 348 | test.Run(tc.name, func(t *testing.T) { 349 | t.Parallel() 350 | 351 | db, m, err := sqlmock.New() 352 | if err != nil { 353 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 354 | } 355 | 356 | assert.NoError(t, err) 357 | 358 | m.ExpectExec(regexp.QuoteMeta(spCreate)).WithArgs( 359 | &tc.dbUser.ID, 360 | &tc.dbUser.Email, 361 | &tc.dbUser.Phone, 362 | &tc.dbUser.Password, 363 | ).WillReturnResult(sqlmock.NewResult(0, 0)) 364 | 365 | r := repository.NewRepository(db) 366 | err = r.Create(tc.input) 367 | assert.NoError(t, err) 368 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 369 | 370 | }) 371 | } 372 | 373 | for _, tc := range failedCases { 374 | tc := tc 375 | 376 | test.Run(tc.name, func(t *testing.T) { 377 | t.Parallel() 378 | 379 | db, m, err := sqlmock.New() 380 | if err != nil { 381 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 382 | } 383 | 384 | assert.NoError(t, err) 385 | 386 | m.ExpectExec(regexp.QuoteMeta(spCreate)).WithArgs( 387 | &tc.dbUser.ID, 388 | &tc.dbUser.Email, 389 | &tc.dbUser.Phone, 390 | &tc.dbUser.Password, 391 | ).WillReturnResult(sqlmock.NewResult(0, 0)) 392 | 393 | r := repository.NewRepository(db) 394 | err = r.Create(tc.input) 395 | assert.Error(t, err) 396 | 397 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 398 | 399 | }) 400 | } 401 | } 402 | 403 | func TestRead(test *testing.T) { 404 | dbUserOne := &internal.User{ 405 | ID: "im an id", 406 | Email: "test@test.com", 407 | Phone: "+7812324524", 408 | Password: "12345pAsSWORd*", 409 | } 410 | 411 | rowsSetOne := sqlmock.NewRows([]string{ 412 | "id_user", 413 | "user_email", 414 | "user_phone", 415 | }).AddRow( 416 | &dbUserOne.ID, 417 | &dbUserOne.Email, 418 | &dbUserOne.Phone, 419 | ) 420 | 421 | rowsSetTwo := sqlmock.NewRows([]string{ 422 | "id_user", 423 | "user_email", 424 | "user_phone", 425 | }) 426 | 427 | inputUserOne := &internal.User{ 428 | ID: "im an id", 429 | } 430 | 431 | inputUserTwo := &internal.User{ 432 | ID: "", 433 | } 434 | 435 | inputUserThree := &internal.User{ 436 | ID: "im an id two", 437 | } 438 | 439 | expectedOne := &internal.User{ 440 | ID: "im an id", 441 | Email: "test@test.com", 442 | Phone: "+7812324524", 443 | } 444 | 445 | successfulCases := []struct { 446 | name string 447 | input *internal.User 448 | rows *sqlmock.Rows 449 | dbUser *internal.User 450 | expected *internal.User 451 | }{ 452 | { 453 | name: "it should read an user (mocked)", 454 | input: inputUserOne, 455 | rows: rowsSetOne, 456 | dbUser: dbUserOne, 457 | expected: expectedOne, 458 | }, 459 | } 460 | 461 | failedCases := []struct { 462 | name string 463 | input *internal.User 464 | rows *sqlmock.Rows 465 | dbUser *internal.User 466 | expected *internal.User 467 | }{ 468 | { 469 | name: "it should not read an user (mocked), empty id", 470 | input: inputUserTwo, 471 | rows: rowsSetOne, 472 | dbUser: dbUserOne, 473 | expected: expectedOne, 474 | }, 475 | { 476 | name: "it should not read an user (mocked), empty user", 477 | input: nil, 478 | rows: rowsSetOne, 479 | dbUser: dbUserOne, 480 | expected: expectedOne, 481 | }, 482 | { 483 | name: "it should not read an user (mocked), empty db", 484 | input: inputUserTwo, 485 | rows: rowsSetTwo, 486 | dbUser: dbUserOne, 487 | expected: expectedOne, 488 | }, 489 | { 490 | name: "it should not read an user (mocked), no id found", 491 | input: inputUserThree, 492 | rows: rowsSetOne, 493 | dbUser: dbUserOne, 494 | expected: expectedOne, 495 | }, 496 | } 497 | 498 | for _, tc := range successfulCases { 499 | tc := tc 500 | 501 | test.Run(tc.name, func(t *testing.T) { 502 | t.Parallel() 503 | 504 | db, m, err := sqlmock.New() 505 | if err != nil { 506 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 507 | } 508 | 509 | assert.NoError(t, err) 510 | 511 | m.ExpectQuery(regexp.QuoteMeta(spRead)).WithArgs( 512 | &tc.dbUser.ID, 513 | ).WillReturnRows(tc.rows) 514 | 515 | r := repository.NewRepository(db) 516 | res, err := r.Read(tc.input) 517 | assert.NoError(t, err) 518 | assert.Equal(t, tc.expected, res) 519 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 520 | 521 | }) 522 | } 523 | 524 | for _, tc := range failedCases { 525 | tc := tc 526 | 527 | test.Run(tc.name, func(t *testing.T) { 528 | t.Parallel() 529 | db, m, err := sqlmock.New() 530 | if err != nil { 531 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 532 | } 533 | 534 | assert.NoError(t, err) 535 | 536 | m.ExpectQuery(regexp.QuoteMeta(spRead)).WithArgs( 537 | &tc.dbUser.ID, 538 | ).WillReturnRows(tc.rows) 539 | 540 | r := repository.NewRepository(db) 541 | res, err := r.Read(tc.input) 542 | assert.Error(t, err) 543 | assert.NotEqual(t, tc.expected, res, "") 544 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 545 | 546 | }) 547 | } 548 | } 549 | 550 | func TestReadAll(test *testing.T) { 551 | dbUserOne := &internal.User{ 552 | ID: "im an id", 553 | Email: "test@test.com", 554 | Phone: "+7812324524", 555 | } 556 | dbUserTwo := &internal.User{ 557 | ID: "im an id 2", 558 | Email: "test2@test.com", 559 | Phone: "+8812324524", 560 | } 561 | dbUserThree := &internal.User{ 562 | ID: "im an id 3", 563 | Email: "test2@test.com", 564 | Phone: "+8812324524", 565 | } 566 | 567 | rowsSetOne := sqlmock.NewRows([]string{ 568 | "id_user", 569 | "user_email", 570 | "user_phone", 571 | }).AddRow( 572 | &dbUserOne.ID, 573 | &dbUserOne.Email, 574 | &dbUserOne.Phone, 575 | ).AddRow( 576 | &dbUserTwo.ID, 577 | &dbUserTwo.Email, 578 | &dbUserTwo.Phone, 579 | ) 580 | 581 | rowsSetTwo := sqlmock.NewRows([]string{"id_user", "user_email", "user_phone"}) 582 | 583 | expectedOnes := make(internal.Users, 0) 584 | expectedOnes = append(expectedOnes, dbUserOne) 585 | expectedOnes = append(expectedOnes, dbUserTwo) 586 | 587 | expectedOnesTwo := make(internal.Users, 0) 588 | 589 | expectedOnesThree := make(internal.Users, 0) 590 | expectedOnesThree = append(expectedOnesThree, dbUserOne) 591 | expectedOnesThree = append(expectedOnesThree, dbUserThree) 592 | 593 | successfulCases := []struct { 594 | name string 595 | rows *sqlmock.Rows 596 | expected internal.Users 597 | }{ 598 | { 599 | name: "it should read many users (mocked)", 600 | rows: rowsSetOne, 601 | expected: expectedOnes, 602 | }, 603 | { 604 | name: "it should read many users (mocked), but empty db", 605 | rows: rowsSetTwo, 606 | expected: expectedOnesTwo, 607 | }, 608 | } 609 | 610 | failedCases := []struct { 611 | name string 612 | rows *sqlmock.Rows 613 | dbUser internal.Users 614 | expected internal.Users 615 | }{ 616 | { 617 | name: "it should not read many users (mocked), wrong values returned", 618 | rows: rowsSetOne, 619 | expected: expectedOnesThree, 620 | }, 621 | } 622 | 623 | for _, tc := range successfulCases { 624 | tc := tc 625 | test.Run(tc.name, func(t *testing.T) { 626 | t.Parallel() 627 | db, m, err := sqlmock.New() 628 | if err != nil { 629 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 630 | } 631 | 632 | assert.NoError(t, err) 633 | 634 | m.ExpectQuery(regexp.QuoteMeta(spReadAll)).WillReturnRows(tc.rows) 635 | 636 | r := repository.NewRepository(db) 637 | res, err := r.ReadAll() 638 | assert.NoError(t, err) 639 | assert.Equal(t, tc.expected, res) 640 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 641 | 642 | }) 643 | } 644 | 645 | for _, tc := range failedCases { 646 | tc := tc 647 | test.Run(tc.name, func(t *testing.T) { 648 | t.Parallel() 649 | 650 | db, m, err := sqlmock.New() 651 | if err != nil { 652 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 653 | } 654 | 655 | assert.NoError(t, err) 656 | m.ExpectQuery(regexp.QuoteMeta(spReadAll)).WillReturnRows(tc.rows) 657 | 658 | r := repository.NewRepository(db) 659 | res, err := r.ReadAll() 660 | assert.NoError(t, err) 661 | assert.NotEqual(t, tc.expected, res) 662 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 663 | 664 | }) 665 | } 666 | } 667 | 668 | func TestUpdate(test *testing.T) { 669 | dbUserOne := &internal.User{ 670 | ID: "im an id", 671 | Email: "test@test.com", 672 | Phone: "+7812324524", 673 | Password: "12345pAsSWORd*", 674 | } 675 | 676 | dbUserTwo := &internal.User{ 677 | ID: "im an id", 678 | Email: "test@test.com", 679 | Phone: "", 680 | Password: "12345pAsSWORd*", 681 | } 682 | 683 | dbUserThree := &internal.User{ 684 | ID: "im an id", 685 | Email: "", 686 | Phone: "+7812324524", 687 | Password: "12345pAsSWORd*", 688 | } 689 | 690 | inputUserOne := &internal.User{ 691 | ID: "im an id", 692 | Email: "test@test.com", 693 | Phone: "+7812324524", 694 | Password: "12345pAsSWORd*", 695 | } 696 | 697 | inputUserTwo := &internal.User{ 698 | ID: "im an id", 699 | Email: "test@test.com", 700 | Phone: "", 701 | Password: "12345pAsSWORd*", 702 | } 703 | 704 | inputUserThree := &internal.User{ 705 | ID: "im an id", 706 | Email: "", 707 | Phone: "+7812324524", 708 | Password: "12345pAsSWORd*", 709 | } 710 | 711 | inputUserFour := &internal.User{ 712 | ID: "", 713 | Email: "test@test.com", 714 | Phone: "+7812324524", 715 | Password: "12345pAsSWORd*", 716 | } 717 | 718 | inputUserFive := &internal.User{ 719 | ID: "im an id", 720 | Email: "test@test.com", 721 | Phone: "+7812324524", 722 | Password: "", 723 | } 724 | 725 | inputUserSix := &internal.User{ 726 | ID: "im an id but wrong", 727 | Email: "test@test.com", 728 | Phone: "+7812324524", 729 | Password: "12345pAsSWORd*", 730 | } 731 | 732 | successfulCases := []struct { 733 | name string 734 | input *internal.User 735 | dbUser *internal.User 736 | }{ 737 | { 738 | name: "it should update an user (mocked)", 739 | input: inputUserOne, 740 | dbUser: dbUserOne, 741 | }, 742 | { 743 | name: "it should update an user (mocked) besides empty phone", 744 | input: inputUserTwo, 745 | dbUser: dbUserTwo, 746 | }, 747 | { 748 | name: "it should update an user (mocked) besides empty email", 749 | input: inputUserThree, 750 | dbUser: dbUserThree, 751 | }, 752 | } 753 | 754 | failedCases := []struct { 755 | name string 756 | input *internal.User 757 | dbUser *internal.User 758 | }{ 759 | { 760 | name: "it should not update an user (mocked), empty id", 761 | input: inputUserFour, 762 | dbUser: dbUserOne, 763 | }, 764 | { 765 | name: "it should not update an user (mocked), empty password", 766 | input: inputUserFive, 767 | dbUser: dbUserOne, 768 | }, 769 | { 770 | name: "it should not update an user (mocked), nil user", 771 | input: nil, 772 | dbUser: dbUserOne, 773 | }, 774 | { 775 | name: "it should not update an user (mocked), id not found", 776 | input: inputUserSix, 777 | dbUser: dbUserOne, 778 | }, 779 | } 780 | 781 | for _, tc := range successfulCases { 782 | tc := tc 783 | 784 | test.Run(tc.name, func(t *testing.T) { 785 | t.Parallel() 786 | 787 | db, m, err := sqlmock.New() 788 | if err != nil { 789 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 790 | } 791 | 792 | assert.NoError(t, err) 793 | 794 | m.ExpectExec(regexp.QuoteMeta(spUpdate)).WithArgs( 795 | &tc.dbUser.ID, 796 | &tc.dbUser.Email, 797 | &tc.dbUser.Phone, 798 | &tc.dbUser.Password, 799 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 800 | 801 | r := repository.NewRepository(db) 802 | err = r.Update(tc.input) 803 | assert.NoError(t, err) 804 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 805 | 806 | }) 807 | } 808 | 809 | for _, tc := range failedCases { 810 | tc := tc 811 | 812 | test.Run(tc.name, func(t *testing.T) { 813 | t.Parallel() 814 | 815 | db, m, err := sqlmock.New() 816 | if err != nil { 817 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 818 | } 819 | 820 | assert.NoError(t, err) 821 | 822 | m.ExpectExec(regexp.QuoteMeta(spUpdate)).WithArgs( 823 | &tc.dbUser.ID, 824 | &tc.dbUser.Email, 825 | &tc.dbUser.Phone, 826 | &tc.dbUser.Password, 827 | ).WillReturnResult(sqlmock.NewResult(0, 0)) 828 | 829 | r := repository.NewRepository(db) 830 | err = r.Update(tc.input) 831 | assert.Error(t, err) 832 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 833 | 834 | }) 835 | } 836 | } 837 | 838 | func TestDelete(test *testing.T) { 839 | dbUserOne := &internal.User{ 840 | ID: "im an id", 841 | Email: "test@test.com", 842 | Phone: "+7812324524", 843 | Password: "12345pAsSWORd*", 844 | } 845 | 846 | inputUserOne := &internal.User{ 847 | ID: "im an id", 848 | Email: "test@test.com", 849 | Phone: "+7812324524", 850 | Password: "12345pAsSWORd*", 851 | } 852 | 853 | inputUserFour := &internal.User{ 854 | ID: "", 855 | Email: "test@test.com", 856 | Phone: "+7812324524", 857 | Password: "12345pAsSWORd*", 858 | } 859 | 860 | inputUserFive := &internal.User{ 861 | ID: "im an id", 862 | Email: "test@test.com", 863 | Phone: "+7812324524", 864 | Password: "", 865 | } 866 | 867 | inputUserSix := &internal.User{ 868 | ID: "im an id but wrong", 869 | Email: "test@test.com", 870 | Phone: "+7812324524", 871 | Password: "12345pAsSWORd*", 872 | } 873 | 874 | successfulCases := []struct { 875 | name string 876 | input *internal.User 877 | dbUser *internal.User 878 | }{ 879 | { 880 | name: "it should delete an user (mocked)", 881 | input: inputUserOne, 882 | dbUser: dbUserOne, 883 | }, 884 | } 885 | 886 | failedCases := []struct { 887 | name string 888 | input *internal.User 889 | dbUser *internal.User 890 | }{ 891 | { 892 | name: "it should not delete an user (mocked), empty id", 893 | input: inputUserFour, 894 | dbUser: dbUserOne, 895 | }, 896 | { 897 | name: "it should not delete an user (mocked), empty password", 898 | input: inputUserFive, 899 | dbUser: dbUserOne, 900 | }, 901 | { 902 | name: "it should not delete an user (mocked), nil user", 903 | input: nil, 904 | dbUser: dbUserOne, 905 | }, 906 | { 907 | name: "it should not delete an user (mocked), id not found", 908 | input: inputUserSix, 909 | dbUser: dbUserOne, 910 | }, 911 | } 912 | 913 | for _, tc := range successfulCases { 914 | tc := tc 915 | 916 | test.Run(tc.name, func(t *testing.T) { 917 | t.Parallel() 918 | 919 | db, m, err := sqlmock.New() 920 | if err != nil { 921 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 922 | } 923 | 924 | assert.NoError(t, err) 925 | 926 | m.ExpectExec(regexp.QuoteMeta(spDelete)).WithArgs( 927 | &tc.dbUser.ID, 928 | &tc.dbUser.Password, 929 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 930 | 931 | r := repository.NewRepository(db) 932 | err = r.Delete(tc.input) 933 | assert.NoError(t, err) 934 | 935 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 936 | 937 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 938 | 939 | }) 940 | } 941 | 942 | for _, tc := range failedCases { 943 | tc := tc 944 | 945 | test.Run(tc.name, func(t *testing.T) { 946 | t.Parallel() 947 | 948 | db, m, err := sqlmock.New() 949 | if err != nil { 950 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 951 | } 952 | 953 | assert.NoError(t, err) 954 | 955 | m.ExpectExec(regexp.QuoteMeta(spDelete)).WithArgs( 956 | &tc.dbUser.ID, 957 | &tc.dbUser.Password, 958 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 959 | 960 | r := repository.NewRepository(db) 961 | err = r.Delete(tc.input) 962 | assert.Error(t, err) 963 | 964 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 965 | 966 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 967 | 968 | }) 969 | } 970 | } 971 | -------------------------------------------------------------------------------- /pkg/internal/usecases/usecases_test.go: -------------------------------------------------------------------------------- 1 | // Package usecaes_test contains UseCases test 2 | package usecases_test 3 | 4 | import ( 5 | "dall06/go-cleanapi/pkg/adapter/controller" 6 | "dall06/go-cleanapi/pkg/internal" 7 | "dall06/go-cleanapi/pkg/internal/repository" 8 | "dall06/go-cleanapi/pkg/internal/usecases" 9 | "dall06/go-cleanapi/utils" 10 | "database/sql" 11 | "fmt" 12 | "regexp" 13 | "testing" 14 | 15 | "github.com/DATA-DOG/go-sqlmock" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | const ( 20 | spCreate = "CALL `go_cleanapi`.`sp_create_user`(?, ?, ?, ?);" 21 | spRead = "CALL `go_cleanapi`.`sp_read_user`(?);" 22 | spReadAll = "CALL `go_cleanapi`.`sp_read_users`();" 23 | spUpdate = "CALL `go_cleanapi`.`sp_update_user`(?, ?, ?, ?);" 24 | spDelete = "CALL `go_cleanapi`.`sp_delete_user`(?, ?);" 25 | spLogin = "CALL `go_cleanapi`.`sp_login_user`(?, ?, ?);" 26 | ) 27 | 28 | func TestAuthUser(test *testing.T) { 29 | dbUserOne := &internal.User{ 30 | ID: "im an id", 31 | Email: "test@test.com", 32 | Phone: "+991234567890", 33 | Password: "12345pAsSWORd*", 34 | } 35 | 36 | dbUserTwo := &internal.User{ 37 | Email: "test@test.com", 38 | Phone: "", 39 | Password: "12345pAsSWORd*", 40 | } 41 | dbUserThree := &internal.User{ 42 | Email: "", 43 | Phone: "+991234567890", 44 | Password: "12345pAsSWORd*", 45 | } 46 | 47 | rowsSetOne := sqlmock.NewRows([]string{ 48 | "id_user", 49 | }).AddRow( 50 | &dbUserOne.ID, 51 | ) 52 | 53 | rowsSetOneTwo := sqlmock.NewRows([]string{ 54 | "id_user", 55 | }).AddRow( 56 | &dbUserOne.ID, 57 | ) 58 | 59 | rowsSetTwo := sqlmock.NewRows([]string{ 60 | "id_user", 61 | }) 62 | 63 | inputUserOne := &controller.User{ 64 | Email: "test@test.com", 65 | Phone: "", 66 | Password: "12345pAsSWORd*", 67 | } 68 | 69 | inputUserTwo := &controller.User{ 70 | Email: "", 71 | Phone: "+991234567890", 72 | Password: "12345pAsSWORd*", 73 | } 74 | 75 | inputUserThree := &controller.User{ 76 | Email: "test@test.com", 77 | Phone: "+991234567890", 78 | Password: "12345pAsSWORd*", 79 | } 80 | 81 | inputUserSix := &controller.User{ 82 | Email: "test2@test.com", 83 | Password: "12345pAsSWORd*", 84 | } 85 | 86 | inputUserFive := &controller.User{ 87 | Email: "", 88 | Phone: "+521234567890", 89 | Password: "12345pAsSWORd*", 90 | } 91 | 92 | expectedOne := &internal.User{ 93 | ID: "im an id", 94 | } 95 | 96 | successfulCases := []struct { 97 | name string 98 | input *controller.User 99 | rows *sqlmock.Rows 100 | dbUser *internal.User 101 | expected *internal.User 102 | }{ 103 | { 104 | name: "it should login (mocked), whit email", 105 | input: inputUserOne, 106 | rows: rowsSetOne, 107 | dbUser: dbUserTwo, 108 | expected: expectedOne, 109 | }, 110 | { 111 | name: "it should login (mocked), with phone", 112 | input: inputUserTwo, 113 | rows: rowsSetOneTwo, 114 | dbUser: dbUserThree, 115 | expected: expectedOne, 116 | }, 117 | } 118 | 119 | failedCases := []struct { 120 | name string 121 | input *controller.User 122 | rows *sqlmock.Rows 123 | dbUser *internal.User 124 | expected *internal.User 125 | }{ 126 | { 127 | name: "it should not login (mocked), empty user", 128 | input: nil, 129 | rows: rowsSetOne, 130 | dbUser: dbUserOne, 131 | expected: expectedOne, 132 | }, 133 | { 134 | name: "it should not login (mocked), empty db", 135 | input: inputUserTwo, 136 | rows: rowsSetTwo, 137 | dbUser: dbUserOne, 138 | expected: expectedOne, 139 | }, 140 | { 141 | name: "it should not login (mocked), no id found", 142 | input: inputUserThree, 143 | rows: rowsSetOne, 144 | dbUser: dbUserOne, 145 | expected: expectedOne, 146 | }, 147 | { 148 | name: "it should not login (mocked), no password found", 149 | input: inputUserThree, 150 | rows: rowsSetOne, 151 | dbUser: dbUserOne, 152 | expected: expectedOne, 153 | }, 154 | { 155 | name: "it should not login (mocked), no email or phone", 156 | input: inputUserThree, 157 | rows: rowsSetOne, 158 | dbUser: dbUserOne, 159 | expected: expectedOne, 160 | }, 161 | { 162 | name: "it should not login (mocked), both params found", 163 | input: inputUserThree, 164 | rows: rowsSetOne, 165 | dbUser: dbUserOne, 166 | expected: expectedOne, 167 | }, 168 | { 169 | name: "it should not login (mocked), no email found", 170 | input: inputUserThree, 171 | rows: rowsSetOne, 172 | dbUser: dbUserOne, 173 | expected: expectedOne, 174 | }, 175 | { 176 | name: "it should not login (mocked), no phone found", 177 | input: inputUserFive, 178 | rows: rowsSetOne, 179 | dbUser: dbUserOne, 180 | expected: expectedOne, 181 | }, 182 | { 183 | name: "it should not login (mocked), no email found", 184 | input: inputUserSix, 185 | rows: rowsSetOne, 186 | dbUser: dbUserOne, 187 | expected: expectedOne, 188 | }, 189 | { 190 | name: "it should not login (mocked), no phone found", 191 | input: inputUserThree, 192 | rows: rowsSetOne, 193 | dbUser: dbUserOne, 194 | expected: expectedOne, 195 | }, 196 | } 197 | 198 | for _, tc := range successfulCases { 199 | tc := tc 200 | 201 | test.Run(tc.name, func(t *testing.T) { 202 | t.Parallel() 203 | 204 | db, m, err := sqlmock.New() 205 | if err != nil { 206 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 207 | } 208 | assert.NoError(t, err) 209 | 210 | m.ExpectQuery(regexp.QuoteMeta(spLogin)).WithArgs( 211 | tc.dbUser.Email, 212 | tc.dbUser.Phone, 213 | tc.dbUser.Password, 214 | ).WillReturnRows(tc.rows) 215 | assert.NoError(t, err) 216 | 217 | r := repository.NewRepository(db) 218 | uuid := utils.NewUUIDMock() 219 | uc := usecases.NewUseCases(r, uuid) 220 | res, err := uc.AuthUser(tc.input) 221 | 222 | assert.NoError(t, err) 223 | assert.Equal(t, tc.expected, res) 224 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 225 | 226 | }) 227 | } 228 | 229 | for _, tc := range failedCases { 230 | tc := tc 231 | 232 | test.Run(tc.name, func(t *testing.T) { 233 | t.Parallel() 234 | db, m, err := sqlmock.New() 235 | if err != nil { 236 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 237 | } 238 | assert.NoError(t, err) 239 | 240 | m.ExpectQuery(regexp.QuoteMeta(spLogin)).WithArgs( 241 | tc.dbUser.Email, 242 | tc.dbUser.Phone, 243 | tc.dbUser.Password, 244 | ).WillReturnRows(tc.rows) 245 | assert.NoError(t, err) 246 | 247 | r := repository.NewRepository(db) 248 | uuid := utils.NewUUIDMock() 249 | uc := usecases.NewUseCases(r, uuid) 250 | res, err := uc.AuthUser(tc.input) 251 | 252 | assert.Error(t, err) 253 | assert.NotEqual(t, tc.expected, res) 254 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 255 | 256 | }) 257 | } 258 | } 259 | 260 | func TestRegisterUser(test *testing.T) { 261 | dbUserOne := &internal.User{ 262 | Email: "test@test.com", 263 | Phone: "+991234567890", 264 | Password: "12345pAsSWORd*", 265 | } 266 | 267 | dbUserTwo := &internal.User{ 268 | Email: "test@test.com", 269 | Phone: "", 270 | Password: "12345pAsSWORd*", 271 | } 272 | 273 | inputUserOne := &controller.User{ 274 | Email: "test@test.com", 275 | Phone: "+991234567890", 276 | Password: "12345pAsSWORd*", 277 | } 278 | 279 | inputUserThree := &controller.User{ 280 | Email: "", 281 | Phone: "+991234567890", 282 | Password: "12345pAsSWORd*", 283 | } 284 | 285 | inputUserFour := &controller.User{ 286 | Email: "test@test.com", 287 | Phone: "", 288 | Password: "12345pAsSWORd*", 289 | } 290 | 291 | inputUserFive := &controller.User{ 292 | Email: "test@test.com", 293 | Phone: "+991234567890", 294 | Password: "", 295 | } 296 | 297 | inputUserSix := &controller.User{ 298 | Email: "test2@test.com", 299 | Phone: "+991234567890", 300 | Password: "12345pAsSWORd*", 301 | } 302 | 303 | successfulCases := []struct { 304 | name string 305 | input *controller.User 306 | dbUser *internal.User 307 | }{ 308 | { 309 | name: "it should create an user (mocked)", 310 | input: inputUserOne, 311 | dbUser: dbUserOne, 312 | }, 313 | { 314 | name: "it should create an user (mocked) besides empty phone", 315 | input: inputUserFour, 316 | dbUser: dbUserTwo, 317 | }, 318 | } 319 | 320 | failedCases := []struct { 321 | name string 322 | input *controller.User 323 | dbUser *internal.User 324 | }{ 325 | { 326 | name: "it should not create an user (mocked), empty email", 327 | input: inputUserThree, 328 | dbUser: dbUserOne, 329 | }, 330 | { 331 | name: "it should not create an user (mocked), empty password", 332 | input: inputUserFive, 333 | dbUser: dbUserOne, 334 | }, 335 | { 336 | name: "it should not create an user (mocked), nil user", 337 | input: nil, 338 | dbUser: dbUserOne, 339 | }, 340 | { 341 | name: "it should not create an user (mocked), internal error", 342 | input: inputUserSix, 343 | dbUser: dbUserOne, 344 | }, 345 | } 346 | 347 | for _, tc := range successfulCases { 348 | tc := tc 349 | 350 | test.Run(tc.name, func(t *testing.T) { 351 | t.Parallel() 352 | 353 | db, m, err := sqlmock.New() 354 | if err != nil { 355 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 356 | } 357 | assert.NoError(t, err) 358 | 359 | m.ExpectExec(regexp.QuoteMeta(spCreate)).WithArgs( 360 | sqlmock.AnyArg(), 361 | &tc.dbUser.Email, 362 | &tc.dbUser.Phone, 363 | &tc.dbUser.Password, 364 | ).WillReturnResult(sqlmock.NewResult(0, 0)) 365 | 366 | r := repository.NewRepository(db) 367 | uuid := utils.NewUUIDMock() 368 | uc := usecases.NewUseCases(r, uuid) 369 | err = uc.RegisterUser(tc.input) 370 | assert.NoError(t, err) 371 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 372 | 373 | }) 374 | } 375 | 376 | for _, tc := range failedCases { 377 | tc := tc 378 | 379 | test.Run(tc.name, func(t *testing.T) { 380 | t.Parallel() 381 | 382 | db, m, err := sqlmock.New() 383 | if err != nil { 384 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 385 | } 386 | assert.NoError(t, err) 387 | 388 | m.ExpectExec(regexp.QuoteMeta(spCreate)).WithArgs( 389 | sqlmock.AnyArg(), 390 | &tc.dbUser.Email, 391 | &tc.dbUser.Phone, 392 | &tc.dbUser.Password, 393 | ).WillReturnResult(sqlmock.NewResult(0, 0)) 394 | 395 | r := repository.NewRepository(db) 396 | uuid := utils.NewUUIDMock() 397 | uc := usecases.NewUseCases(r, uuid) 398 | err = uc.RegisterUser(tc.input) 399 | assert.NotEmpty(t, err, "expected error, but got:", err) 400 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 401 | 402 | }) 403 | } 404 | } 405 | 406 | func TestIndexUserByID(test *testing.T) { 407 | dbUserOne := &internal.User{ 408 | ID: "im an id", 409 | Email: "test@test.com", 410 | Phone: "+991234567890", 411 | Password: "12345pAsSWORd*", 412 | } 413 | 414 | rowsSetOne := sqlmock.NewRows([]string{ 415 | "id_user", 416 | "user_email", 417 | "user_phone", 418 | }).AddRow( 419 | &dbUserOne.ID, 420 | &dbUserOne.Email, 421 | &dbUserOne.Phone, 422 | ) 423 | 424 | rowsSetTwo := sqlmock.NewRows([]string{ 425 | "id_user", 426 | "user_email", 427 | "user_phone", 428 | }) 429 | 430 | inputUserOne := &controller.User{ 431 | ID: "im an id", 432 | } 433 | 434 | inputUserTwo := &controller.User{ 435 | ID: "", 436 | } 437 | 438 | inputUserThree := &controller.User{ 439 | ID: "im an id two", 440 | } 441 | 442 | expectedOne := &internal.User{ 443 | ID: "im an id", 444 | Email: "test@test.com", 445 | Phone: "+991234567890", 446 | } 447 | 448 | successfulCases := []struct { 449 | name string 450 | input *controller.User 451 | rows *sqlmock.Rows 452 | dbUser *internal.User 453 | expected *internal.User 454 | }{ 455 | { 456 | name: "it should read an user (mocked)", 457 | input: inputUserOne, 458 | rows: rowsSetOne, 459 | dbUser: dbUserOne, 460 | expected: expectedOne, 461 | }, 462 | } 463 | 464 | failedCases := []struct { 465 | name string 466 | input *controller.User 467 | rows *sqlmock.Rows 468 | dbUser *internal.User 469 | expected *internal.User 470 | }{ 471 | { 472 | name: "it should not read an user (mocked), empty id", 473 | input: inputUserTwo, 474 | rows: rowsSetOne, 475 | dbUser: dbUserOne, 476 | expected: expectedOne, 477 | }, 478 | { 479 | name: "it should not read an user (mocked), empty user", 480 | input: nil, 481 | rows: rowsSetOne, 482 | dbUser: dbUserOne, 483 | expected: expectedOne, 484 | }, 485 | { 486 | name: "it should not read an user (mocked), empty db", 487 | input: inputUserTwo, 488 | rows: rowsSetTwo, 489 | dbUser: dbUserOne, 490 | expected: expectedOne, 491 | }, 492 | { 493 | name: "it should not read an user (mocked), no id found", 494 | input: inputUserThree, 495 | rows: rowsSetOne, 496 | dbUser: dbUserOne, 497 | expected: expectedOne, 498 | }, 499 | } 500 | 501 | for _, tc := range successfulCases { 502 | tc := tc 503 | 504 | test.Run(tc.name, func(t *testing.T) { 505 | t.Parallel() 506 | 507 | db, m, err := sqlmock.New() 508 | if err != nil { 509 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 510 | } 511 | assert.NoError(t, err) 512 | 513 | m.ExpectQuery(regexp.QuoteMeta(spRead)).WithArgs( 514 | &tc.dbUser.ID, 515 | ).WillReturnRows(tc.rows) 516 | 517 | r := repository.NewRepository(db) 518 | uuid := utils.NewUUIDMock() 519 | uc := usecases.NewUseCases(r, uuid) 520 | res, err := uc.IndexUserByID(tc.input) 521 | assert.NoError(t, err) 522 | assert.Equal(t, tc.expected, res) 523 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 524 | 525 | }) 526 | } 527 | 528 | for _, tc := range failedCases { 529 | tc := tc 530 | 531 | test.Run(tc.name, func(t *testing.T) { 532 | t.Parallel() 533 | db, m, err := sqlmock.New() 534 | if err != nil { 535 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 536 | } 537 | assert.NoError(t, err) 538 | 539 | m.ExpectQuery(regexp.QuoteMeta(spRead)).WithArgs( 540 | &tc.dbUser.ID, 541 | ).WillReturnRows(tc.rows) 542 | 543 | r := repository.NewRepository(db) 544 | uuid := utils.NewUUIDMock() 545 | uc := usecases.NewUseCases(r, uuid) 546 | res, err := uc.IndexUserByID(tc.input) 547 | assert.Error(t, err) 548 | assert.NotEqual(t, tc.expected, res) 549 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 550 | 551 | }) 552 | } 553 | } 554 | 555 | func TestIndexUsers(test *testing.T) { 556 | dbUserOne := &internal.User{ 557 | ID: "im an id", 558 | Email: "test@test.com", 559 | Phone: "+991234567890", 560 | } 561 | dbUserTwo := &internal.User{ 562 | ID: "im an id 2", 563 | Email: "test2@test.com", 564 | Phone: "+891234567891", 565 | } 566 | dbUserThree := &internal.User{ 567 | ID: "im an id 3", 568 | Email: "test2@test.com", 569 | Phone: "+891234567891", 570 | } 571 | 572 | rowsSetOne := sqlmock.NewRows([]string{ 573 | "id_user", 574 | "user_email", 575 | "user_phone", 576 | }).AddRow( 577 | dbUserOne.ID, 578 | dbUserOne.Email, 579 | dbUserOne.Phone, 580 | ).AddRow( 581 | dbUserTwo.ID, 582 | dbUserTwo.Email, 583 | dbUserTwo.Phone, 584 | ) 585 | 586 | rowsSetTwo := sqlmock.NewRows([]string{"id_user", "user_email", "user_phone"}) 587 | 588 | expectedOnes := make(internal.Users, 0) 589 | expectedOnes = append(expectedOnes, dbUserOne) 590 | expectedOnes = append(expectedOnes, dbUserTwo) 591 | 592 | expectedOnesTwo := make(internal.Users, 0) 593 | 594 | expectedOnesThree := make(internal.Users, 0) 595 | expectedOnesThree = append(expectedOnesThree, dbUserOne) 596 | expectedOnesThree = append(expectedOnesThree, dbUserThree) 597 | 598 | successfulCases := []struct { 599 | name string 600 | rows *sqlmock.Rows 601 | expected internal.Users 602 | }{ 603 | { 604 | name: "it should read many users (mocked)", 605 | rows: rowsSetOne, 606 | expected: expectedOnes, 607 | }, 608 | { 609 | name: "it should read many users (mocked), but empty db", 610 | rows: rowsSetTwo, 611 | expected: expectedOnesTwo, 612 | }, 613 | } 614 | 615 | failedCases := []struct { 616 | name string 617 | rows *sqlmock.Rows 618 | dbUser internal.Users 619 | expected internal.Users 620 | }{ 621 | { 622 | name: "it should not read many users (mocked), wrong values returned", 623 | rows: rowsSetOne, 624 | expected: expectedOnesThree, 625 | }, 626 | } 627 | 628 | for _, tc := range successfulCases { 629 | tc := tc 630 | test.Run(tc.name, func(t *testing.T) { 631 | t.Parallel() 632 | 633 | db, m, err := sqlmock.New() 634 | if err != nil { 635 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 636 | } 637 | assert.NoError(t, err) 638 | 639 | m.ExpectQuery(regexp.QuoteMeta(spReadAll)).WillReturnRows(tc.rows) 640 | 641 | r := repository.NewRepository(db) 642 | uuid := utils.NewUUIDMock() 643 | uc := usecases.NewUseCases(r, uuid) 644 | res, err := uc.IndexUsers() 645 | 646 | fmt.Println("expected: ", tc.expected) 647 | fmt.Println("actual: ", res) 648 | 649 | assert.NoError(t, err) 650 | assert.Equal(t, tc.expected, res) 651 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 652 | 653 | }) 654 | } 655 | 656 | for _, tc := range failedCases { 657 | tc := tc 658 | test.Run(tc.name, func(t *testing.T) { 659 | t.Parallel() 660 | 661 | db, m, err := sqlmock.New() 662 | if err != nil { 663 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 664 | } 665 | assert.NoError(t, err) 666 | m.ExpectQuery(regexp.QuoteMeta(spReadAll)).WillReturnRows(tc.rows) 667 | 668 | r := repository.NewRepository(db) 669 | uuid := utils.NewUUIDMock() 670 | uc := usecases.NewUseCases(r, uuid) 671 | res, err := uc.IndexUsers() 672 | assert.NoError(t, err) 673 | assert.NotEqual(t, tc.expected, res) 674 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 675 | 676 | }) 677 | } 678 | } 679 | 680 | func TestModifyUser(test *testing.T) { 681 | dbUserOne := &internal.User{ 682 | ID: "im an id", 683 | Email: "test@test.com", 684 | Phone: "+991234567890", 685 | Password: "12345pAsSWORd*", 686 | } 687 | 688 | dbUserTwo := &internal.User{ 689 | ID: "im an id", 690 | Email: "test@test.com", 691 | Phone: "", 692 | Password: "12345pAsSWORd*", 693 | } 694 | 695 | dbUserThree := &internal.User{ 696 | ID: "im an id", 697 | Email: "", 698 | Phone: "+991234567890", 699 | Password: "12345pAsSWORd*", 700 | } 701 | 702 | inputUserOne := &controller.User{ 703 | ID: "im an id", 704 | Email: "test@test.com", 705 | Phone: "+991234567890", 706 | Password: "12345pAsSWORd*", 707 | } 708 | 709 | inputUserTwo := &controller.User{ 710 | ID: "im an id", 711 | Email: "test@test.com", 712 | Phone: "", 713 | Password: "12345pAsSWORd*", 714 | } 715 | 716 | inputUserThree := &controller.User{ 717 | ID: "im an id", 718 | Email: "", 719 | Phone: "+991234567890", 720 | Password: "12345pAsSWORd*", 721 | } 722 | 723 | inputUserFour := &controller.User{ 724 | ID: "", 725 | Email: "test@test.com", 726 | Phone: "+991234567890", 727 | Password: "12345pAsSWORd*", 728 | } 729 | 730 | inputUserFive := &controller.User{ 731 | ID: "im an id", 732 | Email: "test@test.com", 733 | Phone: "+991234567890", 734 | Password: "", 735 | } 736 | 737 | inputUserSix := &controller.User{ 738 | ID: "im an id but wrong", 739 | Email: "test@test.com", 740 | Phone: "+991234567890", 741 | Password: "12345pAsSWORd*", 742 | } 743 | 744 | successfulCases := []struct { 745 | name string 746 | input *controller.User 747 | dbUser *internal.User 748 | }{ 749 | { 750 | name: "it should update an user (mocked)", 751 | input: inputUserOne, 752 | dbUser: dbUserOne, 753 | }, 754 | { 755 | name: "it should update an user (mocked) besides empty phone", 756 | input: inputUserTwo, 757 | dbUser: dbUserTwo, 758 | }, 759 | { 760 | name: "it should update an user (mocked) besides empty email", 761 | input: inputUserThree, 762 | dbUser: dbUserThree, 763 | }, 764 | } 765 | 766 | failedCases := []struct { 767 | name string 768 | input *controller.User 769 | dbUser *internal.User 770 | }{ 771 | { 772 | name: "it should not update an user (mocked), empty id", 773 | input: inputUserFour, 774 | dbUser: dbUserOne, 775 | }, 776 | { 777 | name: "it should not update an user (mocked), empty password", 778 | input: inputUserFive, 779 | dbUser: dbUserOne, 780 | }, 781 | { 782 | name: "it should not update an user (mocked), nil user", 783 | input: nil, 784 | dbUser: dbUserOne, 785 | }, 786 | { 787 | name: "it should not update an user (mocked), id not found", 788 | input: inputUserSix, 789 | dbUser: dbUserOne, 790 | }, 791 | } 792 | 793 | for _, tc := range successfulCases { 794 | tc := tc 795 | 796 | test.Run(tc.name, func(t *testing.T) { 797 | t.Parallel() 798 | 799 | db, m, err := sqlmock.New() 800 | if err != nil { 801 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 802 | } 803 | assert.NoError(t, err) 804 | 805 | m.ExpectExec(regexp.QuoteMeta(spUpdate)).WithArgs( 806 | &tc.dbUser.ID, 807 | &tc.dbUser.Email, 808 | &tc.dbUser.Phone, 809 | &tc.dbUser.Password, 810 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 811 | 812 | r := repository.NewRepository(db) 813 | uuid := utils.NewUUIDMock() 814 | uc := usecases.NewUseCases(r, uuid) 815 | err = uc.ModifyUser(tc.input) 816 | assert.NoError(t, err) 817 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 818 | 819 | }) 820 | } 821 | 822 | for _, tc := range failedCases { 823 | tc := tc 824 | 825 | test.Run(tc.name, func(t *testing.T) { 826 | t.Parallel() 827 | 828 | db, m, err := sqlmock.New() 829 | if err != nil { 830 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 831 | } 832 | assert.NoError(t, err) 833 | 834 | m.ExpectExec(regexp.QuoteMeta(spUpdate)).WithArgs( 835 | &tc.dbUser.ID, 836 | &tc.dbUser.Email, 837 | &tc.dbUser.Phone, 838 | &tc.dbUser.Password, 839 | ).WillReturnResult(sqlmock.NewResult(0, 0)) 840 | 841 | r := repository.NewRepository(db) 842 | uuid := utils.NewUUIDMock() 843 | uc := usecases.NewUseCases(r, uuid) 844 | err = uc.ModifyUser(tc.input) 845 | assert.Error(t, err) 846 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 847 | 848 | }) 849 | } 850 | } 851 | 852 | func TestDelete(test *testing.T) { 853 | dbUserOne := &internal.User{ 854 | ID: "im an id", 855 | Email: "test@test.com", 856 | Phone: "+991234567890", 857 | Password: "12345pAsSWORd*", 858 | } 859 | 860 | inputUserOne := &internal.User{ 861 | ID: "im an id", 862 | Email: "test@test.com", 863 | Phone: "+991234567890", 864 | Password: "12345pAsSWORd*", 865 | } 866 | 867 | inputUserFour := &internal.User{ 868 | ID: "", 869 | Email: "test@test.com", 870 | Phone: "+991234567890", 871 | Password: "12345pAsSWORd*", 872 | } 873 | 874 | inputUserFive := &internal.User{ 875 | ID: "im an id", 876 | Email: "test@test.com", 877 | Phone: "+991234567890", 878 | Password: "", 879 | } 880 | 881 | inputUserSix := &internal.User{ 882 | ID: "im an id but wrong", 883 | Email: "test@test.com", 884 | Phone: "+991234567890", 885 | Password: "12345pAsSWORd*", 886 | } 887 | 888 | successfulCases := []struct { 889 | name string 890 | input *internal.User 891 | dbUser *internal.User 892 | }{ 893 | { 894 | name: "it should delete an user (mocked)", 895 | input: inputUserOne, 896 | dbUser: dbUserOne, 897 | }, 898 | } 899 | 900 | failedCases := []struct { 901 | name string 902 | input *internal.User 903 | dbUser *internal.User 904 | }{ 905 | { 906 | name: "it should not delete an user (mocked), empty id", 907 | input: inputUserFour, 908 | dbUser: dbUserOne, 909 | }, 910 | { 911 | name: "it should not delete an user (mocked), empty password", 912 | input: inputUserFive, 913 | dbUser: dbUserOne, 914 | }, 915 | { 916 | name: "it should not delete an user (mocked), nil user", 917 | input: nil, 918 | dbUser: dbUserOne, 919 | }, 920 | { 921 | name: "it should not delete an user (mocked), id not found", 922 | input: inputUserSix, 923 | dbUser: dbUserOne, 924 | }, 925 | } 926 | 927 | for _, tc := range successfulCases { 928 | tc := tc 929 | 930 | test.Run(tc.name, func(t *testing.T) { 931 | t.Parallel() 932 | 933 | db, m, err := sqlmock.New() 934 | if err != nil { 935 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 936 | } 937 | assert.NoError(t, err) 938 | 939 | m.ExpectExec(regexp.QuoteMeta(spDelete)).WithArgs( 940 | &tc.dbUser.ID, 941 | &tc.dbUser.Password, 942 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 943 | 944 | r := repository.NewRepository(db) 945 | uuid := utils.NewUUIDMock() 946 | uc := usecases.NewUseCases(r, uuid) 947 | err = uc.DestroyUser(tc.input) 948 | assert.NoError(t, err) 949 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 950 | 951 | }) 952 | } 953 | 954 | for _, tc := range failedCases { 955 | tc := tc 956 | 957 | test.Run(tc.name, func(t *testing.T) { 958 | t.Parallel() 959 | 960 | db, m, err := sqlmock.New() 961 | if err != nil { 962 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 963 | } 964 | assert.NoError(t, err) 965 | 966 | m.ExpectExec(regexp.QuoteMeta(spDelete)).WithArgs( 967 | &tc.dbUser.ID, 968 | &tc.dbUser.Password, 969 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 970 | 971 | r := repository.NewRepository(db) 972 | uuid := utils.NewUUIDMock() 973 | uc := usecases.NewUseCases(r, uuid) 974 | err = uc.DestroyUser(tc.input) 975 | assert.Error(t, err) 976 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 977 | 978 | }) 979 | } 980 | } 981 | -------------------------------------------------------------------------------- /pkg/adapter/controller/controller_test.go: -------------------------------------------------------------------------------- 1 | // Package controller_test is a test for controller 2 | package controller_test 3 | 4 | import ( 5 | "bytes" 6 | "dall06/go-cleanapi/config" 7 | "dall06/go-cleanapi/pkg/adapter/controller" 8 | "dall06/go-cleanapi/pkg/internal" 9 | "dall06/go-cleanapi/pkg/internal/repository" 10 | "dall06/go-cleanapi/pkg/internal/usecases" 11 | "dall06/go-cleanapi/utils" 12 | "database/sql" 13 | "net/http/httptest" 14 | "net/url" 15 | "regexp" 16 | "strings" 17 | "testing" 18 | "time" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-playground/validator/v10" 22 | "github.com/gofiber/fiber/v2" 23 | "github.com/patrickmn/go-cache" 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | const ( 28 | spCreate = "CALL `go_cleanapi`.`sp_create_user`(?, ?, ?, ?);" 29 | spRead = "CALL `go_cleanapi`.`sp_read_user`(?);" 30 | spReadAll = "CALL `go_cleanapi`.`sp_read_users`();" 31 | spUpdate = "CALL `go_cleanapi`.`sp_update_user`(?, ?, ?, ?);" 32 | spDelete = "CALL `go_cleanapi`.`sp_delete_user`(?, ?);" 33 | spLogin = "CALL `go_cleanapi`.`sp_login_user`(?, ?, ?);" 34 | ) 35 | 36 | func TestAuth(test *testing.T) { 37 | dbUserModel := &internal.User{ 38 | Email: "test@test.com", 39 | Phone: "+991234567890", 40 | Password: "12345pAsSWORd*", 41 | } 42 | 43 | dbUserOne := &internal.User{ 44 | Email: "test@test.com", 45 | Phone: "", 46 | Password: "12345pAsSWORd*", 47 | } 48 | 49 | dbUserTwo := &internal.User{ 50 | Email: "", 51 | Phone: "+991234567890", 52 | Password: "12345pAsSWORd*", 53 | } 54 | 55 | rowsSetOne := sqlmock.NewRows([]string{ 56 | "id_user", 57 | }).AddRow( 58 | "im an ID", 59 | ) 60 | rowsSetTwo := sqlmock.NewRows([]string{ 61 | "id_user", 62 | }).AddRow( 63 | "im an ID", 64 | ) 65 | rowsSetThree := sqlmock.NewRows([]string{ 66 | "id_user", 67 | }).AddRow( 68 | "im an ID", 69 | ) 70 | rowsSetFour := sqlmock.NewRows([]string{ 71 | "id_user", 72 | }).AddRow( 73 | "im an ID", 74 | ) 75 | rowsSetFive := sqlmock.NewRows([]string{ 76 | "id_user", 77 | }).AddRow( 78 | "im an ID", 79 | ) 80 | rowsSetSix := sqlmock.NewRows([]string{ 81 | "id_user", 82 | }).AddRow( 83 | "im an ID", 84 | ) 85 | rowsSetSeven := sqlmock.NewRows([]string{ 86 | "id_user", 87 | }).AddRow( 88 | "im an ID", 89 | ) 90 | rowsSetEight := sqlmock.NewRows([]string{ 91 | "id_user", 92 | }).AddRow( 93 | "im an ID", 94 | ) 95 | rowsSetNine := sqlmock.NewRows([]string{ 96 | "id_user", 97 | }).AddRow( 98 | "im an ID", 99 | ) 100 | 101 | formValuesEmail := url.Values{} 102 | formValuesEmail.Set("user", "test@test.com") 103 | formValuesEmail.Set("password", "12345pAsSWORd*") 104 | formValuesEmail2 := url.Values{} 105 | formValuesEmail2.Set("user", "test2@test.com") 106 | formValuesEmail2.Set("password", "12345pAsSWORd*") 107 | formValuesBadEmail := url.Values{} 108 | formValuesBadEmail.Set("user", "testtest.com") 109 | formValuesBadEmail.Set("password", "12345pAsSWORd*") 110 | 111 | formValuesPhone := url.Values{} 112 | formValuesPhone.Set("user", "+991234567890") 113 | formValuesPhone.Set("password", "12345pAsSWORd*") 114 | formValuesPhone2 := url.Values{} 115 | formValuesPhone2.Set("user", "+521234567890") 116 | formValuesPhone2.Set("password", "12345pAsSWORd*") 117 | formValuesBadPhone := url.Values{} 118 | formValuesBadPhone.Set("user", "+991234ee7890") 119 | formValuesBadPhone.Set("password", "12345pAsSWORd*") 120 | 121 | formValuesEmpty := url.Values{} 122 | formValuesEmpty.Set("user", "") 123 | formValuesEmpty.Set("password", "") 124 | formValuesEmptyPass := url.Values{} 125 | formValuesEmptyPass.Set("user", "test@test.com") 126 | formValuesEmptyPass.Set("password", "") 127 | 128 | formValuesNil := url.Values{} 129 | 130 | successfulCases := []struct { 131 | testID string 132 | name string 133 | rows *sqlmock.Rows 134 | dbUser *internal.User 135 | reqForm url.Values 136 | expectedStatus int 137 | expectedBody *controller.User 138 | }{ 139 | { 140 | testID: "test1", 141 | name: "it should login (mocked), whit email", 142 | rows: rowsSetOne, 143 | dbUser: dbUserOne, 144 | reqForm: formValuesEmail, 145 | expectedStatus: fiber.StatusAccepted, 146 | expectedBody: &controller.User{ 147 | ID: "im an ID", 148 | }, 149 | }, 150 | { 151 | testID: "test2", 152 | name: "it should login (mocked), whit phone", 153 | rows: rowsSetTwo, 154 | dbUser: dbUserTwo, 155 | reqForm: formValuesPhone, 156 | expectedStatus: fiber.StatusAccepted, 157 | expectedBody: &controller.User{ 158 | ID: "im an ID", 159 | }, 160 | }, 161 | } 162 | 163 | failedCases := []struct { 164 | testID string 165 | name string 166 | rows *sqlmock.Rows 167 | dbUser *internal.User 168 | reqForm url.Values 169 | expectedStatus int 170 | expectedBody *controller.User 171 | }{ 172 | { 173 | testID: "test3", 174 | name: "it should not login (mocked), empty values", 175 | rows: rowsSetThree, 176 | dbUser: dbUserModel, 177 | reqForm: formValuesEmpty, 178 | expectedStatus: fiber.StatusBadRequest, 179 | expectedBody: &controller.User{ 180 | ID: "im an ID", 181 | }, 182 | }, 183 | { 184 | testID: "test4", 185 | name: "it should not login (mocked), nil values", 186 | rows: rowsSetFour, 187 | dbUser: dbUserModel, 188 | reqForm: formValuesNil, 189 | expectedStatus: fiber.StatusBadRequest, 190 | expectedBody: &controller.User{ 191 | ID: "im an ID", 192 | }, 193 | }, 194 | { 195 | testID: "test5", 196 | name: "it should not login (mocked), bad email", 197 | rows: rowsSetFive, 198 | dbUser: dbUserModel, 199 | reqForm: formValuesBadEmail, 200 | expectedStatus: fiber.StatusBadRequest, 201 | expectedBody: &controller.User{ 202 | ID: "im an ID", 203 | }, 204 | }, 205 | { 206 | testID: "test6", 207 | name: "it should not login (mocked), bad phone", 208 | rows: rowsSetSix, 209 | dbUser: dbUserModel, 210 | reqForm: formValuesBadPhone, 211 | expectedStatus: fiber.StatusBadRequest, 212 | expectedBody: &controller.User{ 213 | ID: "im an ID", 214 | }, 215 | }, 216 | { 217 | testID: "test7", 218 | name: "it should not login (mocked), empty pass", 219 | rows: rowsSetSeven, 220 | dbUser: dbUserModel, 221 | reqForm: formValuesEmptyPass, 222 | expectedStatus: fiber.StatusBadRequest, 223 | expectedBody: &controller.User{ 224 | ID: "im an ID", 225 | }, 226 | }, 227 | { 228 | testID: "test8", 229 | name: "it should not login (mocked), phone not found", 230 | rows: rowsSetEight, 231 | dbUser: dbUserModel, 232 | reqForm: formValuesPhone2, 233 | expectedStatus: fiber.StatusInternalServerError, 234 | expectedBody: &controller.User{ 235 | ID: "im an ID", 236 | }, 237 | }, 238 | { 239 | testID: "test9", 240 | name: "it should not login (mocked), email not found", 241 | rows: rowsSetNine, 242 | dbUser: dbUserModel, 243 | reqForm: formValuesEmail2, 244 | expectedStatus: fiber.StatusInternalServerError, 245 | expectedBody: &controller.User{ 246 | ID: "im an ID", 247 | }, 248 | }, 249 | } 250 | 251 | sCfg := fiber.Config{ 252 | ReadTimeout: 10 * time.Second, 253 | WriteTimeout: 10 * time.Second, 254 | IdleTimeout: 60 * time.Second, 255 | } 256 | app := fiber.New(sCfg) 257 | 258 | cfg := config.NewConfig("8080", "1.0.0") 259 | vars, err := cfg.SetConfig() 260 | if err != nil { 261 | test.Fatalf("expected no error but got %v", err) 262 | } 263 | v := validator.New() 264 | l := utils.NewLogger(*vars) 265 | err = l.Initialize() 266 | if err != nil { 267 | test.Fatalf("expected no error but got %v", err) 268 | } 269 | uuid := utils.NewUUIDMock() 270 | jwt := utils.NewJWTMock() 271 | val := utils.NewValidations() 272 | 273 | for _, tc := range successfulCases { 274 | tc := tc 275 | 276 | test.Run(tc.name, func(t *testing.T) { 277 | db, m, err := sqlmock.New() 278 | if err != nil { 279 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 280 | } 281 | 282 | assert.NoError(t, err) 283 | 284 | m.ExpectQuery(regexp.QuoteMeta(spLogin)).WithArgs( 285 | &tc.dbUser.Email, 286 | &tc.dbUser.Phone, 287 | &tc.dbUser.Password, 288 | ).WillReturnRows(tc.rows) 289 | assert.Empty(t, err, "expected no error, but got:", err) 290 | 291 | myCache := cache.New(5*time.Minute, 10*time.Minute) 292 | 293 | r := repository.NewRepository(db) 294 | uc := usecases.NewUseCases(r, uuid) 295 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 296 | 297 | app.Post("/auth/"+tc.testID, ctrl.Auth) 298 | 299 | req := httptest.NewRequest("POST", "/auth/"+tc.testID, strings.NewReader(tc.reqForm.Encode())) 300 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 301 | 302 | resp, err := app.Test(req, 1) 303 | assert.NoError(t, err) 304 | 305 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 306 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 307 | 308 | }) 309 | } 310 | 311 | for _, tc := range failedCases { 312 | tc := tc 313 | 314 | test.Run(tc.name, func(t *testing.T) { 315 | db, m, err := sqlmock.New() 316 | if err != nil { 317 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 318 | } 319 | 320 | assert.NoError(t, err) 321 | 322 | m.ExpectQuery(regexp.QuoteMeta(spLogin)).WithArgs( 323 | &tc.dbUser.Email, 324 | &tc.dbUser.Phone, 325 | &tc.dbUser.Password, 326 | ).WillReturnRows(tc.rows) 327 | assert.Empty(t, err, "expected no error, but got:", err) 328 | 329 | myCache := cache.New(5*time.Minute, 10*time.Minute) 330 | 331 | r := repository.NewRepository(db) 332 | uc := usecases.NewUseCases(r, uuid) 333 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 334 | 335 | app.Post("/auth/"+tc.testID, ctrl.Auth) 336 | 337 | req := httptest.NewRequest("POST", "/auth/"+tc.testID, strings.NewReader(tc.reqForm.Encode())) 338 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 339 | 340 | resp, err := app.Test(req, 1) 341 | assert.NoError(t, err) 342 | 343 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 344 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 345 | 346 | }) 347 | } 348 | } 349 | 350 | func TestPost(test *testing.T) { 351 | dbUser1 := &internal.User{ 352 | Email: "test@test.com", 353 | Phone: "+991234567890", 354 | Password: "12345pAsSWORd*", 355 | } 356 | dbUser2 := &internal.User{ 357 | Email: "test@test.com", 358 | Phone: "", 359 | Password: "12345pAsSWORd*", 360 | } 361 | 362 | successfulCases := []struct { 363 | testID string 364 | name string 365 | dbUser *internal.User 366 | body string 367 | expectedStatus int 368 | }{ 369 | { 370 | testID: "test1", 371 | name: "it should post user (mocked)", 372 | dbUser: dbUser1, 373 | expectedStatus: fiber.StatusCreated, 374 | body: `{"email":"test@test.com","phone":"+991234567890","password":"12345pAsSWORd*"}`, 375 | }, 376 | { 377 | testID: "test2", 378 | name: "it should post user (mocked), whit empty phone", 379 | dbUser: dbUser2, 380 | expectedStatus: fiber.StatusCreated, 381 | body: `{"email":"test@test.com","phone":"","password":"12345pAsSWORd*"}`, 382 | }, 383 | } 384 | 385 | failedCases := []struct { 386 | testID string 387 | name string 388 | dbUser *internal.User 389 | body string 390 | expectedStatus int 391 | }{ 392 | { 393 | testID: "test3", 394 | name: "it should post user (mocked), empty email", 395 | dbUser: dbUser1, 396 | expectedStatus: fiber.StatusBadRequest, 397 | body: `{"email":"","phone":"+991234567890","password":"12345pAsSWORd*"}`, 398 | }, 399 | { 400 | testID: "test4", 401 | name: "it should post user (mocked), empty password", 402 | dbUser: dbUser2, 403 | expectedStatus: fiber.StatusBadRequest, 404 | body: `{"email":"test@test.com","phone":"","password":""}`, 405 | }, 406 | { 407 | testID: "test5", 408 | name: "it should not post user (mocked), empty string", 409 | dbUser: dbUser1, 410 | expectedStatus: fiber.StatusInternalServerError, 411 | body: `{""}`, 412 | }, 413 | { 414 | testID: "test6", 415 | name: "it should post user (mocked), internal error", 416 | dbUser: dbUser2, 417 | expectedStatus: fiber.StatusInternalServerError, 418 | body: `{"email":"test2@test.com","phone":"","password":"12345pAsSWORd*"}`, 419 | }, 420 | } 421 | 422 | sCfg := fiber.Config{ 423 | ReadTimeout: 10 * time.Second, 424 | WriteTimeout: 10 * time.Second, 425 | IdleTimeout: 60 * time.Second, 426 | } 427 | app := fiber.New(sCfg) 428 | 429 | cfg := config.NewConfig("8080", "1.0.0") 430 | vars, err := cfg.SetConfig() 431 | if err != nil { 432 | test.Fatalf("expected no error but got %v", err) 433 | } 434 | v := validator.New() 435 | l := utils.NewLogger(*vars) 436 | err = l.Initialize() 437 | if err != nil { 438 | test.Fatalf("expected no error but got %v", err) 439 | } 440 | uuid := utils.NewUUIDMock() 441 | jwt := utils.NewJWTMock() 442 | val := utils.NewValidations() 443 | 444 | for _, tc := range successfulCases { 445 | tc := tc 446 | 447 | test.Run(tc.name, func(t *testing.T) { 448 | db, m, err := sqlmock.New() 449 | if err != nil { 450 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 451 | } 452 | 453 | assert.NoError(t, err) 454 | 455 | m.ExpectExec(regexp.QuoteMeta(spCreate)).WithArgs( 456 | sqlmock.AnyArg(), 457 | &tc.dbUser.Email, 458 | &tc.dbUser.Phone, 459 | &tc.dbUser.Password, 460 | ).WillReturnResult(sqlmock.NewResult(0, 0)) 461 | assert.Empty(t, err, "expected no error, but got:", err) 462 | 463 | myCache := cache.New(5*time.Minute, 10*time.Minute) 464 | 465 | r := repository.NewRepository(db) 466 | uc := usecases.NewUseCases(r, uuid) 467 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 468 | 469 | app.Post("/post/"+tc.testID, ctrl.Post) 470 | 471 | req := httptest.NewRequest("POST", "/post/"+tc.testID, bytes.NewBufferString(tc.body)) 472 | req.Header.Set("Content-Type", "application/json") 473 | resp, err := app.Test(req) 474 | assert.NoError(t, err) 475 | defer func() { 476 | if err := resp.Body.Close(); err != nil { 477 | test.Fatalf("error closing database connection: %v", err) 478 | } 479 | }() 480 | 481 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 482 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 483 | 484 | }) 485 | } 486 | 487 | for _, tc := range failedCases { 488 | tc := tc 489 | 490 | test.Run(tc.name, func(t *testing.T) { 491 | db, m, err := sqlmock.New() 492 | if err != nil { 493 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 494 | } 495 | 496 | assert.NoError(t, err) 497 | 498 | m.ExpectExec(regexp.QuoteMeta(spCreate)).WithArgs( 499 | sqlmock.AnyArg(), 500 | &tc.dbUser.Email, 501 | &tc.dbUser.Phone, 502 | &tc.dbUser.Password, 503 | ).WillReturnResult(sqlmock.NewResult(0, 0)) 504 | assert.Empty(t, err, "expected no error, but got:", err) 505 | 506 | myCache := cache.New(5*time.Minute, 10*time.Minute) 507 | 508 | r := repository.NewRepository(db) 509 | uc := usecases.NewUseCases(r, uuid) 510 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 511 | 512 | app.Post("/post/"+tc.testID, ctrl.Post) 513 | 514 | req := httptest.NewRequest("POST", "/post/"+tc.testID, bytes.NewBufferString(tc.body)) 515 | req.Header.Set("Content-Type", "application/json") 516 | resp, err := app.Test(req) 517 | assert.NoError(t, err) 518 | defer func() { 519 | if err := resp.Body.Close(); err != nil { 520 | test.Fatalf("error closing database connection: %v", err) 521 | } 522 | }() 523 | 524 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 525 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 526 | 527 | }) 528 | } 529 | } 530 | 531 | func TestGet(test *testing.T) { 532 | dbUser1 := &internal.User{ 533 | ID: "im_an_id", 534 | Email: "test@test.com", 535 | Phone: "+991234567890", 536 | Password: "12345pAsSWORd*", 537 | } 538 | 539 | rowsSet1 := sqlmock.NewRows([]string{ 540 | "id_user", 541 | "user_email", 542 | "user_phone", 543 | }).AddRow( 544 | dbUser1.ID, 545 | dbUser1.Email, 546 | dbUser1.Phone, 547 | ) 548 | 549 | rowsSet2 := sqlmock.NewRows([]string{ 550 | "id_user", 551 | "user_email", 552 | "user_phone", 553 | }) 554 | 555 | successfulCases := []struct { 556 | testID string 557 | name string 558 | dbUser *internal.User 559 | id string 560 | expectedStatus int 561 | rows *sqlmock.Rows 562 | }{ 563 | { 564 | testID: "test1", 565 | name: "it should read user (mocked)", 566 | dbUser: dbUser1, 567 | expectedStatus: fiber.StatusOK, 568 | rows: rowsSet1, 569 | id: "im_an_id", 570 | }, 571 | } 572 | 573 | failedCases := []struct { 574 | testID string 575 | name string 576 | dbUser *internal.User 577 | id string 578 | expectedStatus int 579 | rows *sqlmock.Rows 580 | }{ 581 | { 582 | testID: "test2", 583 | name: "it should not read user (mocked), empty id", 584 | dbUser: dbUser1, 585 | expectedStatus: fiber.StatusOK, 586 | rows: rowsSet1, 587 | id: "im_an_id", 588 | }, 589 | { 590 | testID: "test3", 591 | name: "it should not read user (mocked), empty db", 592 | dbUser: dbUser1, 593 | expectedStatus: fiber.StatusNotFound, 594 | rows: rowsSet2, 595 | id: "", 596 | }, 597 | { 598 | testID: "test4", 599 | name: "it should not read user (mocked), id not found", 600 | dbUser: dbUser1, 601 | expectedStatus: fiber.StatusInternalServerError, 602 | rows: rowsSet1, 603 | id: "im_an_id_but_worng", 604 | }, 605 | } 606 | 607 | sCfg := fiber.Config{ 608 | ReadTimeout: 10 * time.Second, 609 | WriteTimeout: 10 * time.Second, 610 | IdleTimeout: 60 * time.Second, 611 | } 612 | app := fiber.New(sCfg) 613 | 614 | cfg := config.NewConfig("8080", "1.0.0") 615 | vars, err := cfg.SetConfig() 616 | if err != nil { 617 | test.Fatalf("expected no error but got %v", err) 618 | } 619 | v := validator.New() 620 | l := utils.NewLogger(*vars) 621 | err = l.Initialize() 622 | if err != nil { 623 | test.Fatalf("expected no error but got %v", err) 624 | } 625 | uuid := utils.NewUUIDMock() 626 | jwt := utils.NewJWTMock() 627 | val := utils.NewValidations() 628 | 629 | for _, tc := range successfulCases { 630 | tc := tc 631 | 632 | test.Run(tc.name, func(t *testing.T) { 633 | db, m, err := sqlmock.New() 634 | if err != nil { 635 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 636 | } 637 | 638 | assert.NoError(t, err) 639 | 640 | m.ExpectQuery(regexp.QuoteMeta(spRead)).WithArgs(&dbUser1.ID).WillReturnRows(tc.rows) 641 | assert.Empty(t, err, "expected no error, but got:", err) 642 | 643 | myCache := cache.New(5*time.Minute, 10*time.Minute) 644 | 645 | r := repository.NewRepository(db) 646 | uc := usecases.NewUseCases(r, uuid) 647 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 648 | 649 | app.Get("/users/"+tc.testID+"/:id", ctrl.Get) 650 | 651 | // Make a request to the route with the test user ID 652 | req := httptest.NewRequest(fiber.MethodGet, "/users/"+tc.testID+"/"+tc.id, nil) 653 | resp, err := app.Test(req) 654 | assert.NoError(t, err) 655 | 656 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 657 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 658 | 659 | }) 660 | } 661 | 662 | for _, tc := range failedCases { 663 | tc := tc 664 | 665 | test.Run(tc.name, func(t *testing.T) { 666 | db, m, err := sqlmock.New() 667 | if err != nil { 668 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 669 | } 670 | 671 | assert.NoError(t, err) 672 | 673 | m.ExpectQuery(regexp.QuoteMeta(spRead)).WithArgs(&dbUser1.ID).WillReturnRows(tc.rows) 674 | assert.Empty(t, err, "expected no error, but got:", err) 675 | 676 | myCache := cache.New(5*time.Minute, 10*time.Minute) 677 | 678 | r := repository.NewRepository(db) 679 | uc := usecases.NewUseCases(r, uuid) 680 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 681 | 682 | app.Get("/users/"+tc.testID+"/:id", ctrl.Get) 683 | 684 | // Make a request to the route with the test user ID 685 | req := httptest.NewRequest(fiber.MethodGet, "/users/"+tc.testID+"/"+tc.id, nil) 686 | resp, err := app.Test(req) 687 | assert.NoError(t, err) 688 | 689 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 690 | m.ExpectClose().WillReturnError(sql.ErrConnDone) 691 | }) 692 | } 693 | } 694 | 695 | func TestGetAll(test *testing.T) { 696 | dbUser1 := &internal.User{ 697 | ID: "im_an_id", 698 | Email: "test@test.com", 699 | Phone: "+991234567890", 700 | Password: "12345pAsSWORd*", 701 | } 702 | dbUser2 := &internal.User{ 703 | ID: "im an id 2", 704 | Email: "test2@test.com", 705 | Phone: "+891234567891", 706 | } 707 | 708 | rowsSet1 := sqlmock.NewRows([]string{ 709 | "id_user", 710 | "user_email", 711 | "user_phone", 712 | }).AddRow( 713 | dbUser1.ID, 714 | dbUser1.Email, 715 | dbUser1.Phone, 716 | ).AddRow( 717 | dbUser2.ID, 718 | dbUser2.Email, 719 | dbUser2.Phone, 720 | ) 721 | 722 | rowsSet2 := sqlmock.NewRows([]string{ 723 | "id_user", 724 | "user_email", 725 | "user_phone", 726 | }) 727 | 728 | successfulCases := []struct { 729 | testID string 730 | name string 731 | expectedStatus int 732 | rows *sqlmock.Rows 733 | }{ 734 | { 735 | testID: "test1", 736 | name: "it should read users (mocked)", 737 | expectedStatus: fiber.StatusOK, 738 | rows: rowsSet1, 739 | }, 740 | { 741 | testID: "test2", 742 | name: "it should read users (mocked), but empty db", 743 | expectedStatus: fiber.StatusOK, 744 | rows: rowsSet2, 745 | }, 746 | } 747 | 748 | sCfg := fiber.Config{ 749 | ReadTimeout: 10 * time.Second, 750 | WriteTimeout: 10 * time.Second, 751 | IdleTimeout: 60 * time.Second, 752 | } 753 | app := fiber.New(sCfg) 754 | 755 | cfg := config.NewConfig("8080", "1.0.0") 756 | vars, err := cfg.SetConfig() 757 | if err != nil { 758 | test.Fatalf("expected no error but got %v", err) 759 | } 760 | v := validator.New() 761 | l := utils.NewLogger(*vars) 762 | err = l.Initialize() 763 | if err != nil { 764 | test.Fatalf("expected no error but got %v", err) 765 | } 766 | uuid := utils.NewUUIDMock() 767 | jwt := utils.NewJWTMock() 768 | val := utils.NewValidations() 769 | 770 | for _, tc := range successfulCases { 771 | tc := tc 772 | 773 | test.Run(tc.name, func(t *testing.T) { 774 | db, m, err := sqlmock.New() 775 | if err != nil { 776 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 777 | } 778 | 779 | assert.NoError(t, err) 780 | 781 | m.ExpectQuery(regexp.QuoteMeta(spReadAll)).WillReturnRows(tc.rows) 782 | assert.Empty(t, err, "expected no error, but got:", err) 783 | 784 | myCache := cache.New(5*time.Minute, 10*time.Minute) 785 | 786 | r := repository.NewRepository(db) 787 | uc := usecases.NewUseCases(r, uuid) 788 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 789 | 790 | app.Get("/users/"+tc.testID, ctrl.GetAll) 791 | 792 | // Make a request to the route with the test user ID 793 | req := httptest.NewRequest(fiber.MethodGet, "/users/"+tc.testID, nil) 794 | resp, err := app.Test(req) 795 | assert.NoError(t, err) 796 | 797 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 798 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 799 | 800 | }) 801 | } 802 | } 803 | 804 | func TestPut(test *testing.T) { 805 | dbUser1 := &internal.User{ 806 | ID: "im_an_id", 807 | Email: "test@test.com", 808 | Phone: "+991234567890", 809 | Password: "12345pAsSWORd*", 810 | } 811 | dbUser2 := &internal.User{ 812 | ID: "im_an_id", 813 | Email: "test@test.com", 814 | Phone: "", 815 | Password: "12345pAsSWORd*", 816 | } 817 | dbUser3 := &internal.User{ 818 | ID: "im_an_id", 819 | Email: "", 820 | Phone: "+991234567890", 821 | Password: "12345pAsSWORd*", 822 | } 823 | 824 | successfulCases := []struct { 825 | testID string 826 | name string 827 | dbUser *internal.User 828 | body string 829 | id string 830 | expectedStatus int 831 | }{ 832 | { 833 | testID: "test1", 834 | name: "it should put user (mocked)", 835 | dbUser: dbUser1, 836 | expectedStatus: fiber.StatusOK, 837 | id: "im_an_id", 838 | body: `{"email":"test@test.com","phone":"+991234567890","password":"12345pAsSWORd*"}`, 839 | }, 840 | { 841 | testID: "test2", 842 | name: "it should put user (mocked), whit empty phone", 843 | dbUser: dbUser2, 844 | expectedStatus: fiber.StatusOK, 845 | id: "im_an_id", 846 | body: `{"email":"test@test.com","phone":"","password":"12345pAsSWORd*"}`, 847 | }, 848 | { 849 | testID: "test3", 850 | name: "it should put user (mocked), whit empty email", 851 | dbUser: dbUser3, 852 | expectedStatus: fiber.StatusOK, 853 | id: "im_an_id", 854 | body: `{"email":"","phone":"+991234567890","password":"12345pAsSWORd*"}`, 855 | }, 856 | } 857 | 858 | failedCases := []struct { 859 | testID string 860 | name string 861 | dbUser *internal.User 862 | body string 863 | id string 864 | expectedStatus int 865 | }{ 866 | { 867 | testID: "test4", 868 | name: "it should post user (mocked), empty id", 869 | dbUser: dbUser1, 870 | expectedStatus: fiber.StatusNotFound, 871 | id: "", 872 | body: `{"email":"","phone":"+991234567890","password":"12345pAsSWORd*"}`, 873 | }, 874 | { 875 | testID: "test5", 876 | name: "it should post user (mocked), empty password", 877 | dbUser: dbUser2, 878 | id: "im_an_id", 879 | expectedStatus: fiber.StatusBadRequest, 880 | body: `{"email":"test@test.com","phone":"","password":""}`, 881 | }, 882 | { 883 | testID: "test6", 884 | name: "it should not post user (mocked), empty string", 885 | dbUser: dbUser1, 886 | expectedStatus: fiber.StatusInternalServerError, 887 | id: "im_an_id", 888 | body: `{""}`, 889 | }, 890 | { 891 | testID: "test7", 892 | name: "it should post user (mocked), internal error", 893 | dbUser: dbUser2, 894 | id: "im_an_id_2", 895 | expectedStatus: fiber.StatusInternalServerError, 896 | body: `{"email":"test@test.com","phone":"","password":"12345pAsSWORd*"}`, 897 | }, 898 | } 899 | 900 | sCfg := fiber.Config{ 901 | ReadTimeout: 10 * time.Second, 902 | WriteTimeout: 10 * time.Second, 903 | IdleTimeout: 60 * time.Second, 904 | } 905 | app := fiber.New(sCfg) 906 | 907 | cfg := config.NewConfig("8080", "1.0.0") 908 | vars, err := cfg.SetConfig() 909 | if err != nil { 910 | test.Fatalf("expected no error but got %v", err) 911 | } 912 | v := validator.New() 913 | l := utils.NewLogger(*vars) 914 | err = l.Initialize() 915 | if err != nil { 916 | test.Fatalf("expected no error but got %v", err) 917 | } 918 | uuid := utils.NewUUIDMock() 919 | jwt := utils.NewJWTMock() 920 | val := utils.NewValidations() 921 | 922 | for _, tc := range successfulCases { 923 | tc := tc 924 | 925 | test.Run(tc.name, func(t *testing.T) { 926 | db, m, err := sqlmock.New() 927 | if err != nil { 928 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 929 | } 930 | 931 | assert.NoError(t, err) 932 | 933 | m.ExpectExec(regexp.QuoteMeta(spUpdate)).WithArgs( 934 | &tc.dbUser.ID, 935 | &tc.dbUser.Email, 936 | &tc.dbUser.Phone, 937 | &tc.dbUser.Password, 938 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 939 | assert.Empty(t, err, "expected no error, but got:", err) 940 | 941 | myCache := cache.New(5*time.Minute, 10*time.Minute) 942 | 943 | r := repository.NewRepository(db) 944 | uc := usecases.NewUseCases(r, uuid) 945 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 946 | 947 | app.Put("/put/"+tc.testID+"/:id", ctrl.Put) 948 | 949 | req := httptest.NewRequest("PUT", "/put/"+tc.testID+"/"+tc.id, bytes.NewBufferString(tc.body)) 950 | req.Header.Set("Content-Type", "application/json") 951 | resp, err := app.Test(req) 952 | assert.NoError(t, err) 953 | defer func() { 954 | if err := resp.Body.Close(); err != nil { 955 | test.Fatalf("error closing database connection: %v", err) 956 | } 957 | }() 958 | 959 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 960 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 961 | 962 | }) 963 | } 964 | 965 | for _, tc := range failedCases { 966 | tc := tc 967 | 968 | test.Run(tc.name, func(t *testing.T) { 969 | db, m, err := sqlmock.New() 970 | if err != nil { 971 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 972 | } 973 | 974 | assert.NoError(t, err) 975 | 976 | m.ExpectExec(regexp.QuoteMeta(spUpdate)).WithArgs( 977 | &tc.dbUser.ID, 978 | &tc.dbUser.Email, 979 | &tc.dbUser.Phone, 980 | &tc.dbUser.Password, 981 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 982 | assert.Empty(t, err, "expected no error, but got:", err) 983 | 984 | myCache := cache.New(5*time.Minute, 10*time.Minute) 985 | 986 | r := repository.NewRepository(db) 987 | uc := usecases.NewUseCases(r, uuid) 988 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 989 | 990 | app.Put("/put/"+tc.testID+"/:id", ctrl.Put) 991 | 992 | req := httptest.NewRequest("PUT", "/put/"+tc.testID+"/"+tc.id, bytes.NewBufferString(tc.body)) 993 | req.Header.Set("Content-Type", "application/json") 994 | resp, err := app.Test(req) 995 | assert.NoError(t, err) 996 | defer func() { 997 | if err := resp.Body.Close(); err != nil { 998 | test.Fatalf("error closing database connection: %v", err) 999 | } 1000 | }() 1001 | 1002 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 1003 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 1004 | 1005 | }) 1006 | } 1007 | } 1008 | 1009 | func TestDelet(test *testing.T) { 1010 | dbUser1 := &internal.User{ 1011 | ID: "im_an_id", 1012 | Email: "test@test.com", 1013 | Phone: "+991234567890", 1014 | Password: "12345pAsSWORd*", 1015 | } 1016 | successfulCases := []struct { 1017 | testID string 1018 | name string 1019 | dbUser *internal.User 1020 | body string 1021 | id string 1022 | expectedStatus int 1023 | }{ 1024 | { 1025 | testID: "test1", 1026 | name: "it should delete user (mocked)", 1027 | dbUser: dbUser1, 1028 | expectedStatus: fiber.StatusNoContent, 1029 | id: "im_an_id", 1030 | body: `{"password":"12345pAsSWORd*"}`, 1031 | }, 1032 | } 1033 | 1034 | failedCases := []struct { 1035 | testID string 1036 | name string 1037 | dbUser *internal.User 1038 | body string 1039 | id string 1040 | expectedStatus int 1041 | }{ 1042 | { 1043 | testID: "test4", 1044 | name: "it should post user (mocked), empty id", 1045 | dbUser: dbUser1, 1046 | expectedStatus: fiber.StatusNotFound, 1047 | id: "", 1048 | body: `{"password":"12345pAsSWORd*"}`, 1049 | }, 1050 | { 1051 | testID: "test5", 1052 | name: "it should post user (mocked), empty password", 1053 | dbUser: dbUser1, 1054 | id: "im_an_id", 1055 | expectedStatus: fiber.StatusBadRequest, 1056 | body: `{"password":""}`, 1057 | }, 1058 | { 1059 | testID: "test6", 1060 | name: "it should not post user (mocked), empty string", 1061 | dbUser: dbUser1, 1062 | expectedStatus: fiber.StatusInternalServerError, 1063 | id: "im_an_id", 1064 | body: `{""}`, 1065 | }, 1066 | { 1067 | testID: "test7", 1068 | name: "it should post user (mocked), internal error", 1069 | dbUser: dbUser1, 1070 | id: "im_an_id_2", 1071 | expectedStatus: fiber.StatusInternalServerError, 1072 | body: `{"password":"12345pAsSWORd*"}`, 1073 | }, 1074 | } 1075 | 1076 | sCfg := fiber.Config{ 1077 | ReadTimeout: 10 * time.Second, 1078 | WriteTimeout: 10 * time.Second, 1079 | IdleTimeout: 60 * time.Second, 1080 | } 1081 | app := fiber.New(sCfg) 1082 | 1083 | cfg := config.NewConfig("8080", "1.0.0") 1084 | vars, err := cfg.SetConfig() 1085 | if err != nil { 1086 | test.Fatalf("expected no error but got %v", err) 1087 | } 1088 | v := validator.New() 1089 | l := utils.NewLogger(*vars) 1090 | err = l.Initialize() 1091 | if err != nil { 1092 | test.Fatalf("expected no error but got %v", err) 1093 | } 1094 | uuid := utils.NewUUIDMock() 1095 | jwt := utils.NewJWTMock() 1096 | val := utils.NewValidations() 1097 | 1098 | for _, tc := range successfulCases { 1099 | tc := tc 1100 | 1101 | test.Run(tc.name, func(t *testing.T) { 1102 | db, m, err := sqlmock.New() 1103 | if err != nil { 1104 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 1105 | } 1106 | 1107 | assert.NoError(t, err) 1108 | 1109 | m.ExpectExec(regexp.QuoteMeta(spDelete)).WithArgs( 1110 | &tc.dbUser.ID, 1111 | &tc.dbUser.Password, 1112 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 1113 | assert.Empty(t, err, "expected no error, but got:", err) 1114 | 1115 | myCache := cache.New(5*time.Minute, 10*time.Minute) 1116 | 1117 | r := repository.NewRepository(db) 1118 | uc := usecases.NewUseCases(r, uuid) 1119 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 1120 | 1121 | app.Delete("/delete/"+tc.testID+"/:id", ctrl.Delete) 1122 | 1123 | req := httptest.NewRequest("DELETE", "/delete/"+tc.testID+"/"+tc.id, bytes.NewBufferString(tc.body)) 1124 | req.Header.Set("Content-Type", "application/json") 1125 | resp, err := app.Test(req) 1126 | assert.NoError(t, err) 1127 | defer func() { 1128 | if err := resp.Body.Close(); err != nil { 1129 | test.Fatalf("error closing database connection: %v", err) 1130 | } 1131 | }() 1132 | 1133 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 1134 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 1135 | 1136 | }) 1137 | } 1138 | 1139 | for _, tc := range failedCases { 1140 | tc := tc 1141 | 1142 | test.Run(tc.name, func(t *testing.T) { 1143 | db, m, err := sqlmock.New() 1144 | if err != nil { 1145 | test.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 1146 | } 1147 | 1148 | assert.NoError(t, err) 1149 | 1150 | m.ExpectExec(regexp.QuoteMeta(spDelete)).WithArgs( 1151 | &tc.dbUser.ID, 1152 | &tc.dbUser.Password, 1153 | ).WillReturnResult(sqlmock.NewResult(0, 1)) 1154 | assert.Empty(t, err, "expected no error, but got:", err) 1155 | 1156 | myCache := cache.New(5*time.Minute, 10*time.Minute) 1157 | 1158 | r := repository.NewRepository(db) 1159 | uc := usecases.NewUseCases(r, uuid) 1160 | ctrl := controller.NewController(uc, *v, l, jwt, val, *myCache) 1161 | 1162 | app.Delete("/delete/"+tc.testID+"/:id", ctrl.Delete) 1163 | 1164 | req := httptest.NewRequest("DELETE", "/delete/"+tc.testID+"/"+tc.id, bytes.NewBufferString(tc.body)) 1165 | req.Header.Set("Content-Type", "application/json") 1166 | resp, err := app.Test(req) 1167 | assert.NoError(t, err) 1168 | defer func() { 1169 | if err := resp.Body.Close(); err != nil { 1170 | test.Fatalf("error closing database connection: %v", err) 1171 | } 1172 | }() 1173 | 1174 | assert.Equal(t, tc.expectedStatus, resp.StatusCode) 1175 | m.ExpectClose().WillReturnError(sql.ErrConnDone) // expect a call to Close() but return an error to indicate that it was not expected 1176 | 1177 | }) 1178 | } 1179 | } 1180 | --------------------------------------------------------------------------------