├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── api ├── cli │ └── cli.go └── http │ ├── context.go │ ├── error.go │ ├── router.go │ └── server.go ├── assets ├── api-collection.insomnia-v4.json └── gopher-icon.gif ├── cmd └── http │ └── main.go ├── config ├── config.go └── env │ └── .env.template ├── docker-compose.yaml ├── go.mod ├── go.sum ├── internal ├── auth │ ├── dto.go │ ├── impl │ │ ├── service.go │ │ └── service_test.go │ ├── mock │ │ ├── config.go │ │ └── service.go │ └── service.go ├── base │ ├── crypto │ │ ├── crypto.go │ │ ├── impl │ │ │ └── crypto.go │ │ └── mock │ │ │ └── crypto.go │ ├── database │ │ ├── database.go │ │ ├── impl │ │ │ ├── client.go │ │ │ └── service.go │ │ └── mock │ │ │ └── tx.go │ ├── errors │ │ ├── error.go │ │ └── status.go │ └── request │ │ └── request.go └── user │ ├── dto.go │ ├── impl │ ├── repository.go │ ├── usecase.go │ └── usecase_test.go │ ├── mock │ ├── repository.go │ └── usecase.go │ ├── model.go │ ├── repository.go │ └── usecase.go └── migrations ├── 000001_init.down.sql └── 000001_init.up.sql /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | 3 | bin 4 | 5 | config/env/* 6 | !config/env/.env.template 7 | -------------------------------------------------------------------------------- /.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": "http-server", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceRoot}/cmd/http/main.go", 13 | "envFile": "${workspaceRoot}/config/env/.env", 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "explorer.experimental.fileNesting.patterns": { 3 | "*.go": "${capture}_test.go", 4 | "go.mod": "go.sum" 5 | }, 6 | "explorer.experimental.fileNesting.enabled": true 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Pavel Varentsov 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Constants 2 | 3 | PROJECT_NAME = 'go-backend-template' 4 | DB_URL = 'postgres://go-backend-template:go-backend-template@localhost:5454/go-backend-template?sslmode=disable' 5 | 6 | ifeq ($(OS),Windows_NT) 7 | DETECTED_OS := Windows 8 | else 9 | DETECTED_OS := $(shell sh -c 'uname 2>/dev/null || echo Unknown') 10 | endif 11 | 12 | # Help 13 | 14 | .SILENT: help 15 | help: 16 | @echo 17 | @echo "Usage: make [command]" 18 | @echo 19 | @echo "Commands:" 20 | @echo " rename-project name={name} Rename project" 21 | @echo 22 | @echo " build-http Build http server" 23 | @echo 24 | @echo " migration-create name={name} Create migration" 25 | @echo " migration-up Up migrations" 26 | @echo " migration-down Down last migration" 27 | @echo 28 | @echo " docker-up Up docker services" 29 | @echo " docker-down Down docker services" 30 | @echo 31 | @echo " fmt Format source code" 32 | @echo " test Run unit tests" 33 | @echo 34 | 35 | # Build 36 | 37 | .SILENT: rename-project 38 | rename-project: 39 | ifeq ($(name),) 40 | @echo 'new project name not set' 41 | else 42 | ifeq ($(DETECTED_OS),Darwin) 43 | @grep -RiIl '$(PROJECT_NAME)' | xargs sed -i '' 's/$(PROJECT_NAME)/$(name)/g' 44 | endif 45 | 46 | ifeq ($(DETECTED_OS),Linux) 47 | @grep -RiIl '$(PROJECT_NAME)' | xargs sed -i 's/$(PROJECT_NAME)/$(name)/g' 48 | endif 49 | 50 | ifeq ($(DETECTED_OS),Windows) 51 | @grep 'target is not implemented on Windows platform' 52 | endif 53 | endif 54 | 55 | .SILENT: build-http 56 | build-http: 57 | @go build -o ./bin/http-server ./cmd/http/main.go 58 | @echo executable file \"http-server\" saved in ./bin/http-server 59 | 60 | # Test 61 | 62 | .SILENT: test 63 | test: 64 | @go test ./... -v 65 | 66 | # Create migration 67 | 68 | .SILENT: migration-create 69 | migration-create: 70 | @migrate create -ext sql -dir ./migrations -seq $(name) 71 | 72 | # Up migration 73 | 74 | .SILENT: migration-up 75 | migration-up: 76 | @migrate -database $(DB_URL) -path ./migrations up 77 | 78 | # Down migration 79 | 80 | .SILENT: "migration-down" 81 | migration-down: 82 | @migrate -database $(DB_URL) -path ./migrations down 1 83 | 84 | # Docker 85 | 86 | .SILENT: docker-up 87 | docker-up: 88 | @docker-compose up -d 89 | 90 | .SILENT: docker-down 91 | docker-down: 92 | @docker-compose down 93 | 94 | # Format 95 | 96 | .SILENT: fmt 97 | fmt: 98 | @go fmt ./... 99 | 100 | # Default 101 | 102 | .DEFAULT_GOAL := help 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Go
Backend Template 3 |

4 | 5 | > Clean architecture based backend template in Go. 6 | 7 | ## Makefile 8 | 9 | Makefile requires installed dependecies: 10 | * [go](https://go.dev/doc/install) 11 | * [docker-compose](https://docs.docker.com/compose/reference) 12 | * [migrate](https://github.com/golang-migrate/migrate) 13 | 14 | 15 | ```shell 16 | $ make 17 | 18 | Usage: make [command] 19 | 20 | Commands: 21 | rename-project name={name} Rename project 22 | 23 | build-http Build http server 24 | 25 | migration-create name={name} Create migration 26 | migration-up Up migrations 27 | migration-down Down last migration 28 | 29 | docker-up Up docker services 30 | docker-down Down docker services 31 | 32 | fmt Format source code 33 | test Run unit tests 34 | 35 | ``` 36 | 37 | ## HTTP Server 38 | 39 | ```shell 40 | $ ./bin/http-server --help 41 | 42 | Usage: http-server 43 | 44 | Flags: 45 | -h, --help Show context-sensitive help. 46 | --env-path=STRING Path to env config file 47 | ``` 48 | 49 | **Configuration** is based on the environment variables. See [.env.template](./config/env/.env.template). 50 | 51 | ```shell 52 | # Expose env vars before and start server 53 | $ ./bin/http-server 54 | 55 | # Expose env vars from the file and start server 56 | $ ./bin/http-server --env-path ./config/env/.env 57 | ``` 58 | 59 | ## Request Collection 60 | * [InsomniaV4](./assets/api-collection.insomnia-v4.json) 61 | 62 | ## License 63 | 64 | This project is licensed under the [MIT License](https://github.com/pvarentsov/go-backend-template/blob/main/LICENSE). 65 | -------------------------------------------------------------------------------- /api/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/alecthomas/kong" 5 | "go-backend-template/config" 6 | ) 7 | 8 | // Scheme 9 | 10 | type scheme struct { 11 | EnvPath string `help:"Path to env config file" type:"path" optional:""` 12 | } 13 | 14 | // Parser 15 | 16 | type Parser struct { 17 | scheme scheme 18 | } 19 | 20 | func NewParser() *Parser { 21 | return &Parser{scheme: scheme{}} 22 | } 23 | 24 | func (p *Parser) ParseConfig() (*config.Config, error) { 25 | kong.Parse(&p.scheme) 26 | return config.ParseEnv(p.scheme.EnvPath) 27 | } 28 | -------------------------------------------------------------------------------- /api/http/context.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "go-backend-template/internal/base/request" 9 | ) 10 | 11 | type reqInfoKeyType = string 12 | 13 | const ( 14 | reqInfoKey reqInfoKeyType = "request-info" 15 | ) 16 | 17 | func setTraceId(c *gin.Context, traceId string) { 18 | info, exists := c.Get(reqInfoKey) 19 | if exists { 20 | parsedInfo := info.(request.RequestInfo) 21 | parsedInfo.TraceId = traceId 22 | 23 | c.Set(reqInfoKey, parsedInfo) 24 | 25 | return 26 | } 27 | 28 | c.Set(reqInfoKey, request.RequestInfo{TraceId: traceId}) 29 | } 30 | 31 | func setUserId(c *gin.Context, userId int64) { 32 | info, exists := c.Get(reqInfoKey) 33 | if exists { 34 | parsedInfo := info.(request.RequestInfo) 35 | parsedInfo.UserId = userId 36 | 37 | c.Set(reqInfoKey, parsedInfo) 38 | 39 | return 40 | } 41 | 42 | c.Set(reqInfoKey, request.RequestInfo{UserId: userId}) 43 | } 44 | 45 | func getReqInfo(c *gin.Context) request.RequestInfo { 46 | info, ok := c.Get(reqInfoKey) 47 | if ok { 48 | return info.(request.RequestInfo) 49 | } 50 | 51 | return request.RequestInfo{} 52 | } 53 | 54 | func contextWithReqInfo(c *gin.Context) context.Context { 55 | info, ok := c.Get(reqInfoKey) 56 | if ok { 57 | return request.WithRequestInfo(c, info.(request.RequestInfo)) 58 | } 59 | 60 | return request.WithRequestInfo(c, request.RequestInfo{}) 61 | } 62 | -------------------------------------------------------------------------------- /api/http/error.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go-backend-template/internal/base/errors" 7 | ) 8 | 9 | func parseError(err error) (status int, message, details string) { 10 | var baseErr *errors.Error 11 | 12 | if castErr, ok := err.(*errors.Error); ok { 13 | baseErr = castErr 14 | } 15 | if baseErr == nil { 16 | baseErr = errors.Wrap(err, errors.InternalError, "") 17 | } 18 | 19 | status = convertErrorStatusToHTTP(baseErr.Status()) 20 | message = baseErr.Error() 21 | details = baseErr.DetailedError() 22 | 23 | return 24 | } 25 | 26 | func convertErrorStatusToHTTP(status errors.Status) int { 27 | switch status { 28 | case errors.BadRequestError: 29 | return http.StatusBadRequest 30 | case errors.ValidationError: 31 | return http.StatusBadRequest 32 | case errors.UnauthorizedError: 33 | return http.StatusUnauthorized 34 | case errors.WrongCredentialsError: 35 | return http.StatusUnauthorized 36 | case errors.NotFoundError: 37 | return http.StatusNotFound 38 | case errors.AlreadyExistsError: 39 | return http.StatusConflict 40 | default: 41 | return http.StatusInternalServerError 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /api/http/router.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "go-backend-template/internal/auth" 11 | "go-backend-template/internal/base/errors" 12 | "go-backend-template/internal/base/request" 13 | "go-backend-template/internal/user" 14 | ) 15 | 16 | func initRouter(server *Server) { 17 | router := &router{ 18 | Server: server, 19 | } 20 | 21 | router.init() 22 | } 23 | 24 | type router struct { 25 | *Server 26 | } 27 | 28 | func (r *router) init() { 29 | r.engine.Use(r.trace()) 30 | r.engine.Use(r.recover()) 31 | r.engine.Use(r.logger()) 32 | 33 | r.engine.POST("/login", r.login) 34 | 35 | r.engine.POST("/users", r.addUser) 36 | r.engine.GET("/users/me", r.authenticate, r.getMe) 37 | r.engine.PUT("/users/me", r.authenticate, r.updateMe) 38 | r.engine.PATCH("/users/me/password", r.authenticate, r.changeMyPassword) 39 | 40 | r.engine.NoRoute(r.methodNotFound) 41 | } 42 | 43 | func (r *router) login(c *gin.Context) { 44 | var loginUserDto auth.LoginUserDto 45 | 46 | if err := bindBody(&loginUserDto, c); err != nil { 47 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 48 | return 49 | } 50 | 51 | user, err := r.authService.Login(c, loginUserDto) 52 | if err != nil { 53 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 54 | return 55 | } 56 | 57 | okResponse(user).reply(c) 58 | } 59 | 60 | func (r *router) authenticate(c *gin.Context) { 61 | token := c.Request.Header.Get("Authorization") 62 | 63 | userId, err := r.authService.VerifyAccessToken(token) 64 | if err != nil { 65 | response := errorResponse(err, nil, r.config.DetailedError()) 66 | c.AbortWithStatusJSON(response.Status, response) 67 | } 68 | 69 | setUserId(c, userId) 70 | } 71 | 72 | func (r *router) addUser(c *gin.Context) { 73 | var addUserDto user.AddUserDto 74 | 75 | if err := bindBody(&addUserDto, c); err != nil { 76 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 77 | return 78 | } 79 | 80 | user, err := r.userUsecases.Add(contextWithReqInfo(c), addUserDto) 81 | if err != nil { 82 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 83 | return 84 | } 85 | 86 | okResponse(user).reply(c) 87 | } 88 | 89 | func (r *router) updateMe(c *gin.Context) { 90 | var updateUserDto user.UpdateUserDto 91 | 92 | reqInfo := getReqInfo(c) 93 | updateUserDto.Id = reqInfo.UserId 94 | 95 | if err := bindBody(&updateUserDto, c); err != nil { 96 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 97 | return 98 | } 99 | 100 | err := r.userUsecases.Update(contextWithReqInfo(c), updateUserDto) 101 | if err != nil { 102 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 103 | return 104 | } 105 | 106 | okResponse(nil).reply(c) 107 | } 108 | 109 | func (r *router) changeMyPassword(c *gin.Context) { 110 | var changeUserPasswordDto user.ChangeUserPasswordDto 111 | 112 | reqInfo := getReqInfo(c) 113 | changeUserPasswordDto.Id = reqInfo.UserId 114 | 115 | if err := bindBody(&changeUserPasswordDto, c); err != nil { 116 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 117 | return 118 | } 119 | 120 | err := r.userUsecases.ChangePassword(contextWithReqInfo(c), changeUserPasswordDto) 121 | if err != nil { 122 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 123 | return 124 | } 125 | 126 | okResponse(nil).reply(c) 127 | } 128 | 129 | func (r *router) getMe(c *gin.Context) { 130 | reqInfo := getReqInfo(c) 131 | 132 | user, err := r.userUsecases.GetById(contextWithReqInfo(c), reqInfo.UserId) 133 | if err != nil { 134 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 135 | return 136 | } 137 | 138 | okResponse(user).reply(c) 139 | } 140 | 141 | func (r *router) methodNotFound(c *gin.Context) { 142 | err := errors.New(errors.NotFoundError, "method not found") 143 | errorResponse(err, nil, r.config.DetailedError()).reply(c) 144 | } 145 | 146 | func (r *router) recover() gin.HandlerFunc { 147 | return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { 148 | response := internalErrorResponse(nil) 149 | c.AbortWithStatusJSON(response.Status, response) 150 | }) 151 | } 152 | 153 | func (r *router) trace() gin.HandlerFunc { 154 | return func(c *gin.Context) { 155 | traceId := c.Request.Header.Get("Trace-Id") 156 | if traceId == "" { 157 | traceId, _ = r.crypto.GenerateUUID() 158 | } 159 | 160 | setTraceId(c, traceId) 161 | } 162 | } 163 | 164 | func (r *router) logger() gin.HandlerFunc { 165 | return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { 166 | var parsedReqInfo request.RequestInfo 167 | 168 | reqInfo, exists := param.Keys[reqInfoKey] 169 | if exists { 170 | parsedReqInfo = reqInfo.(request.RequestInfo) 171 | } 172 | 173 | return fmt.Sprintf("%s - [HTTP] TraceId: %s; UserId: %d; Method: %s; Path: %s; Status: %d, Latency: %s;\n\n", 174 | param.TimeStamp.Format(time.RFC1123), 175 | parsedReqInfo.TraceId, 176 | parsedReqInfo.UserId, 177 | param.Method, 178 | param.Path, 179 | param.StatusCode, 180 | param.Latency, 181 | ) 182 | }) 183 | } 184 | 185 | func bindBody(payload interface{}, c *gin.Context) error { 186 | err := c.BindJSON(payload) 187 | 188 | if err != nil { 189 | return errors.New(errors.BadRequestError, err.Error()) 190 | } 191 | 192 | return nil 193 | } 194 | 195 | type response struct { 196 | Status int `json:"status"` 197 | Message string `json:"message"` 198 | Data interface{} `json:"data"` 199 | } 200 | 201 | func okResponse(data interface{}) *response { 202 | return &response{ 203 | Status: http.StatusOK, 204 | Message: "ok", 205 | Data: data, 206 | } 207 | } 208 | 209 | func internalErrorResponse(data interface{}) *response { 210 | status, message := http.StatusInternalServerError, "internal error" 211 | 212 | return &response{ 213 | Status: status, 214 | Message: message, 215 | Data: data, 216 | } 217 | } 218 | 219 | func errorResponse(err error, data interface{}, withDetails bool) *response { 220 | status, message, details := parseError(err) 221 | 222 | if withDetails && details != "" { 223 | message = details 224 | } 225 | return &response{ 226 | Status: status, 227 | Message: message, 228 | Data: data, 229 | } 230 | } 231 | 232 | func (r *response) reply(c *gin.Context) { 233 | c.JSON(r.Status, r) 234 | } 235 | -------------------------------------------------------------------------------- /api/http/server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "go-backend-template/internal/auth" 9 | "go-backend-template/internal/base/crypto" 10 | "go-backend-template/internal/user" 11 | ) 12 | 13 | type Config interface { 14 | DetailedError() bool 15 | Address() string 16 | } 17 | 18 | type ServerOpts struct { 19 | UserUsecases user.UserUsecases 20 | AuthService auth.AuthService 21 | Crypto crypto.Crypto 22 | Config Config 23 | } 24 | 25 | func NewServer(opts ServerOpts) *Server { 26 | gin.SetMode(gin.ReleaseMode) 27 | 28 | server := &Server{ 29 | engine: gin.New(), 30 | config: opts.Config, 31 | crypto: opts.Crypto, 32 | userUsecases: opts.UserUsecases, 33 | authService: opts.AuthService, 34 | } 35 | 36 | initRouter(server) 37 | 38 | return server 39 | } 40 | 41 | type Server struct { 42 | engine *gin.Engine 43 | config Config 44 | crypto crypto.Crypto 45 | userUsecases user.UserUsecases 46 | authService auth.AuthService 47 | } 48 | 49 | func (s Server) Listen() error { 50 | fmt.Printf("API server listening at: %s\n\n", s.config.Address()) 51 | return s.engine.Run(s.config.Address()) 52 | } 53 | -------------------------------------------------------------------------------- /assets/api-collection.insomnia-v4.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "export", 3 | "__export_format": 4, 4 | "__export_date": "2022-01-09T18:25:42.371Z", 5 | "__export_source": "insomnia.desktop.app:v2021.3.0", 6 | "resources": [ 7 | { 8 | "_id": "req_dcc2a107f73749bf995afe724452a251", 9 | "parentId": "wrk_fdc8eb301881436596a6ad61b36ee257", 10 | "modified": 1641752615652, 11 | "created": 1641733104003, 12 | "url": "localhost:3000/login", 13 | "name": "Login", 14 | "description": "", 15 | "method": "POST", 16 | "body": { 17 | "mimeType": "application/json", 18 | "text": "{\n\t\"email\": \"user@email.com\",\n\t\"password\": \"qwerty1\"\n}" 19 | }, 20 | "parameters": [], 21 | "headers": [ 22 | { 23 | "name": "Content-Type", 24 | "value": "application/json", 25 | "id": "pair_33f5ace374414a5089edf3feb65f0b41" 26 | } 27 | ], 28 | "authentication": {}, 29 | "metaSortKey": -1640816933631, 30 | "isPrivate": false, 31 | "settingStoreCookies": true, 32 | "settingSendCookies": true, 33 | "settingDisableRenderRequestBody": false, 34 | "settingEncodeUrl": true, 35 | "settingRebuildPath": true, 36 | "settingFollowRedirects": "global", 37 | "_type": "request" 38 | }, 39 | { 40 | "_id": "wrk_fdc8eb301881436596a6ad61b36ee257", 41 | "parentId": null, 42 | "modified": 1641733103981, 43 | "created": 1641733103981, 44 | "name": "go-backend-template", 45 | "description": "", 46 | "scope": "collection", 47 | "_type": "workspace" 48 | }, 49 | { 50 | "_id": "req_0fe1640aa39142879aac513611db093c", 51 | "parentId": "wrk_fdc8eb301881436596a6ad61b36ee257", 52 | "modified": 1641752608890, 53 | "created": 1641733103996, 54 | "url": "localhost:3000/users", 55 | "name": "Add User", 56 | "description": "", 57 | "method": "POST", 58 | "body": { 59 | "mimeType": "application/json", 60 | "text": "{\n\t\"firstName\": \"FirstName\",\n\t\"lastName\": \"LastName\",\n\t\"email\": \"user@email.com\",\n\t\"password\": \"qwerty1\"\n}" 61 | }, 62 | "parameters": [], 63 | "headers": [ 64 | { 65 | "name": "Content-Type", 66 | "value": "application/json", 67 | "id": "pair_33f5ace374414a5089edf3feb65f0b41" 68 | } 69 | ], 70 | "authentication": {}, 71 | "metaSortKey": -1640816933606, 72 | "isPrivate": false, 73 | "settingStoreCookies": true, 74 | "settingSendCookies": true, 75 | "settingDisableRenderRequestBody": false, 76 | "settingEncodeUrl": true, 77 | "settingRebuildPath": true, 78 | "settingFollowRedirects": "global", 79 | "_type": "request" 80 | }, 81 | { 82 | "_id": "req_c973783467d7486ba8ff984eacc97165", 83 | "parentId": "wrk_fdc8eb301881436596a6ad61b36ee257", 84 | "modified": 1641752651102, 85 | "created": 1641733103997, 86 | "url": "localhost:3000/users/me", 87 | "name": "Update User Info", 88 | "description": "", 89 | "method": "PUT", 90 | "body": { 91 | "mimeType": "application/json", 92 | "text": "{\n\t\"firstName\": \"FirstName\",\n\t\"lastName\": \"LastName\",\n\t\"email\": \"user@email.com\"\n}" 93 | }, 94 | "parameters": [], 95 | "headers": [ 96 | { 97 | "name": "Content-Type", 98 | "value": "application/json", 99 | "id": "pair_33f5ace374414a5089edf3feb65f0b41" 100 | }, 101 | { 102 | "name": "Authorization", 103 | "value": "{{ _.accessToken }}", 104 | "description": "", 105 | "id": "pair_45439469e0324fdaa44f64e1a68c0b4c" 106 | } 107 | ], 108 | "authentication": {}, 109 | "metaSortKey": -1640816933593.5, 110 | "isPrivate": false, 111 | "settingStoreCookies": true, 112 | "settingSendCookies": true, 113 | "settingDisableRenderRequestBody": false, 114 | "settingEncodeUrl": true, 115 | "settingRebuildPath": true, 116 | "settingFollowRedirects": "global", 117 | "_type": "request" 118 | }, 119 | { 120 | "_id": "req_7d22e3b85d0f43dd8028a0c300c155d8", 121 | "parentId": "wrk_fdc8eb301881436596a6ad61b36ee257", 122 | "modified": 1641747858457, 123 | "created": 1641747784714, 124 | "url": "localhost:3000/users/me/password", 125 | "name": "Change User Password", 126 | "description": "", 127 | "method": "PATCH", 128 | "body": { 129 | "mimeType": "application/json", 130 | "text": "{\n\t\"password\": \"qwerty1\"\n}" 131 | }, 132 | "parameters": [], 133 | "headers": [ 134 | { 135 | "name": "Content-Type", 136 | "value": "application/json", 137 | "id": "pair_33f5ace374414a5089edf3feb65f0b41" 138 | }, 139 | { 140 | "name": "Authorization", 141 | "value": "{{ _.accessToken }}", 142 | "description": "", 143 | "id": "pair_45439469e0324fdaa44f64e1a68c0b4c" 144 | } 145 | ], 146 | "authentication": {}, 147 | "metaSortKey": -1640816933590.375, 148 | "isPrivate": false, 149 | "settingStoreCookies": true, 150 | "settingSendCookies": true, 151 | "settingDisableRenderRequestBody": false, 152 | "settingEncodeUrl": true, 153 | "settingRebuildPath": true, 154 | "settingFollowRedirects": "global", 155 | "_type": "request" 156 | }, 157 | { 158 | "_id": "req_d2fc3f00cde443a090d90b2030eefd58", 159 | "parentId": "wrk_fdc8eb301881436596a6ad61b36ee257", 160 | "modified": 1641733533708, 161 | "created": 1641733103996, 162 | "url": "localhost:3000/users/me", 163 | "name": "Get My Profile", 164 | "description": "", 165 | "method": "GET", 166 | "body": {}, 167 | "parameters": [], 168 | "headers": [ 169 | { 170 | "name": "Authorization", 171 | "value": "{{ _.accessToken }}", 172 | "description": "", 173 | "id": "pair_f51f09f8fd194d64ab8fc6b7e80d6d2c" 174 | } 175 | ], 176 | "authentication": {}, 177 | "metaSortKey": -1640816933587.25, 178 | "isPrivate": false, 179 | "settingStoreCookies": true, 180 | "settingSendCookies": true, 181 | "settingDisableRenderRequestBody": false, 182 | "settingEncodeUrl": true, 183 | "settingRebuildPath": true, 184 | "settingFollowRedirects": "global", 185 | "_type": "request" 186 | }, 187 | { 188 | "_id": "env_93860e324e0f42deaefef826a7c27731", 189 | "parentId": "wrk_fdc8eb301881436596a6ad61b36ee257", 190 | "modified": 1641733103984, 191 | "created": 1641733103984, 192 | "name": "Base Environment", 193 | "data": {}, 194 | "dataPropertyOrder": {}, 195 | "color": null, 196 | "isPrivate": false, 197 | "metaSortKey": 1640816927246, 198 | "_type": "environment" 199 | }, 200 | { 201 | "_id": "jar_09ce4d0ffaa24451820ef31fda7e04f3", 202 | "parentId": "wrk_fdc8eb301881436596a6ad61b36ee257", 203 | "modified": 1641733103992, 204 | "created": 1641733103992, 205 | "name": "Default Jar", 206 | "cookies": [], 207 | "_type": "cookie_jar" 208 | }, 209 | { 210 | "_id": "spc_f0dd8a8b8f054d84be42a41762875352", 211 | "parentId": "wrk_fdc8eb301881436596a6ad61b36ee257", 212 | "modified": 1641733104025, 213 | "created": 1641733103993, 214 | "fileName": "Go Backend Template", 215 | "contents": "", 216 | "contentType": "yaml", 217 | "_type": "api_spec" 218 | }, 219 | { 220 | "_id": "env_e84bf254b8544c8cb81cc70e175022a7", 221 | "parentId": "env_93860e324e0f42deaefef826a7c27731", 222 | "modified": 1641752719387, 223 | "created": 1641733103985, 224 | "name": "Local", 225 | "data": { 226 | "accessToken": "" 227 | }, 228 | "dataPropertyOrder": { 229 | "&": [ 230 | "accessToken" 231 | ] 232 | }, 233 | "color": null, 234 | "isPrivate": false, 235 | "metaSortKey": 1641121117528, 236 | "_type": "environment" 237 | } 238 | ] 239 | } 240 | -------------------------------------------------------------------------------- /assets/gopher-icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvarentsov/go-backend-template/0b047a95b1864286870b8b03ea7e30686dcee171/assets/gopher-icon.gif -------------------------------------------------------------------------------- /cmd/http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "go-backend-template/api/cli" 8 | "go-backend-template/api/http" 9 | 10 | authImpl "go-backend-template/internal/auth/impl" 11 | cryptoImpl "go-backend-template/internal/base/crypto/impl" 12 | databaseImpl "go-backend-template/internal/base/database/impl" 13 | userImpl "go-backend-template/internal/user/impl" 14 | ) 15 | 16 | func main() { 17 | ctx := context.Background() 18 | parser := cli.NewParser() 19 | 20 | conf, err := parser.ParseConfig() 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | dbClient := databaseImpl.NewClient(ctx, conf.Database()) 26 | 27 | err = dbClient.Connect() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | defer dbClient.Close() 33 | 34 | crypto := cryptoImpl.NewCrypto() 35 | dbService := databaseImpl.NewService(dbClient) 36 | 37 | userRepositoryOpts := userImpl.UserRepositoryOpts{ 38 | ConnManager: dbService, 39 | } 40 | userRepository := userImpl.NewUserRepository(userRepositoryOpts) 41 | 42 | authServiceOpts := authImpl.AuthServiceOpts{ 43 | Crypto: crypto, 44 | Config: conf.Auth(), 45 | UserRepository: userRepository, 46 | } 47 | authService := authImpl.NewAuthService(authServiceOpts) 48 | 49 | userUsecasesOpts := userImpl.UserUsecasesOpts{ 50 | TxManager: dbService, 51 | UserRepository: userRepository, 52 | Crypto: crypto, 53 | } 54 | userUsecases := userImpl.NewUserUsecases(userUsecasesOpts) 55 | 56 | serverOpts := http.ServerOpts{ 57 | UserUsecases: userUsecases, 58 | AuthService: authService, 59 | Crypto: crypto, 60 | Config: conf.HTTP(), 61 | } 62 | server := http.NewServer(serverOpts) 63 | 64 | log.Fatal(server.Listen()) 65 | } 66 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "go-backend-template/api/http" 8 | "go-backend-template/internal/auth" 9 | "go-backend-template/internal/base/database" 10 | 11 | "github.com/kelseyhightower/envconfig" 12 | "github.com/subosito/gotenv" 13 | ) 14 | 15 | // Config 16 | 17 | type Config struct { 18 | HttpHost string `envconfig:"HTTP_HOST"` 19 | HttpPort int `envconfig:"HTTP_PORT"` 20 | HttpDetailedError bool `envconfig:"HTTP_DETAILED_ERROR"` 21 | 22 | DatabaseURL string `envconfig:"DATABASE_URL"` 23 | 24 | AccessTokenExpiresTTL int `envconfig:"ACCESS_TOKEN_EXPIRES_TTL"` 25 | AccessTokenSecret string `envconfig:"ACCESS_TOKEN_SECRET"` 26 | } 27 | 28 | func ParseEnv(envPath string) (*Config, error) { 29 | if envPath != "" { 30 | if err := gotenv.OverLoad(envPath); err != nil { 31 | return nil, err 32 | } 33 | } 34 | 35 | var config Config 36 | 37 | if err := envconfig.Process("", &config); err != nil { 38 | return nil, err 39 | } 40 | 41 | return &config, nil 42 | } 43 | 44 | func (c *Config) HTTP() http.Config { 45 | return &httpConfig{ 46 | host: c.HttpHost, 47 | port: c.HttpPort, 48 | detailedError: c.HttpDetailedError, 49 | } 50 | } 51 | 52 | func (c *Config) Database() database.Config { 53 | return &databaseConfig{ 54 | url: c.DatabaseURL, 55 | } 56 | } 57 | 58 | func (c *Config) Auth() auth.Config { 59 | return &authConfig{ 60 | accessTokenExpiresTTL: c.AccessTokenExpiresTTL, 61 | accessTokenSecret: c.AccessTokenSecret, 62 | } 63 | } 64 | 65 | // HTTP 66 | 67 | type httpConfig struct { 68 | host string 69 | port int 70 | detailedError bool 71 | } 72 | 73 | func (c *httpConfig) Address() string { 74 | return fmt.Sprintf("%s:%d", c.host, c.port) 75 | } 76 | 77 | func (c *httpConfig) DetailedError() bool { 78 | return c.detailedError 79 | } 80 | 81 | // Database 82 | 83 | type databaseConfig struct { 84 | url string 85 | } 86 | 87 | func (c *databaseConfig) ConnString() string { 88 | return c.url 89 | } 90 | 91 | // Auth 92 | 93 | type authConfig struct { 94 | accessTokenExpiresTTL int 95 | accessTokenSecret string 96 | } 97 | 98 | func (c *authConfig) AccessTokenSecret() string { 99 | return c.accessTokenSecret 100 | } 101 | 102 | func (c *authConfig) AccessTokenExpiresDate() time.Time { 103 | duration := time.Duration(c.accessTokenExpiresTTL) 104 | return time.Now().UTC().Add(time.Minute * duration) 105 | } 106 | -------------------------------------------------------------------------------- /config/env/.env.template: -------------------------------------------------------------------------------- 1 | HTTP_HOST=127.0.0.1 2 | HTTP_PORT=3000 3 | HTTP_DETAILED_ERROR=false 4 | 5 | DATABASE_URL=postgres://go-backend-template:go-backend-template@localhost:5454/go-backend-template 6 | 7 | ACCESS_TOKEN_EXPIRES_TTL=180 #In minutes 8 | ACCESS_TOKEN_SECRET=secret 9 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | postgres: 5 | image: postgres:14-alpine 6 | container_name: go-backend-template-postgres 7 | ports: 8 | - '5454:5432' 9 | volumes: 10 | - postgres_data:/var/lib/postgresql/data 11 | environment: 12 | - POSTGRES_PASSWORD=go-backend-template 13 | - POSTGRES_USER=go-backend-template 14 | - POSTGRES_DB=go-backend-template 15 | networks: 16 | - network 17 | 18 | volumes: 19 | postgres_data: 20 | name: 'go-backend-template-postgres-data' 21 | 22 | networks: 23 | network: 24 | name: 'go-backend-template-network' 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-backend-template 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/doug-martin/goqu/v9 v9.18.0 7 | github.com/gin-gonic/gin v1.7.7 8 | github.com/go-ozzo/ozzo-validation v3.6.0+incompatible 9 | github.com/gofrs/uuid v4.2.0+incompatible 10 | github.com/golang-jwt/jwt v3.2.2+incompatible 11 | github.com/jackc/pgconn v1.10.1 12 | github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 13 | github.com/jackc/pgx/v4 v4.14.1 14 | github.com/kelseyhightower/envconfig v1.4.0 15 | github.com/stretchr/testify v1.7.1 16 | github.com/subosito/gotenv v1.2.0 17 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/gin-contrib/sse v0.1.0 // indirect 23 | github.com/go-playground/locales v0.13.0 // indirect 24 | github.com/go-playground/universal-translator v0.17.0 // indirect 25 | github.com/go-playground/validator/v10 v10.4.1 // indirect 26 | github.com/golang/protobuf v1.5.2 // indirect 27 | github.com/google/go-cmp v0.5.6 // indirect 28 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 29 | github.com/jackc/pgio v1.0.0 // indirect 30 | github.com/jackc/pgpassfile v1.0.0 // indirect 31 | github.com/jackc/pgproto3/v2 v2.2.0 // indirect 32 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 33 | github.com/jackc/pgtype v1.9.1 // indirect 34 | github.com/jackc/puddle v1.2.0 // indirect 35 | github.com/json-iterator/go v1.1.12 // indirect 36 | github.com/kr/pretty v0.2.0 // indirect 37 | github.com/leodido/go-urn v1.2.0 // indirect 38 | github.com/mattn/go-isatty v0.0.14 // indirect 39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 40 | github.com/modern-go/reflect2 v1.0.2 // indirect 41 | github.com/pkg/errors v0.9.1 // indirect 42 | github.com/pmezard/go-difflib v1.0.0 // indirect 43 | github.com/stretchr/objx v0.2.0 // indirect 44 | github.com/ugorji/go/codec v1.1.7 // indirect 45 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect 46 | golang.org/x/text v0.3.7 // indirect 47 | google.golang.org/protobuf v1.27.1 // indirect 48 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 49 | gopkg.in/yaml.v2 v2.4.0 // indirect 50 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 51 | ) 52 | 53 | require ( 54 | github.com/alecthomas/kong v0.3.0 55 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect 56 | ) 57 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 3 | github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 4 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 5 | github.com/alecthomas/kong v0.3.0 h1:qOLFzu0dGPNz8z5TiXGzgW3gb3RXfWVJKeAxcghVW88= 6 | github.com/alecthomas/kong v0.3.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0= 7 | github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= 8 | github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 9 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= 10 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 11 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 12 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 13 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 14 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 15 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 20 | github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY= 21 | github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ= 22 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 23 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 24 | github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= 25 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 26 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 27 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 28 | github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= 29 | github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= 30 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 31 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 32 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 33 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 34 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 35 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 36 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 37 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 38 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 39 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 40 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 41 | github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= 42 | github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 43 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 44 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 45 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 46 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 47 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 48 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 49 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 50 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 52 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 53 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 54 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 55 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 56 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 57 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 58 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 59 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 60 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 61 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 62 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 63 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 64 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 65 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 66 | github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8= 67 | github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 68 | github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 h1:WAvSpGf7MsFuzAtK4Vk7R4EVe+liW4x83r4oWu0WHKw= 69 | github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= 70 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 71 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 72 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 73 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 74 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 75 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 76 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 77 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 78 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 79 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 80 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 81 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 82 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 83 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 84 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 85 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 86 | github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= 87 | github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 88 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 89 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 90 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 91 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 92 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 93 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 94 | github.com/jackc/pgtype v1.9.1 h1:MJc2s0MFS8C3ok1wQTdQxWuXQcB6+HwAm5x1CzW7mf0= 95 | github.com/jackc/pgtype v1.9.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 96 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 97 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 98 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 99 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 100 | github.com/jackc/pgx/v4 v4.14.1 h1:71oo1KAGI6mXhLiTMn6iDFcp3e7+zon/capWjl2OEFU= 101 | github.com/jackc/pgx/v4 v4.14.1/go.mod h1:RgDuE4Z34o7XE92RpLsvFiOEfrAUT0Xt2KxvX73W06M= 102 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 103 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 104 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 105 | github.com/jackc/puddle v1.2.0 h1:DNDKdn/pDrWvDWyT2FYvpZVE81OAhWrjCv19I9n108Q= 106 | github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 107 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 108 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 109 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 110 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 111 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 112 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 113 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 114 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 115 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 116 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 117 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 118 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 119 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 120 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 121 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 122 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 123 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 124 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 125 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 126 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 127 | github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 128 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 129 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 130 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 131 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 132 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 133 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 134 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 135 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 136 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 137 | github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 138 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 139 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 140 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 141 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 142 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 143 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 144 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 145 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 146 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 147 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 148 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 149 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 150 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 151 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 152 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 153 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 154 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 155 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 156 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 157 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 158 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 159 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 160 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 161 | github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 162 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 163 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 164 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 165 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 166 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 167 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 168 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 169 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 170 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 171 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 172 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 173 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 174 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 175 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 176 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 177 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 178 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 179 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 180 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 181 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 182 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 183 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 184 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 185 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 186 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 187 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 188 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 189 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 190 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 191 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 192 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 193 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 194 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 195 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 196 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 197 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 198 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 199 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= 200 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 201 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 202 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 203 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 204 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 205 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 206 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 207 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 208 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 209 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 210 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 211 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 212 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 213 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 214 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 217 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 219 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 223 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 224 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 225 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk= 226 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 227 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 228 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 229 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 230 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 231 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 232 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 233 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 234 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 235 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 236 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 237 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 238 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 239 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 240 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 241 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 242 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 243 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 244 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 245 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 246 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 247 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 248 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 249 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 250 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 251 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 252 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 253 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 254 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 255 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 256 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 257 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 258 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 259 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 260 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 261 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 262 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 263 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 264 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 265 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 266 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 267 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 268 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 269 | -------------------------------------------------------------------------------- /internal/auth/dto.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "go-backend-template/internal/user" 4 | 5 | type LoginUserDto struct { 6 | Email string `json:"email"` 7 | Password string `json:"password"` 8 | } 9 | 10 | type LoggedUserDto struct { 11 | user.UserDto 12 | Token string `json:"token"` 13 | } 14 | 15 | func (dto LoggedUserDto) MapFromModel(model user.UserModel, token string) LoggedUserDto { 16 | dto.Id = model.Id 17 | dto.FirstName = model.FirstName 18 | dto.LastName = model.LastName 19 | dto.Email = model.Email 20 | dto.Token = token 21 | 22 | return dto 23 | } 24 | -------------------------------------------------------------------------------- /internal/auth/impl/service.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | 6 | "go-backend-template/internal/auth" 7 | "go-backend-template/internal/base/crypto" 8 | "go-backend-template/internal/base/errors" 9 | "go-backend-template/internal/user" 10 | ) 11 | 12 | type AuthServiceOpts struct { 13 | UserRepository user.UserRepository 14 | Crypto crypto.Crypto 15 | Config auth.Config 16 | } 17 | 18 | func NewAuthService(opts AuthServiceOpts) auth.AuthService { 19 | return &authService{ 20 | UserRepository: opts.UserRepository, 21 | Crypto: opts.Crypto, 22 | Config: opts.Config, 23 | } 24 | } 25 | 26 | type authService struct { 27 | user.UserRepository 28 | crypto.Crypto 29 | auth.Config 30 | } 31 | 32 | func (u *authService) Login(ctx context.Context, in auth.LoginUserDto) (out auth.LoggedUserDto, err error) { 33 | user, err := u.UserRepository.GetByEmail(ctx, in.Email) 34 | if err != nil { 35 | return out, errors.Wrap(err, errors.WrongCredentialsError, "") 36 | } 37 | if !user.ComparePassword(in.Password, u.Crypto) { 38 | return out, errors.New(errors.WrongCredentialsError, "") 39 | } 40 | token, err := u.generateAccessToken(user.Id) 41 | if err != nil { 42 | return out, err 43 | } 44 | 45 | return out.MapFromModel(user, token), nil 46 | } 47 | 48 | func (u *authService) VerifyAccessToken(accessToken string) (int64, error) { 49 | payload, err := u.ParseAndValidateJWT(accessToken, u.AccessTokenSecret()) 50 | if err != nil { 51 | return 0, errors.New(errors.UnauthorizedError, "") 52 | } 53 | 54 | userId, ok := payload["userId"].(float64) 55 | if !ok { 56 | return 0, errors.New(errors.UnauthorizedError, "") 57 | } 58 | 59 | return int64(userId), nil 60 | } 61 | 62 | func (u *authService) ParseAccessToken(accessToken string) (int64, error) { 63 | payload, err := u.ParseJWT(accessToken, u.AccessTokenSecret()) 64 | if err != nil { 65 | return 0, errors.New(errors.UnauthorizedError, "") 66 | } 67 | 68 | userId, ok := payload["userId"].(float64) 69 | if !ok { 70 | return 0, errors.New(errors.UnauthorizedError, "") 71 | } 72 | 73 | return int64(userId), nil 74 | } 75 | 76 | func (u *authService) generateAccessToken(userId int64) (string, error) { 77 | payload := map[string]interface{}{"userId": userId} 78 | 79 | return u.GenerateJWT( 80 | payload, 81 | u.AccessTokenSecret(), 82 | u.AccessTokenExpiresDate(), 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /internal/auth/impl/service_test.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/mock" 10 | "github.com/stretchr/testify/require" 11 | 12 | auth "go-backend-template/internal/auth" 13 | authMock "go-backend-template/internal/auth/mock" 14 | cryptoMock "go-backend-template/internal/base/crypto/mock" 15 | baseErrors "go-backend-template/internal/base/errors" 16 | user "go-backend-template/internal/user" 17 | userMock "go-backend-template/internal/user/mock" 18 | ) 19 | 20 | func TestAuthUsecases_Login(t *testing.T) { 21 | userId := int64(1) 22 | 23 | token := "token" 24 | tokenSecret := "token-secret" 25 | tokenExpires := time.Now().Add(time.Hour) 26 | tokenPayload := map[string]interface{}{"userId": userId} 27 | 28 | password := "password" 29 | passwordHash := "password-hash" 30 | 31 | in := auth.LoginUserDto{ 32 | Email: "user@email.com", 33 | Password: password, 34 | } 35 | getUser := user.UserModel{ 36 | Id: userId, 37 | FirstName: "FirstName", 38 | LastName: "LastName", 39 | Email: in.Email, 40 | Password: passwordHash, 41 | } 42 | loginUser := auth.LoggedUserDto{ 43 | UserDto: user.UserDto{ 44 | Id: getUser.Id, 45 | FirstName: getUser.FirstName, 46 | LastName: getUser.LastName, 47 | Email: getUser.Email, 48 | }, 49 | Token: token, 50 | } 51 | 52 | t.Run("expect it logins user", func(t *testing.T) { 53 | prep := newTestPrep() 54 | 55 | prep.userRepo.EXPECT().GetByEmail(mock.Anything, in.Email).Return(getUser, nil) 56 | prep.crypto.EXPECT().CompareHashAndPassword(passwordHash, password).Return(true) 57 | 58 | prep.config.EXPECT().AccessTokenSecret().Return(tokenSecret) 59 | prep.config.EXPECT().AccessTokenExpiresDate().Return(tokenExpires) 60 | prep.crypto.EXPECT().GenerateJWT(tokenPayload, tokenSecret, tokenExpires).Return(token, nil) 61 | 62 | actualLoginUser, err := prep.authService.Login(prep.ctx, in) 63 | 64 | require.NoError(t, err) 65 | require.Equal(t, loginUser, actualLoginUser) 66 | }) 67 | 68 | t.Run("expect it fails if user with such email does't exist", func(t *testing.T) { 69 | prep := newTestPrep() 70 | 71 | err := errors.New("user not found") 72 | wrapErr := baseErrors.New(baseErrors.WrongCredentialsError, "") 73 | 74 | prep.userRepo.EXPECT().GetByEmail(mock.Anything, in.Email).Return(getUser, err) 75 | 76 | _, actualErr := prep.authService.Login(prep.ctx, in) 77 | 78 | require.Error(t, actualErr) 79 | require.EqualError(t, wrapErr, actualErr.Error()) 80 | }) 81 | 82 | t.Run("expect it fails if password is wrong", func(t *testing.T) { 83 | prep := newTestPrep() 84 | err := baseErrors.New(baseErrors.WrongCredentialsError, "") 85 | 86 | prep.userRepo.EXPECT().GetByEmail(mock.Anything, in.Email).Return(getUser, nil) 87 | prep.crypto.EXPECT().CompareHashAndPassword(passwordHash, password).Return(false) 88 | 89 | _, actualErr := prep.authService.Login(prep.ctx, in) 90 | 91 | require.Error(t, actualErr) 92 | require.EqualError(t, err, actualErr.Error()) 93 | }) 94 | 95 | t.Run("expect it fails if token generation fails", func(t *testing.T) { 96 | prep := newTestPrep() 97 | err := errors.New("token generation failed") 98 | 99 | prep.userRepo.EXPECT().GetByEmail(mock.Anything, in.Email).Return(getUser, nil) 100 | prep.crypto.EXPECT().CompareHashAndPassword(passwordHash, password).Return(true) 101 | 102 | prep.config.EXPECT().AccessTokenSecret().Return(tokenSecret) 103 | prep.config.EXPECT().AccessTokenExpiresDate().Return(tokenExpires) 104 | prep.crypto.EXPECT().GenerateJWT(tokenPayload, tokenSecret, tokenExpires).Return(token, err) 105 | 106 | _, actualErr := prep.authService.Login(prep.ctx, in) 107 | 108 | require.Error(t, actualErr) 109 | require.EqualError(t, err, actualErr.Error()) 110 | }) 111 | } 112 | 113 | func TestAuthUsecases_VerifyAccessToken(t *testing.T) { 114 | userId := int64(1) 115 | 116 | token := "token" 117 | tokenSecret := "token-secret" 118 | tokenPayload := map[string]interface{}{"userId": float64(userId)} 119 | 120 | t.Run("expect it virifies token", func(t *testing.T) { 121 | prep := newTestPrep() 122 | 123 | prep.config.EXPECT().AccessTokenSecret().Return(tokenSecret) 124 | prep.crypto.EXPECT().ParseAndValidateJWT(token, tokenSecret).Return(tokenPayload, nil) 125 | 126 | actualUserId, err := prep.authService.VerifyAccessToken(token) 127 | 128 | require.NoError(t, err) 129 | require.Equal(t, userId, actualUserId) 130 | }) 131 | 132 | t.Run("expect it fails if token is not valid", func(t *testing.T) { 133 | prep := newTestPrep() 134 | 135 | err := errors.New("token is not valid") 136 | wrapErr := baseErrors.New(baseErrors.UnauthorizedError, "") 137 | 138 | prep.config.EXPECT().AccessTokenSecret().Return(tokenSecret) 139 | prep.crypto.EXPECT().ParseAndValidateJWT(token, tokenSecret).Return(tokenPayload, err) 140 | 141 | _, actualErr := prep.authService.VerifyAccessToken(token) 142 | 143 | require.Error(t, actualErr) 144 | require.Equal(t, wrapErr, actualErr) 145 | }) 146 | } 147 | 148 | func TestAuthUsecases_ParseAccessToken(t *testing.T) { 149 | userId := int64(1) 150 | 151 | token := "token" 152 | tokenSecret := "token-secret" 153 | tokenPayload := map[string]interface{}{"userId": float64(userId)} 154 | 155 | t.Run("expect it virifies token", func(t *testing.T) { 156 | prep := newTestPrep() 157 | 158 | prep.config.EXPECT().AccessTokenSecret().Return(tokenSecret) 159 | prep.crypto.EXPECT().ParseJWT(token, tokenSecret).Return(tokenPayload, nil) 160 | 161 | actualUserId, err := prep.authService.ParseAccessToken(token) 162 | 163 | require.NoError(t, err) 164 | require.Equal(t, userId, actualUserId) 165 | }) 166 | 167 | t.Run("expect it fails if token parsing fails", func(t *testing.T) { 168 | prep := newTestPrep() 169 | 170 | err := errors.New("token parsing failed") 171 | wrapErr := baseErrors.New(baseErrors.UnauthorizedError, "") 172 | 173 | prep.config.EXPECT().AccessTokenSecret().Return(tokenSecret) 174 | prep.crypto.EXPECT().ParseJWT(token, tokenSecret).Return(tokenPayload, err) 175 | 176 | _, actualErr := prep.authService.ParseAccessToken(token) 177 | 178 | require.Error(t, actualErr) 179 | require.Equal(t, wrapErr, actualErr) 180 | }) 181 | } 182 | 183 | type testPrep struct { 184 | ctx context.Context 185 | config *authMock.Config 186 | crypto *cryptoMock.Crypto 187 | userRepo *userMock.UserRepository 188 | 189 | authService auth.AuthService 190 | } 191 | 192 | func newTestPrep() testPrep { 193 | crypto := &cryptoMock.Crypto{} 194 | userRepo := &userMock.UserRepository{} 195 | config := &authMock.Config{} 196 | 197 | authServiceOpts := AuthServiceOpts{ 198 | Config: config, 199 | UserRepository: userRepo, 200 | Crypto: crypto, 201 | } 202 | authService := NewAuthService(authServiceOpts) 203 | 204 | return testPrep{ 205 | ctx: context.Background(), 206 | config: config, 207 | crypto: crypto, 208 | userRepo: userRepo, 209 | authService: authService, 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /internal/auth/mock/config.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.10.4. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | time "time" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Config is an autogenerated mock type for the Config type 12 | type Config struct { 13 | mock.Mock 14 | } 15 | 16 | type Config_Expecter struct { 17 | mock *mock.Mock 18 | } 19 | 20 | func (_m *Config) EXPECT() *Config_Expecter { 21 | return &Config_Expecter{mock: &_m.Mock} 22 | } 23 | 24 | // AccessTokenExpiresDate provides a mock function with given fields: 25 | func (_m *Config) AccessTokenExpiresDate() time.Time { 26 | ret := _m.Called() 27 | 28 | var r0 time.Time 29 | if rf, ok := ret.Get(0).(func() time.Time); ok { 30 | r0 = rf() 31 | } else { 32 | r0 = ret.Get(0).(time.Time) 33 | } 34 | 35 | return r0 36 | } 37 | 38 | // Config_AccessTokenExpiresDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AccessTokenExpiresDate' 39 | type Config_AccessTokenExpiresDate_Call struct { 40 | *mock.Call 41 | } 42 | 43 | // AccessTokenExpiresDate is a helper method to define mock.On call 44 | func (_e *Config_Expecter) AccessTokenExpiresDate() *Config_AccessTokenExpiresDate_Call { 45 | return &Config_AccessTokenExpiresDate_Call{Call: _e.mock.On("AccessTokenExpiresDate")} 46 | } 47 | 48 | func (_c *Config_AccessTokenExpiresDate_Call) Run(run func()) *Config_AccessTokenExpiresDate_Call { 49 | _c.Call.Run(func(args mock.Arguments) { 50 | run() 51 | }) 52 | return _c 53 | } 54 | 55 | func (_c *Config_AccessTokenExpiresDate_Call) Return(_a0 time.Time) *Config_AccessTokenExpiresDate_Call { 56 | _c.Call.Return(_a0) 57 | return _c 58 | } 59 | 60 | // AccessTokenSecret provides a mock function with given fields: 61 | func (_m *Config) AccessTokenSecret() string { 62 | ret := _m.Called() 63 | 64 | var r0 string 65 | if rf, ok := ret.Get(0).(func() string); ok { 66 | r0 = rf() 67 | } else { 68 | r0 = ret.Get(0).(string) 69 | } 70 | 71 | return r0 72 | } 73 | 74 | // Config_AccessTokenSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AccessTokenSecret' 75 | type Config_AccessTokenSecret_Call struct { 76 | *mock.Call 77 | } 78 | 79 | // AccessTokenSecret is a helper method to define mock.On call 80 | func (_e *Config_Expecter) AccessTokenSecret() *Config_AccessTokenSecret_Call { 81 | return &Config_AccessTokenSecret_Call{Call: _e.mock.On("AccessTokenSecret")} 82 | } 83 | 84 | func (_c *Config_AccessTokenSecret_Call) Run(run func()) *Config_AccessTokenSecret_Call { 85 | _c.Call.Run(func(args mock.Arguments) { 86 | run() 87 | }) 88 | return _c 89 | } 90 | 91 | func (_c *Config_AccessTokenSecret_Call) Return(_a0 string) *Config_AccessTokenSecret_Call { 92 | _c.Call.Return(_a0) 93 | return _c 94 | } 95 | -------------------------------------------------------------------------------- /internal/auth/mock/service.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.10.4. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | auth "go-backend-template/internal/auth" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // AuthService is an autogenerated mock type for the AuthService type 13 | type AuthService struct { 14 | mock.Mock 15 | } 16 | 17 | type AuthService_Expecter struct { 18 | mock *mock.Mock 19 | } 20 | 21 | func (_m *AuthService) EXPECT() *AuthService_Expecter { 22 | return &AuthService_Expecter{mock: &_m.Mock} 23 | } 24 | 25 | // Login provides a mock function with given fields: ctx, dto 26 | func (_m *AuthService) Login(ctx context.Context, dto auth.LoginUserDto) (auth.LoggedUserDto, error) { 27 | ret := _m.Called(ctx, dto) 28 | 29 | var r0 auth.LoggedUserDto 30 | if rf, ok := ret.Get(0).(func(context.Context, auth.LoginUserDto) auth.LoggedUserDto); ok { 31 | r0 = rf(ctx, dto) 32 | } else { 33 | r0 = ret.Get(0).(auth.LoggedUserDto) 34 | } 35 | 36 | var r1 error 37 | if rf, ok := ret.Get(1).(func(context.Context, auth.LoginUserDto) error); ok { 38 | r1 = rf(ctx, dto) 39 | } else { 40 | r1 = ret.Error(1) 41 | } 42 | 43 | return r0, r1 44 | } 45 | 46 | // AuthService_Login_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Login' 47 | type AuthService_Login_Call struct { 48 | *mock.Call 49 | } 50 | 51 | // Login is a helper method to define mock.On call 52 | // - ctx context.Context 53 | // - dto auth.LoginUserDto 54 | func (_e *AuthService_Expecter) Login(ctx interface{}, dto interface{}) *AuthService_Login_Call { 55 | return &AuthService_Login_Call{Call: _e.mock.On("Login", ctx, dto)} 56 | } 57 | 58 | func (_c *AuthService_Login_Call) Run(run func(ctx context.Context, dto auth.LoginUserDto)) *AuthService_Login_Call { 59 | _c.Call.Run(func(args mock.Arguments) { 60 | run(args[0].(context.Context), args[1].(auth.LoginUserDto)) 61 | }) 62 | return _c 63 | } 64 | 65 | func (_c *AuthService_Login_Call) Return(_a0 auth.LoggedUserDto, _a1 error) *AuthService_Login_Call { 66 | _c.Call.Return(_a0, _a1) 67 | return _c 68 | } 69 | 70 | // ParseAccessToken provides a mock function with given fields: accessToken 71 | func (_m *AuthService) ParseAccessToken(accessToken string) (int64, error) { 72 | ret := _m.Called(accessToken) 73 | 74 | var r0 int64 75 | if rf, ok := ret.Get(0).(func(string) int64); ok { 76 | r0 = rf(accessToken) 77 | } else { 78 | r0 = ret.Get(0).(int64) 79 | } 80 | 81 | var r1 error 82 | if rf, ok := ret.Get(1).(func(string) error); ok { 83 | r1 = rf(accessToken) 84 | } else { 85 | r1 = ret.Error(1) 86 | } 87 | 88 | return r0, r1 89 | } 90 | 91 | // AuthService_ParseAccessToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ParseAccessToken' 92 | type AuthService_ParseAccessToken_Call struct { 93 | *mock.Call 94 | } 95 | 96 | // ParseAccessToken is a helper method to define mock.On call 97 | // - accessToken string 98 | func (_e *AuthService_Expecter) ParseAccessToken(accessToken interface{}) *AuthService_ParseAccessToken_Call { 99 | return &AuthService_ParseAccessToken_Call{Call: _e.mock.On("ParseAccessToken", accessToken)} 100 | } 101 | 102 | func (_c *AuthService_ParseAccessToken_Call) Run(run func(accessToken string)) *AuthService_ParseAccessToken_Call { 103 | _c.Call.Run(func(args mock.Arguments) { 104 | run(args[0].(string)) 105 | }) 106 | return _c 107 | } 108 | 109 | func (_c *AuthService_ParseAccessToken_Call) Return(_a0 int64, _a1 error) *AuthService_ParseAccessToken_Call { 110 | _c.Call.Return(_a0, _a1) 111 | return _c 112 | } 113 | 114 | // VerifyAccessToken provides a mock function with given fields: accessToken 115 | func (_m *AuthService) VerifyAccessToken(accessToken string) (int64, error) { 116 | ret := _m.Called(accessToken) 117 | 118 | var r0 int64 119 | if rf, ok := ret.Get(0).(func(string) int64); ok { 120 | r0 = rf(accessToken) 121 | } else { 122 | r0 = ret.Get(0).(int64) 123 | } 124 | 125 | var r1 error 126 | if rf, ok := ret.Get(1).(func(string) error); ok { 127 | r1 = rf(accessToken) 128 | } else { 129 | r1 = ret.Error(1) 130 | } 131 | 132 | return r0, r1 133 | } 134 | 135 | // AuthService_VerifyAccessToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyAccessToken' 136 | type AuthService_VerifyAccessToken_Call struct { 137 | *mock.Call 138 | } 139 | 140 | // VerifyAccessToken is a helper method to define mock.On call 141 | // - accessToken string 142 | func (_e *AuthService_Expecter) VerifyAccessToken(accessToken interface{}) *AuthService_VerifyAccessToken_Call { 143 | return &AuthService_VerifyAccessToken_Call{Call: _e.mock.On("VerifyAccessToken", accessToken)} 144 | } 145 | 146 | func (_c *AuthService_VerifyAccessToken_Call) Run(run func(accessToken string)) *AuthService_VerifyAccessToken_Call { 147 | _c.Call.Run(func(args mock.Arguments) { 148 | run(args[0].(string)) 149 | }) 150 | return _c 151 | } 152 | 153 | func (_c *AuthService_VerifyAccessToken_Call) Return(_a0 int64, _a1 error) *AuthService_VerifyAccessToken_Call { 154 | _c.Call.Return(_a0, _a1) 155 | return _c 156 | } 157 | -------------------------------------------------------------------------------- /internal/auth/service.go: -------------------------------------------------------------------------------- 1 | //go:generate mockery --name AuthService --filename service.go --output ./mock --with-expecter 2 | //go:generate mockery --name Config --filename config.go --output ./mock --with-expecter 3 | 4 | package auth 5 | 6 | import ( 7 | "context" 8 | "time" 9 | ) 10 | 11 | type AuthService interface { 12 | Login(ctx context.Context, dto LoginUserDto) (LoggedUserDto, error) 13 | VerifyAccessToken(accessToken string) (int64, error) 14 | ParseAccessToken(accessToken string) (int64, error) 15 | } 16 | 17 | type Config interface { 18 | AccessTokenSecret() string 19 | AccessTokenExpiresDate() time.Time 20 | } 21 | -------------------------------------------------------------------------------- /internal/base/crypto/crypto.go: -------------------------------------------------------------------------------- 1 | //go:generate mockery --name Crypto --filename crypto.go --output ./mock --with-expecter 2 | 3 | package crypto 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | type Crypto interface { 10 | HashPassword(password string) (string, error) 11 | CompareHashAndPassword(hash string, password string) bool 12 | 13 | GenerateJWT(payload map[string]interface{}, secret string, exp time.Time) (string, error) 14 | ParseAndValidateJWT(token string, secret string) (map[string]interface{}, error) 15 | ParseJWT(token string, secret string) (map[string]interface{}, error) 16 | 17 | GenerateUUID() (string, error) 18 | } 19 | -------------------------------------------------------------------------------- /internal/base/crypto/impl/crypto.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gofrs/uuid" 7 | "github.com/golang-jwt/jwt" 8 | "golang.org/x/crypto/bcrypt" 9 | 10 | "go-backend-template/internal/base/crypto" 11 | "go-backend-template/internal/base/errors" 12 | ) 13 | 14 | func NewCrypto() crypto.Crypto { 15 | return &cryptoImpl{} 16 | } 17 | 18 | type cryptoImpl struct{} 19 | 20 | func (*cryptoImpl) HashPassword(password string) (string, error) { 21 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | return string(bytes), nil 27 | } 28 | 29 | func (*cryptoImpl) CompareHashAndPassword(hash string, password string) bool { 30 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 31 | return err == nil 32 | } 33 | 34 | func (*cryptoImpl) GenerateJWT(payload map[string]interface{}, secret string, exp time.Time) (string, error) { 35 | claims := make(jwt.MapClaims) 36 | claims["exp"] = exp.Unix() 37 | 38 | for key, value := range payload { 39 | claims[key] = value 40 | } 41 | 42 | token, err := jwt. 43 | NewWithClaims(jwt.SigningMethodHS256, claims). 44 | SignedString([]byte(secret)) 45 | 46 | if err != nil { 47 | return "", err 48 | } 49 | 50 | return token, nil 51 | } 52 | 53 | func (*cryptoImpl) ParseAndValidateJWT(token string, secret string) (map[string]interface{}, error) { 54 | parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { 55 | return []byte(secret), nil 56 | }) 57 | if err != nil { 58 | return map[string]interface{}{}, err 59 | } 60 | if !parsedToken.Valid { 61 | return map[string]interface{}{}, errors.New(errors.InternalError, "token validation error") 62 | } 63 | 64 | claims, ok := parsedToken.Claims.(jwt.MapClaims) 65 | if !ok { 66 | return map[string]interface{}{}, errors.New(errors.InternalError, "token validation error") 67 | } 68 | 69 | payload := make(map[string]interface{}) 70 | 71 | for key, value := range claims { 72 | payload[key] = value 73 | } 74 | 75 | return payload, nil 76 | } 77 | 78 | func (*cryptoImpl) ParseJWT(token string, secret string) (map[string]interface{}, error) { 79 | parsedToken, _ := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { 80 | return []byte(secret), nil 81 | }) 82 | 83 | claims, ok := parsedToken.Claims.(jwt.MapClaims) 84 | if !ok { 85 | return map[string]interface{}{}, errors.New(errors.InternalError, "token parsing error") 86 | } 87 | 88 | payload := make(map[string]interface{}) 89 | 90 | for key, value := range claims { 91 | payload[key] = value 92 | } 93 | 94 | return payload, nil 95 | } 96 | 97 | func (*cryptoImpl) GenerateUUID() (string, error) { 98 | id, err := uuid.NewV4() 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | return id.String(), nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/base/crypto/mock/crypto.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.10.4. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | time "time" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Crypto is an autogenerated mock type for the Crypto type 12 | type Crypto struct { 13 | mock.Mock 14 | } 15 | 16 | type Crypto_Expecter struct { 17 | mock *mock.Mock 18 | } 19 | 20 | func (_m *Crypto) EXPECT() *Crypto_Expecter { 21 | return &Crypto_Expecter{mock: &_m.Mock} 22 | } 23 | 24 | // CompareHashAndPassword provides a mock function with given fields: hash, password 25 | func (_m *Crypto) CompareHashAndPassword(hash string, password string) bool { 26 | ret := _m.Called(hash, password) 27 | 28 | var r0 bool 29 | if rf, ok := ret.Get(0).(func(string, string) bool); ok { 30 | r0 = rf(hash, password) 31 | } else { 32 | r0 = ret.Get(0).(bool) 33 | } 34 | 35 | return r0 36 | } 37 | 38 | // Crypto_CompareHashAndPassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompareHashAndPassword' 39 | type Crypto_CompareHashAndPassword_Call struct { 40 | *mock.Call 41 | } 42 | 43 | // CompareHashAndPassword is a helper method to define mock.On call 44 | // - hash string 45 | // - password string 46 | func (_e *Crypto_Expecter) CompareHashAndPassword(hash interface{}, password interface{}) *Crypto_CompareHashAndPassword_Call { 47 | return &Crypto_CompareHashAndPassword_Call{Call: _e.mock.On("CompareHashAndPassword", hash, password)} 48 | } 49 | 50 | func (_c *Crypto_CompareHashAndPassword_Call) Run(run func(hash string, password string)) *Crypto_CompareHashAndPassword_Call { 51 | _c.Call.Run(func(args mock.Arguments) { 52 | run(args[0].(string), args[1].(string)) 53 | }) 54 | return _c 55 | } 56 | 57 | func (_c *Crypto_CompareHashAndPassword_Call) Return(_a0 bool) *Crypto_CompareHashAndPassword_Call { 58 | _c.Call.Return(_a0) 59 | return _c 60 | } 61 | 62 | // GenerateJWT provides a mock function with given fields: payload, secret, exp 63 | func (_m *Crypto) GenerateJWT(payload map[string]interface{}, secret string, exp time.Time) (string, error) { 64 | ret := _m.Called(payload, secret, exp) 65 | 66 | var r0 string 67 | if rf, ok := ret.Get(0).(func(map[string]interface{}, string, time.Time) string); ok { 68 | r0 = rf(payload, secret, exp) 69 | } else { 70 | r0 = ret.Get(0).(string) 71 | } 72 | 73 | var r1 error 74 | if rf, ok := ret.Get(1).(func(map[string]interface{}, string, time.Time) error); ok { 75 | r1 = rf(payload, secret, exp) 76 | } else { 77 | r1 = ret.Error(1) 78 | } 79 | 80 | return r0, r1 81 | } 82 | 83 | // Crypto_GenerateJWT_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateJWT' 84 | type Crypto_GenerateJWT_Call struct { 85 | *mock.Call 86 | } 87 | 88 | // GenerateJWT is a helper method to define mock.On call 89 | // - payload map[string]interface{} 90 | // - secret string 91 | // - exp time.Time 92 | func (_e *Crypto_Expecter) GenerateJWT(payload interface{}, secret interface{}, exp interface{}) *Crypto_GenerateJWT_Call { 93 | return &Crypto_GenerateJWT_Call{Call: _e.mock.On("GenerateJWT", payload, secret, exp)} 94 | } 95 | 96 | func (_c *Crypto_GenerateJWT_Call) Run(run func(payload map[string]interface{}, secret string, exp time.Time)) *Crypto_GenerateJWT_Call { 97 | _c.Call.Run(func(args mock.Arguments) { 98 | run(args[0].(map[string]interface{}), args[1].(string), args[2].(time.Time)) 99 | }) 100 | return _c 101 | } 102 | 103 | func (_c *Crypto_GenerateJWT_Call) Return(_a0 string, _a1 error) *Crypto_GenerateJWT_Call { 104 | _c.Call.Return(_a0, _a1) 105 | return _c 106 | } 107 | 108 | // GenerateUUID provides a mock function with given fields: 109 | func (_m *Crypto) GenerateUUID() (string, error) { 110 | ret := _m.Called() 111 | 112 | var r0 string 113 | if rf, ok := ret.Get(0).(func() string); ok { 114 | r0 = rf() 115 | } else { 116 | r0 = ret.Get(0).(string) 117 | } 118 | 119 | var r1 error 120 | if rf, ok := ret.Get(1).(func() error); ok { 121 | r1 = rf() 122 | } else { 123 | r1 = ret.Error(1) 124 | } 125 | 126 | return r0, r1 127 | } 128 | 129 | // Crypto_GenerateUUID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateUUID' 130 | type Crypto_GenerateUUID_Call struct { 131 | *mock.Call 132 | } 133 | 134 | // GenerateUUID is a helper method to define mock.On call 135 | func (_e *Crypto_Expecter) GenerateUUID() *Crypto_GenerateUUID_Call { 136 | return &Crypto_GenerateUUID_Call{Call: _e.mock.On("GenerateUUID")} 137 | } 138 | 139 | func (_c *Crypto_GenerateUUID_Call) Run(run func()) *Crypto_GenerateUUID_Call { 140 | _c.Call.Run(func(args mock.Arguments) { 141 | run() 142 | }) 143 | return _c 144 | } 145 | 146 | func (_c *Crypto_GenerateUUID_Call) Return(_a0 string, _a1 error) *Crypto_GenerateUUID_Call { 147 | _c.Call.Return(_a0, _a1) 148 | return _c 149 | } 150 | 151 | // HashPassword provides a mock function with given fields: password 152 | func (_m *Crypto) HashPassword(password string) (string, error) { 153 | ret := _m.Called(password) 154 | 155 | var r0 string 156 | if rf, ok := ret.Get(0).(func(string) string); ok { 157 | r0 = rf(password) 158 | } else { 159 | r0 = ret.Get(0).(string) 160 | } 161 | 162 | var r1 error 163 | if rf, ok := ret.Get(1).(func(string) error); ok { 164 | r1 = rf(password) 165 | } else { 166 | r1 = ret.Error(1) 167 | } 168 | 169 | return r0, r1 170 | } 171 | 172 | // Crypto_HashPassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HashPassword' 173 | type Crypto_HashPassword_Call struct { 174 | *mock.Call 175 | } 176 | 177 | // HashPassword is a helper method to define mock.On call 178 | // - password string 179 | func (_e *Crypto_Expecter) HashPassword(password interface{}) *Crypto_HashPassword_Call { 180 | return &Crypto_HashPassword_Call{Call: _e.mock.On("HashPassword", password)} 181 | } 182 | 183 | func (_c *Crypto_HashPassword_Call) Run(run func(password string)) *Crypto_HashPassword_Call { 184 | _c.Call.Run(func(args mock.Arguments) { 185 | run(args[0].(string)) 186 | }) 187 | return _c 188 | } 189 | 190 | func (_c *Crypto_HashPassword_Call) Return(_a0 string, _a1 error) *Crypto_HashPassword_Call { 191 | _c.Call.Return(_a0, _a1) 192 | return _c 193 | } 194 | 195 | // ParseAndValidateJWT provides a mock function with given fields: token, secret 196 | func (_m *Crypto) ParseAndValidateJWT(token string, secret string) (map[string]interface{}, error) { 197 | ret := _m.Called(token, secret) 198 | 199 | var r0 map[string]interface{} 200 | if rf, ok := ret.Get(0).(func(string, string) map[string]interface{}); ok { 201 | r0 = rf(token, secret) 202 | } else { 203 | if ret.Get(0) != nil { 204 | r0 = ret.Get(0).(map[string]interface{}) 205 | } 206 | } 207 | 208 | var r1 error 209 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 210 | r1 = rf(token, secret) 211 | } else { 212 | r1 = ret.Error(1) 213 | } 214 | 215 | return r0, r1 216 | } 217 | 218 | // Crypto_ParseAndValidateJWT_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ParseAndValidateJWT' 219 | type Crypto_ParseAndValidateJWT_Call struct { 220 | *mock.Call 221 | } 222 | 223 | // ParseAndValidateJWT is a helper method to define mock.On call 224 | // - token string 225 | // - secret string 226 | func (_e *Crypto_Expecter) ParseAndValidateJWT(token interface{}, secret interface{}) *Crypto_ParseAndValidateJWT_Call { 227 | return &Crypto_ParseAndValidateJWT_Call{Call: _e.mock.On("ParseAndValidateJWT", token, secret)} 228 | } 229 | 230 | func (_c *Crypto_ParseAndValidateJWT_Call) Run(run func(token string, secret string)) *Crypto_ParseAndValidateJWT_Call { 231 | _c.Call.Run(func(args mock.Arguments) { 232 | run(args[0].(string), args[1].(string)) 233 | }) 234 | return _c 235 | } 236 | 237 | func (_c *Crypto_ParseAndValidateJWT_Call) Return(_a0 map[string]interface{}, _a1 error) *Crypto_ParseAndValidateJWT_Call { 238 | _c.Call.Return(_a0, _a1) 239 | return _c 240 | } 241 | 242 | // ParseJWT provides a mock function with given fields: token, secret 243 | func (_m *Crypto) ParseJWT(token string, secret string) (map[string]interface{}, error) { 244 | ret := _m.Called(token, secret) 245 | 246 | var r0 map[string]interface{} 247 | if rf, ok := ret.Get(0).(func(string, string) map[string]interface{}); ok { 248 | r0 = rf(token, secret) 249 | } else { 250 | if ret.Get(0) != nil { 251 | r0 = ret.Get(0).(map[string]interface{}) 252 | } 253 | } 254 | 255 | var r1 error 256 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 257 | r1 = rf(token, secret) 258 | } else { 259 | r1 = ret.Error(1) 260 | } 261 | 262 | return r0, r1 263 | } 264 | 265 | // Crypto_ParseJWT_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ParseJWT' 266 | type Crypto_ParseJWT_Call struct { 267 | *mock.Call 268 | } 269 | 270 | // ParseJWT is a helper method to define mock.On call 271 | // - token string 272 | // - secret string 273 | func (_e *Crypto_Expecter) ParseJWT(token interface{}, secret interface{}) *Crypto_ParseJWT_Call { 274 | return &Crypto_ParseJWT_Call{Call: _e.mock.On("ParseJWT", token, secret)} 275 | } 276 | 277 | func (_c *Crypto_ParseJWT_Call) Run(run func(token string, secret string)) *Crypto_ParseJWT_Call { 278 | _c.Call.Run(func(args mock.Arguments) { 279 | run(args[0].(string), args[1].(string)) 280 | }) 281 | return _c 282 | } 283 | 284 | func (_c *Crypto_ParseJWT_Call) Return(_a0 map[string]interface{}, _a1 error) *Crypto_ParseJWT_Call { 285 | _c.Call.Return(_a0, _a1) 286 | return _c 287 | } 288 | -------------------------------------------------------------------------------- /internal/base/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Config interface { 8 | ConnString() string 9 | } 10 | 11 | type TxManager interface { 12 | RunTx(ctx context.Context, do func(ctx context.Context) error) error 13 | } 14 | -------------------------------------------------------------------------------- /internal/base/database/impl/client.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v4/pgxpool" 7 | 8 | "go-backend-template/internal/base/database" 9 | "go-backend-template/internal/base/errors" 10 | ) 11 | 12 | type Client struct { 13 | pool *pgxpool.Pool 14 | url string 15 | ctx context.Context 16 | } 17 | 18 | func NewClient(ctx context.Context, config database.Config) *Client { 19 | return &Client{ 20 | ctx: ctx, 21 | url: config.ConnString(), 22 | } 23 | } 24 | 25 | func (c *Client) Connect() error { 26 | c.Close() 27 | 28 | config, err := pgxpool.ParseConfig(c.url) 29 | if err != nil { 30 | return errors.Wrap(err, errors.DatabaseError, "cannot connect to database") 31 | } 32 | 33 | pool, err := pgxpool.ConnectConfig(c.ctx, config) 34 | if err != nil { 35 | return errors.Wrap(err, errors.DatabaseError, "cannot connect to database") 36 | } 37 | 38 | c.pool = pool 39 | 40 | return nil 41 | } 42 | 43 | func (c *Client) Close() { 44 | if c.pool != nil { 45 | c.pool.Close() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/base/database/impl/service.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/doug-martin/goqu/v9" 7 | "github.com/jackc/pgconn" 8 | "github.com/jackc/pgx/v4" 9 | 10 | "go-backend-template/internal/base/errors" 11 | ) 12 | 13 | type ConnManager interface { 14 | Conn(ctx context.Context) Connection 15 | } 16 | 17 | type Connection interface { 18 | Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) 19 | QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row 20 | Exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error) 21 | } 22 | 23 | func NewService(client *Client) *Service { 24 | return &Service{ 25 | client: client, 26 | } 27 | } 28 | 29 | type Service struct { 30 | client *Client 31 | } 32 | 33 | func (s *Service) RunTx(ctx context.Context, do func(ctx context.Context) error) error { 34 | _, ok := hasTx(ctx) 35 | if ok { 36 | return do(ctx) 37 | } 38 | 39 | return runTx(ctx, s.client, do) 40 | } 41 | 42 | func (s *Service) Conn(ctx context.Context) Connection { 43 | tx, ok := hasTx(ctx) 44 | if ok { 45 | return tx.conn 46 | } 47 | 48 | return s.client.pool 49 | } 50 | 51 | type txKey int 52 | 53 | const ( 54 | key txKey = iota 55 | ) 56 | 57 | type transaction struct { 58 | conn pgx.Tx 59 | } 60 | 61 | func (t *transaction) commit(ctx context.Context) error { 62 | err := t.conn.Commit(ctx) 63 | if err != nil { 64 | return errors.Wrap(err, errors.DatabaseError, "cannot commit transaction") 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (t *transaction) rollback(ctx context.Context) error { 71 | err := t.conn.Rollback(ctx) 72 | if err != nil { 73 | return errors.Wrap(err, errors.DatabaseError, "cannot rollback transaction") 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func withTx(ctx context.Context, tx transaction) context.Context { 80 | return context.WithValue(ctx, key, tx) 81 | } 82 | 83 | func hasTx(ctx context.Context) (transaction, bool) { 84 | tx, ok := ctx.Value(key).(transaction) 85 | if ok { 86 | return tx, true 87 | } 88 | 89 | return transaction{}, false 90 | } 91 | 92 | func runTx(ctx context.Context, client *Client, do func(ctx context.Context) error) error { 93 | conn, err := client.pool.Begin(ctx) 94 | if err != nil { 95 | return errors.Wrap(err, errors.DatabaseError, "cannot open transaction") 96 | } 97 | 98 | tx := transaction{conn: conn} 99 | txCtx := withTx(ctx, tx) 100 | 101 | err = do(txCtx) 102 | if err != nil { 103 | if err := tx.rollback(txCtx); err != nil { 104 | return err 105 | } 106 | return err 107 | } 108 | if err := tx.commit(txCtx); err != nil { 109 | return err 110 | } 111 | 112 | return nil 113 | } 114 | 115 | var QueryBuilder = goqu.Dialect("postgres") 116 | 117 | type Ex = goqu.Ex 118 | type Record = goqu.Record 119 | -------------------------------------------------------------------------------- /internal/base/database/mock/tx.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import "context" 4 | 5 | type MockTxManager struct{} 6 | 7 | func (*MockTxManager) RunTx(ctx context.Context, do func(ctx context.Context) error) error { 8 | return do(ctx) 9 | } 10 | -------------------------------------------------------------------------------- /internal/base/errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "fmt" 4 | 5 | type Error struct { 6 | status Status 7 | message string 8 | err error 9 | } 10 | 11 | func (e *Error) Error() string { 12 | return e.message 13 | } 14 | 15 | func (e *Error) DetailedError() string { 16 | if e.err != nil { 17 | if baseErr, ok := e.err.(*Error); ok { 18 | return fmt.Sprintf("%s: %s", e.message, baseErr.DetailedError()) 19 | } 20 | return fmt.Sprintf("%s: %s", e.message, e.err.Error()) 21 | } 22 | return e.message 23 | } 24 | 25 | func (e *Error) Status() Status { 26 | return e.status 27 | } 28 | 29 | func (e *Error) Unwrap() error { 30 | return e.err 31 | } 32 | 33 | func New(status Status, message string) *Error { 34 | err := Error{ 35 | status: status, 36 | message: message, 37 | } 38 | if len(message) == 0 { 39 | err.message = status.Message() 40 | } 41 | 42 | return &err 43 | } 44 | 45 | func Errorf(status Status, message string, a ...interface{}) *Error { 46 | err := Error{ 47 | status: status, 48 | message: fmt.Sprintf(message, a...), 49 | } 50 | if len(message) == 0 { 51 | err.message = status.Message() 52 | } 53 | 54 | return &err 55 | } 56 | 57 | func Wrap(err error, status Status, message string) *Error { 58 | newErr := Error{ 59 | status: status, 60 | message: message, 61 | err: err, 62 | } 63 | if len(message) == 0 { 64 | newErr.message = status.Message() 65 | } 66 | 67 | return &newErr 68 | } 69 | 70 | func Wrapf(err error, status Status, message string, a ...interface{}) *Error { 71 | newErr := Error{ 72 | status: status, 73 | message: fmt.Sprintf(message, a...), 74 | err: err, 75 | } 76 | if len(message) == 0 { 77 | newErr.message = status.Message() 78 | } 79 | 80 | return &newErr 81 | } 82 | -------------------------------------------------------------------------------- /internal/base/errors/status.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | type Status string 4 | 5 | const ( 6 | BadRequestError Status = "BadRequestError" 7 | InternalError Status = "InternalError" 8 | ValidationError Status = "ValidationError" 9 | DatabaseError Status = "DatabaseError" 10 | NotFoundError Status = "NotFoundError" 11 | AlreadyExistsError Status = "AlreadyExistsError" 12 | WrongCredentialsError Status = "WrongCredentialsError" 13 | UnauthorizedError Status = "UnauthorizedError" 14 | ) 15 | 16 | func (s Status) Message() string { 17 | switch s { 18 | case BadRequestError: 19 | return "bad request error" 20 | case InternalError: 21 | return "internal error" 22 | case ValidationError: 23 | return "validation error" 24 | case DatabaseError: 25 | return "database error" 26 | case NotFoundError: 27 | return "not found error" 28 | case AlreadyExistsError: 29 | return "already exists error" 30 | case WrongCredentialsError: 31 | return "wrong credentials error" 32 | case UnauthorizedError: 33 | return "unauthorized error" 34 | default: 35 | return "internal error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/base/request/request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "context" 4 | 5 | type requestInfoKey int 6 | 7 | const ( 8 | key requestInfoKey = iota 9 | ) 10 | 11 | type RequestInfo struct { 12 | UserId int64 13 | TraceId string 14 | } 15 | 16 | func WithRequestInfo(ctx context.Context, info RequestInfo) context.Context { 17 | return context.WithValue(ctx, key, info) 18 | } 19 | 20 | func GetRequestInfo(ctx context.Context) (requestInfo RequestInfo, ok bool) { 21 | requestInfo, ok = ctx.Value(key).(RequestInfo) 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /internal/user/dto.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | type UserDto struct { 4 | Id int64 `json:"id"` 5 | FirstName string `json:"firstName"` 6 | LastName string `json:"lastName"` 7 | Email string `json:"email"` 8 | } 9 | 10 | func (dto UserDto) MapFromModel(user UserModel) UserDto { 11 | dto.Id = user.Id 12 | dto.FirstName = user.FirstName 13 | dto.LastName = user.LastName 14 | dto.Email = user.Email 15 | 16 | return dto 17 | } 18 | 19 | type AddUserDto struct { 20 | FirstName string `json:"firstName"` 21 | LastName string `json:"lastName"` 22 | Email string `json:"email"` 23 | Password string `json:"password"` 24 | } 25 | 26 | func (dto AddUserDto) MapToModel() (UserModel, error) { 27 | return NewUser( 28 | dto.FirstName, 29 | dto.LastName, 30 | dto.Email, 31 | dto.Password, 32 | ) 33 | } 34 | 35 | type UpdateUserDto struct { 36 | Id int64 `json:"id"` 37 | FirstName string `json:"firstName"` 38 | LastName string `json:"lastName"` 39 | Email string `json:"email"` 40 | } 41 | 42 | type ChangeUserPasswordDto struct { 43 | Id int64 `json:"id"` 44 | Password string `json:"password"` 45 | } 46 | -------------------------------------------------------------------------------- /internal/user/impl/repository.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgconn" 7 | "github.com/jackc/pgerrcode" 8 | 9 | "go-backend-template/internal/base/errors" 10 | "go-backend-template/internal/user" 11 | 12 | databaseImpl "go-backend-template/internal/base/database/impl" 13 | ) 14 | 15 | type UserRepositoryOpts struct { 16 | ConnManager databaseImpl.ConnManager 17 | } 18 | 19 | func NewUserRepository(opts UserRepositoryOpts) user.UserRepository { 20 | return &userRepository{ 21 | ConnManager: opts.ConnManager, 22 | } 23 | } 24 | 25 | type userRepository struct { 26 | databaseImpl.ConnManager 27 | } 28 | 29 | func (r *userRepository) Add(ctx context.Context, model user.UserModel) (int64, error) { 30 | sql, _, err := databaseImpl.QueryBuilder. 31 | Insert("users"). 32 | Rows(databaseImpl.Record{ 33 | "firstname": model.FirstName, 34 | "lastname": model.LastName, 35 | "email": model.Email, 36 | "password": model.Password, 37 | }). 38 | Returning("user_id"). 39 | ToSQL() 40 | 41 | if err != nil { 42 | return 0, errors.Wrap(err, errors.DatabaseError, "syntax error") 43 | } 44 | 45 | row := r.Conn(ctx).QueryRow(ctx, sql) 46 | 47 | if err := row.Scan(&model.Id); err != nil { 48 | return 0, parseAddUserError(&model, err) 49 | } 50 | 51 | return model.Id, nil 52 | } 53 | 54 | func (r *userRepository) Update(ctx context.Context, model user.UserModel) (int64, error) { 55 | sql, _, err := databaseImpl.QueryBuilder. 56 | Update("users"). 57 | Set(databaseImpl.Record{ 58 | "firstname": model.FirstName, 59 | "lastname": model.LastName, 60 | "email": model.Email, 61 | "password": model.Password, 62 | }). 63 | Where(databaseImpl.Ex{"user_id": model.Id}). 64 | Returning("user_id"). 65 | ToSQL() 66 | 67 | if err != nil { 68 | return 0, errors.Wrap(err, errors.DatabaseError, "syntax error") 69 | } 70 | 71 | row := r.Conn(ctx).QueryRow(ctx, sql) 72 | 73 | if err := row.Scan(&model.Id); err != nil { 74 | return 0, parseUpdateUserError(&model, err) 75 | } 76 | 77 | return model.Id, nil 78 | } 79 | 80 | func (r *userRepository) GetById(ctx context.Context, userId int64) (user.UserModel, error) { 81 | sql, _, err := databaseImpl.QueryBuilder. 82 | Select( 83 | "firstname", 84 | "lastname", 85 | "email", 86 | "password", 87 | ). 88 | From("users"). 89 | Where(databaseImpl.Ex{"user_id": userId}). 90 | ToSQL() 91 | 92 | if err != nil { 93 | return user.UserModel{}, errors.Wrap(err, errors.DatabaseError, "syntax error") 94 | } 95 | 96 | row := r.Conn(ctx).QueryRow(ctx, sql) 97 | 98 | model := user.UserModel{Id: userId} 99 | 100 | err = row.Scan( 101 | &model.FirstName, 102 | &model.LastName, 103 | &model.Email, 104 | &model.Password, 105 | ) 106 | if err != nil { 107 | return user.UserModel{}, parseGetUserByIdError(userId, err) 108 | } 109 | 110 | return model, nil 111 | } 112 | 113 | func (r *userRepository) GetByEmail(ctx context.Context, email string) (user.UserModel, error) { 114 | sql, _, err := databaseImpl.QueryBuilder. 115 | Select( 116 | "user_id", 117 | "firstname", 118 | "lastname", 119 | "password", 120 | ). 121 | From("users"). 122 | Where(databaseImpl.Ex{"email": email}). 123 | ToSQL() 124 | 125 | if err != nil { 126 | return user.UserModel{}, errors.Wrap(err, errors.DatabaseError, "syntax error") 127 | } 128 | 129 | row := r.Conn(ctx).QueryRow(ctx, sql) 130 | 131 | model := user.UserModel{Email: email} 132 | 133 | err = row.Scan( 134 | &model.Id, 135 | &model.FirstName, 136 | &model.LastName, 137 | &model.Password, 138 | ) 139 | if err != nil { 140 | return user.UserModel{}, parseGetUserByEmailError(email, err) 141 | } 142 | 143 | return model, nil 144 | } 145 | 146 | func parseAddUserError(user *user.UserModel, err error) error { 147 | pgError, isPgError := err.(*pgconn.PgError) 148 | 149 | if isPgError && pgError.Code == pgerrcode.UniqueViolation { 150 | switch pgError.ConstraintName { 151 | case "users_email_key": 152 | return errors.Wrapf(err, errors.AlreadyExistsError, "user with email \"%s\" already exists", user.Email) 153 | default: 154 | return errors.Wrapf(err, errors.DatabaseError, "add user failed") 155 | } 156 | } 157 | 158 | return errors.Wrapf(err, errors.DatabaseError, "add user failed") 159 | } 160 | 161 | func parseUpdateUserError(user *user.UserModel, err error) error { 162 | pgError, isPgError := err.(*pgconn.PgError) 163 | 164 | if isPgError && pgError.Code == pgerrcode.UniqueViolation { 165 | return errors.Wrapf(err, errors.AlreadyExistsError, "user with email \"%s\" already exists", user.Email) 166 | } 167 | 168 | return errors.Wrapf(err, errors.DatabaseError, "update user failed") 169 | } 170 | 171 | func parseGetUserByIdError(userId int64, err error) error { 172 | pgError, isPgError := err.(*pgconn.PgError) 173 | 174 | if isPgError && pgError.Code == pgerrcode.NoDataFound { 175 | return errors.Wrapf(err, errors.NotFoundError, "user with id \"%d\" not found", userId) 176 | } 177 | if err.Error() == "no rows in result set" { 178 | return errors.Wrapf(err, errors.NotFoundError, "user with id \"%d\" not found", userId) 179 | } 180 | 181 | return errors.Wrap(err, errors.DatabaseError, "get user by id failed") 182 | } 183 | 184 | func parseGetUserByEmailError(email string, err error) error { 185 | pgError, isPgError := err.(*pgconn.PgError) 186 | 187 | if isPgError && pgError.Code == pgerrcode.NoDataFound { 188 | return errors.Wrapf(err, errors.NotFoundError, "user with email \"%s\" not found", email) 189 | } 190 | if err.Error() == "no rows in result set" { 191 | return errors.Wrapf(err, errors.NotFoundError, "user with email \"%s\" not found", email) 192 | } 193 | 194 | return errors.Wrap(err, errors.DatabaseError, "get user by email failed") 195 | } 196 | -------------------------------------------------------------------------------- /internal/user/impl/usecase.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | 6 | "go-backend-template/internal/base/crypto" 7 | "go-backend-template/internal/base/database" 8 | "go-backend-template/internal/user" 9 | ) 10 | 11 | type UserUsecasesOpts struct { 12 | TxManager database.TxManager 13 | UserRepository user.UserRepository 14 | Crypto crypto.Crypto 15 | } 16 | 17 | func NewUserUsecases(opts UserUsecasesOpts) user.UserUsecases { 18 | return &userUsecases{ 19 | TxManager: opts.TxManager, 20 | UserRepository: opts.UserRepository, 21 | Crypto: opts.Crypto, 22 | } 23 | } 24 | 25 | type userUsecases struct { 26 | database.TxManager 27 | user.UserRepository 28 | crypto.Crypto 29 | } 30 | 31 | func (u *userUsecases) Add(ctx context.Context, in user.AddUserDto) (userId int64, err error) { 32 | model, err := in.MapToModel() 33 | if err != nil { 34 | return 0, err 35 | } 36 | if err := model.HashPassword(u.Crypto); err != nil { 37 | return 0, err 38 | } 39 | 40 | // Transaction demonstration 41 | err = u.RunTx(ctx, func(ctx context.Context) error { 42 | userId, err = u.UserRepository.Add(ctx, model) 43 | if err != nil { 44 | return err 45 | } 46 | model.Id = userId 47 | 48 | userId, err = u.UserRepository.Update(ctx, model) 49 | if err != nil { 50 | return err 51 | } 52 | return nil 53 | }) 54 | 55 | return userId, err 56 | } 57 | 58 | func (u *userUsecases) Update(ctx context.Context, in user.UpdateUserDto) (err error) { 59 | model, err := u.UserRepository.GetById(ctx, in.Id) 60 | if err != nil { 61 | return err 62 | } 63 | err = model.Update(in.FirstName, in.LastName, in.Email) 64 | if err != nil { 65 | return err 66 | } 67 | _, err = u.UserRepository.Update(ctx, model) 68 | 69 | return err 70 | } 71 | 72 | func (u *userUsecases) ChangePassword(ctx context.Context, in user.ChangeUserPasswordDto) (err error) { 73 | user, err := u.UserRepository.GetById(ctx, in.Id) 74 | if err != nil { 75 | return err 76 | } 77 | if err = user.ChangePassword(in.Password, u.Crypto); err != nil { 78 | return err 79 | } 80 | _, err = u.UserRepository.Update(ctx, user) 81 | 82 | return err 83 | } 84 | 85 | func (u *userUsecases) GetById(ctx context.Context, userId int64) (out user.UserDto, err error) { 86 | model, err := u.UserRepository.GetById(ctx, userId) 87 | if err != nil { 88 | return out, err 89 | } 90 | 91 | return out.MapFromModel(model), nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/user/impl/usecase_test.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/mock" 9 | "github.com/stretchr/testify/require" 10 | 11 | "go-backend-template/internal/user" 12 | 13 | cryptoMock "go-backend-template/internal/base/crypto/mock" 14 | dbMock "go-backend-template/internal/base/database/mock" 15 | userMock "go-backend-template/internal/user/mock" 16 | ) 17 | 18 | func TestUserUsecases_Add(t *testing.T) { 19 | userId := int64(1) 20 | password := "password" 21 | passwordHash := "password-hash" 22 | 23 | in := user.AddUserDto{ 24 | FirstName: "FirstName", 25 | LastName: "LastName", 26 | Email: "user@email.com", 27 | Password: password, 28 | } 29 | createUser := user.UserModel{ 30 | FirstName: in.FirstName, 31 | LastName: in.LastName, 32 | Email: in.Email, 33 | Password: passwordHash, 34 | } 35 | updateUser := user.UserModel{ 36 | Id: userId, 37 | FirstName: createUser.FirstName, 38 | LastName: createUser.LastName, 39 | Email: createUser.Email, 40 | Password: createUser.Password, 41 | } 42 | 43 | t.Run("expect it adds new user", func(t *testing.T) { 44 | prep := newTestPrep() 45 | 46 | prep.crypto.EXPECT().HashPassword(password).Return(passwordHash, nil) 47 | prep.userRepo.EXPECT().Add(mock.Anything, createUser).Return(userId, nil) 48 | prep.userRepo.EXPECT().Update(mock.Anything, updateUser).Return(userId, nil) 49 | 50 | actualUserId, err := prep.userUsecases.Add(prep.ctx, in) 51 | 52 | require.NoError(t, err) 53 | require.Equal(t, userId, actualUserId) 54 | }) 55 | 56 | t.Run("expect it fails if password hashing fails", func(t *testing.T) { 57 | prep := newTestPrep() 58 | err := errors.New("password hashing failed") 59 | 60 | prep.crypto.EXPECT().HashPassword(password).Return("", err) 61 | 62 | _, actualErr := prep.userUsecases.Add(prep.ctx, in) 63 | 64 | require.Error(t, actualErr) 65 | require.EqualError(t, err, actualErr.Error()) 66 | }) 67 | 68 | t.Run("expect it fails if user creating fails", func(t *testing.T) { 69 | prep := newTestPrep() 70 | err := errors.New("user creating failed") 71 | 72 | prep.crypto.EXPECT().HashPassword(password).Return(passwordHash, nil) 73 | prep.userRepo.EXPECT().Add(mock.Anything, createUser).Return(userId, err) 74 | 75 | _, actualErr := prep.userUsecases.Add(prep.ctx, in) 76 | 77 | require.Error(t, actualErr) 78 | require.EqualError(t, err, actualErr.Error()) 79 | }) 80 | 81 | t.Run("expect it fails if user updating fails", func(t *testing.T) { 82 | prep := newTestPrep() 83 | err := errors.New("user updating failed") 84 | 85 | prep.crypto.EXPECT().HashPassword(password).Return(passwordHash, nil) 86 | prep.userRepo.EXPECT().Add(mock.Anything, createUser).Return(userId, nil) 87 | prep.userRepo.EXPECT().Update(mock.Anything, updateUser).Return(userId, err) 88 | 89 | _, actualErr := prep.userUsecases.Add(prep.ctx, in) 90 | 91 | require.Error(t, actualErr) 92 | require.EqualError(t, err, actualErr.Error()) 93 | }) 94 | } 95 | 96 | func TestUserUsecases_UpdateInfo(t *testing.T) { 97 | in := user.UpdateUserDto{ 98 | Id: int64(2), 99 | FirstName: "UpdateFirstName", 100 | LastName: "UpdateLastName", 101 | Email: "user+update@email.com", 102 | } 103 | getUser := user.UserModel{ 104 | Id: in.Id, 105 | FirstName: "FirstName", 106 | LastName: "LastName", 107 | Email: "user@email.com", 108 | Password: "password-hash", 109 | } 110 | updateUser := user.UserModel{ 111 | Id: in.Id, 112 | FirstName: in.FirstName, 113 | LastName: in.LastName, 114 | Email: in.Email, 115 | Password: getUser.Password, 116 | } 117 | 118 | t.Run("expect it updates user", func(t *testing.T) { 119 | prep := newTestPrep() 120 | 121 | prep.userRepo.EXPECT().GetById(mock.Anything, in.Id).Return(getUser, nil) 122 | prep.userRepo.EXPECT().Update(mock.Anything, updateUser).Return(in.Id, nil) 123 | 124 | err := prep.userUsecases.Update(prep.ctx, in) 125 | 126 | require.NoError(t, err) 127 | }) 128 | 129 | t.Run("expect it fails if user getting fails", func(t *testing.T) { 130 | prep := newTestPrep() 131 | err := errors.New("user getting failed") 132 | 133 | prep.userRepo.EXPECT().GetById(mock.Anything, in.Id).Return(getUser, err) 134 | 135 | actualErr := prep.userUsecases.Update(prep.ctx, in) 136 | 137 | require.Error(t, actualErr) 138 | require.EqualError(t, err, actualErr.Error()) 139 | }) 140 | 141 | t.Run("expect it fails if user updating fails", func(t *testing.T) { 142 | prep := newTestPrep() 143 | err := errors.New("user updating failed") 144 | 145 | prep.userRepo.EXPECT().GetById(mock.Anything, in.Id).Return(getUser, nil) 146 | prep.userRepo.EXPECT().Update(mock.Anything, updateUser).Return(in.Id, err) 147 | 148 | actualErr := prep.userUsecases.Update(prep.ctx, in) 149 | 150 | require.Error(t, actualErr) 151 | require.EqualError(t, err, actualErr.Error()) 152 | }) 153 | } 154 | 155 | func TestUserUsecases_ChangePassword(t *testing.T) { 156 | in := user.ChangeUserPasswordDto{ 157 | Id: int64(3), 158 | Password: "new-password", 159 | } 160 | getUser := user.UserModel{ 161 | Id: in.Id, 162 | FirstName: "FirstName", 163 | LastName: "LastName", 164 | Email: "user@email.com", 165 | Password: "old-password-hash", 166 | } 167 | updateUser := user.UserModel{ 168 | Id: getUser.Id, 169 | FirstName: getUser.FirstName, 170 | LastName: getUser.LastName, 171 | Email: getUser.Email, 172 | Password: "new-password-hash", 173 | } 174 | 175 | t.Run("expect it changes user password", func(t *testing.T) { 176 | prep := newTestPrep() 177 | 178 | prep.userRepo.EXPECT().GetById(mock.Anything, in.Id).Return(getUser, nil) 179 | prep.crypto.EXPECT().HashPassword(in.Password).Return(updateUser.Password, nil) 180 | prep.userRepo.EXPECT().Update(mock.Anything, updateUser).Return(in.Id, nil) 181 | 182 | err := prep.userUsecases.ChangePassword(prep.ctx, in) 183 | 184 | require.NoError(t, err) 185 | }) 186 | 187 | t.Run("expect it fails if user getting fails", func(t *testing.T) { 188 | prep := newTestPrep() 189 | err := errors.New("user getting failed") 190 | 191 | prep.userRepo.EXPECT().GetById(mock.Anything, in.Id).Return(getUser, err) 192 | 193 | actualErr := prep.userUsecases.ChangePassword(prep.ctx, in) 194 | 195 | require.Error(t, actualErr) 196 | require.EqualError(t, err, actualErr.Error()) 197 | }) 198 | 199 | t.Run("expect it fails if password hashing fails", func(t *testing.T) { 200 | prep := newTestPrep() 201 | err := errors.New("password hashing failed") 202 | 203 | prep.userRepo.EXPECT().GetById(mock.Anything, in.Id).Return(getUser, nil) 204 | prep.crypto.EXPECT().HashPassword(in.Password).Return(updateUser.Password, err) 205 | 206 | actualErr := prep.userUsecases.ChangePassword(prep.ctx, in) 207 | 208 | require.Error(t, actualErr) 209 | require.EqualError(t, err, actualErr.Error()) 210 | }) 211 | 212 | t.Run("expect it fails if user updating fails", func(t *testing.T) { 213 | prep := newTestPrep() 214 | err := errors.New("user updating failed") 215 | 216 | prep.userRepo.EXPECT().GetById(mock.Anything, in.Id).Return(getUser, nil) 217 | prep.crypto.EXPECT().HashPassword(in.Password).Return(updateUser.Password, nil) 218 | prep.userRepo.EXPECT().Update(mock.Anything, updateUser).Return(in.Id, err) 219 | 220 | actualErr := prep.userUsecases.ChangePassword(prep.ctx, in) 221 | 222 | require.Error(t, actualErr) 223 | require.EqualError(t, err, actualErr.Error()) 224 | }) 225 | } 226 | 227 | func TestUserUsecases_GetById(t *testing.T) { 228 | userId := int64(4) 229 | 230 | getUser := user.UserModel{ 231 | Id: userId, 232 | FirstName: "FirstName", 233 | LastName: "LastName", 234 | Email: "user@email.com", 235 | Password: "password-hash", 236 | } 237 | out := user.UserDto{ 238 | Id: getUser.Id, 239 | FirstName: getUser.FirstName, 240 | LastName: getUser.LastName, 241 | Email: getUser.Email, 242 | } 243 | 244 | t.Run("expect it gets user", func(t *testing.T) { 245 | prep := newTestPrep() 246 | 247 | prep.userRepo.EXPECT().GetById(mock.Anything, userId).Return(getUser, nil) 248 | 249 | actualOut, err := prep.userUsecases.GetById(prep.ctx, userId) 250 | 251 | require.NoError(t, err) 252 | require.Equal(t, out, actualOut) 253 | }) 254 | 255 | t.Run("expect it fails if user getting fails", func(t *testing.T) { 256 | prep := newTestPrep() 257 | err := errors.New("user getting failed") 258 | 259 | prep.userRepo.EXPECT().GetById(mock.Anything, userId).Return(getUser, err) 260 | 261 | _, actualErr := prep.userUsecases.GetById(prep.ctx, userId) 262 | 263 | require.Error(t, actualErr) 264 | require.EqualError(t, err, actualErr.Error()) 265 | }) 266 | } 267 | 268 | type testPrep struct { 269 | ctx context.Context 270 | crypto *cryptoMock.Crypto 271 | userRepo *userMock.UserRepository 272 | 273 | userUsecases user.UserUsecases 274 | } 275 | 276 | func newTestPrep() testPrep { 277 | crypto := &cryptoMock.Crypto{} 278 | userRepo := &userMock.UserRepository{} 279 | txManager := &dbMock.MockTxManager{} 280 | 281 | userUsecasesOpts := UserUsecasesOpts{ 282 | TxManager: txManager, 283 | UserRepository: userRepo, 284 | Crypto: crypto, 285 | } 286 | userUsecases := NewUserUsecases(userUsecasesOpts) 287 | 288 | return testPrep{ 289 | ctx: context.Background(), 290 | crypto: crypto, 291 | userRepo: userRepo, 292 | userUsecases: userUsecases, 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /internal/user/mock/repository.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.10.4. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | user "go-backend-template/internal/user" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // UserRepository is an autogenerated mock type for the UserRepository type 13 | type UserRepository struct { 14 | mock.Mock 15 | } 16 | 17 | type UserRepository_Expecter struct { 18 | mock *mock.Mock 19 | } 20 | 21 | func (_m *UserRepository) EXPECT() *UserRepository_Expecter { 22 | return &UserRepository_Expecter{mock: &_m.Mock} 23 | } 24 | 25 | // Add provides a mock function with given fields: ctx, _a1 26 | func (_m *UserRepository) Add(ctx context.Context, _a1 user.UserModel) (int64, error) { 27 | ret := _m.Called(ctx, _a1) 28 | 29 | var r0 int64 30 | if rf, ok := ret.Get(0).(func(context.Context, user.UserModel) int64); ok { 31 | r0 = rf(ctx, _a1) 32 | } else { 33 | r0 = ret.Get(0).(int64) 34 | } 35 | 36 | var r1 error 37 | if rf, ok := ret.Get(1).(func(context.Context, user.UserModel) error); ok { 38 | r1 = rf(ctx, _a1) 39 | } else { 40 | r1 = ret.Error(1) 41 | } 42 | 43 | return r0, r1 44 | } 45 | 46 | // UserRepository_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' 47 | type UserRepository_Add_Call struct { 48 | *mock.Call 49 | } 50 | 51 | // Add is a helper method to define mock.On call 52 | // - ctx context.Context 53 | // - _a1 user.UserModel 54 | func (_e *UserRepository_Expecter) Add(ctx interface{}, _a1 interface{}) *UserRepository_Add_Call { 55 | return &UserRepository_Add_Call{Call: _e.mock.On("Add", ctx, _a1)} 56 | } 57 | 58 | func (_c *UserRepository_Add_Call) Run(run func(ctx context.Context, _a1 user.UserModel)) *UserRepository_Add_Call { 59 | _c.Call.Run(func(args mock.Arguments) { 60 | run(args[0].(context.Context), args[1].(user.UserModel)) 61 | }) 62 | return _c 63 | } 64 | 65 | func (_c *UserRepository_Add_Call) Return(_a0 int64, _a1 error) *UserRepository_Add_Call { 66 | _c.Call.Return(_a0, _a1) 67 | return _c 68 | } 69 | 70 | // GetByEmail provides a mock function with given fields: ctx, email 71 | func (_m *UserRepository) GetByEmail(ctx context.Context, email string) (user.UserModel, error) { 72 | ret := _m.Called(ctx, email) 73 | 74 | var r0 user.UserModel 75 | if rf, ok := ret.Get(0).(func(context.Context, string) user.UserModel); ok { 76 | r0 = rf(ctx, email) 77 | } else { 78 | r0 = ret.Get(0).(user.UserModel) 79 | } 80 | 81 | var r1 error 82 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 83 | r1 = rf(ctx, email) 84 | } else { 85 | r1 = ret.Error(1) 86 | } 87 | 88 | return r0, r1 89 | } 90 | 91 | // UserRepository_GetByEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByEmail' 92 | type UserRepository_GetByEmail_Call struct { 93 | *mock.Call 94 | } 95 | 96 | // GetByEmail is a helper method to define mock.On call 97 | // - ctx context.Context 98 | // - email string 99 | func (_e *UserRepository_Expecter) GetByEmail(ctx interface{}, email interface{}) *UserRepository_GetByEmail_Call { 100 | return &UserRepository_GetByEmail_Call{Call: _e.mock.On("GetByEmail", ctx, email)} 101 | } 102 | 103 | func (_c *UserRepository_GetByEmail_Call) Run(run func(ctx context.Context, email string)) *UserRepository_GetByEmail_Call { 104 | _c.Call.Run(func(args mock.Arguments) { 105 | run(args[0].(context.Context), args[1].(string)) 106 | }) 107 | return _c 108 | } 109 | 110 | func (_c *UserRepository_GetByEmail_Call) Return(_a0 user.UserModel, _a1 error) *UserRepository_GetByEmail_Call { 111 | _c.Call.Return(_a0, _a1) 112 | return _c 113 | } 114 | 115 | // GetById provides a mock function with given fields: ctx, userId 116 | func (_m *UserRepository) GetById(ctx context.Context, userId int64) (user.UserModel, error) { 117 | ret := _m.Called(ctx, userId) 118 | 119 | var r0 user.UserModel 120 | if rf, ok := ret.Get(0).(func(context.Context, int64) user.UserModel); ok { 121 | r0 = rf(ctx, userId) 122 | } else { 123 | r0 = ret.Get(0).(user.UserModel) 124 | } 125 | 126 | var r1 error 127 | if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { 128 | r1 = rf(ctx, userId) 129 | } else { 130 | r1 = ret.Error(1) 131 | } 132 | 133 | return r0, r1 134 | } 135 | 136 | // UserRepository_GetById_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetById' 137 | type UserRepository_GetById_Call struct { 138 | *mock.Call 139 | } 140 | 141 | // GetById is a helper method to define mock.On call 142 | // - ctx context.Context 143 | // - userId int64 144 | func (_e *UserRepository_Expecter) GetById(ctx interface{}, userId interface{}) *UserRepository_GetById_Call { 145 | return &UserRepository_GetById_Call{Call: _e.mock.On("GetById", ctx, userId)} 146 | } 147 | 148 | func (_c *UserRepository_GetById_Call) Run(run func(ctx context.Context, userId int64)) *UserRepository_GetById_Call { 149 | _c.Call.Run(func(args mock.Arguments) { 150 | run(args[0].(context.Context), args[1].(int64)) 151 | }) 152 | return _c 153 | } 154 | 155 | func (_c *UserRepository_GetById_Call) Return(_a0 user.UserModel, _a1 error) *UserRepository_GetById_Call { 156 | _c.Call.Return(_a0, _a1) 157 | return _c 158 | } 159 | 160 | // Update provides a mock function with given fields: ctx, _a1 161 | func (_m *UserRepository) Update(ctx context.Context, _a1 user.UserModel) (int64, error) { 162 | ret := _m.Called(ctx, _a1) 163 | 164 | var r0 int64 165 | if rf, ok := ret.Get(0).(func(context.Context, user.UserModel) int64); ok { 166 | r0 = rf(ctx, _a1) 167 | } else { 168 | r0 = ret.Get(0).(int64) 169 | } 170 | 171 | var r1 error 172 | if rf, ok := ret.Get(1).(func(context.Context, user.UserModel) error); ok { 173 | r1 = rf(ctx, _a1) 174 | } else { 175 | r1 = ret.Error(1) 176 | } 177 | 178 | return r0, r1 179 | } 180 | 181 | // UserRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' 182 | type UserRepository_Update_Call struct { 183 | *mock.Call 184 | } 185 | 186 | // Update is a helper method to define mock.On call 187 | // - ctx context.Context 188 | // - _a1 user.UserModel 189 | func (_e *UserRepository_Expecter) Update(ctx interface{}, _a1 interface{}) *UserRepository_Update_Call { 190 | return &UserRepository_Update_Call{Call: _e.mock.On("Update", ctx, _a1)} 191 | } 192 | 193 | func (_c *UserRepository_Update_Call) Run(run func(ctx context.Context, _a1 user.UserModel)) *UserRepository_Update_Call { 194 | _c.Call.Run(func(args mock.Arguments) { 195 | run(args[0].(context.Context), args[1].(user.UserModel)) 196 | }) 197 | return _c 198 | } 199 | 200 | func (_c *UserRepository_Update_Call) Return(_a0 int64, _a1 error) *UserRepository_Update_Call { 201 | _c.Call.Return(_a0, _a1) 202 | return _c 203 | } 204 | -------------------------------------------------------------------------------- /internal/user/mock/usecase.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.10.4. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | user "go-backend-template/internal/user" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // UserUsecases is an autogenerated mock type for the UserUsecases type 13 | type UserUsecases struct { 14 | mock.Mock 15 | } 16 | 17 | type UserUsecases_Expecter struct { 18 | mock *mock.Mock 19 | } 20 | 21 | func (_m *UserUsecases) EXPECT() *UserUsecases_Expecter { 22 | return &UserUsecases_Expecter{mock: &_m.Mock} 23 | } 24 | 25 | // Add provides a mock function with given fields: ctx, dto 26 | func (_m *UserUsecases) Add(ctx context.Context, dto user.AddUserDto) (int64, error) { 27 | ret := _m.Called(ctx, dto) 28 | 29 | var r0 int64 30 | if rf, ok := ret.Get(0).(func(context.Context, user.AddUserDto) int64); ok { 31 | r0 = rf(ctx, dto) 32 | } else { 33 | r0 = ret.Get(0).(int64) 34 | } 35 | 36 | var r1 error 37 | if rf, ok := ret.Get(1).(func(context.Context, user.AddUserDto) error); ok { 38 | r1 = rf(ctx, dto) 39 | } else { 40 | r1 = ret.Error(1) 41 | } 42 | 43 | return r0, r1 44 | } 45 | 46 | // UserUsecases_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' 47 | type UserUsecases_Add_Call struct { 48 | *mock.Call 49 | } 50 | 51 | // Add is a helper method to define mock.On call 52 | // - ctx context.Context 53 | // - dto user.AddUserDto 54 | func (_e *UserUsecases_Expecter) Add(ctx interface{}, dto interface{}) *UserUsecases_Add_Call { 55 | return &UserUsecases_Add_Call{Call: _e.mock.On("Add", ctx, dto)} 56 | } 57 | 58 | func (_c *UserUsecases_Add_Call) Run(run func(ctx context.Context, dto user.AddUserDto)) *UserUsecases_Add_Call { 59 | _c.Call.Run(func(args mock.Arguments) { 60 | run(args[0].(context.Context), args[1].(user.AddUserDto)) 61 | }) 62 | return _c 63 | } 64 | 65 | func (_c *UserUsecases_Add_Call) Return(_a0 int64, _a1 error) *UserUsecases_Add_Call { 66 | _c.Call.Return(_a0, _a1) 67 | return _c 68 | } 69 | 70 | // ChangePassword provides a mock function with given fields: ctx, dto 71 | func (_m *UserUsecases) ChangePassword(ctx context.Context, dto user.ChangeUserPasswordDto) error { 72 | ret := _m.Called(ctx, dto) 73 | 74 | var r0 error 75 | if rf, ok := ret.Get(0).(func(context.Context, user.ChangeUserPasswordDto) error); ok { 76 | r0 = rf(ctx, dto) 77 | } else { 78 | r0 = ret.Error(0) 79 | } 80 | 81 | return r0 82 | } 83 | 84 | // UserUsecases_ChangePassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangePassword' 85 | type UserUsecases_ChangePassword_Call struct { 86 | *mock.Call 87 | } 88 | 89 | // ChangePassword is a helper method to define mock.On call 90 | // - ctx context.Context 91 | // - dto user.ChangeUserPasswordDto 92 | func (_e *UserUsecases_Expecter) ChangePassword(ctx interface{}, dto interface{}) *UserUsecases_ChangePassword_Call { 93 | return &UserUsecases_ChangePassword_Call{Call: _e.mock.On("ChangePassword", ctx, dto)} 94 | } 95 | 96 | func (_c *UserUsecases_ChangePassword_Call) Run(run func(ctx context.Context, dto user.ChangeUserPasswordDto)) *UserUsecases_ChangePassword_Call { 97 | _c.Call.Run(func(args mock.Arguments) { 98 | run(args[0].(context.Context), args[1].(user.ChangeUserPasswordDto)) 99 | }) 100 | return _c 101 | } 102 | 103 | func (_c *UserUsecases_ChangePassword_Call) Return(_a0 error) *UserUsecases_ChangePassword_Call { 104 | _c.Call.Return(_a0) 105 | return _c 106 | } 107 | 108 | // GetById provides a mock function with given fields: ctx, userId 109 | func (_m *UserUsecases) GetById(ctx context.Context, userId int64) (user.UserDto, error) { 110 | ret := _m.Called(ctx, userId) 111 | 112 | var r0 user.UserDto 113 | if rf, ok := ret.Get(0).(func(context.Context, int64) user.UserDto); ok { 114 | r0 = rf(ctx, userId) 115 | } else { 116 | r0 = ret.Get(0).(user.UserDto) 117 | } 118 | 119 | var r1 error 120 | if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { 121 | r1 = rf(ctx, userId) 122 | } else { 123 | r1 = ret.Error(1) 124 | } 125 | 126 | return r0, r1 127 | } 128 | 129 | // UserUsecases_GetById_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetById' 130 | type UserUsecases_GetById_Call struct { 131 | *mock.Call 132 | } 133 | 134 | // GetById is a helper method to define mock.On call 135 | // - ctx context.Context 136 | // - userId int64 137 | func (_e *UserUsecases_Expecter) GetById(ctx interface{}, userId interface{}) *UserUsecases_GetById_Call { 138 | return &UserUsecases_GetById_Call{Call: _e.mock.On("GetById", ctx, userId)} 139 | } 140 | 141 | func (_c *UserUsecases_GetById_Call) Run(run func(ctx context.Context, userId int64)) *UserUsecases_GetById_Call { 142 | _c.Call.Run(func(args mock.Arguments) { 143 | run(args[0].(context.Context), args[1].(int64)) 144 | }) 145 | return _c 146 | } 147 | 148 | func (_c *UserUsecases_GetById_Call) Return(_a0 user.UserDto, _a1 error) *UserUsecases_GetById_Call { 149 | _c.Call.Return(_a0, _a1) 150 | return _c 151 | } 152 | 153 | // Update provides a mock function with given fields: ctx, dto 154 | func (_m *UserUsecases) Update(ctx context.Context, dto user.UpdateUserDto) error { 155 | ret := _m.Called(ctx, dto) 156 | 157 | var r0 error 158 | if rf, ok := ret.Get(0).(func(context.Context, user.UpdateUserDto) error); ok { 159 | r0 = rf(ctx, dto) 160 | } else { 161 | r0 = ret.Error(0) 162 | } 163 | 164 | return r0 165 | } 166 | 167 | // UserUsecases_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' 168 | type UserUsecases_Update_Call struct { 169 | *mock.Call 170 | } 171 | 172 | // Update is a helper method to define mock.On call 173 | // - ctx context.Context 174 | // - dto user.UpdateUserDto 175 | func (_e *UserUsecases_Expecter) Update(ctx interface{}, dto interface{}) *UserUsecases_Update_Call { 176 | return &UserUsecases_Update_Call{Call: _e.mock.On("Update", ctx, dto)} 177 | } 178 | 179 | func (_c *UserUsecases_Update_Call) Run(run func(ctx context.Context, dto user.UpdateUserDto)) *UserUsecases_Update_Call { 180 | _c.Call.Run(func(args mock.Arguments) { 181 | run(args[0].(context.Context), args[1].(user.UpdateUserDto)) 182 | }) 183 | return _c 184 | } 185 | 186 | func (_c *UserUsecases_Update_Call) Return(_a0 error) *UserUsecases_Update_Call { 187 | _c.Call.Return(_a0) 188 | return _c 189 | } 190 | -------------------------------------------------------------------------------- /internal/user/model.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | validation "github.com/go-ozzo/ozzo-validation" 5 | "github.com/go-ozzo/ozzo-validation/is" 6 | 7 | "go-backend-template/internal/base/crypto" 8 | "go-backend-template/internal/base/errors" 9 | ) 10 | 11 | type UserModel struct { 12 | Id int64 13 | FirstName string 14 | LastName string 15 | Email string 16 | Password string 17 | } 18 | 19 | func NewUser(firstName, lastName, email, password string) (UserModel, error) { 20 | user := UserModel{ 21 | FirstName: firstName, 22 | LastName: lastName, 23 | Email: email, 24 | Password: password, 25 | } 26 | if err := user.Validate(); err != nil { 27 | return UserModel{}, err 28 | } 29 | 30 | return user, nil 31 | } 32 | 33 | func (user *UserModel) Update(firstName, lastName, email string) error { 34 | if len(firstName) > 0 { 35 | user.FirstName = firstName 36 | } 37 | if len(lastName) > 0 { 38 | user.LastName = lastName 39 | } 40 | if len(email) > 0 { 41 | user.Email = email 42 | } 43 | 44 | return user.Validate() 45 | } 46 | 47 | func (user *UserModel) ChangePassword(newPassword string, crypto crypto.Crypto) error { 48 | user.Password = newPassword 49 | 50 | if err := user.HashPassword(crypto); err != nil { 51 | return err 52 | } 53 | 54 | return user.Validate() 55 | } 56 | 57 | func (user *UserModel) Validate() error { 58 | err := validation.ValidateStruct(user, 59 | validation.Field(&user.FirstName, validation.Required, validation.Length(2, 100)), 60 | validation.Field(&user.LastName, validation.Required, validation.Length(2, 100)), 61 | validation.Field(&user.Email, validation.Required, is.Email), 62 | validation.Field(&user.Password, validation.Required, validation.Length(5, 100)), 63 | ) 64 | if err != nil { 65 | return errors.New(errors.ValidationError, err.Error()) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (user *UserModel) ComparePassword(password string, crypto crypto.Crypto) bool { 72 | return crypto.CompareHashAndPassword(user.Password, password) 73 | } 74 | 75 | func (user *UserModel) HashPassword(crypto crypto.Crypto) error { 76 | hash, err := crypto.HashPassword(user.Password) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | user.Password = hash 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /internal/user/repository.go: -------------------------------------------------------------------------------- 1 | //go:generate mockery --name UserRepository --filename repository.go --output ./mock --with-expecter 2 | 3 | package user 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | type UserRepository interface { 10 | Add(ctx context.Context, user UserModel) (int64, error) 11 | Update(ctx context.Context, user UserModel) (int64, error) 12 | GetById(ctx context.Context, userId int64) (UserModel, error) 13 | GetByEmail(ctx context.Context, email string) (UserModel, error) 14 | } 15 | -------------------------------------------------------------------------------- /internal/user/usecase.go: -------------------------------------------------------------------------------- 1 | //go:generate mockery --name UserUsecases --filename usecase.go --output ./mock --with-expecter 2 | 3 | package user 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | type UserUsecases interface { 10 | Add(ctx context.Context, dto AddUserDto) (int64, error) 11 | Update(ctx context.Context, dto UpdateUserDto) error 12 | ChangePassword(ctx context.Context, dto ChangeUserPasswordDto) error 13 | GetById(ctx context.Context, userId int64) (UserDto, error) 14 | } 15 | -------------------------------------------------------------------------------- /migrations/000001_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users; 2 | -------------------------------------------------------------------------------- /migrations/000001_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users( 2 | user_id BIGSERIAL , 3 | firstname VARCHAR (50) NOT NULL, 4 | lastname VARCHAR (50) , 5 | email VARCHAR (100) UNIQUE NOT NULL, 6 | password VARCHAR (100) NOT NULL, 7 | 8 | PRIMARY KEY (user_id) 9 | ); 10 | --------------------------------------------------------------------------------