├── .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 | 
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 |
--------------------------------------------------------------------------------