├── .gitignore ├── README.md ├── config ├── config.go └── config.yaml.example ├── db └── db.go ├── go-base-backend.postman_collection.json ├── go.mod ├── go.sum ├── internal ├── dtos │ ├── article_dto.go │ ├── auth_dto.go │ └── user_dto.go ├── errors │ ├── error.go │ └── pgerrcode.go ├── handlers │ ├── article_handler.go │ ├── article_handler_test.go │ ├── auth_handler.go │ ├── auth_handler_test.go │ ├── init.go │ ├── init_test.go │ ├── user_handler.go │ └── user_handler_test.go ├── helpers │ ├── hash.go │ ├── jwt.go │ └── response.go ├── middlewares │ └── authorize_jwt.go ├── mocks │ ├── repositories │ │ ├── mock_article_repository.go │ │ └── mock_user_repository.go │ └── services │ │ ├── mock_article_service.go │ │ ├── mock_auth_service.go │ │ └── mock_user_service.go ├── models │ ├── article.go │ └── user.go ├── repositories │ ├── article_repository.go │ ├── article_repository_test.go │ ├── user_repository.go │ └── user_repository_test.go ├── routes │ └── routes.go └── services │ ├── article_service.go │ ├── article_service_test.go │ ├── auth_service.go │ ├── auth_service_test.go │ ├── user_service.go │ └── user_service_test.go ├── main.go └── makefile /.gitignore: -------------------------------------------------------------------------------- 1 | config/*.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Focused on PostgreSQL database (master branch) 2 | 3 | # go-base-backend 4 | 5 | ## Release List 6 | 1. Database using PostgreSQL: master branch 7 | 2. Database using MySQL: checkout branch [mysql](https://github.com/geshtng/go-base-backend/tree/mysql) 8 | 3. Support for gRPC (database using PostgreSQL): checkout branch [grpc-postgresql](https://github.com/geshtng/go-base-backend/tree/grpc-postgresql) 9 | 4. Support for gRPC (database using MySQL): checkout branch [grpc-mysql](https://github.com/geshtng/go-base-backend/tree/grpc-mysql) 10 | 11 | ## Description 12 | Example implementation go backend (clean) architecture. It is very easy to configure. 13 | 14 | This project has 4 domain layers: 15 | 16 | - Model 17 |
18 | This layer will save models that were used in the other domains. Can be accessed from any other layer and other domains. 19 | - Handler 20 |
21 | This layer will do the job as the presenter of the application. 22 | - Service 23 |
24 | This layer will do the job as a controller and handle the business logic. 25 | - Repository 26 |
27 | This layer is the one that stores the database handler. Any operation on database like querying, inserting, updating, and deleting, will be done on this layer. 28 | 29 | ## Setup 30 | 1. Clone project. 31 | ```bash 32 | $ git clone https://github.com/geshtng/go-base-backend 33 | ``` 34 | 2. Init Database. 35 | - Create a new database. Example database name: `go_base_backend`.
36 | - After you run the server, it will automatically create tables and relations in the database `go_base_backend`.
37 | 3. Change config. 38 | 39 | Make a new file named `config.yaml` inside the folder `/config`.
40 | Use `config.yaml.example` to see the example or see the config sample below.
41 | ```yaml 42 | app: 43 | name: go-base-backend 44 | 45 | server: 46 | host: localhost 47 | port: 8080 48 | 49 | database: 50 | db_host: localhost 51 | db_port: 5432 52 | db_name: go_base_backend 53 | db_username: postgres 54 | db_password: postgres 55 | db_postgres_ssl_mode: disable 56 | 57 | jwt: 58 | expired: 60 59 | issuer: go-base-backend 60 | secret: sKk6E5gpVD 61 | ``` 62 | 63 | ## Run the Project 64 | ```bash 65 | $ make run 66 | ``` 67 | 68 | ## API List 69 | You can find API list on file `routes/routes.go` 70 | 71 | ## Example API with Authentication 72 | I have set up an example of an API that uses authentication: 73 | ```http 74 | GET localhost:8080/profiles 75 | ``` 76 | 77 | ## Postman Collection 78 | Import files `go-base-backend.postman_collection.json` to your postman 79 | 80 | ## Framework and Library 81 | - [Gin](https://github.com/gin-gonic/gin) 82 | - [Gorm](https://github.com/go-gorm/gorm) 83 | - [Copier](https://github.com/jinzhu/copier) 84 | - [Golang-jwt](https://github.com/golang-jwt/jwt) 85 | - [Viper](https://github.com/spf13/viper) 86 | - [Testify](https://github.com/stretchr/testify) 87 | - [Go-Sqlmock](https://github.com/DATA-DOG/go-sqlmock) 88 | - Other libraries listed in `go.mod` 89 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | type Server struct { 11 | Host string `mapstructure:"host"` 12 | Port int `mapstructure:"port"` 13 | } 14 | 15 | type Database struct { 16 | DBHost string `mapstructure:"db_host"` 17 | DBPort string `mapstructure:"db_port"` 18 | DBName string `mapstructure:"db_name"` 19 | DBUsername string `mapstructure:"db_username"` 20 | DBPassword string `mapstructure:"db_password"` 21 | DBPostgresSslMode string `mapstructure:"db_postgres_ssl_mode"` 22 | } 23 | 24 | type JwtConfig struct { 25 | Expired string 26 | Issuer string 27 | Secret string 28 | } 29 | 30 | type Jwt struct { 31 | Expired string `mapstructure:"expired"` 32 | Issuer string `mapstructure:"issuer"` 33 | Secret string `mapstructure:"secret"` 34 | } 35 | 36 | type Configuration struct { 37 | Server `mapstructure:"server"` 38 | Database `mapstructure:"database"` 39 | Jwt `mapstructure:"jwt"` 40 | } 41 | 42 | func initConfig() { 43 | viper.AddConfigPath("./config") 44 | viper.SetConfigName("config") 45 | viper.SetConfigType("yaml") 46 | 47 | if err := viper.ReadInConfig(); err != nil { 48 | fmt.Println("Error reading config file: ", err) 49 | os.Exit(1) 50 | } 51 | } 52 | 53 | func InitConfigDsn() string { 54 | initConfig() 55 | 56 | var config Configuration 57 | 58 | err := viper.Unmarshal(&config) 59 | if err != nil { 60 | fmt.Println("[Config][InitConfigDsn] Unable to decode into struct:", err) 61 | } 62 | 63 | dsn := `postgres://` + config.DBUsername + `:` + config.DBPassword + `@` + config.DBHost + `:` + config.DBPort + `/` + config.DBName + `?sslmode=` + config.DBPostgresSslMode 64 | 65 | return dsn 66 | } 67 | 68 | func InitConfigPort() string { 69 | initConfig() 70 | 71 | var config Configuration 72 | 73 | err := viper.Unmarshal(&config) 74 | if err != nil { 75 | fmt.Println("[Config][InitConfigPort] Uncable to decode into struct:", err) 76 | } 77 | 78 | port := `:` + fmt.Sprint(config.Port) 79 | 80 | return port 81 | } 82 | 83 | func InitJwt() JwtConfig { 84 | initConfig() 85 | 86 | var config Configuration 87 | 88 | err := viper.Unmarshal(&config) 89 | if err != nil { 90 | fmt.Println("[Config][InitJwt] Uncable to decode into struct:", err) 91 | } 92 | 93 | jwtConfig := JwtConfig{ 94 | Expired: config.Expired, 95 | Issuer: config.Issuer, 96 | Secret: config.Secret, 97 | } 98 | 99 | return jwtConfig 100 | } 101 | -------------------------------------------------------------------------------- /config/config.yaml.example: -------------------------------------------------------------------------------- 1 | app: 2 | name: go-base-backend 3 | 4 | server: 5 | host: 6 | port: 7 | 8 | database: 9 | db_host: 10 | db_port: 11 | db_name: 12 | db_username: 13 | db_password: 14 | db_postgres_ssl_mode: 15 | 16 | jwt: 17 | expired: 18 | issuer: 19 | secret: -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "gorm.io/driver/postgres" 5 | "gorm.io/gorm" 6 | 7 | "github.com/geshtng/go-base-backend/config" 8 | "github.com/geshtng/go-base-backend/internal/models" 9 | ) 10 | 11 | var db *gorm.DB 12 | 13 | func Connect() (err error) { 14 | conf := config.InitConfigDsn() 15 | 16 | db, err = gorm.Open(postgres.Open(conf), &gorm.Config{}) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | err = db.AutoMigrate( 22 | &models.Article{}, 23 | &models.User{}, 24 | ) 25 | 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func Get() *gorm.DB { 34 | return db 35 | } 36 | -------------------------------------------------------------------------------- /go-base-backend.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "5d0fca69-2f09-4b0c-ba64-f1bbeeff9754", 4 | "name": "go-base-backend", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "23028979" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Articles", 11 | "item": [ 12 | { 13 | "name": "Get All Articles", 14 | "request": { 15 | "method": "GET", 16 | "header": [], 17 | "url": { 18 | "raw": "{{host}}/articles", 19 | "host": [ 20 | "{{host}}" 21 | ], 22 | "path": [ 23 | "articles" 24 | ] 25 | } 26 | }, 27 | "response": [] 28 | }, 29 | { 30 | "name": "Get Article By ID", 31 | "request": { 32 | "method": "GET", 33 | "header": [], 34 | "url": { 35 | "raw": "{{host}}/articles/:id", 36 | "host": [ 37 | "{{host}}" 38 | ], 39 | "path": [ 40 | "articles", 41 | ":id" 42 | ], 43 | "variable": [ 44 | { 45 | "key": "id", 46 | "value": "" 47 | } 48 | ] 49 | } 50 | }, 51 | "response": [] 52 | }, 53 | { 54 | "name": "Create Article", 55 | "request": { 56 | "method": "POST", 57 | "header": [], 58 | "body": { 59 | "mode": "raw", 60 | "raw": "{\n \"title\": \"Sample Title\",\n \"description\": \"Sample Description\"\n}", 61 | "options": { 62 | "raw": { 63 | "language": "json" 64 | } 65 | } 66 | }, 67 | "url": { 68 | "raw": "{{host}}/articles", 69 | "host": [ 70 | "{{host}}" 71 | ], 72 | "path": [ 73 | "articles" 74 | ] 75 | } 76 | }, 77 | "response": [] 78 | }, 79 | { 80 | "name": "Update Article", 81 | "request": { 82 | "method": "PUT", 83 | "header": [], 84 | "body": { 85 | "mode": "raw", 86 | "raw": "{\n \"title\": \"Sample Title Update\",\n \"description\": \"Sample Description Update\"\n}", 87 | "options": { 88 | "raw": { 89 | "language": "json" 90 | } 91 | } 92 | }, 93 | "url": { 94 | "raw": "{{host}}/articles/:id", 95 | "host": [ 96 | "{{host}}" 97 | ], 98 | "path": [ 99 | "articles", 100 | ":id" 101 | ], 102 | "variable": [ 103 | { 104 | "key": "id", 105 | "value": "1" 106 | } 107 | ] 108 | } 109 | }, 110 | "response": [] 111 | }, 112 | { 113 | "name": "Delete Article", 114 | "request": { 115 | "method": "DELETE", 116 | "header": [], 117 | "url": { 118 | "raw": "{{host}}/articles/:id", 119 | "host": [ 120 | "{{host}}" 121 | ], 122 | "path": [ 123 | "articles", 124 | ":id" 125 | ], 126 | "variable": [ 127 | { 128 | "key": "id", 129 | "value": "1" 130 | } 131 | ] 132 | } 133 | }, 134 | "response": [] 135 | } 136 | ] 137 | }, 138 | { 139 | "name": "Auth", 140 | "item": [ 141 | { 142 | "name": "Login", 143 | "request": { 144 | "method": "POST", 145 | "header": [], 146 | "body": { 147 | "mode": "raw", 148 | "raw": "{\n \"username\": \"username\",\n \"password\": \"password\"\n}", 149 | "options": { 150 | "raw": { 151 | "language": "json" 152 | } 153 | } 154 | }, 155 | "url": { 156 | "raw": "{{host}}/login", 157 | "host": [ 158 | "{{host}}" 159 | ], 160 | "path": [ 161 | "login" 162 | ] 163 | } 164 | }, 165 | "response": [] 166 | }, 167 | { 168 | "name": "Register", 169 | "request": { 170 | "method": "POST", 171 | "header": [], 172 | "body": { 173 | "mode": "raw", 174 | "raw": "{\n \"username\": \"username\",\n \"password\": \"password\"\n}", 175 | "options": { 176 | "raw": { 177 | "language": "json" 178 | } 179 | } 180 | }, 181 | "url": { 182 | "raw": "{{host}}/register", 183 | "host": [ 184 | "{{host}}" 185 | ], 186 | "path": [ 187 | "register" 188 | ] 189 | } 190 | }, 191 | "response": [] 192 | } 193 | ] 194 | }, 195 | { 196 | "name": "User", 197 | "item": [ 198 | { 199 | "name": "Get By ID", 200 | "request": { 201 | "auth": { 202 | "type": "bearer", 203 | "bearer": [ 204 | { 205 | "key": "token", 206 | "value": "", 207 | "type": "string" 208 | } 209 | ] 210 | }, 211 | "method": "GET", 212 | "header": [], 213 | "url": { 214 | "raw": "{{host}}/profiles", 215 | "host": [ 216 | "{{host}}" 217 | ], 218 | "path": [ 219 | "users" 220 | ] 221 | } 222 | }, 223 | "response": [] 224 | } 225 | ], 226 | "auth": { 227 | "type": "bearer", 228 | "bearer": [ 229 | { 230 | "key": "token", 231 | "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnby1iYXNlLWJhY2tlbmQiLCJleHAiOjE2NjU2NjYyNDYsImlhdCI6MTY2NTY2MjY0NiwiZGF0YSI6eyJJRCI6MSwiVXNlcm5hbWUiOiJ1c2VybmFtZVgiLCJQYXNzd29yZCI6IiJ9fQ.UR3ZB-J9FtB6DZwSC128ya8xPzX1JJwWF2tt4IYBzeE", 232 | "type": "string" 233 | } 234 | ] 235 | }, 236 | "event": [ 237 | { 238 | "listen": "prerequest", 239 | "script": { 240 | "type": "text/javascript", 241 | "exec": [ 242 | "" 243 | ] 244 | } 245 | }, 246 | { 247 | "listen": "test", 248 | "script": { 249 | "type": "text/javascript", 250 | "exec": [ 251 | "" 252 | ] 253 | } 254 | } 255 | ] 256 | } 257 | ], 258 | "event": [ 259 | { 260 | "listen": "prerequest", 261 | "script": { 262 | "type": "text/javascript", 263 | "exec": [ 264 | "" 265 | ] 266 | } 267 | }, 268 | { 269 | "listen": "test", 270 | "script": { 271 | "type": "text/javascript", 272 | "exec": [ 273 | "" 274 | ] 275 | } 276 | } 277 | ], 278 | "variable": [ 279 | { 280 | "key": "host", 281 | "value": "localhost:8080", 282 | "type": "string" 283 | } 284 | ] 285 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/geshtng/go-base-backend 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.2 7 | github.com/gin-gonic/gin v1.8.1 8 | github.com/golang-jwt/jwt/v4 v4.4.2 9 | github.com/jackc/pgconn v1.13.0 10 | github.com/jinzhu/copier v0.3.5 11 | github.com/spf13/viper v1.13.0 12 | github.com/stretchr/testify v1.9.0 13 | golang.org/x/crypto v0.0.0-20221012134737-56aed061732a 14 | gorm.io/driver/postgres v1.4.4 15 | gorm.io/gorm v1.24.0 16 | ) 17 | 18 | require ( 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/fsnotify/fsnotify v1.5.4 // indirect 21 | github.com/gin-contrib/sse v0.1.0 // indirect 22 | github.com/go-playground/locales v0.14.0 // indirect 23 | github.com/go-playground/universal-translator v0.18.0 // indirect 24 | github.com/go-playground/validator/v10 v10.10.0 // indirect 25 | github.com/goccy/go-json v0.9.7 // indirect 26 | github.com/hashicorp/hcl v1.0.0 // indirect 27 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 28 | github.com/jackc/pgio v1.0.0 // indirect 29 | github.com/jackc/pgpassfile v1.0.0 // indirect 30 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 31 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 32 | github.com/jackc/pgtype v1.12.0 // indirect 33 | github.com/jackc/pgx/v4 v4.17.2 // indirect 34 | github.com/jinzhu/inflection v1.0.0 // indirect 35 | github.com/jinzhu/now v1.1.5 // indirect 36 | github.com/json-iterator/go v1.1.12 // indirect 37 | github.com/leodido/go-urn v1.2.1 // indirect 38 | github.com/magiconair/properties v1.8.6 // indirect 39 | github.com/mattn/go-isatty v0.0.14 // indirect 40 | github.com/mitchellh/mapstructure v1.5.0 // indirect 41 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 42 | github.com/modern-go/reflect2 v1.0.2 // indirect 43 | github.com/pelletier/go-toml v1.9.5 // indirect 44 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 45 | github.com/pmezard/go-difflib v1.0.0 // indirect 46 | github.com/spf13/afero v1.8.2 // indirect 47 | github.com/spf13/cast v1.5.0 // indirect 48 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 49 | github.com/spf13/pflag v1.0.5 // indirect 50 | github.com/stretchr/objx v0.5.2 // indirect 51 | github.com/subosito/gotenv v1.4.1 // indirect 52 | github.com/ugorji/go/codec v1.2.7 // indirect 53 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect 54 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect 55 | golang.org/x/text v0.3.8 // indirect 56 | google.golang.org/protobuf v1.28.0 // indirect 57 | gopkg.in/ini.v1 v1.67.0 // indirect 58 | gopkg.in/yaml.v2 v2.4.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= 42 | github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= 43 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 44 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 45 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 46 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 47 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 48 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 49 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 50 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 51 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 52 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 53 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 54 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 55 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 56 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 57 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 58 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 59 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 60 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 61 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 62 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 63 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 64 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 65 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 66 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 67 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 68 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 69 | github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= 70 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 71 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 72 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 73 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 74 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 75 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 76 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 77 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 78 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 79 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 80 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 81 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 82 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 83 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 84 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 85 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 86 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 87 | github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= 88 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 89 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 90 | github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= 91 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 92 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 93 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 94 | github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= 95 | github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 96 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 97 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 98 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 99 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 100 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 101 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 102 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 103 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 104 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 105 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 106 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 107 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 108 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 109 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 110 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 111 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 112 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 113 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 114 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 115 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 116 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 117 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 118 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 119 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 120 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 121 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 122 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 123 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 124 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 125 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 126 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 127 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 129 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 131 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 132 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 133 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 134 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 135 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 136 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 137 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 138 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 139 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 140 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 141 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 142 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 143 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 144 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 145 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 146 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 147 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 148 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 149 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 150 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 151 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 152 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 153 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 154 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 155 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 156 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 157 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 158 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 159 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 160 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 161 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 162 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 163 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 164 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 165 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 166 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 167 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 168 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 169 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 170 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 171 | github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= 172 | github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= 173 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 174 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 175 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 176 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 177 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 178 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 179 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 180 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 181 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 182 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 183 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 184 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 185 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 186 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 187 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 188 | github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= 189 | github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 190 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 191 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 192 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 193 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 194 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 195 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 196 | github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= 197 | github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 198 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 199 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 200 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 201 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 202 | github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= 203 | github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= 204 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 205 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 206 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 207 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 208 | github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= 209 | github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 210 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 211 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 212 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 213 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 214 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 215 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 216 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 217 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 218 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 219 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 220 | github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= 221 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 222 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 223 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 224 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 225 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 226 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 227 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 228 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 229 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 230 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 231 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 232 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 233 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 234 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 235 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 236 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 237 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 238 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 239 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 240 | github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= 241 | github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 242 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 243 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 244 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 245 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 246 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 247 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 248 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 249 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 250 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 251 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 252 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 253 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 254 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 255 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 256 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 257 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 258 | github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= 259 | github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= 260 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 261 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 262 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 263 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 264 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 265 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 266 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 267 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 268 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 269 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 270 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 271 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 272 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 273 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 274 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 275 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 276 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 277 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 278 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 279 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 280 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 281 | github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= 282 | github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= 283 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 284 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 285 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 286 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 287 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 288 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 289 | github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= 290 | github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= 291 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 292 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 293 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 294 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 295 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 296 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 297 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 298 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 299 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 300 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 301 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 302 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 303 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 304 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 305 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 306 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 307 | github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= 308 | github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 309 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 310 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 311 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 312 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 313 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 314 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 315 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 316 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 317 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 318 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 319 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 320 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 321 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 322 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 323 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 324 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 325 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 326 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 327 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 328 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 329 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 330 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 331 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 332 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 333 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 334 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 335 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 336 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 337 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 338 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 339 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 340 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 341 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 342 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 343 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 344 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 345 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 346 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 347 | golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg= 348 | golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 349 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 350 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 351 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 352 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 353 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 354 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 355 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 356 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 357 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 358 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 359 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 360 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 361 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 362 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 363 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 364 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 365 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 366 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 367 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 368 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 369 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 370 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 371 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 372 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 373 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 374 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 375 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 376 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 377 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 378 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 379 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 380 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 381 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 382 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 383 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 384 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 385 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 386 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 387 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 388 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 389 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 390 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 391 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 392 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 393 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 394 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 395 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 396 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 397 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 398 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 399 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 400 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 401 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 402 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 403 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 404 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 405 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 406 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 407 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 408 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 409 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 410 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 411 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 412 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 413 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 414 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 415 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= 416 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 417 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 418 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 419 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 420 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 421 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 422 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 423 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 424 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 425 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 426 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 427 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 428 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 429 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 430 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 431 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 432 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 433 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 434 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 435 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 436 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 437 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 438 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 439 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 440 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 441 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 442 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 443 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 444 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 445 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 450 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 451 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 452 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 453 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 454 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 455 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 456 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 457 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 458 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 459 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 460 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 462 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 463 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 464 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 465 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 466 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 467 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 468 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 469 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 470 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 471 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 472 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 473 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 474 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 475 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 476 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 477 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 478 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 479 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 480 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 481 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 482 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 483 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 484 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 485 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 486 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 487 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 488 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 489 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 490 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 491 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 492 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 493 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 494 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 495 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 496 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 497 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 498 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 499 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 500 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 501 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 502 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 503 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 504 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 505 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 506 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 507 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 508 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 509 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 510 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 511 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 512 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 513 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 514 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 515 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 516 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 517 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 518 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 519 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 520 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 521 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 522 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 523 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 524 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 525 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 526 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 527 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 528 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 529 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 530 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 531 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 532 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 533 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 534 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 535 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 536 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 537 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 538 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 539 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 540 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 541 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 542 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 543 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 544 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 545 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 546 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 547 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 548 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 549 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 550 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 551 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 552 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 553 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 554 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 555 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 556 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 557 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 558 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 559 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 560 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 561 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 562 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 563 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 564 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 565 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 566 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 567 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 568 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 569 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 570 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 571 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 572 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 573 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 574 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 575 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 576 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 577 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 578 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 579 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 580 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 581 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 582 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 583 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 584 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 585 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 586 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 587 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 588 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 589 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 590 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 591 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 592 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 593 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 594 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 595 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 596 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 597 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 598 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 599 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 600 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 601 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 602 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 603 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 604 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 605 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 606 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 607 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 608 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 609 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 610 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 611 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 612 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 613 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 614 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 615 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 616 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 617 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 618 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 619 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 620 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 621 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 622 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 623 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 624 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 625 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 626 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 627 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 628 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 629 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 630 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 631 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 632 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 633 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 634 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 635 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 636 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 637 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 638 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 639 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 640 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 641 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 642 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 643 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 644 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 645 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 646 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 647 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 648 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 649 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 650 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 651 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 652 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 653 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 654 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 655 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 656 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 657 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 658 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 659 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 660 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 661 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 662 | gorm.io/driver/postgres v1.4.4 h1:zt1fxJ+C+ajparn0SteEnkoPg0BQ6wOWXEQ99bteAmw= 663 | gorm.io/driver/postgres v1.4.4/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw= 664 | gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 665 | gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74= 666 | gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 667 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 668 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 669 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 670 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 671 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 672 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 673 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 674 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 675 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 676 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 677 | -------------------------------------------------------------------------------- /internal/dtos/article_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | type CreateArticleRequestDTO struct { 4 | Title string `json:"title" binding:"required"` 5 | Description string `json:"description" binding:"required"` 6 | } 7 | 8 | type CreateArticleResponseDTO struct { 9 | ID int64 `json:"id"` 10 | Title string `json:"title"` 11 | Description string `json:"description"` 12 | } 13 | 14 | type UpdateArticleRequestDTO struct { 15 | Title string `json:"title"` 16 | Description string `json:"description"` 17 | } 18 | 19 | type UpdateArticleResponseDTO struct { 20 | ID int64 `json:"id"` 21 | Title string `json:"title"` 22 | Description string `json:"description"` 23 | } 24 | -------------------------------------------------------------------------------- /internal/dtos/auth_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | type RegisterRequestDTO struct { 4 | Username string `json:"username" binding:"required"` 5 | Password string `json:"password" binding:"required"` 6 | } 7 | 8 | type RegisterResponseDTO struct { 9 | ID int64 `json:"id"` 10 | Username string `json:"username"` 11 | Token string `json:"token"` 12 | } 13 | 14 | type LoginRequestDTO struct { 15 | Username string `json:"username" binding:"required"` 16 | Password string `json:"password" binding:"required"` 17 | } 18 | 19 | type LoginResponseDTO struct { 20 | ID int64 `json:"id"` 21 | Username string `json:"username"` 22 | Token string `json:"token"` 23 | } 24 | 25 | type JwtData struct { 26 | ID int64 `json:"id"` 27 | Username string `json:"username"` 28 | Token string `json:"token"` 29 | } 30 | -------------------------------------------------------------------------------- /internal/dtos/user_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | type ShowProfileResponseDTO struct { 4 | ID int64 `json:"id"` 5 | Username string `json:"username"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | // Create your custom error 6 | var ( 7 | // ErrSampleCustomError example custom error 8 | ErrSampleCustomError = errors.New("example custom error") 9 | 10 | // ErrWrongUsernameOrPassword when user try to login with wrong username or password 11 | ErrWrongUsernameOrPassword = errors.New("wrong username or password") 12 | 13 | // ErrUsernameAlreadyExist when user register with registered username 14 | ErrUsernameAlreadyExist = errors.New("username is already exist") 15 | ) 16 | -------------------------------------------------------------------------------- /internal/errors/pgerrcode.go: -------------------------------------------------------------------------------- 1 | // pgerrcode contains constants for PostgreSQL error codes. 2 | // Source https://github.com/jackc/pgerrcode/blob/master/errcode.go 3 | package errors 4 | 5 | // Source: https://www.postgresql.org/docs/14/errcodes-appendix.html 6 | // See gen.rb for script that can convert the error code table to Go code. 7 | 8 | const ( 9 | 10 | // Class 00 — Successful Completion 11 | SuccessfulCompletion = "00000" 12 | 13 | // Class 01 — Warning 14 | Warning = "01000" 15 | DynamicResultSetsReturned = "0100C" 16 | ImplicitZeroBitPadding = "01008" 17 | NullValueEliminatedInSetFunction = "01003" 18 | PrivilegeNotGranted = "01007" 19 | PrivilegeNotRevoked = "01006" 20 | StringDataRightTruncationWarning = "01004" 21 | DeprecatedFeature = "01P01" 22 | 23 | // Class 02 — No Data (this is also a warning class per the SQL standard) 24 | NoData = "02000" 25 | NoAdditionalDynamicResultSetsReturned = "02001" 26 | 27 | // Class 03 — SQL Statement Not Yet Complete 28 | SQLStatementNotYetComplete = "03000" 29 | 30 | // Class 08 — Connection Exception 31 | ConnectionException = "08000" 32 | ConnectionDoesNotExist = "08003" 33 | ConnectionFailure = "08006" 34 | SQLClientUnableToEstablishSQLConnection = "08001" 35 | SQLServerRejectedEstablishmentOfSQLConnection = "08004" 36 | TransactionResolutionUnknown = "08007" 37 | ProtocolViolation = "08P01" 38 | 39 | // Class 09 — Triggered Action Exception 40 | TriggeredActionException = "09000" 41 | 42 | // Class 0A — Feature Not Supported 43 | FeatureNotSupported = "0A000" 44 | 45 | // Class 0B — Invalid Transaction Initiation 46 | InvalidTransactionInitiation = "0B000" 47 | 48 | // Class 0F — Locator Exception 49 | LocatorException = "0F000" 50 | InvalidLocatorSpecification = "0F001" 51 | 52 | // Class 0L — Invalid Grantor 53 | InvalidGrantor = "0L000" 54 | InvalidGrantOperation = "0LP01" 55 | 56 | // Class 0P — Invalid Role Specification 57 | InvalidRoleSpecification = "0P000" 58 | 59 | // Class 0Z — Diagnostics Exception 60 | DiagnosticsException = "0Z000" 61 | StackedDiagnosticsAccessedWithoutActiveHandler = "0Z002" 62 | 63 | // Class 20 — Case Not Found 64 | CaseNotFound = "20000" 65 | 66 | // Class 21 — Cardinality Violation 67 | CardinalityViolation = "21000" 68 | 69 | // Class 22 — Data Exception 70 | DataException = "22000" 71 | ArraySubscriptError = "2202E" 72 | CharacterNotInRepertoire = "22021" 73 | DatetimeFieldOverflow = "22008" 74 | DivisionByZero = "22012" 75 | ErrorInAssignment = "22005" 76 | EscapeCharacterConflict = "2200B" 77 | IndicatorOverflow = "22022" 78 | IntervalFieldOverflow = "22015" 79 | InvalidArgumentForLogarithm = "2201E" 80 | InvalidArgumentForNtileFunction = "22014" 81 | InvalidArgumentForNthValueFunction = "22016" 82 | InvalidArgumentForPowerFunction = "2201F" 83 | InvalidArgumentForWidthBucketFunction = "2201G" 84 | InvalidCharacterValueForCast = "22018" 85 | InvalidDatetimeFormat = "22007" 86 | InvalidEscapeCharacter = "22019" 87 | InvalidEscapeOctet = "2200D" 88 | InvalidEscapeSequence = "22025" 89 | NonstandardUseOfEscapeCharacter = "22P06" 90 | InvalidIndicatorParameterValue = "22010" 91 | InvalidParameterValue = "22023" 92 | InvalidPrecedingOrFollowingSize = "22013" 93 | InvalidRegularExpression = "2201B" 94 | InvalidRowCountInLimitClause = "2201W" 95 | InvalidRowCountInResultOffsetClause = "2201X" 96 | InvalidTablesampleArgument = "2202H" 97 | InvalidTablesampleRepeat = "2202G" 98 | InvalidTimeZoneDisplacementValue = "22009" 99 | InvalidUseOfEscapeCharacter = "2200C" 100 | MostSpecificTypeMismatch = "2200G" 101 | NullValueNotAllowedDataException = "22004" 102 | NullValueNoIndicatorParameter = "22002" 103 | NumericValueOutOfRange = "22003" 104 | SequenceGeneratorLimitExceeded = "2200H" 105 | StringDataLengthMismatch = "22026" 106 | StringDataRightTruncationDataException = "22001" 107 | SubstringError = "22011" 108 | TrimError = "22027" 109 | UnterminatedCString = "22024" 110 | ZeroLengthCharacterString = "2200F" 111 | FloatingPointException = "22P01" 112 | InvalidTextRepresentation = "22P02" 113 | InvalidBinaryRepresentation = "22P03" 114 | BadCopyFileFormat = "22P04" 115 | UntranslatableCharacter = "22P05" 116 | NotAnXMLDocument = "2200L" 117 | InvalidXMLDocument = "2200M" 118 | InvalidXMLContent = "2200N" 119 | InvalidXMLComment = "2200S" 120 | InvalidXMLProcessingInstruction = "2200T" 121 | DuplicateJSONObjectKeyValue = "22030" 122 | InvalidArgumentForSQLJSONDatetimeFunction = "22031" 123 | InvalidJSONText = "22032" 124 | InvalidSQLJSONSubscript = "22033" 125 | MoreThanOneSQLJSONItem = "22034" 126 | NoSQLJSONItem = "22035" 127 | NonNumericSQLJSONItem = "22036" 128 | NonUniqueKeysInAJSONObject = "22037" 129 | SingletonSQLJSONItemRequired = "22038" 130 | SQLJSONArrayNotFound = "22039" 131 | SQLJSONMemberNotFound = "2203A" 132 | SQLJSONNumberNotFound = "2203B" 133 | SQLJSONObjectNotFound = "2203C" 134 | TooManyJSONArrayElements = "2203D" 135 | TooManyJSONObjectMembers = "2203E" 136 | SQLJSONScalarRequired = "2203F" 137 | 138 | // Class 23 — Integrity Constraint Violation 139 | IntegrityConstraintViolation = "23000" 140 | RestrictViolation = "23001" 141 | NotNullViolation = "23502" 142 | ForeignKeyViolation = "23503" 143 | UniqueViolation = "23505" 144 | CheckViolation = "23514" 145 | ExclusionViolation = "23P01" 146 | 147 | // Class 24 — Invalid Cursor State 148 | InvalidCursorState = "24000" 149 | 150 | // Class 25 — Invalid Transaction State 151 | InvalidTransactionState = "25000" 152 | ActiveSQLTransaction = "25001" 153 | BranchTransactionAlreadyActive = "25002" 154 | HeldCursorRequiresSameIsolationLevel = "25008" 155 | InappropriateAccessModeForBranchTransaction = "25003" 156 | InappropriateIsolationLevelForBranchTransaction = "25004" 157 | NoActiveSQLTransactionForBranchTransaction = "25005" 158 | ReadOnlySQLTransaction = "25006" 159 | SchemaAndDataStatementMixingNotSupported = "25007" 160 | NoActiveSQLTransaction = "25P01" 161 | InFailedSQLTransaction = "25P02" 162 | IdleInTransactionSessionTimeout = "25P03" 163 | 164 | // Class 26 — Invalid SQL Statement Name 165 | InvalidSQLStatementName = "26000" 166 | 167 | // Class 27 — Triggered Data Change Violation 168 | TriggeredDataChangeViolation = "27000" 169 | 170 | // Class 28 — Invalid Authorization Specification 171 | InvalidAuthorizationSpecification = "28000" 172 | InvalidPassword = "28P01" 173 | 174 | // Class 2B — Dependent Privilege Descriptors Still Exist 175 | DependentPrivilegeDescriptorsStillExist = "2B000" 176 | DependentObjectsStillExist = "2BP01" 177 | 178 | // Class 2D — Invalid Transaction Termination 179 | InvalidTransactionTermination = "2D000" 180 | 181 | // Class 2F — SQL Routine Exception 182 | SQLRoutineException = "2F000" 183 | FunctionExecutedNoReturnStatement = "2F005" 184 | ModifyingSQLDataNotPermittedSQLRoutineException = "2F002" 185 | ProhibitedSQLStatementAttemptedSQLRoutineException = "2F003" 186 | ReadingSQLDataNotPermittedSQLRoutineException = "2F004" 187 | 188 | // Class 34 — Invalid Cursor Name 189 | InvalidCursorName = "34000" 190 | 191 | // Class 38 — External Routine Exception 192 | ExternalRoutineException = "38000" 193 | ContainingSQLNotPermitted = "38001" 194 | ModifyingSQLDataNotPermittedExternalRoutineException = "38002" 195 | ProhibitedSQLStatementAttemptedExternalRoutineException = "38003" 196 | ReadingSQLDataNotPermittedExternalRoutineException = "38004" 197 | 198 | // Class 39 — External Routine Invocation Exception 199 | ExternalRoutineInvocationException = "39000" 200 | InvalidSQLstateReturned = "39001" 201 | NullValueNotAllowedExternalRoutineInvocationException = "39004" 202 | TriggerProtocolViolated = "39P01" 203 | SRFProtocolViolated = "39P02" 204 | EventTriggerProtocolViolated = "39P03" 205 | 206 | // Class 3B — Savepoint Exception 207 | SavepointException = "3B000" 208 | InvalidSavepointSpecification = "3B001" 209 | 210 | // Class 3D — Invalid Catalog Name 211 | InvalidCatalogName = "3D000" 212 | 213 | // Class 3F — Invalid Schema Name 214 | InvalidSchemaName = "3F000" 215 | 216 | // Class 40 — Transaction Rollback 217 | TransactionRollback = "40000" 218 | TransactionIntegrityConstraintViolation = "40002" 219 | SerializationFailure = "40001" 220 | StatementCompletionUnknown = "40003" 221 | DeadlockDetected = "40P01" 222 | 223 | // Class 42 — Syntax Error or Access Rule Violation 224 | SyntaxErrorOrAccessRuleViolation = "42000" 225 | SyntaxError = "42601" 226 | InsufficientPrivilege = "42501" 227 | CannotCoerce = "42846" 228 | GroupingError = "42803" 229 | WindowingError = "42P20" 230 | InvalidRecursion = "42P19" 231 | InvalidForeignKey = "42830" 232 | InvalidName = "42602" 233 | NameTooLong = "42622" 234 | ReservedName = "42939" 235 | DatatypeMismatch = "42804" 236 | IndeterminateDatatype = "42P18" 237 | CollationMismatch = "42P21" 238 | IndeterminateCollation = "42P22" 239 | WrongObjectType = "42809" 240 | GeneratedAlways = "428C9" 241 | UndefinedColumn = "42703" 242 | UndefinedFunction = "42883" 243 | UndefinedTable = "42P01" 244 | UndefinedParameter = "42P02" 245 | UndefinedObject = "42704" 246 | DuplicateColumn = "42701" 247 | DuplicateCursor = "42P03" 248 | DuplicateDatabase = "42P04" 249 | DuplicateFunction = "42723" 250 | DuplicatePreparedStatement = "42P05" 251 | DuplicateSchema = "42P06" 252 | DuplicateTable = "42P07" 253 | DuplicateAlias = "42712" 254 | DuplicateObject = "42710" 255 | AmbiguousColumn = "42702" 256 | AmbiguousFunction = "42725" 257 | AmbiguousParameter = "42P08" 258 | AmbiguousAlias = "42P09" 259 | InvalidColumnReference = "42P10" 260 | InvalidColumnDefinition = "42611" 261 | InvalidCursorDefinition = "42P11" 262 | InvalidDatabaseDefinition = "42P12" 263 | InvalidFunctionDefinition = "42P13" 264 | InvalidPreparedStatementDefinition = "42P14" 265 | InvalidSchemaDefinition = "42P15" 266 | InvalidTableDefinition = "42P16" 267 | InvalidObjectDefinition = "42P17" 268 | 269 | // Class 44 — WITH CHECK OPTION Violation 270 | WithCheckOptionViolation = "44000" 271 | 272 | // Class 53 — Insufficient Resources 273 | InsufficientResources = "53000" 274 | DiskFull = "53100" 275 | OutOfMemory = "53200" 276 | TooManyConnections = "53300" 277 | ConfigurationLimitExceeded = "53400" 278 | 279 | // Class 54 — Program Limit Exceeded 280 | ProgramLimitExceeded = "54000" 281 | StatementTooComplex = "54001" 282 | TooManyColumns = "54011" 283 | TooManyArguments = "54023" 284 | 285 | // Class 55 — Object Not In Prerequisite State 286 | ObjectNotInPrerequisiteState = "55000" 287 | ObjectInUse = "55006" 288 | CantChangeRuntimeParam = "55P02" 289 | LockNotAvailable = "55P03" 290 | UnsafeNewEnumValueUsage = "55P04" 291 | 292 | // Class 57 — Operator Intervention 293 | OperatorIntervention = "57000" 294 | QueryCanceled = "57014" 295 | AdminShutdown = "57P01" 296 | CrashShutdown = "57P02" 297 | CannotConnectNow = "57P03" 298 | DatabaseDropped = "57P04" 299 | IdleSessionTimeout = "57P05" 300 | 301 | // Class 58 — System Error (errors external to PostgreSQL itself) 302 | SystemError = "58000" 303 | IOError = "58030" 304 | UndefinedFile = "58P01" 305 | DuplicateFile = "58P02" 306 | 307 | // Class 72 — Snapshot Failure 308 | SnapshotTooOld = "72000" 309 | 310 | // Class F0 — Configuration File Error 311 | ConfigFileError = "F0000" 312 | LockFileExists = "F0001" 313 | 314 | // Class HV — Foreign Data Wrapper Error (SQL/MED) 315 | FDWError = "HV000" 316 | FDWColumnNameNotFound = "HV005" 317 | FDWDynamicParameterValueNeeded = "HV002" 318 | FDWFunctionSequenceError = "HV010" 319 | FDWInconsistentDescriptorInformation = "HV021" 320 | FDWInvalidAttributeValue = "HV024" 321 | FDWInvalidColumnName = "HV007" 322 | FDWInvalidColumnNumber = "HV008" 323 | FDWInvalidDataType = "HV004" 324 | FDWInvalidDataTypeDescriptors = "HV006" 325 | FDWInvalidDescriptorFieldIdentifier = "HV091" 326 | FDWInvalidHandle = "HV00B" 327 | FDWInvalidOptionIndex = "HV00C" 328 | FDWInvalidOptionName = "HV00D" 329 | FDWInvalidStringLengthOrBufferLength = "HV090" 330 | FDWInvalidStringFormat = "HV00A" 331 | FDWInvalidUseOfNullPointer = "HV009" 332 | FDWTooManyHandles = "HV014" 333 | FDWOutOfMemory = "HV001" 334 | FDWNoSchemas = "HV00P" 335 | FDWOptionNameNotFound = "HV00J" 336 | FDWReplyHandle = "HV00K" 337 | FDWSchemaNotFound = "HV00Q" 338 | FDWTableNotFound = "HV00R" 339 | FDWUnableToCreateExecution = "HV00L" 340 | FDWUnableToCreateReply = "HV00M" 341 | FDWUnableToEstablishConnection = "HV00N" 342 | 343 | // Class P0 — PL/pgSQL Error 344 | PLpgSQLError = "P0000" 345 | RaiseException = "P0001" 346 | NoDataFound = "P0002" 347 | TooManyRows = "P0003" 348 | AssertFailure = "P0004" 349 | 350 | // Class XX — Internal Error 351 | InternalError = "XX000" 352 | DataCorrupted = "XX001" 353 | IndexCorrupted = "XX002" 354 | ) 355 | 356 | // IsSuccessfulCompletion asserts the error code class is Class 00 — Successful Completion 357 | func IsSuccessfulCompletion(code string) bool { 358 | switch code { 359 | case SuccessfulCompletion: 360 | return true 361 | } 362 | return false 363 | } 364 | 365 | // IsWarning asserts the error code class is Class 01 — Warning 366 | func IsWarning(code string) bool { 367 | switch code { 368 | case Warning, DynamicResultSetsReturned, ImplicitZeroBitPadding, NullValueEliminatedInSetFunction, PrivilegeNotGranted, PrivilegeNotRevoked, StringDataRightTruncationWarning, DeprecatedFeature: 369 | return true 370 | } 371 | return false 372 | } 373 | 374 | // IsNoData asserts the error code class is Class 02 — No Data (this is also a warning class per the SQL standard) 375 | func IsNoData(code string) bool { 376 | switch code { 377 | case NoData, NoAdditionalDynamicResultSetsReturned: 378 | return true 379 | } 380 | return false 381 | } 382 | 383 | // IsSQLStatementNotYetComplete asserts the error code class is Class 03 — SQL Statement Not Yet Complete 384 | func IsSQLStatementNotYetComplete(code string) bool { 385 | switch code { 386 | case SQLStatementNotYetComplete: 387 | return true 388 | } 389 | return false 390 | } 391 | 392 | // IsConnectionException asserts the error code class is Class 08 — Connection Exception 393 | func IsConnectionException(code string) bool { 394 | switch code { 395 | case ConnectionException, ConnectionDoesNotExist, ConnectionFailure, SQLClientUnableToEstablishSQLConnection, SQLServerRejectedEstablishmentOfSQLConnection, TransactionResolutionUnknown, ProtocolViolation: 396 | return true 397 | } 398 | return false 399 | } 400 | 401 | // IsTriggeredActionException asserts the error code class is Class 09 — Triggered Action Exception 402 | func IsTriggeredActionException(code string) bool { 403 | switch code { 404 | case TriggeredActionException: 405 | return true 406 | } 407 | return false 408 | } 409 | 410 | // IsFeatureNotSupported asserts the error code class is Class 0A — Feature Not Supported 411 | func IsFeatureNotSupported(code string) bool { 412 | switch code { 413 | case FeatureNotSupported: 414 | return true 415 | } 416 | return false 417 | } 418 | 419 | // IsInvalidTransactionInitiation asserts the error code class is Class 0B — Invalid Transaction Initiation 420 | func IsInvalidTransactionInitiation(code string) bool { 421 | switch code { 422 | case InvalidTransactionInitiation: 423 | return true 424 | } 425 | return false 426 | } 427 | 428 | // IsLocatorException asserts the error code class is Class 0F — Locator Exception 429 | func IsLocatorException(code string) bool { 430 | switch code { 431 | case LocatorException, InvalidLocatorSpecification: 432 | return true 433 | } 434 | return false 435 | } 436 | 437 | // IsInvalidGrantor asserts the error code class is Class 0L — Invalid Grantor 438 | func IsInvalidGrantor(code string) bool { 439 | switch code { 440 | case InvalidGrantor, InvalidGrantOperation: 441 | return true 442 | } 443 | return false 444 | } 445 | 446 | // IsInvalidRoleSpecification asserts the error code class is Class 0P — Invalid Role Specification 447 | func IsInvalidRoleSpecification(code string) bool { 448 | switch code { 449 | case InvalidRoleSpecification: 450 | return true 451 | } 452 | return false 453 | } 454 | 455 | // IsDiagnosticsException asserts the error code class is Class 0Z — Diagnostics Exception 456 | func IsDiagnosticsException(code string) bool { 457 | switch code { 458 | case DiagnosticsException, StackedDiagnosticsAccessedWithoutActiveHandler: 459 | return true 460 | } 461 | return false 462 | } 463 | 464 | // IsCaseNotFound asserts the error code class is Class 20 — Case Not Found 465 | func IsCaseNotFound(code string) bool { 466 | switch code { 467 | case CaseNotFound: 468 | return true 469 | } 470 | return false 471 | } 472 | 473 | // IsCardinalityViolation asserts the error code class is Class 21 — Cardinality Violation 474 | func IsCardinalityViolation(code string) bool { 475 | switch code { 476 | case CardinalityViolation: 477 | return true 478 | } 479 | return false 480 | } 481 | 482 | // IsDataException asserts the error code class is Class 22 — Data Exception 483 | func IsDataException(code string) bool { 484 | switch code { 485 | case DataException, ArraySubscriptError, CharacterNotInRepertoire, DatetimeFieldOverflow, DivisionByZero, ErrorInAssignment, EscapeCharacterConflict, IndicatorOverflow, IntervalFieldOverflow, InvalidArgumentForLogarithm, InvalidArgumentForNtileFunction, InvalidArgumentForNthValueFunction, InvalidArgumentForPowerFunction, InvalidArgumentForWidthBucketFunction, InvalidCharacterValueForCast, InvalidDatetimeFormat, InvalidEscapeCharacter, InvalidEscapeOctet, InvalidEscapeSequence, NonstandardUseOfEscapeCharacter, InvalidIndicatorParameterValue, InvalidParameterValue, InvalidPrecedingOrFollowingSize, InvalidRegularExpression, InvalidRowCountInLimitClause, InvalidRowCountInResultOffsetClause, InvalidTablesampleArgument, InvalidTablesampleRepeat, InvalidTimeZoneDisplacementValue, InvalidUseOfEscapeCharacter, MostSpecificTypeMismatch, NullValueNotAllowedDataException, NullValueNoIndicatorParameter, NumericValueOutOfRange, SequenceGeneratorLimitExceeded, StringDataLengthMismatch, StringDataRightTruncationDataException, SubstringError, TrimError, UnterminatedCString, ZeroLengthCharacterString, FloatingPointException, InvalidTextRepresentation, InvalidBinaryRepresentation, BadCopyFileFormat, UntranslatableCharacter, NotAnXMLDocument, InvalidXMLDocument, InvalidXMLContent, InvalidXMLComment, InvalidXMLProcessingInstruction, DuplicateJSONObjectKeyValue, InvalidArgumentForSQLJSONDatetimeFunction, InvalidJSONText, InvalidSQLJSONSubscript, MoreThanOneSQLJSONItem, NoSQLJSONItem, NonNumericSQLJSONItem, NonUniqueKeysInAJSONObject, SingletonSQLJSONItemRequired, SQLJSONArrayNotFound, SQLJSONMemberNotFound, SQLJSONNumberNotFound, SQLJSONObjectNotFound, TooManyJSONArrayElements, TooManyJSONObjectMembers, SQLJSONScalarRequired: 486 | return true 487 | } 488 | return false 489 | } 490 | 491 | // IsIntegrityConstraintViolation asserts the error code class is Class 23 — Integrity Constraint Violation 492 | func IsIntegrityConstraintViolation(code string) bool { 493 | switch code { 494 | case IntegrityConstraintViolation, RestrictViolation, NotNullViolation, ForeignKeyViolation, UniqueViolation, CheckViolation, ExclusionViolation: 495 | return true 496 | } 497 | return false 498 | } 499 | 500 | // IsInvalidCursorState asserts the error code class is Class 24 — Invalid Cursor State 501 | func IsInvalidCursorState(code string) bool { 502 | switch code { 503 | case InvalidCursorState: 504 | return true 505 | } 506 | return false 507 | } 508 | 509 | // IsInvalidTransactionState asserts the error code class is Class 25 — Invalid Transaction State 510 | func IsInvalidTransactionState(code string) bool { 511 | switch code { 512 | case InvalidTransactionState, ActiveSQLTransaction, BranchTransactionAlreadyActive, HeldCursorRequiresSameIsolationLevel, InappropriateAccessModeForBranchTransaction, InappropriateIsolationLevelForBranchTransaction, NoActiveSQLTransactionForBranchTransaction, ReadOnlySQLTransaction, SchemaAndDataStatementMixingNotSupported, NoActiveSQLTransaction, InFailedSQLTransaction, IdleInTransactionSessionTimeout: 513 | return true 514 | } 515 | return false 516 | } 517 | 518 | // IsInvalidSQLStatementName asserts the error code class is Class 26 — Invalid SQL Statement Name 519 | func IsInvalidSQLStatementName(code string) bool { 520 | switch code { 521 | case InvalidSQLStatementName: 522 | return true 523 | } 524 | return false 525 | } 526 | 527 | // IsTriggeredDataChangeViolation asserts the error code class is Class 27 — Triggered Data Change Violation 528 | func IsTriggeredDataChangeViolation(code string) bool { 529 | switch code { 530 | case TriggeredDataChangeViolation: 531 | return true 532 | } 533 | return false 534 | } 535 | 536 | // IsInvalidAuthorizationSpecification asserts the error code class is Class 28 — Invalid Authorization Specification 537 | func IsInvalidAuthorizationSpecification(code string) bool { 538 | switch code { 539 | case InvalidAuthorizationSpecification, InvalidPassword: 540 | return true 541 | } 542 | return false 543 | } 544 | 545 | // IsDependentPrivilegeDescriptorsStillExist asserts the error code class is Class 2B — Dependent Privilege Descriptors Still Exist 546 | func IsDependentPrivilegeDescriptorsStillExist(code string) bool { 547 | switch code { 548 | case DependentPrivilegeDescriptorsStillExist, DependentObjectsStillExist: 549 | return true 550 | } 551 | return false 552 | } 553 | 554 | // IsInvalidTransactionTermination asserts the error code class is Class 2D — Invalid Transaction Termination 555 | func IsInvalidTransactionTermination(code string) bool { 556 | switch code { 557 | case InvalidTransactionTermination: 558 | return true 559 | } 560 | return false 561 | } 562 | 563 | // IsSQLRoutineException asserts the error code class is Class 2F — SQL Routine Exception 564 | func IsSQLRoutineException(code string) bool { 565 | switch code { 566 | case SQLRoutineException, FunctionExecutedNoReturnStatement, ModifyingSQLDataNotPermittedSQLRoutineException, ProhibitedSQLStatementAttemptedSQLRoutineException, ReadingSQLDataNotPermittedSQLRoutineException: 567 | return true 568 | } 569 | return false 570 | } 571 | 572 | // IsInvalidCursorName asserts the error code class is Class 34 — Invalid Cursor Name 573 | func IsInvalidCursorName(code string) bool { 574 | switch code { 575 | case InvalidCursorName: 576 | return true 577 | } 578 | return false 579 | } 580 | 581 | // IsExternalRoutineException asserts the error code class is Class 38 — External Routine Exception 582 | func IsExternalRoutineException(code string) bool { 583 | switch code { 584 | case ExternalRoutineException, ContainingSQLNotPermitted, ModifyingSQLDataNotPermittedExternalRoutineException, ProhibitedSQLStatementAttemptedExternalRoutineException, ReadingSQLDataNotPermittedExternalRoutineException: 585 | return true 586 | } 587 | return false 588 | } 589 | 590 | // IsExternalRoutineInvocationException asserts the error code class is Class 39 — External Routine Invocation Exception 591 | func IsExternalRoutineInvocationException(code string) bool { 592 | switch code { 593 | case ExternalRoutineInvocationException, InvalidSQLstateReturned, NullValueNotAllowedExternalRoutineInvocationException, TriggerProtocolViolated, SRFProtocolViolated, EventTriggerProtocolViolated: 594 | return true 595 | } 596 | return false 597 | } 598 | 599 | // IsSavepointException asserts the error code class is Class 3B — Savepoint Exception 600 | func IsSavepointException(code string) bool { 601 | switch code { 602 | case SavepointException, InvalidSavepointSpecification: 603 | return true 604 | } 605 | return false 606 | } 607 | 608 | // IsInvalidCatalogName asserts the error code class is Class 3D — Invalid Catalog Name 609 | func IsInvalidCatalogName(code string) bool { 610 | switch code { 611 | case InvalidCatalogName: 612 | return true 613 | } 614 | return false 615 | } 616 | 617 | // IsInvalidSchemaName asserts the error code class is Class 3F — Invalid Schema Name 618 | func IsInvalidSchemaName(code string) bool { 619 | switch code { 620 | case InvalidSchemaName: 621 | return true 622 | } 623 | return false 624 | } 625 | 626 | // IsTransactionRollback asserts the error code class is Class 40 — Transaction Rollback 627 | func IsTransactionRollback(code string) bool { 628 | switch code { 629 | case TransactionRollback, TransactionIntegrityConstraintViolation, SerializationFailure, StatementCompletionUnknown, DeadlockDetected: 630 | return true 631 | } 632 | return false 633 | } 634 | 635 | // IsSyntaxErrororAccessRuleViolation asserts the error code class is Class 42 — Syntax Error or Access Rule Violation 636 | func IsSyntaxErrororAccessRuleViolation(code string) bool { 637 | switch code { 638 | case SyntaxErrorOrAccessRuleViolation, SyntaxError, InsufficientPrivilege, CannotCoerce, GroupingError, WindowingError, InvalidRecursion, InvalidForeignKey, InvalidName, NameTooLong, ReservedName, DatatypeMismatch, IndeterminateDatatype, CollationMismatch, IndeterminateCollation, WrongObjectType, GeneratedAlways, UndefinedColumn, UndefinedFunction, UndefinedTable, UndefinedParameter, UndefinedObject, DuplicateColumn, DuplicateCursor, DuplicateDatabase, DuplicateFunction, DuplicatePreparedStatement, DuplicateSchema, DuplicateTable, DuplicateAlias, DuplicateObject, AmbiguousColumn, AmbiguousFunction, AmbiguousParameter, AmbiguousAlias, InvalidColumnReference, InvalidColumnDefinition, InvalidCursorDefinition, InvalidDatabaseDefinition, InvalidFunctionDefinition, InvalidPreparedStatementDefinition, InvalidSchemaDefinition, InvalidTableDefinition, InvalidObjectDefinition: 639 | return true 640 | } 641 | return false 642 | } 643 | 644 | // IsWithCheckOptionViolation asserts the error code class is Class 44 — WITH CHECK OPTION Violation 645 | func IsWithCheckOptionViolation(code string) bool { 646 | switch code { 647 | case WithCheckOptionViolation: 648 | return true 649 | } 650 | return false 651 | } 652 | 653 | // IsInsufficientResources asserts the error code class is Class 53 — Insufficient Resources 654 | func IsInsufficientResources(code string) bool { 655 | switch code { 656 | case InsufficientResources, DiskFull, OutOfMemory, TooManyConnections, ConfigurationLimitExceeded: 657 | return true 658 | } 659 | return false 660 | } 661 | 662 | // IsProgramLimitExceeded asserts the error code class is Class 54 — Program Limit Exceeded 663 | func IsProgramLimitExceeded(code string) bool { 664 | switch code { 665 | case ProgramLimitExceeded, StatementTooComplex, TooManyColumns, TooManyArguments: 666 | return true 667 | } 668 | return false 669 | } 670 | 671 | // IsObjectNotInPrerequisiteState asserts the error code class is Class 55 — Object Not In Prerequisite State 672 | func IsObjectNotInPrerequisiteState(code string) bool { 673 | switch code { 674 | case ObjectNotInPrerequisiteState, ObjectInUse, CantChangeRuntimeParam, LockNotAvailable, UnsafeNewEnumValueUsage: 675 | return true 676 | } 677 | return false 678 | } 679 | 680 | // IsOperatorIntervention asserts the error code class is Class 57 — Operator Intervention 681 | func IsOperatorIntervention(code string) bool { 682 | switch code { 683 | case OperatorIntervention, QueryCanceled, AdminShutdown, CrashShutdown, CannotConnectNow, DatabaseDropped, IdleSessionTimeout: 684 | return true 685 | } 686 | return false 687 | } 688 | 689 | // IsSystemError asserts the error code class is Class 58 — System Error (errors external to PostgreSQL itself) 690 | func IsSystemError(code string) bool { 691 | switch code { 692 | case SystemError, IOError, UndefinedFile, DuplicateFile: 693 | return true 694 | } 695 | return false 696 | } 697 | 698 | // IsSnapshotFailure asserts the error code class is Class 72 — Snapshot Failure 699 | func IsSnapshotFailure(code string) bool { 700 | switch code { 701 | case SnapshotTooOld: 702 | return true 703 | } 704 | return false 705 | } 706 | 707 | // IsConfigurationFileError asserts the error code class is Class F0 — Configuration File Error 708 | func IsConfigurationFileError(code string) bool { 709 | switch code { 710 | case ConfigFileError, LockFileExists: 711 | return true 712 | } 713 | return false 714 | } 715 | 716 | // IsForeignDataWrapperError asserts the error code class is Class HV — Foreign Data Wrapper Error (SQL/MED) 717 | func IsForeignDataWrapperError(code string) bool { 718 | switch code { 719 | case FDWError, FDWColumnNameNotFound, FDWDynamicParameterValueNeeded, FDWFunctionSequenceError, FDWInconsistentDescriptorInformation, FDWInvalidAttributeValue, FDWInvalidColumnName, FDWInvalidColumnNumber, FDWInvalidDataType, FDWInvalidDataTypeDescriptors, FDWInvalidDescriptorFieldIdentifier, FDWInvalidHandle, FDWInvalidOptionIndex, FDWInvalidOptionName, FDWInvalidStringLengthOrBufferLength, FDWInvalidStringFormat, FDWInvalidUseOfNullPointer, FDWTooManyHandles, FDWOutOfMemory, FDWNoSchemas, FDWOptionNameNotFound, FDWReplyHandle, FDWSchemaNotFound, FDWTableNotFound, FDWUnableToCreateExecution, FDWUnableToCreateReply, FDWUnableToEstablishConnection: 720 | return true 721 | } 722 | return false 723 | } 724 | 725 | // IsPLpgSQLError asserts the error code class is Class P0 — PL/pgSQL Error 726 | func IsPLpgSQLError(code string) bool { 727 | switch code { 728 | case PLpgSQLError, RaiseException, NoDataFound, TooManyRows, AssertFailure: 729 | return true 730 | } 731 | return false 732 | } 733 | 734 | // IsInternalError asserts the error code class is Class XX — Internal Error 735 | func IsInternalError(code string) bool { 736 | switch code { 737 | case InternalError, DataCorrupted, IndexCorrupted: 738 | return true 739 | } 740 | return false 741 | } 742 | -------------------------------------------------------------------------------- /internal/handlers/article_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/jinzhu/copier" 10 | "gorm.io/gorm" 11 | 12 | "github.com/geshtng/go-base-backend/internal/dtos" 13 | "github.com/geshtng/go-base-backend/internal/helpers" 14 | "github.com/geshtng/go-base-backend/internal/models" 15 | ) 16 | 17 | func (h *Handler) GetAllArticlesHandler(c *gin.Context) { 18 | result, err := h.articleService.GetAll() 19 | if err != nil { 20 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 21 | return 22 | } 23 | 24 | c.JSON(http.StatusOK, helpers.BuildSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), result)) 25 | } 26 | 27 | func (h *Handler) GetArticleByIDHandler(c *gin.Context) { 28 | id, err := strconv.ParseInt(c.Param("id"), 10, 64) 29 | if err != nil { 30 | c.JSON(http.StatusBadRequest, helpers.BuildErrorResponse(http.StatusBadRequest, err.Error())) 31 | return 32 | } 33 | 34 | result, err := h.articleService.GetByID(id) 35 | if err != nil { 36 | if errors.Is(err, gorm.ErrRecordNotFound) { 37 | c.JSON(http.StatusNotFound, helpers.BuildErrorResponse(http.StatusNotFound, err.Error())) 38 | return 39 | } 40 | 41 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 42 | return 43 | } 44 | 45 | c.JSON(http.StatusOK, helpers.BuildSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), result)) 46 | } 47 | 48 | func (h *Handler) CreateArticleHandler(c *gin.Context) { 49 | var request dtos.CreateArticleRequestDTO 50 | var response dtos.CreateArticleResponseDTO 51 | 52 | err := c.ShouldBindJSON(&request) 53 | if err != nil { 54 | c.JSON(http.StatusBadRequest, helpers.BuildErrorResponse(http.StatusBadRequest, err.Error())) 55 | return 56 | } 57 | 58 | article := models.Article{ 59 | Title: request.Title, 60 | Description: request.Description, 61 | } 62 | 63 | newArticle, err := h.articleService.Create(article) 64 | if err != nil { 65 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 66 | return 67 | } 68 | 69 | err = copier.Copy(&response, &newArticle) 70 | if err != nil { 71 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 72 | return 73 | } 74 | 75 | c.JSON(http.StatusCreated, helpers.BuildSuccessResponse(http.StatusCreated, http.StatusText(http.StatusCreated), newArticle)) 76 | } 77 | 78 | func (h *Handler) UpdateArticleHandler(c *gin.Context) { 79 | var request dtos.UpdateArticleRequestDTO 80 | var response dtos.UpdateArticleResponseDTO 81 | 82 | id, err := strconv.ParseInt(c.Param("id"), 10, 64) 83 | if err != nil { 84 | c.JSON(http.StatusBadRequest, helpers.BuildErrorResponse(http.StatusBadRequest, err.Error())) 85 | return 86 | } 87 | 88 | err = c.ShouldBindJSON(&request) 89 | if err != nil { 90 | c.JSON(http.StatusBadRequest, helpers.BuildErrorResponse(http.StatusBadRequest, err.Error())) 91 | return 92 | } 93 | 94 | article := models.Article{ 95 | Title: request.Title, 96 | Description: request.Description, 97 | } 98 | 99 | updatedArticle, err := h.articleService.Update(id, article) 100 | if err != nil { 101 | if errors.Is(err, gorm.ErrRecordNotFound) { 102 | c.JSON(http.StatusNotFound, helpers.BuildErrorResponse(http.StatusNotFound, err.Error())) 103 | return 104 | } 105 | 106 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 107 | return 108 | } 109 | 110 | err = copier.Copy(&response, &updatedArticle) 111 | if err != nil { 112 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 113 | return 114 | } 115 | 116 | c.JSON(http.StatusOK, helpers.BuildSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), updatedArticle)) 117 | } 118 | 119 | func (h *Handler) DeleteArticleHandler(c *gin.Context) { 120 | id, err := strconv.ParseInt(c.Param("id"), 10, 64) 121 | if err != nil { 122 | c.JSON(http.StatusBadRequest, helpers.BuildErrorResponse(http.StatusBadRequest, err.Error())) 123 | return 124 | } 125 | 126 | err = h.articleService.Delete(id) 127 | if err != nil { 128 | if errors.Is(err, gorm.ErrRecordNotFound) { 129 | c.JSON(http.StatusNotFound, helpers.BuildErrorResponse(http.StatusNotFound, err.Error())) 130 | return 131 | } 132 | 133 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 134 | return 135 | } 136 | 137 | c.JSON(http.StatusOK, helpers.BuildSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), id)) 138 | } 139 | -------------------------------------------------------------------------------- /internal/handlers/article_handler_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | "github.com/stretchr/testify/require" 15 | "gorm.io/gorm" 16 | 17 | "github.com/geshtng/go-base-backend/internal/dtos" 18 | "github.com/geshtng/go-base-backend/internal/helpers" 19 | smock "github.com/geshtng/go-base-backend/internal/mocks/services" 20 | "github.com/geshtng/go-base-backend/internal/models" 21 | ) 22 | 23 | func TestHandler_GetAllArticlesHandler(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | articleService *smock.ArticleService 27 | mock func(*smock.ArticleService) 28 | want helpers.Response 29 | }{ 30 | { 31 | name: "Success", 32 | articleService: smock.NewArticleService(t), 33 | mock: func(ms *smock.ArticleService) { 34 | expectedArticles := []*models.Article{ 35 | { 36 | ID: 1, 37 | Title: "title", 38 | Description: "description", 39 | }, 40 | } 41 | 42 | ms.On("GetAll").Return(expectedArticles, nil) 43 | }, 44 | want: helpers.Response{ 45 | Data: []interface{}{ 46 | map[string]interface{}{ 47 | "ID": float64(1), 48 | "Title": "title", 49 | "Description": "description", 50 | }, 51 | }, 52 | Code: 200, 53 | Message: "OK", 54 | Error: false, 55 | }, 56 | }, 57 | { 58 | name: "Error Service.GetAll", 59 | articleService: smock.NewArticleService(t), 60 | mock: func(ms *smock.ArticleService) { 61 | ms.On("GetAll").Return(nil, errors.New("something error")) 62 | }, 63 | want: helpers.Response{ 64 | Code: 500, 65 | Message: "something error", 66 | Error: true, 67 | }, 68 | }, 69 | } 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | h := &Handler{ 73 | articleService: tt.articleService, 74 | } 75 | 76 | tt.mock(tt.articleService) 77 | 78 | r := gin.Default() 79 | endpoint := "/articles" 80 | 81 | r.GET(endpoint, h.GetAllArticlesHandler) 82 | 83 | req, _ := http.NewRequest(http.MethodGet, endpoint, nil) 84 | 85 | w := httptest.NewRecorder() 86 | r.ServeHTTP(w, req) 87 | 88 | var response helpers.Response 89 | err := json.Unmarshal(w.Body.Bytes(), &response) 90 | require.NoError(t, err) 91 | 92 | assert.Equal(t, tt.want.Code, w.Code) 93 | assert.Equal(t, tt.want, response) 94 | }) 95 | } 96 | } 97 | 98 | func TestHandler_GetArticleByIDHandler(t *testing.T) { 99 | tests := []struct { 100 | name string 101 | articleService *smock.ArticleService 102 | mock func(*smock.ArticleService) 103 | want helpers.Response 104 | }{ 105 | { 106 | name: "Success", 107 | articleService: smock.NewArticleService(t), 108 | mock: func(ms *smock.ArticleService) { 109 | expectedArticle := &models.Article{ 110 | ID: 1, 111 | Title: "title", 112 | Description: "description", 113 | } 114 | 115 | ms.On("GetByID", mock.Anything).Return(expectedArticle, nil) 116 | }, 117 | want: helpers.Response{ 118 | Data: map[string]interface{}{ 119 | "ID": float64(1), 120 | "Title": "title", 121 | "Description": "description", 122 | }, 123 | Code: 200, 124 | Message: "OK", 125 | Error: false, 126 | }, 127 | }, 128 | { 129 | name: "Error Parse articleID", 130 | articleService: smock.NewArticleService(t), 131 | mock: func(ms *smock.ArticleService) {}, 132 | want: helpers.Response{ 133 | Code: 400, 134 | Message: "strconv.ParseInt: parsing \"xx\": invalid syntax", 135 | Error: true, 136 | }, 137 | }, 138 | { 139 | name: "Error Service.ID record not found", 140 | articleService: smock.NewArticleService(t), 141 | mock: func(ms *smock.ArticleService) { 142 | ms.On("GetByID", mock.Anything).Return(nil, gorm.ErrRecordNotFound) 143 | }, 144 | want: helpers.Response{ 145 | Code: 404, 146 | Message: "record not found", 147 | Error: true, 148 | }, 149 | }, 150 | { 151 | name: "Error Service.GetByID", 152 | articleService: smock.NewArticleService(t), 153 | mock: func(ms *smock.ArticleService) { 154 | ms.On("GetByID", mock.Anything).Return(nil, errors.New("something error")) 155 | }, 156 | want: helpers.Response{ 157 | Code: 500, 158 | Message: "something error", 159 | Error: true, 160 | }, 161 | }, 162 | } 163 | for _, tt := range tests { 164 | h := &Handler{ 165 | articleService: tt.articleService, 166 | } 167 | 168 | tt.mock(tt.articleService) 169 | 170 | r := gin.Default() 171 | endpoint := "/articles" 172 | 173 | if tt.name == "Error Parse articleID" { 174 | r.GET(endpoint, MiddlewareMockID("xx"), h.GetArticleByIDHandler) 175 | } else { 176 | r.GET(endpoint, MiddlewareMockID("1"), h.GetArticleByIDHandler) 177 | } 178 | 179 | req, _ := http.NewRequest(http.MethodGet, endpoint, nil) 180 | 181 | w := httptest.NewRecorder() 182 | r.ServeHTTP(w, req) 183 | 184 | var response helpers.Response 185 | err := json.Unmarshal(w.Body.Bytes(), &response) 186 | require.NoError(t, err) 187 | 188 | assert.Equal(t, tt.want.Code, w.Code) 189 | assert.Equal(t, tt.want, response) 190 | } 191 | } 192 | 193 | func TestHandler_CreateArticleHandler(t *testing.T) { 194 | tests := []struct { 195 | name string 196 | body io.Reader 197 | articleService *smock.ArticleService 198 | mock func(*smock.ArticleService) 199 | want helpers.Response 200 | }{ 201 | { 202 | name: "Success", 203 | body: MakeRequestBody(dtos.CreateArticleRequestDTO{ 204 | Title: "title", 205 | Description: "description", 206 | }), 207 | articleService: smock.NewArticleService(t), 208 | mock: func(ms *smock.ArticleService) { 209 | expectedArticle := &models.Article{ 210 | ID: 1, 211 | Title: "title", 212 | Description: "description", 213 | } 214 | 215 | ms.On("Create", mock.Anything).Return(expectedArticle, nil) 216 | }, 217 | want: helpers.Response{ 218 | Data: map[string]interface{}{ 219 | "ID": float64(1), 220 | "Title": "title", 221 | "Description": "description", 222 | }, 223 | Code: 201, 224 | Message: "Created", 225 | Error: false, 226 | }, 227 | }, 228 | { 229 | name: "Error ShouldBindJSON", 230 | body: MakeRequestBody(dtos.CreateArticleRequestDTO{ 231 | Title: "title", 232 | }), 233 | articleService: smock.NewArticleService(t), 234 | mock: func(ms *smock.ArticleService) {}, 235 | want: helpers.Response{ 236 | Code: 400, 237 | Message: "Key: 'CreateArticleRequestDTO.Description' Error:Field validation for 'Description' failed on the 'required' tag", 238 | Error: true, 239 | }, 240 | }, 241 | { 242 | name: "Error Service.Create", 243 | body: MakeRequestBody(dtos.CreateArticleRequestDTO{ 244 | Title: "title", 245 | Description: "description", 246 | }), 247 | articleService: smock.NewArticleService(t), 248 | mock: func(ms *smock.ArticleService) { 249 | ms.On("Create", mock.Anything).Return(nil, errors.New("something error")) 250 | }, 251 | want: helpers.Response{ 252 | Code: 500, 253 | Message: "something error", 254 | Error: true, 255 | }, 256 | }, 257 | { 258 | name: "Error copier", 259 | body: MakeRequestBody(dtos.CreateArticleRequestDTO{ 260 | Title: "title", 261 | Description: "description", 262 | }), 263 | articleService: smock.NewArticleService(t), 264 | mock: func(ms *smock.ArticleService) { 265 | ms.On("Create", mock.Anything).Return(nil, nil) 266 | }, 267 | want: helpers.Response{ 268 | Code: 500, 269 | Message: "copy from is invalid", 270 | Error: true, 271 | }, 272 | }, 273 | } 274 | for _, tt := range tests { 275 | t.Run(tt.name, func(t *testing.T) { 276 | h := &Handler{ 277 | articleService: tt.articleService, 278 | } 279 | 280 | tt.mock(tt.articleService) 281 | 282 | r := gin.Default() 283 | endpoint := "/articles" 284 | 285 | r.POST(endpoint, h.CreateArticleHandler) 286 | 287 | req, _ := http.NewRequest(http.MethodPost, endpoint, tt.body) 288 | 289 | w := httptest.NewRecorder() 290 | r.ServeHTTP(w, req) 291 | 292 | var response helpers.Response 293 | err := json.Unmarshal(w.Body.Bytes(), &response) 294 | require.NoError(t, err) 295 | 296 | assert.Equal(t, tt.want.Code, w.Code) 297 | assert.Equal(t, tt.want, response) 298 | }) 299 | } 300 | } 301 | 302 | func TestHandler_UpdateArticleHandler(t *testing.T) { 303 | tests := []struct { 304 | name string 305 | body io.Reader 306 | articleService *smock.ArticleService 307 | mock func(*smock.ArticleService) 308 | want helpers.Response 309 | }{ 310 | { 311 | name: "Success", 312 | body: MakeRequestBody(dtos.UpdateArticleRequestDTO{ 313 | Title: "title update", 314 | Description: "description update", 315 | }), 316 | articleService: smock.NewArticleService(t), 317 | mock: func(ms *smock.ArticleService) { 318 | expectedArticle := &models.Article{ 319 | ID: 1, 320 | Title: "title update", 321 | Description: "description update", 322 | } 323 | 324 | ms.On("Update", mock.Anything, mock.Anything).Return(expectedArticle, nil) 325 | }, 326 | want: helpers.Response{ 327 | Data: map[string]interface{}{ 328 | "ID": float64(1), 329 | "Title": "title update", 330 | "Description": "description update", 331 | }, 332 | Code: 200, 333 | Message: "OK", 334 | Error: false, 335 | }, 336 | }, 337 | { 338 | name: "Error Parse articleID", 339 | body: MakeRequestBody(dtos.UpdateArticleRequestDTO{ 340 | Title: "title update", 341 | Description: "description update", 342 | }), 343 | articleService: smock.NewArticleService(t), 344 | mock: func(ms *smock.ArticleService) {}, 345 | want: helpers.Response{ 346 | Code: 400, 347 | Message: "strconv.ParseInt: parsing \"xx\": invalid syntax", 348 | Error: true, 349 | }, 350 | }, 351 | { 352 | name: "Error ShouldBindJSON", 353 | body: nil, 354 | articleService: smock.NewArticleService(t), 355 | mock: func(ms *smock.ArticleService) {}, 356 | want: helpers.Response{ 357 | Code: 400, 358 | Message: "invalid request", 359 | Error: true, 360 | }, 361 | }, 362 | { 363 | name: "Error Service.Update", 364 | body: MakeRequestBody(dtos.UpdateArticleRequestDTO{ 365 | Title: "title update", 366 | Description: "description update", 367 | }), 368 | articleService: smock.NewArticleService(t), 369 | mock: func(ms *smock.ArticleService) { 370 | ms.On("Update", mock.Anything, mock.Anything).Return(nil, errors.New("something error")) 371 | }, 372 | want: helpers.Response{ 373 | Code: 500, 374 | Message: "something error", 375 | Error: true, 376 | }, 377 | }, 378 | { 379 | name: "Error Service.Update record not found", 380 | body: MakeRequestBody(dtos.UpdateArticleRequestDTO{ 381 | Title: "title update", 382 | Description: "description update", 383 | }), 384 | articleService: smock.NewArticleService(t), 385 | mock: func(ms *smock.ArticleService) { 386 | ms.On("Update", mock.Anything, mock.Anything).Return(nil, gorm.ErrRecordNotFound) 387 | }, 388 | want: helpers.Response{ 389 | Code: 404, 390 | Message: "record not found", 391 | Error: true, 392 | }, 393 | }, 394 | { 395 | name: "Error copier", 396 | body: MakeRequestBody(dtos.UpdateArticleRequestDTO{ 397 | Title: "title update", 398 | Description: "description update", 399 | }), 400 | articleService: smock.NewArticleService(t), 401 | mock: func(ms *smock.ArticleService) { 402 | ms.On("Update", mock.Anything, mock.Anything).Return(nil, nil) 403 | }, 404 | want: helpers.Response{ 405 | Code: 500, 406 | Message: "copy from is invalid", 407 | Error: true, 408 | }, 409 | }, 410 | } 411 | for _, tt := range tests { 412 | t.Run(tt.name, func(t *testing.T) { 413 | h := &Handler{ 414 | articleService: tt.articleService, 415 | } 416 | 417 | tt.mock(tt.articleService) 418 | 419 | r := gin.Default() 420 | endpoint := "/articles" 421 | 422 | if tt.name == "Error Parse articleID" { 423 | r.PUT(endpoint, MiddlewareMockID("xx"), h.UpdateArticleHandler) 424 | } else { 425 | r.PUT(endpoint, MiddlewareMockID("1"), h.UpdateArticleHandler) 426 | } 427 | 428 | req, _ := http.NewRequest(http.MethodPut, endpoint, tt.body) 429 | 430 | w := httptest.NewRecorder() 431 | r.ServeHTTP(w, req) 432 | 433 | var response helpers.Response 434 | err := json.Unmarshal(w.Body.Bytes(), &response) 435 | require.NoError(t, err) 436 | 437 | assert.Equal(t, tt.want.Code, w.Code) 438 | assert.Equal(t, tt.want, response) 439 | }) 440 | } 441 | } 442 | 443 | func TestHandler_DeleteArticleHandler(t *testing.T) { 444 | tests := []struct { 445 | name string 446 | articleService *smock.ArticleService 447 | mock func(*smock.ArticleService) 448 | want helpers.Response 449 | }{ 450 | { 451 | name: "Success", 452 | articleService: smock.NewArticleService(t), 453 | mock: func(ms *smock.ArticleService) { 454 | ms.On("Delete", mock.Anything).Return(nil) 455 | }, 456 | want: helpers.Response{ 457 | Data: float64(1), 458 | Code: 200, 459 | Message: "OK", 460 | Error: false, 461 | }, 462 | }, 463 | { 464 | name: "Error Parse articleID", 465 | articleService: smock.NewArticleService(t), 466 | mock: func(ms *smock.ArticleService) {}, 467 | want: helpers.Response{ 468 | Code: 400, 469 | Message: "strconv.ParseInt: parsing \"xx\": invalid syntax", 470 | Error: true, 471 | }, 472 | }, 473 | { 474 | name: "Error Service.Delete", 475 | articleService: smock.NewArticleService(t), 476 | mock: func(ms *smock.ArticleService) { 477 | ms.On("Delete", mock.Anything).Return(errors.New("something error")) 478 | }, 479 | want: helpers.Response{ 480 | Code: 500, 481 | Message: "something error", 482 | Error: true, 483 | }, 484 | }, 485 | { 486 | name: "Error Service.Delete record not found", 487 | articleService: smock.NewArticleService(t), 488 | mock: func(ms *smock.ArticleService) { 489 | ms.On("Delete", mock.Anything).Return(gorm.ErrRecordNotFound) 490 | }, 491 | want: helpers.Response{ 492 | Code: 404, 493 | Message: "record not found", 494 | Error: true, 495 | }, 496 | }, 497 | } 498 | for _, tt := range tests { 499 | h := &Handler{ 500 | articleService: tt.articleService, 501 | } 502 | 503 | tt.mock(tt.articleService) 504 | 505 | r := gin.Default() 506 | endpoint := "/articles" 507 | 508 | if tt.name == "Error Parse articleID" { 509 | r.DELETE(endpoint, MiddlewareMockID("xx"), h.DeleteArticleHandler) 510 | } else { 511 | r.DELETE(endpoint, MiddlewareMockID("1"), h.DeleteArticleHandler) 512 | } 513 | 514 | req, _ := http.NewRequest(http.MethodDelete, endpoint, nil) 515 | 516 | w := httptest.NewRecorder() 517 | r.ServeHTTP(w, req) 518 | 519 | var response helpers.Response 520 | err := json.Unmarshal(w.Body.Bytes(), &response) 521 | require.NoError(t, err) 522 | 523 | assert.Equal(t, tt.want.Code, w.Code) 524 | assert.Equal(t, tt.want, response) 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /internal/handlers/auth_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/jackc/pgconn" 9 | "gorm.io/gorm" 10 | 11 | "github.com/geshtng/go-base-backend/internal/dtos" 12 | errn "github.com/geshtng/go-base-backend/internal/errors" 13 | "github.com/geshtng/go-base-backend/internal/helpers" 14 | "github.com/geshtng/go-base-backend/internal/models" 15 | ) 16 | 17 | func (h *Handler) RegisterHandler(c *gin.Context) { 18 | var request dtos.RegisterRequestDTO 19 | var response dtos.RegisterResponseDTO 20 | 21 | err := c.ShouldBindJSON(&request) 22 | if err != nil { 23 | c.JSON(http.StatusBadRequest, helpers.BuildErrorResponse(http.StatusBadRequest, err.Error())) 24 | return 25 | } 26 | 27 | auth := models.User{ 28 | Username: request.Username, 29 | Password: request.Password, 30 | } 31 | 32 | newAuth, token, err := h.authService.Register(auth) 33 | if err != nil { 34 | var pgErr *pgconn.PgError 35 | 36 | if errors.As(err, &pgErr); pgErr.Code == errn.UniqueViolation { 37 | c.JSON(http.StatusUnauthorized, helpers.BuildErrorResponse(http.StatusUnauthorized, errn.ErrUsernameAlreadyExist.Error())) 38 | return 39 | } 40 | 41 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 42 | return 43 | } 44 | 45 | response = dtos.RegisterResponseDTO{ 46 | ID: newAuth.ID, 47 | Username: newAuth.Username, 48 | Token: *token, 49 | } 50 | 51 | c.JSON(http.StatusCreated, helpers.BuildSuccessResponse(http.StatusCreated, http.StatusText(http.StatusCreated), response)) 52 | } 53 | 54 | func (h *Handler) LoginHandler(c *gin.Context) { 55 | var request dtos.LoginRequestDTO 56 | var response dtos.LoginResponseDTO 57 | 58 | err := c.ShouldBindJSON(&request) 59 | if err != nil { 60 | c.JSON(http.StatusBadRequest, helpers.BuildErrorResponse(http.StatusBadRequest, err.Error())) 61 | return 62 | } 63 | 64 | auth, token, err := h.authService.Login(request.Username, request.Password) 65 | if err != nil { 66 | if errors.Is(err, gorm.ErrRecordNotFound) { 67 | c.JSON(http.StatusUnauthorized, helpers.BuildErrorResponse(http.StatusUnauthorized, errn.ErrWrongUsernameOrPassword.Error())) 68 | return 69 | } 70 | 71 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 72 | return 73 | } 74 | 75 | response = dtos.LoginResponseDTO{ 76 | ID: auth.ID, 77 | Username: auth.Username, 78 | Token: *token, 79 | } 80 | 81 | c.JSON(http.StatusOK, helpers.BuildSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), response)) 82 | } 83 | -------------------------------------------------------------------------------- /internal/handlers/auth_handler_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/jackc/pgconn" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/mock" 15 | "github.com/stretchr/testify/require" 16 | "gorm.io/gorm" 17 | 18 | "github.com/geshtng/go-base-backend/internal/dtos" 19 | "github.com/geshtng/go-base-backend/internal/helpers" 20 | smock "github.com/geshtng/go-base-backend/internal/mocks/services" 21 | "github.com/geshtng/go-base-backend/internal/models" 22 | ) 23 | 24 | func TestHandler_RegisterHandler(t *testing.T) { 25 | sampleToken := "sample" 26 | 27 | tests := []struct { 28 | name string 29 | body io.Reader 30 | authService *smock.AuthService 31 | mock func(*smock.AuthService) 32 | want helpers.Response 33 | }{ 34 | { 35 | name: "Success", 36 | body: MakeRequestBody(dtos.RegisterRequestDTO{ 37 | Username: "username", 38 | Password: "password", 39 | }), 40 | authService: smock.NewAuthService(t), 41 | mock: func(as *smock.AuthService) { 42 | expectedUser := models.User{ 43 | ID: 1, 44 | Username: "username", 45 | Password: "password", 46 | } 47 | 48 | as.On("Register", mock.Anything).Return(&expectedUser, &sampleToken, nil) 49 | }, 50 | want: helpers.Response{ 51 | Data: map[string]interface{}{ 52 | "id": float64(1), 53 | "username": "username", 54 | "token": "sample", 55 | }, 56 | Code: 201, 57 | Message: "Created", 58 | Error: false, 59 | }, 60 | }, 61 | { 62 | name: "Error ShouldBindJSON", 63 | body: MakeRequestBody(dtos.RegisterRequestDTO{ 64 | Username: "username", 65 | }), 66 | authService: smock.NewAuthService(t), 67 | mock: func(as *smock.AuthService) {}, 68 | want: helpers.Response{ 69 | Code: 400, 70 | Message: "Key: 'RegisterRequestDTO.Password' Error:Field validation for 'Password' failed on the 'required' tag", 71 | Error: true, 72 | }, 73 | }, 74 | { 75 | name: "Error Service.Register unique violation", 76 | body: MakeRequestBody(dtos.RegisterRequestDTO{ 77 | Username: "username", 78 | Password: "password", 79 | }), 80 | authService: smock.NewAuthService(t), 81 | mock: func(as *smock.AuthService) { 82 | as.On("Register", mock.Anything).Return(nil, nil, &pgconn.PgError{Code: "23505"}) 83 | }, 84 | want: helpers.Response{ 85 | Code: 401, 86 | Message: "username is already exist", 87 | Error: true, 88 | }, 89 | }, 90 | { 91 | name: "Error Service.Register", 92 | body: MakeRequestBody(dtos.RegisterRequestDTO{ 93 | Username: "username", 94 | Password: "password", 95 | }), 96 | authService: smock.NewAuthService(t), 97 | mock: func(as *smock.AuthService) { 98 | as.On("Register", mock.Anything).Return(nil, nil, &pgconn.PgError{Message: "something error"}) 99 | }, 100 | want: helpers.Response{ 101 | Code: 500, 102 | Message: ": something error (SQLSTATE )", 103 | Error: true, 104 | }, 105 | }, 106 | } 107 | for _, tt := range tests { 108 | t.Run(tt.name, func(t *testing.T) { 109 | h := &Handler{ 110 | authService: tt.authService, 111 | } 112 | 113 | tt.mock(tt.authService) 114 | 115 | r := gin.Default() 116 | endpoint := "/register" 117 | 118 | r.POST(endpoint, h.RegisterHandler) 119 | req, _ := http.NewRequest(http.MethodPost, endpoint, tt.body) 120 | 121 | w := httptest.NewRecorder() 122 | r.ServeHTTP(w, req) 123 | 124 | var response helpers.Response 125 | err := json.Unmarshal(w.Body.Bytes(), &response) 126 | require.NoError(t, err) 127 | 128 | assert.Equal(t, tt.want.Code, w.Code) 129 | assert.Equal(t, tt.want, response) 130 | }) 131 | } 132 | } 133 | 134 | func TestHandler_LoginHandler(t *testing.T) { 135 | sampleToken := "sample" 136 | 137 | tests := []struct { 138 | name string 139 | body io.Reader 140 | authService *smock.AuthService 141 | mock func(*smock.AuthService) 142 | want helpers.Response 143 | }{ 144 | { 145 | name: "Success", 146 | body: MakeRequestBody(dtos.LoginRequestDTO{ 147 | Username: "username", 148 | Password: "password", 149 | }), 150 | authService: smock.NewAuthService(t), 151 | mock: func(as *smock.AuthService) { 152 | expectedUser := models.User{ 153 | ID: 1, 154 | Username: "username", 155 | Password: "password", 156 | } 157 | 158 | as.On("Login", mock.Anything, mock.Anything).Return(&expectedUser, &sampleToken, nil) 159 | }, 160 | want: helpers.Response{ 161 | Data: map[string]interface{}{ 162 | "id": float64(1), 163 | "username": "username", 164 | "token": "sample", 165 | }, 166 | Code: 200, 167 | Message: "OK", 168 | Error: false, 169 | }, 170 | }, 171 | { 172 | name: "Error ShouldBindJSON", 173 | body: MakeRequestBody(dtos.LoginRequestDTO{ 174 | Username: "username", 175 | }), 176 | authService: smock.NewAuthService(t), 177 | mock: func(as *smock.AuthService) {}, 178 | want: helpers.Response{ 179 | Code: 400, 180 | Message: "Key: 'LoginRequestDTO.Password' Error:Field validation for 'Password' failed on the 'required' tag", 181 | Error: true, 182 | }, 183 | }, 184 | { 185 | name: "Error Service.Login record not found", 186 | body: MakeRequestBody(dtos.LoginRequestDTO{ 187 | Username: "username", 188 | Password: "password", 189 | }), 190 | authService: smock.NewAuthService(t), 191 | mock: func(as *smock.AuthService) { 192 | as.On("Login", mock.Anything, mock.Anything).Return(nil, nil, gorm.ErrRecordNotFound) 193 | }, 194 | want: helpers.Response{ 195 | Code: 401, 196 | Message: "wrong username or password", 197 | Error: true, 198 | }, 199 | }, 200 | { 201 | name: "Error Service.Login", 202 | body: MakeRequestBody(dtos.LoginRequestDTO{ 203 | Username: "username", 204 | Password: "password", 205 | }), 206 | authService: smock.NewAuthService(t), 207 | mock: func(as *smock.AuthService) { 208 | as.On("Login", mock.Anything, mock.Anything).Return(nil, nil, errors.New("something error")) 209 | }, 210 | want: helpers.Response{ 211 | Code: 500, 212 | Message: "something error", 213 | Error: true, 214 | }, 215 | }, 216 | } 217 | for _, tt := range tests { 218 | t.Run(tt.name, func(t *testing.T) { 219 | h := &Handler{ 220 | authService: tt.authService, 221 | } 222 | 223 | tt.mock(tt.authService) 224 | 225 | r := gin.Default() 226 | endpoint := "/login" 227 | 228 | r.POST(endpoint, h.LoginHandler) 229 | req, _ := http.NewRequest(http.MethodPost, endpoint, tt.body) 230 | 231 | w := httptest.NewRecorder() 232 | r.ServeHTTP(w, req) 233 | 234 | var response helpers.Response 235 | err := json.Unmarshal(w.Body.Bytes(), &response) 236 | require.NoError(t, err) 237 | 238 | assert.Equal(t, tt.want.Code, w.Code) 239 | assert.Equal(t, tt.want, response) 240 | }) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /internal/handlers/init.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/geshtng/go-base-backend/db" 5 | r "github.com/geshtng/go-base-backend/internal/repositories" 6 | s "github.com/geshtng/go-base-backend/internal/services" 7 | ) 8 | 9 | type Handler struct { 10 | articleService s.ArticleService 11 | authService s.AuthService 12 | userService s.UserService 13 | } 14 | 15 | type HandlerConfig struct { 16 | ArticleService s.ArticleService 17 | AuthService s.AuthService 18 | UserService s.UserService 19 | } 20 | 21 | func New(c *HandlerConfig) *Handler { 22 | return &Handler{ 23 | articleService: c.ArticleService, 24 | authService: c.AuthService, 25 | userService: c.UserService, 26 | } 27 | } 28 | 29 | func InitAllHandlers() *Handler { 30 | db := db.Get() 31 | 32 | articleRepo := r.NewArticleRepository((&r.ArticleRepoConfig{DB: db})) 33 | userRepo := r.NewUserRepository((&r.UserRepoConfig{DB: db})) 34 | 35 | articleService := s.NewArticleService(&s.ArticleServiceConfig{ArticleRepository: articleRepo}) 36 | authService := s.NewAuthService(&s.AuthServiceConfig{UserRepository: userRepo}) 37 | userService := s.NewUserService(&s.UserServiceConfig{UserRepository: userRepo}) 38 | 39 | h := New(&HandlerConfig{ 40 | ArticleService: articleService, 41 | AuthService: authService, 42 | UserService: userService, 43 | }) 44 | 45 | return h 46 | } 47 | -------------------------------------------------------------------------------- /internal/handlers/init_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/geshtng/go-base-backend/internal/dtos" 12 | smock "github.com/geshtng/go-base-backend/internal/mocks/services" 13 | ) 14 | 15 | var MockTokenized dtos.JwtData = dtos.JwtData{ 16 | ID: 1, 17 | Username: "username", 18 | } 19 | 20 | func MakeRequestBody(dto interface{}) *strings.Reader { 21 | payload, _ := json.Marshal(dto) 22 | return strings.NewReader(string(payload)) 23 | } 24 | 25 | // To mock endpoint param 26 | func MiddlewareMockID(mockID string) gin.HandlerFunc { 27 | return func(c *gin.Context) { 28 | c.Params = []gin.Param{ 29 | { 30 | Key: "id", 31 | Value: mockID, 32 | }, 33 | } 34 | } 35 | } 36 | 37 | // To mock token 38 | func MiddlewareMockToken(ctx *gin.Context) { 39 | ctx.Set("user", MockTokenized) 40 | ctx.Next() 41 | } 42 | 43 | func TestNew(t *testing.T) { 44 | articleService := smock.NewArticleService(t) 45 | 46 | handlerConfig := HandlerConfig{ 47 | ArticleService: articleService, 48 | } 49 | 50 | New(&handlerConfig) 51 | } 52 | 53 | func TestInitAllHandlers(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | want *Handler 57 | }{ 58 | { 59 | name: "Pass", 60 | want: &Handler{ 61 | articleService: smock.NewArticleService(t), 62 | authService: smock.NewAuthService(t), 63 | userService: smock.NewUserService(t), 64 | }, 65 | }, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | got := InitAllHandlers() 70 | assert.NotNil(t, got, tt.want) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/handlers/user_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/jinzhu/copier" 9 | "gorm.io/gorm" 10 | 11 | "github.com/geshtng/go-base-backend/internal/dtos" 12 | "github.com/geshtng/go-base-backend/internal/helpers" 13 | ) 14 | 15 | func (h *Handler) ShowProfileHandler(c *gin.Context) { 16 | userContext := c.MustGet("user").(dtos.JwtData) 17 | 18 | var response dtos.ShowProfileResponseDTO 19 | 20 | user, err := h.userService.GetByID(userContext.ID) 21 | if err != nil { 22 | if errors.Is(err, gorm.ErrRecordNotFound) { 23 | c.JSON(http.StatusNotFound, helpers.BuildErrorResponse(http.StatusNotFound, err.Error())) 24 | return 25 | } 26 | 27 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 28 | return 29 | } 30 | 31 | err = copier.Copy(&response, &user) 32 | if err != nil { 33 | c.JSON(http.StatusInternalServerError, helpers.BuildErrorResponse(http.StatusInternalServerError, err.Error())) 34 | return 35 | } 36 | 37 | c.JSON(http.StatusOK, helpers.BuildSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), response)) 38 | } 39 | -------------------------------------------------------------------------------- /internal/handlers/user_handler_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | "github.com/stretchr/testify/require" 14 | "gorm.io/gorm" 15 | 16 | "github.com/geshtng/go-base-backend/internal/helpers" 17 | smock "github.com/geshtng/go-base-backend/internal/mocks/services" 18 | "github.com/geshtng/go-base-backend/internal/models" 19 | ) 20 | 21 | func TestHandler_ShowProfileHandler(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | userService *smock.UserService 25 | mock func(*smock.UserService) 26 | want helpers.Response 27 | }{ 28 | { 29 | name: "Success", 30 | userService: smock.NewUserService(t), 31 | mock: func(us *smock.UserService) { 32 | expectedUser := models.User{ 33 | ID: 1, 34 | Username: "username", 35 | Password: "password", 36 | } 37 | 38 | us.On("GetByID", mock.Anything).Return(&expectedUser, nil) 39 | }, 40 | 41 | want: helpers.Response{ 42 | Data: map[string]interface{}{ 43 | "id": float64(1), 44 | "username": "username", 45 | }, 46 | Code: 200, 47 | Message: "OK", 48 | Error: false, 49 | }, 50 | }, 51 | { 52 | name: "Error copier", 53 | userService: smock.NewUserService(t), 54 | mock: func(us *smock.UserService) { 55 | us.On("GetByID", mock.Anything).Return(nil, nil) 56 | }, 57 | 58 | want: helpers.Response{ 59 | Code: 500, 60 | Message: "copy from is invalid", 61 | Error: true, 62 | }, 63 | }, 64 | { 65 | name: "Error Service.GetByID record not found", 66 | userService: smock.NewUserService(t), 67 | mock: func(us *smock.UserService) { 68 | us.On("GetByID", mock.Anything).Return(nil, gorm.ErrRecordNotFound) 69 | }, 70 | 71 | want: helpers.Response{ 72 | Code: 404, 73 | Message: "record not found", 74 | Error: true, 75 | }, 76 | }, 77 | { 78 | name: "Error Service.GetByID", 79 | userService: smock.NewUserService(t), 80 | mock: func(us *smock.UserService) { 81 | us.On("GetByID", mock.Anything).Return(nil, errors.New("something error")) 82 | }, 83 | 84 | want: helpers.Response{ 85 | Code: 500, 86 | Message: "something error", 87 | Error: true, 88 | }, 89 | }, 90 | } 91 | for _, tt := range tests { 92 | t.Run(tt.name, func(t *testing.T) { 93 | h := &Handler{ 94 | userService: tt.userService, 95 | } 96 | 97 | tt.mock((*smock.UserService)(tt.userService)) 98 | 99 | r := gin.Default() 100 | endpoint := "/profiles" 101 | 102 | r.GET(endpoint, MiddlewareMockToken, h.ShowProfileHandler) 103 | 104 | req, _ := http.NewRequest(http.MethodGet, endpoint, nil) 105 | 106 | w := httptest.NewRecorder() 107 | r.ServeHTTP(w, req) 108 | 109 | var response helpers.Response 110 | err := json.Unmarshal(w.Body.Bytes(), &response) 111 | require.NoError(t, err) 112 | 113 | assert.Equal(t, tt.want.Code, w.Code) 114 | assert.Equal(t, tt.want, response) 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /internal/helpers/hash.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "golang.org/x/crypto/bcrypt" 5 | ) 6 | 7 | func Encrypt(str string) (string, error) { 8 | hash, err := bcrypt.GenerateFromPassword([]byte(str), bcrypt.DefaultCost) 9 | return string(hash), err 10 | } 11 | 12 | func CompareEncrypt(str, hash string) bool { 13 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(str)) 14 | return err == nil 15 | } 16 | -------------------------------------------------------------------------------- /internal/helpers/jwt.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/golang-jwt/jwt/v4" 8 | 9 | "github.com/geshtng/go-base-backend/config" 10 | ) 11 | 12 | type IdTokenClaims struct { 13 | jwt.RegisteredClaims 14 | Data interface{} `json:"data"` 15 | } 16 | 17 | func GenerateJwtToken(data interface{}) (*string, error) { 18 | var idExp int64 19 | 20 | jwtEnv := config.InitJwt() 21 | 22 | expired, _ := strconv.ParseInt(jwtEnv.Expired, 10, 64) 23 | 24 | idExp = expired * 60 25 | unixTime := time.Now().Unix() 26 | tokenExp := unixTime + idExp 27 | 28 | claims := &IdTokenClaims{ 29 | RegisteredClaims: jwt.RegisteredClaims{ 30 | Issuer: jwtEnv.Issuer, 31 | ExpiresAt: &jwt.NumericDate{ 32 | Time: time.Unix(tokenExp, 0), 33 | }, 34 | IssuedAt: &jwt.NumericDate{ 35 | Time: time.Now(), 36 | }, 37 | }, 38 | Data: data, 39 | } 40 | 41 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 42 | tokenString, err := token.SignedString([]byte(jwtEnv.Secret)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &tokenString, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/helpers/response.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | type Response struct { 4 | Data interface{} `json:"data"` 5 | Code int `json:"code"` 6 | Message string `json:"message"` 7 | Error bool `json:"error"` 8 | } 9 | 10 | func BuildSuccessResponse(code int, message string, data interface{}) Response { 11 | return Response{ 12 | Code: code, 13 | Error: false, 14 | Message: message, 15 | Data: data, 16 | } 17 | } 18 | 19 | func BuildErrorResponse(code int, errorMessage string) Response { 20 | return Response{ 21 | Code: code, 22 | Error: true, 23 | Message: errorMessage, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/middlewares/authorize_jwt.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/golang-jwt/jwt/v4" 10 | 11 | "github.com/geshtng/go-base-backend/config" 12 | "github.com/geshtng/go-base-backend/internal/dtos" 13 | "github.com/geshtng/go-base-backend/internal/helpers" 14 | ) 15 | 16 | func validateToken(encodedToken string) (*jwt.Token, error) { 17 | jwtEnv := config.InitJwt() 18 | 19 | return jwt.Parse(encodedToken, func(token *jwt.Token) (interface{}, error) { 20 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 21 | return nil, errors.New("invalid token") 22 | } 23 | 24 | return []byte(jwtEnv.Secret), nil 25 | }) 26 | } 27 | 28 | func AuthorizeJWT(c *gin.Context) { 29 | authHeader := c.GetHeader("Authorization") 30 | 31 | if authHeader == "" { 32 | c.AbortWithStatusJSON( 33 | http.StatusUnauthorized, 34 | helpers.BuildErrorResponse(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)), 35 | ) 36 | return 37 | } 38 | 39 | tokenString := authHeader[7:] 40 | 41 | token, err := validateToken(tokenString) 42 | if err != nil { 43 | c.AbortWithStatusJSON( 44 | http.StatusUnauthorized, 45 | helpers.BuildErrorResponse(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)), 46 | ) 47 | return 48 | } 49 | 50 | claims, ok := token.Claims.(jwt.MapClaims) 51 | if !ok { 52 | c.AbortWithStatusJSON( 53 | http.StatusUnauthorized, 54 | helpers.BuildErrorResponse(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)), 55 | ) 56 | return 57 | } 58 | 59 | userJson, _ := json.Marshal(claims["data"]) // change from type map[string]interface{} to []byte 60 | jwtData := dtos.JwtData{} 61 | err = json.Unmarshal(userJson, &jwtData) // change from type []byte to models.User{} 62 | if err != nil { 63 | c.AbortWithStatusJSON( 64 | http.StatusUnauthorized, 65 | helpers.BuildErrorResponse(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)), 66 | ) 67 | return 68 | } 69 | 70 | c.Set("user", jwtData) 71 | } 72 | -------------------------------------------------------------------------------- /internal/mocks/repositories/mock_article_repository.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.44.1. DO NOT EDIT. 2 | 3 | package repomocks 4 | 5 | import ( 6 | models "github.com/geshtng/go-base-backend/internal/models" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // ArticleRepository is an autogenerated mock type for the ArticleRepository type 11 | type ArticleRepository struct { 12 | mock.Mock 13 | } 14 | 15 | // Delete provides a mock function with given fields: id 16 | func (_m *ArticleRepository) Delete(id int64) error { 17 | ret := _m.Called(id) 18 | 19 | if len(ret) == 0 { 20 | panic("no return value specified for Delete") 21 | } 22 | 23 | var r0 error 24 | if rf, ok := ret.Get(0).(func(int64) error); ok { 25 | r0 = rf(id) 26 | } else { 27 | r0 = ret.Error(0) 28 | } 29 | 30 | return r0 31 | } 32 | 33 | // GetAll provides a mock function with given fields: 34 | func (_m *ArticleRepository) GetAll() ([]*models.Article, error) { 35 | ret := _m.Called() 36 | 37 | if len(ret) == 0 { 38 | panic("no return value specified for GetAll") 39 | } 40 | 41 | var r0 []*models.Article 42 | var r1 error 43 | if rf, ok := ret.Get(0).(func() ([]*models.Article, error)); ok { 44 | return rf() 45 | } 46 | if rf, ok := ret.Get(0).(func() []*models.Article); ok { 47 | r0 = rf() 48 | } else { 49 | if ret.Get(0) != nil { 50 | r0 = ret.Get(0).([]*models.Article) 51 | } 52 | } 53 | 54 | if rf, ok := ret.Get(1).(func() error); ok { 55 | r1 = rf() 56 | } else { 57 | r1 = ret.Error(1) 58 | } 59 | 60 | return r0, r1 61 | } 62 | 63 | // GetByID provides a mock function with given fields: id 64 | func (_m *ArticleRepository) GetByID(id int64) (*models.Article, error) { 65 | ret := _m.Called(id) 66 | 67 | if len(ret) == 0 { 68 | panic("no return value specified for GetByID") 69 | } 70 | 71 | var r0 *models.Article 72 | var r1 error 73 | if rf, ok := ret.Get(0).(func(int64) (*models.Article, error)); ok { 74 | return rf(id) 75 | } 76 | if rf, ok := ret.Get(0).(func(int64) *models.Article); ok { 77 | r0 = rf(id) 78 | } else { 79 | if ret.Get(0) != nil { 80 | r0 = ret.Get(0).(*models.Article) 81 | } 82 | } 83 | 84 | if rf, ok := ret.Get(1).(func(int64) error); ok { 85 | r1 = rf(id) 86 | } else { 87 | r1 = ret.Error(1) 88 | } 89 | 90 | return r0, r1 91 | } 92 | 93 | // Insert provides a mock function with given fields: article 94 | func (_m *ArticleRepository) Insert(article models.Article) (*models.Article, error) { 95 | ret := _m.Called(article) 96 | 97 | if len(ret) == 0 { 98 | panic("no return value specified for Insert") 99 | } 100 | 101 | var r0 *models.Article 102 | var r1 error 103 | if rf, ok := ret.Get(0).(func(models.Article) (*models.Article, error)); ok { 104 | return rf(article) 105 | } 106 | if rf, ok := ret.Get(0).(func(models.Article) *models.Article); ok { 107 | r0 = rf(article) 108 | } else { 109 | if ret.Get(0) != nil { 110 | r0 = ret.Get(0).(*models.Article) 111 | } 112 | } 113 | 114 | if rf, ok := ret.Get(1).(func(models.Article) error); ok { 115 | r1 = rf(article) 116 | } else { 117 | r1 = ret.Error(1) 118 | } 119 | 120 | return r0, r1 121 | } 122 | 123 | // Update provides a mock function with given fields: id, article 124 | func (_m *ArticleRepository) Update(id int64, article models.Article) (*models.Article, error) { 125 | ret := _m.Called(id, article) 126 | 127 | if len(ret) == 0 { 128 | panic("no return value specified for Update") 129 | } 130 | 131 | var r0 *models.Article 132 | var r1 error 133 | if rf, ok := ret.Get(0).(func(int64, models.Article) (*models.Article, error)); ok { 134 | return rf(id, article) 135 | } 136 | if rf, ok := ret.Get(0).(func(int64, models.Article) *models.Article); ok { 137 | r0 = rf(id, article) 138 | } else { 139 | if ret.Get(0) != nil { 140 | r0 = ret.Get(0).(*models.Article) 141 | } 142 | } 143 | 144 | if rf, ok := ret.Get(1).(func(int64, models.Article) error); ok { 145 | r1 = rf(id, article) 146 | } else { 147 | r1 = ret.Error(1) 148 | } 149 | 150 | return r0, r1 151 | } 152 | 153 | // NewArticleRepository creates a new instance of ArticleRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 154 | // The first argument is typically a *testing.T value. 155 | func NewArticleRepository(t interface { 156 | mock.TestingT 157 | Cleanup(func()) 158 | }) *ArticleRepository { 159 | mock := &ArticleRepository{} 160 | mock.Mock.Test(t) 161 | 162 | t.Cleanup(func() { mock.AssertExpectations(t) }) 163 | 164 | return mock 165 | } 166 | -------------------------------------------------------------------------------- /internal/mocks/repositories/mock_user_repository.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.44.1. DO NOT EDIT. 2 | 3 | package repomocks 4 | 5 | import ( 6 | models "github.com/geshtng/go-base-backend/internal/models" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // UserRepository is an autogenerated mock type for the UserRepository type 11 | type UserRepository struct { 12 | mock.Mock 13 | } 14 | 15 | // GetByID provides a mock function with given fields: id 16 | func (_m *UserRepository) GetByID(id int64) (*models.User, error) { 17 | ret := _m.Called(id) 18 | 19 | if len(ret) == 0 { 20 | panic("no return value specified for GetByID") 21 | } 22 | 23 | var r0 *models.User 24 | var r1 error 25 | if rf, ok := ret.Get(0).(func(int64) (*models.User, error)); ok { 26 | return rf(id) 27 | } 28 | if rf, ok := ret.Get(0).(func(int64) *models.User); ok { 29 | r0 = rf(id) 30 | } else { 31 | if ret.Get(0) != nil { 32 | r0 = ret.Get(0).(*models.User) 33 | } 34 | } 35 | 36 | if rf, ok := ret.Get(1).(func(int64) error); ok { 37 | r1 = rf(id) 38 | } else { 39 | r1 = ret.Error(1) 40 | } 41 | 42 | return r0, r1 43 | } 44 | 45 | // GetByUsername provides a mock function with given fields: username 46 | func (_m *UserRepository) GetByUsername(username string) (*models.User, error) { 47 | ret := _m.Called(username) 48 | 49 | if len(ret) == 0 { 50 | panic("no return value specified for GetByUsername") 51 | } 52 | 53 | var r0 *models.User 54 | var r1 error 55 | if rf, ok := ret.Get(0).(func(string) (*models.User, error)); ok { 56 | return rf(username) 57 | } 58 | if rf, ok := ret.Get(0).(func(string) *models.User); ok { 59 | r0 = rf(username) 60 | } else { 61 | if ret.Get(0) != nil { 62 | r0 = ret.Get(0).(*models.User) 63 | } 64 | } 65 | 66 | if rf, ok := ret.Get(1).(func(string) error); ok { 67 | r1 = rf(username) 68 | } else { 69 | r1 = ret.Error(1) 70 | } 71 | 72 | return r0, r1 73 | } 74 | 75 | // Insert provides a mock function with given fields: user 76 | func (_m *UserRepository) Insert(user models.User) (*models.User, error) { 77 | ret := _m.Called(user) 78 | 79 | if len(ret) == 0 { 80 | panic("no return value specified for Insert") 81 | } 82 | 83 | var r0 *models.User 84 | var r1 error 85 | if rf, ok := ret.Get(0).(func(models.User) (*models.User, error)); ok { 86 | return rf(user) 87 | } 88 | if rf, ok := ret.Get(0).(func(models.User) *models.User); ok { 89 | r0 = rf(user) 90 | } else { 91 | if ret.Get(0) != nil { 92 | r0 = ret.Get(0).(*models.User) 93 | } 94 | } 95 | 96 | if rf, ok := ret.Get(1).(func(models.User) error); ok { 97 | r1 = rf(user) 98 | } else { 99 | r1 = ret.Error(1) 100 | } 101 | 102 | return r0, r1 103 | } 104 | 105 | // NewUserRepository creates a new instance of UserRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 106 | // The first argument is typically a *testing.T value. 107 | func NewUserRepository(t interface { 108 | mock.TestingT 109 | Cleanup(func()) 110 | }) *UserRepository { 111 | mock := &UserRepository{} 112 | mock.Mock.Test(t) 113 | 114 | t.Cleanup(func() { mock.AssertExpectations(t) }) 115 | 116 | return mock 117 | } 118 | -------------------------------------------------------------------------------- /internal/mocks/services/mock_article_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.44.1. DO NOT EDIT. 2 | 3 | package servicemocks 4 | 5 | import ( 6 | models "github.com/geshtng/go-base-backend/internal/models" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // ArticleService is an autogenerated mock type for the ArticleService type 11 | type ArticleService struct { 12 | mock.Mock 13 | } 14 | 15 | // Create provides a mock function with given fields: article 16 | func (_m *ArticleService) Create(article models.Article) (*models.Article, error) { 17 | ret := _m.Called(article) 18 | 19 | if len(ret) == 0 { 20 | panic("no return value specified for Create") 21 | } 22 | 23 | var r0 *models.Article 24 | var r1 error 25 | if rf, ok := ret.Get(0).(func(models.Article) (*models.Article, error)); ok { 26 | return rf(article) 27 | } 28 | if rf, ok := ret.Get(0).(func(models.Article) *models.Article); ok { 29 | r0 = rf(article) 30 | } else { 31 | if ret.Get(0) != nil { 32 | r0 = ret.Get(0).(*models.Article) 33 | } 34 | } 35 | 36 | if rf, ok := ret.Get(1).(func(models.Article) error); ok { 37 | r1 = rf(article) 38 | } else { 39 | r1 = ret.Error(1) 40 | } 41 | 42 | return r0, r1 43 | } 44 | 45 | // Delete provides a mock function with given fields: id 46 | func (_m *ArticleService) Delete(id int64) error { 47 | ret := _m.Called(id) 48 | 49 | if len(ret) == 0 { 50 | panic("no return value specified for Delete") 51 | } 52 | 53 | var r0 error 54 | if rf, ok := ret.Get(0).(func(int64) error); ok { 55 | r0 = rf(id) 56 | } else { 57 | r0 = ret.Error(0) 58 | } 59 | 60 | return r0 61 | } 62 | 63 | // GetAll provides a mock function with given fields: 64 | func (_m *ArticleService) GetAll() ([]*models.Article, error) { 65 | ret := _m.Called() 66 | 67 | if len(ret) == 0 { 68 | panic("no return value specified for GetAll") 69 | } 70 | 71 | var r0 []*models.Article 72 | var r1 error 73 | if rf, ok := ret.Get(0).(func() ([]*models.Article, error)); ok { 74 | return rf() 75 | } 76 | if rf, ok := ret.Get(0).(func() []*models.Article); ok { 77 | r0 = rf() 78 | } else { 79 | if ret.Get(0) != nil { 80 | r0 = ret.Get(0).([]*models.Article) 81 | } 82 | } 83 | 84 | if rf, ok := ret.Get(1).(func() error); ok { 85 | r1 = rf() 86 | } else { 87 | r1 = ret.Error(1) 88 | } 89 | 90 | return r0, r1 91 | } 92 | 93 | // GetByID provides a mock function with given fields: id 94 | func (_m *ArticleService) GetByID(id int64) (*models.Article, error) { 95 | ret := _m.Called(id) 96 | 97 | if len(ret) == 0 { 98 | panic("no return value specified for GetByID") 99 | } 100 | 101 | var r0 *models.Article 102 | var r1 error 103 | if rf, ok := ret.Get(0).(func(int64) (*models.Article, error)); ok { 104 | return rf(id) 105 | } 106 | if rf, ok := ret.Get(0).(func(int64) *models.Article); ok { 107 | r0 = rf(id) 108 | } else { 109 | if ret.Get(0) != nil { 110 | r0 = ret.Get(0).(*models.Article) 111 | } 112 | } 113 | 114 | if rf, ok := ret.Get(1).(func(int64) error); ok { 115 | r1 = rf(id) 116 | } else { 117 | r1 = ret.Error(1) 118 | } 119 | 120 | return r0, r1 121 | } 122 | 123 | // Update provides a mock function with given fields: id, article 124 | func (_m *ArticleService) Update(id int64, article models.Article) (*models.Article, error) { 125 | ret := _m.Called(id, article) 126 | 127 | if len(ret) == 0 { 128 | panic("no return value specified for Update") 129 | } 130 | 131 | var r0 *models.Article 132 | var r1 error 133 | if rf, ok := ret.Get(0).(func(int64, models.Article) (*models.Article, error)); ok { 134 | return rf(id, article) 135 | } 136 | if rf, ok := ret.Get(0).(func(int64, models.Article) *models.Article); ok { 137 | r0 = rf(id, article) 138 | } else { 139 | if ret.Get(0) != nil { 140 | r0 = ret.Get(0).(*models.Article) 141 | } 142 | } 143 | 144 | if rf, ok := ret.Get(1).(func(int64, models.Article) error); ok { 145 | r1 = rf(id, article) 146 | } else { 147 | r1 = ret.Error(1) 148 | } 149 | 150 | return r0, r1 151 | } 152 | 153 | // NewArticleService creates a new instance of ArticleService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 154 | // The first argument is typically a *testing.T value. 155 | func NewArticleService(t interface { 156 | mock.TestingT 157 | Cleanup(func()) 158 | }) *ArticleService { 159 | mock := &ArticleService{} 160 | mock.Mock.Test(t) 161 | 162 | t.Cleanup(func() { mock.AssertExpectations(t) }) 163 | 164 | return mock 165 | } 166 | -------------------------------------------------------------------------------- /internal/mocks/services/mock_auth_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.44.1. DO NOT EDIT. 2 | 3 | package servicemocks 4 | 5 | import ( 6 | models "github.com/geshtng/go-base-backend/internal/models" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // AuthService is an autogenerated mock type for the AuthService type 11 | type AuthService struct { 12 | mock.Mock 13 | } 14 | 15 | // Login provides a mock function with given fields: username, password 16 | func (_m *AuthService) Login(username string, password string) (*models.User, *string, error) { 17 | ret := _m.Called(username, password) 18 | 19 | if len(ret) == 0 { 20 | panic("no return value specified for Login") 21 | } 22 | 23 | var r0 *models.User 24 | var r1 *string 25 | var r2 error 26 | if rf, ok := ret.Get(0).(func(string, string) (*models.User, *string, error)); ok { 27 | return rf(username, password) 28 | } 29 | if rf, ok := ret.Get(0).(func(string, string) *models.User); ok { 30 | r0 = rf(username, password) 31 | } else { 32 | if ret.Get(0) != nil { 33 | r0 = ret.Get(0).(*models.User) 34 | } 35 | } 36 | 37 | if rf, ok := ret.Get(1).(func(string, string) *string); ok { 38 | r1 = rf(username, password) 39 | } else { 40 | if ret.Get(1) != nil { 41 | r1 = ret.Get(1).(*string) 42 | } 43 | } 44 | 45 | if rf, ok := ret.Get(2).(func(string, string) error); ok { 46 | r2 = rf(username, password) 47 | } else { 48 | r2 = ret.Error(2) 49 | } 50 | 51 | return r0, r1, r2 52 | } 53 | 54 | // Register provides a mock function with given fields: auth 55 | func (_m *AuthService) Register(auth models.User) (*models.User, *string, error) { 56 | ret := _m.Called(auth) 57 | 58 | if len(ret) == 0 { 59 | panic("no return value specified for Register") 60 | } 61 | 62 | var r0 *models.User 63 | var r1 *string 64 | var r2 error 65 | if rf, ok := ret.Get(0).(func(models.User) (*models.User, *string, error)); ok { 66 | return rf(auth) 67 | } 68 | if rf, ok := ret.Get(0).(func(models.User) *models.User); ok { 69 | r0 = rf(auth) 70 | } else { 71 | if ret.Get(0) != nil { 72 | r0 = ret.Get(0).(*models.User) 73 | } 74 | } 75 | 76 | if rf, ok := ret.Get(1).(func(models.User) *string); ok { 77 | r1 = rf(auth) 78 | } else { 79 | if ret.Get(1) != nil { 80 | r1 = ret.Get(1).(*string) 81 | } 82 | } 83 | 84 | if rf, ok := ret.Get(2).(func(models.User) error); ok { 85 | r2 = rf(auth) 86 | } else { 87 | r2 = ret.Error(2) 88 | } 89 | 90 | return r0, r1, r2 91 | } 92 | 93 | // NewAuthService creates a new instance of AuthService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 94 | // The first argument is typically a *testing.T value. 95 | func NewAuthService(t interface { 96 | mock.TestingT 97 | Cleanup(func()) 98 | }) *AuthService { 99 | mock := &AuthService{} 100 | mock.Mock.Test(t) 101 | 102 | t.Cleanup(func() { mock.AssertExpectations(t) }) 103 | 104 | return mock 105 | } 106 | -------------------------------------------------------------------------------- /internal/mocks/services/mock_user_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.44.1. DO NOT EDIT. 2 | 3 | package servicemocks 4 | 5 | import ( 6 | models "github.com/geshtng/go-base-backend/internal/models" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // UserService is an autogenerated mock type for the UserService type 11 | type UserService struct { 12 | mock.Mock 13 | } 14 | 15 | // GetByID provides a mock function with given fields: id 16 | func (_m *UserService) GetByID(id int64) (*models.User, error) { 17 | ret := _m.Called(id) 18 | 19 | if len(ret) == 0 { 20 | panic("no return value specified for GetByID") 21 | } 22 | 23 | var r0 *models.User 24 | var r1 error 25 | if rf, ok := ret.Get(0).(func(int64) (*models.User, error)); ok { 26 | return rf(id) 27 | } 28 | if rf, ok := ret.Get(0).(func(int64) *models.User); ok { 29 | r0 = rf(id) 30 | } else { 31 | if ret.Get(0) != nil { 32 | r0 = ret.Get(0).(*models.User) 33 | } 34 | } 35 | 36 | if rf, ok := ret.Get(1).(func(int64) error); ok { 37 | r1 = rf(id) 38 | } else { 39 | r1 = ret.Error(1) 40 | } 41 | 42 | return r0, r1 43 | } 44 | 45 | // NewUserService creates a new instance of UserService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 46 | // The first argument is typically a *testing.T value. 47 | func NewUserService(t interface { 48 | mock.TestingT 49 | Cleanup(func()) 50 | }) *UserService { 51 | mock := &UserService{} 52 | mock.Mock.Test(t) 53 | 54 | t.Cleanup(func() { mock.AssertExpectations(t) }) 55 | 56 | return mock 57 | } 58 | -------------------------------------------------------------------------------- /internal/models/article.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Article struct { 4 | ID int64 `gorm:"primary_key"` 5 | Title string 6 | Description string 7 | } 8 | -------------------------------------------------------------------------------- /internal/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID int64 `gorm:"primary_key"` 5 | Username string `gorm:"unique"` 6 | Password string 7 | } 8 | -------------------------------------------------------------------------------- /internal/repositories/article_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "gorm.io/gorm/clause" 6 | 7 | "github.com/geshtng/go-base-backend/internal/models" 8 | ) 9 | 10 | type ArticleRepository interface { 11 | GetAll() ([]*models.Article, error) 12 | GetByID(id int64) (*models.Article, error) 13 | Insert(article models.Article) (*models.Article, error) 14 | Update(id int64, article models.Article) (*models.Article, error) 15 | Delete(id int64) error 16 | } 17 | 18 | type articleRepository struct { 19 | db *gorm.DB 20 | } 21 | 22 | type ArticleRepoConfig struct { 23 | DB *gorm.DB 24 | } 25 | 26 | func NewArticleRepository(a *ArticleRepoConfig) ArticleRepository { 27 | return &articleRepository{ 28 | db: a.DB, 29 | } 30 | } 31 | 32 | func (a *articleRepository) GetAll() ([]*models.Article, error) { 33 | var articles []*models.Article 34 | 35 | result := a.db.Find(&articles) 36 | if result.Error != nil { 37 | return nil, result.Error 38 | } 39 | 40 | return articles, nil 41 | } 42 | 43 | func (a *articleRepository) GetByID(id int64) (*models.Article, error) { 44 | var article *models.Article 45 | 46 | result := a.db.First(&article, id) 47 | if result.Error != nil { 48 | return nil, result.Error 49 | } 50 | 51 | return article, nil 52 | } 53 | 54 | func (a *articleRepository) Insert(article models.Article) (*models.Article, error) { 55 | result := a.db.Create(&article) 56 | if result.Error != nil { 57 | return nil, result.Error 58 | } 59 | 60 | return &article, nil 61 | } 62 | 63 | func (a *articleRepository) Update(id int64, article models.Article) (*models.Article, error) { 64 | result := a.db. 65 | Clauses(clause.Returning{}). 66 | Where("id = ?", id). 67 | Updates(&article) 68 | 69 | if result.Error != nil { 70 | return nil, result.Error 71 | } 72 | 73 | if result.RowsAffected == 0 { 74 | return nil, gorm.ErrRecordNotFound 75 | } 76 | 77 | return &article, nil 78 | } 79 | 80 | func (a *articleRepository) Delete(id int64) error { 81 | result := a.db.Delete(&models.Article{}, id) 82 | if result.Error != nil { 83 | return result.Error 84 | } 85 | 86 | if result.RowsAffected == 0 { 87 | return gorm.ErrRecordNotFound 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /internal/repositories/article_repository_test.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/DATA-DOG/go-sqlmock" 10 | "github.com/stretchr/testify/assert" 11 | "gorm.io/driver/postgres" 12 | "gorm.io/gorm" 13 | 14 | "github.com/geshtng/go-base-backend/internal/models" 15 | ) 16 | 17 | func TestNewArticleRepository(t *testing.T) { 18 | dbConn, _, _ := sqlmock.New() 19 | dia := postgres.New(postgres.Config{ 20 | DriverName: "postgres", 21 | Conn: dbConn, 22 | }) 23 | 24 | db, _ := gorm.Open(dia) 25 | articleRepo := NewArticleRepository(&ArticleRepoConfig{DB: db}) 26 | 27 | assert.NotNil(t, articleRepo) 28 | } 29 | 30 | func Test_articleRepository_GetAll(t *testing.T) { 31 | dbConn, mock, _ := sqlmock.New() 32 | dia := postgres.New(postgres.Config{ 33 | DriverName: "postgres", 34 | Conn: dbConn, 35 | }) 36 | 37 | db, _ := gorm.Open(dia) 38 | 39 | tests := []struct { 40 | name string 41 | a *articleRepository 42 | mock func() 43 | want []*models.Article 44 | wantErr bool 45 | }{ 46 | { 47 | name: "Success", 48 | a: &articleRepository{ 49 | db: db, 50 | }, 51 | mock: func() { 52 | mock.ExpectQuery("SELECT (.+)"). 53 | WillReturnRows(sqlmock.NewRows([]string{"id", "title", "description"}). 54 | AddRow(1, "title", "description")) 55 | }, 56 | want: []*models.Article{ 57 | { 58 | ID: 1, 59 | Title: "title", 60 | Description: "description", 61 | }, 62 | }, 63 | wantErr: false, 64 | }, 65 | { 66 | name: "Error DB Find", 67 | a: &articleRepository{ 68 | db: db, 69 | }, 70 | mock: func() { 71 | mock.ExpectQuery("SELECT (.+)"). 72 | WillReturnError(errors.New("something error")) 73 | }, 74 | want: nil, 75 | wantErr: true, 76 | }, 77 | } 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | tt.mock() 81 | 82 | got, err := tt.a.GetAll() 83 | if (err != nil) != tt.wantErr { 84 | t.Errorf("articleRepository.GetAll() error = %v, wantErr %v", err, tt.wantErr) 85 | return 86 | } 87 | 88 | if !reflect.DeepEqual(got, tt.want) { 89 | t.Errorf("articleRepository.GetAll() = %v, want %v", got, tt.want) 90 | } 91 | }) 92 | } 93 | } 94 | 95 | func Test_articleRepository_GetByID(t *testing.T) { 96 | dbConn, mock, _ := sqlmock.New() 97 | dia := postgres.New(postgres.Config{ 98 | DriverName: "postgres", 99 | Conn: dbConn, 100 | }) 101 | 102 | db, _ := gorm.Open(dia) 103 | 104 | type args struct { 105 | id int64 106 | } 107 | tests := []struct { 108 | name string 109 | a *articleRepository 110 | mock func() 111 | args args 112 | want *models.Article 113 | wantErr bool 114 | }{ 115 | { 116 | name: "Success", 117 | a: &articleRepository{ 118 | db: db, 119 | }, 120 | mock: func() { 121 | mock.ExpectQuery("SELECT (.+)"). 122 | WithArgs(1). 123 | WillReturnRows(sqlmock.NewRows([]string{"id", "title", "description"}). 124 | AddRow(1, "title", "description")) 125 | }, 126 | args: args{ 127 | id: 1, 128 | }, 129 | want: &models.Article{ 130 | ID: 1, 131 | Title: "title", 132 | Description: "description", 133 | }, 134 | wantErr: false, 135 | }, 136 | { 137 | name: "Error DB First", 138 | a: &articleRepository{ 139 | db: db, 140 | }, 141 | mock: func() { 142 | mock.ExpectQuery("SELECT (.+)"). 143 | WithArgs(1). 144 | WillReturnError(errors.New("something error")) 145 | }, 146 | args: args{ 147 | id: 1, 148 | }, 149 | want: nil, 150 | wantErr: true, 151 | }, 152 | } 153 | for _, tt := range tests { 154 | t.Run(tt.name, func(t *testing.T) { 155 | tt.mock() 156 | 157 | got, err := tt.a.GetByID(tt.args.id) 158 | if (err != nil) != tt.wantErr { 159 | t.Errorf("articleRepository.GetByID() error = %v, wantErr %v", err, tt.wantErr) 160 | return 161 | } 162 | if !reflect.DeepEqual(got, tt.want) { 163 | t.Errorf("articleRepository.GetByID() = %v, want %v", got, tt.want) 164 | } 165 | }) 166 | } 167 | } 168 | 169 | func Test_articleRepository_Insert(t *testing.T) { 170 | dbConn, mock, _ := sqlmock.New() 171 | dia := postgres.New(postgres.Config{ 172 | DriverName: "postgres", 173 | Conn: dbConn, 174 | }) 175 | 176 | db, _ := gorm.Open(dia) 177 | 178 | type args struct { 179 | article models.Article 180 | } 181 | tests := []struct { 182 | name string 183 | a *articleRepository 184 | mock func() 185 | args args 186 | want *models.Article 187 | wantErr bool 188 | }{ 189 | { 190 | name: "Success", 191 | a: &articleRepository{ 192 | db: db, 193 | }, 194 | mock: func() { 195 | mock.ExpectBegin() 196 | mock.ExpectQuery(`INSERT INTO "articles" (.+) VALUES (.+)`). 197 | WithArgs("title", "description"). 198 | WillReturnRows(sqlmock.NewRows([]string{"id", "title", "description"}). 199 | AddRow(1, "title", "description")) 200 | mock.ExpectCommit() 201 | }, 202 | args: args{ 203 | article: models.Article{ 204 | Title: "title", 205 | Description: "description", 206 | }, 207 | }, 208 | want: &models.Article{ 209 | ID: 1, 210 | Title: "title", 211 | Description: "description", 212 | }, 213 | wantErr: false, 214 | }, 215 | { 216 | name: "Error DB Create", 217 | a: &articleRepository{ 218 | db: db, 219 | }, 220 | mock: func() { 221 | mock.ExpectBegin() 222 | mock.ExpectQuery(`INSERT INTO "articles" (.+) VALUES (.+)`). 223 | WithArgs("title", "description"). 224 | WillReturnError(errors.New("something error")) 225 | mock.ExpectCommit() 226 | }, 227 | args: args{ 228 | article: models.Article{ 229 | Title: "title", 230 | Description: "description", 231 | }, 232 | }, 233 | want: nil, 234 | wantErr: true, 235 | }, 236 | } 237 | for _, tt := range tests { 238 | t.Run(tt.name, func(t *testing.T) { 239 | tt.mock() 240 | 241 | got, err := tt.a.Insert(tt.args.article) 242 | if (err != nil) != tt.wantErr { 243 | t.Errorf("articleRepository.Insert() error = %v, wantErr %v", err, tt.wantErr) 244 | return 245 | } 246 | if !reflect.DeepEqual(got, tt.want) { 247 | t.Errorf("articleRepository.Insert() = %v, want %v", got, tt.want) 248 | } 249 | }) 250 | } 251 | } 252 | 253 | func Test_articleRepository_Update(t *testing.T) { 254 | dbConn, mock, _ := sqlmock.New() 255 | dia := postgres.New(postgres.Config{ 256 | DriverName: "postgres", 257 | Conn: dbConn, 258 | }) 259 | 260 | db, _ := gorm.Open(dia) 261 | 262 | type args struct { 263 | id int64 264 | article models.Article 265 | } 266 | tests := []struct { 267 | name string 268 | a *articleRepository 269 | mock func() 270 | args args 271 | want *models.Article 272 | wantErr bool 273 | }{ 274 | { 275 | name: "Success", 276 | a: &articleRepository{ 277 | db: db, 278 | }, 279 | mock: func() { 280 | mock.ExpectBegin() 281 | mock.ExpectQuery(`UPDATE "articles" SET (.+) WHERE id = (.+)`). 282 | WithArgs("title update", "description update", 1). 283 | WillReturnRows(sqlmock.NewRows([]string{"id", "title", "description"}). 284 | AddRow(1, "title update", "description update")) 285 | mock.ExpectCommit() 286 | }, 287 | args: args{ 288 | id: 1, 289 | article: models.Article{ 290 | Title: "title update", 291 | Description: "description update", 292 | }, 293 | }, 294 | want: &models.Article{ 295 | ID: 1, 296 | Title: "title update", 297 | Description: "description update", 298 | }, 299 | wantErr: false, 300 | }, 301 | { 302 | name: "Error DB Update", 303 | a: &articleRepository{ 304 | db: db, 305 | }, 306 | mock: func() { 307 | mock.ExpectBegin() 308 | mock.ExpectQuery(`UPDATE "articles" SET (.+) WHERE id = (.+)`). 309 | WithArgs("title update", "description update", 1). 310 | WillReturnError(errors.New("something error")) 311 | mock.ExpectCommit() 312 | }, 313 | args: args{ 314 | id: 1, 315 | article: models.Article{ 316 | Title: "title update", 317 | Description: "description update", 318 | }, 319 | }, 320 | want: nil, 321 | wantErr: true, 322 | }, 323 | { 324 | name: "Rows Affected Zero", 325 | a: &articleRepository{ 326 | db: db, 327 | }, 328 | mock: func() { 329 | mock.ExpectBegin() 330 | mock.ExpectExec(`UPDATE "articles" SET (.+) WHERE id = (.+)`). 331 | WithArgs("title update", "description update", 1). 332 | WillReturnResult(sqlmock.NewResult(0, 0)) 333 | mock.ExpectCommit() 334 | }, 335 | args: args{ 336 | id: 1, 337 | article: models.Article{ 338 | Title: "title update", 339 | Description: "description update", 340 | }, 341 | }, 342 | want: nil, 343 | wantErr: true, 344 | }, 345 | } 346 | for _, tt := range tests { 347 | t.Run(tt.name, func(t *testing.T) { 348 | tt.mock() 349 | 350 | got, err := tt.a.Update(tt.args.id, tt.args.article) 351 | if (err != nil) != tt.wantErr { 352 | t.Errorf("articleRepository.Update() error = %v, wantErr %v", err, tt.wantErr) 353 | return 354 | } 355 | if !reflect.DeepEqual(got, tt.want) { 356 | t.Errorf("articleRepository.Update() = %v, want %v", got, tt.want) 357 | } 358 | }) 359 | } 360 | } 361 | 362 | func Test_articleRepository_Delete(t *testing.T) { 363 | dbConn, mock, _ := sqlmock.New() 364 | dia := postgres.New(postgres.Config{ 365 | DriverName: "postgres", 366 | Conn: dbConn, 367 | }) 368 | 369 | db, _ := gorm.Open(dia) 370 | 371 | type args struct { 372 | id int64 373 | } 374 | tests := []struct { 375 | name string 376 | a *articleRepository 377 | mock func() 378 | args args 379 | wantErr bool 380 | }{ 381 | { 382 | name: "Success", 383 | a: &articleRepository{ 384 | db: db, 385 | }, 386 | mock: func() { 387 | mock.ExpectBegin() 388 | mock.ExpectExec(regexp.QuoteMeta("DELETE")). 389 | WithArgs(1). 390 | WillReturnResult(sqlmock.NewResult(1, 1)) 391 | mock.ExpectCommit() 392 | }, 393 | args: args{ 394 | id: 1, 395 | }, 396 | wantErr: false, 397 | }, 398 | { 399 | name: "Error DB Delete", 400 | a: &articleRepository{ 401 | db: db, 402 | }, 403 | mock: func() { 404 | mock.ExpectBegin() 405 | mock.ExpectExec(regexp.QuoteMeta("DELETE")). 406 | WithArgs(1). 407 | WillReturnError(errors.New("something error")) 408 | mock.ExpectCommit() 409 | }, 410 | args: args{ 411 | id: 1, 412 | }, 413 | wantErr: true, 414 | }, 415 | { 416 | name: "Rows Affected Zero", 417 | a: &articleRepository{ 418 | db: db, 419 | }, 420 | mock: func() { 421 | mock.ExpectBegin() 422 | mock.ExpectExec(regexp.QuoteMeta("DELETE")). 423 | WithArgs(1). 424 | WillReturnResult(sqlmock.NewResult(0, 0)) 425 | mock.ExpectCommit() 426 | }, 427 | args: args{ 428 | id: 1, 429 | }, 430 | wantErr: true, 431 | }, 432 | } 433 | for _, tt := range tests { 434 | t.Run(tt.name, func(t *testing.T) { 435 | tt.mock() 436 | 437 | if err := tt.a.Delete(tt.args.id); (err != nil) != tt.wantErr { 438 | t.Errorf("articleRepository.Delete() error = %v, wantErr %v", err, tt.wantErr) 439 | } 440 | }) 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /internal/repositories/user_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | 6 | "github.com/geshtng/go-base-backend/internal/models" 7 | ) 8 | 9 | type UserRepository interface { 10 | GetByID(id int64) (*models.User, error) 11 | GetByUsername(username string) (*models.User, error) 12 | Insert(user models.User) (*models.User, error) 13 | } 14 | 15 | type userRepository struct { 16 | db *gorm.DB 17 | } 18 | 19 | type UserRepoConfig struct { 20 | DB *gorm.DB 21 | } 22 | 23 | func NewUserRepository(u *UserRepoConfig) UserRepository { 24 | return &userRepository{ 25 | db: u.DB, 26 | } 27 | } 28 | 29 | func (u *userRepository) GetByID(id int64) (*models.User, error) { 30 | var user *models.User 31 | 32 | result := u.db. 33 | Where("id = ?", id). 34 | First(&user) 35 | 36 | if result.Error != nil { 37 | return nil, result.Error 38 | } 39 | 40 | return user, nil 41 | } 42 | 43 | func (u *userRepository) GetByUsername(username string) (*models.User, error) { 44 | var user *models.User 45 | 46 | result := u.db. 47 | Where("username = ?", username). 48 | First(&user) 49 | 50 | if result.Error != nil { 51 | return nil, result.Error 52 | } 53 | 54 | return user, nil 55 | } 56 | 57 | func (u *userRepository) Insert(user models.User) (*models.User, error) { 58 | result := u.db.Create(&user) 59 | if result.Error != nil { 60 | return nil, result.Error 61 | } 62 | 63 | return &user, nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/repositories/user_repository_test.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/DATA-DOG/go-sqlmock" 9 | "github.com/stretchr/testify/assert" 10 | "gorm.io/driver/postgres" 11 | "gorm.io/gorm" 12 | 13 | "github.com/geshtng/go-base-backend/internal/models" 14 | ) 15 | 16 | func TestNewUserRepository(t *testing.T) { 17 | dbConn, _, _ := sqlmock.New() 18 | dia := postgres.New(postgres.Config{ 19 | DriverName: "postgres", 20 | Conn: dbConn, 21 | }) 22 | 23 | db, _ := gorm.Open(dia) 24 | userRepo := NewUserRepository(&UserRepoConfig{DB: db}) 25 | 26 | assert.NotNil(t, userRepo) 27 | } 28 | 29 | func Test_userRepository_GetByID(t *testing.T) { 30 | dbConn, mock, _ := sqlmock.New() 31 | dia := postgres.New(postgres.Config{ 32 | DriverName: "postgres", 33 | Conn: dbConn, 34 | }) 35 | 36 | db, _ := gorm.Open(dia) 37 | 38 | type args struct { 39 | id int64 40 | } 41 | tests := []struct { 42 | name string 43 | u *userRepository 44 | mock func() 45 | args args 46 | want *models.User 47 | wantErr bool 48 | }{ 49 | { 50 | name: "Success", 51 | u: &userRepository{ 52 | db: db, 53 | }, 54 | mock: func() { 55 | mock.ExpectQuery("SELECT (.+)"). 56 | WithArgs(1). 57 | WillReturnRows(sqlmock.NewRows([]string{"id", "username"}). 58 | AddRow(1, "username")) 59 | }, 60 | args: args{ 61 | id: 1, 62 | }, 63 | want: &models.User{ 64 | ID: 1, 65 | Username: "username", 66 | }, 67 | wantErr: false, 68 | }, 69 | { 70 | name: "Error DB First", 71 | u: &userRepository{ 72 | db: db, 73 | }, 74 | mock: func() { 75 | mock.ExpectQuery("SELECT (.+)"). 76 | WithArgs(1). 77 | WillReturnError(errors.New("something error")) 78 | }, 79 | args: args{ 80 | id: 1, 81 | }, 82 | want: nil, 83 | wantErr: true, 84 | }, 85 | } 86 | for _, tt := range tests { 87 | t.Run(tt.name, func(t *testing.T) { 88 | tt.mock() 89 | 90 | got, err := tt.u.GetByID(tt.args.id) 91 | if (err != nil) != tt.wantErr { 92 | t.Errorf("userRepository.GetByID() error = %v, wantErr %v", err, tt.wantErr) 93 | return 94 | } 95 | if !reflect.DeepEqual(got, tt.want) { 96 | t.Errorf("userRepository.GetByID() = %v, want %v", got, tt.want) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func Test_userRepository_GetByUsername(t *testing.T) { 103 | dbConn, mock, _ := sqlmock.New() 104 | dia := postgres.New(postgres.Config{ 105 | DriverName: "postgres", 106 | Conn: dbConn, 107 | }) 108 | 109 | db, _ := gorm.Open(dia) 110 | 111 | type args struct { 112 | username string 113 | } 114 | tests := []struct { 115 | name string 116 | u *userRepository 117 | mock func() 118 | args args 119 | want *models.User 120 | wantErr bool 121 | }{ 122 | { 123 | name: "Success", 124 | u: &userRepository{ 125 | db: db, 126 | }, 127 | mock: func() { 128 | mock.ExpectQuery("SELECT (.+)"). 129 | WithArgs("username"). 130 | WillReturnRows(sqlmock.NewRows([]string{"id", "username"}). 131 | AddRow(1, "username")) 132 | }, 133 | args: args{ 134 | username: "username", 135 | }, 136 | want: &models.User{ 137 | ID: 1, 138 | Username: "username", 139 | }, 140 | wantErr: false, 141 | }, 142 | { 143 | name: "Error DB First", 144 | u: &userRepository{ 145 | db: db, 146 | }, 147 | mock: func() { 148 | mock.ExpectQuery("SELECT (.+)"). 149 | WithArgs("username"). 150 | WillReturnError(errors.New("something error")) 151 | }, 152 | args: args{ 153 | username: "username", 154 | }, 155 | want: nil, 156 | wantErr: true, 157 | }, 158 | } 159 | for _, tt := range tests { 160 | t.Run(tt.name, func(t *testing.T) { 161 | tt.mock() 162 | 163 | got, err := tt.u.GetByUsername(tt.args.username) 164 | if (err != nil) != tt.wantErr { 165 | t.Errorf("userRepository.GetByUsername() error = %v, wantErr %v", err, tt.wantErr) 166 | return 167 | } 168 | if !reflect.DeepEqual(got, tt.want) { 169 | t.Errorf("userRepository.GetByUsername() = %v, want %v", got, tt.want) 170 | } 171 | }) 172 | } 173 | } 174 | 175 | func Test_userRepository_Insert(t *testing.T) { 176 | dbConn, mock, _ := sqlmock.New() 177 | dia := postgres.New(postgres.Config{ 178 | DriverName: "postgres", 179 | Conn: dbConn, 180 | }) 181 | 182 | db, _ := gorm.Open(dia) 183 | 184 | type args struct { 185 | user models.User 186 | } 187 | tests := []struct { 188 | name string 189 | u *userRepository 190 | mock func() 191 | args args 192 | want *models.User 193 | wantErr bool 194 | }{ 195 | { 196 | name: "Success", 197 | u: &userRepository{ 198 | db: db, 199 | }, 200 | mock: func() { 201 | mock.ExpectBegin() 202 | mock.ExpectQuery(`INSERT INTO "users" (.+) VALUES (.+)`). 203 | WithArgs("username", "password"). 204 | WillReturnRows(sqlmock.NewRows([]string{"id", "username", "password"}). 205 | AddRow(1, "username", "password")) 206 | mock.ExpectCommit() 207 | }, 208 | args: args{ 209 | user: models.User{ 210 | Username: "username", 211 | Password: "password", 212 | }, 213 | }, 214 | want: &models.User{ 215 | ID: 1, 216 | Username: "username", 217 | Password: "password", 218 | }, 219 | wantErr: false, 220 | }, 221 | { 222 | name: "Error DB Create", 223 | u: &userRepository{ 224 | db: db, 225 | }, 226 | mock: func() { 227 | mock.ExpectBegin() 228 | mock.ExpectQuery(`INSERT INTO "users" (.+) VALUES (.+)`). 229 | WithArgs("username", "password"). 230 | WillReturnError(errors.New("something error")) 231 | mock.ExpectCommit() 232 | }, 233 | args: args{ 234 | user: models.User{ 235 | Username: "username", 236 | Password: "password", 237 | }, 238 | }, 239 | want: nil, 240 | wantErr: true, 241 | }, 242 | } 243 | for _, tt := range tests { 244 | t.Run(tt.name, func(t *testing.T) { 245 | tt.mock() 246 | 247 | got, err := tt.u.Insert(tt.args.user) 248 | if (err != nil) != tt.wantErr { 249 | t.Errorf("userRepository.Insert() error = %v, wantErr %v", err, tt.wantErr) 250 | return 251 | } 252 | if !reflect.DeepEqual(got, tt.want) { 253 | t.Errorf("userRepository.Insert() = %v, want %v", got, tt.want) 254 | } 255 | }) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /internal/routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/geshtng/go-base-backend/internal/handlers" 7 | "github.com/geshtng/go-base-backend/internal/middlewares" 8 | ) 9 | 10 | func InitAllRoutes(r *gin.Engine, h *handlers.Handler) { 11 | // Articles 12 | r.GET("/articles", h.GetAllArticlesHandler) 13 | r.GET("/articles/:id", h.GetArticleByIDHandler) 14 | r.POST("/articles", h.CreateArticleHandler) 15 | r.PUT("/articles/:id", h.UpdateArticleHandler) 16 | r.DELETE("/articles/:id", h.DeleteArticleHandler) 17 | 18 | // Auth 19 | r.POST("/login", h.LoginHandler) 20 | r.POST("/register", h.RegisterHandler) 21 | 22 | // API endpoints below will use authorization 23 | r.Use(middlewares.AuthorizeJWT) 24 | 25 | r.GET("/profiles", h.ShowProfileHandler) 26 | } 27 | -------------------------------------------------------------------------------- /internal/services/article_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/geshtng/go-base-backend/internal/models" 5 | r "github.com/geshtng/go-base-backend/internal/repositories" 6 | ) 7 | 8 | type ArticleService interface { 9 | GetAll() ([]*models.Article, error) 10 | GetByID(id int64) (*models.Article, error) 11 | Create(article models.Article) (*models.Article, error) 12 | Update(id int64, article models.Article) (*models.Article, error) 13 | Delete(id int64) error 14 | } 15 | 16 | type articleService struct { 17 | articleRepository r.ArticleRepository 18 | } 19 | 20 | type ArticleServiceConfig struct { 21 | ArticleRepository r.ArticleRepository 22 | } 23 | 24 | func NewArticleService(a *ArticleServiceConfig) ArticleService { 25 | return &articleService{ 26 | articleRepository: a.ArticleRepository, 27 | } 28 | } 29 | 30 | func (a *articleService) GetAll() ([]*models.Article, error) { 31 | result, err := a.articleRepository.GetAll() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return result, nil 37 | } 38 | 39 | func (a *articleService) GetByID(id int64) (*models.Article, error) { 40 | result, err := a.articleRepository.GetByID(id) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return result, nil 46 | } 47 | 48 | func (a *articleService) Create(article models.Article) (*models.Article, error) { 49 | result, err := a.articleRepository.Insert(article) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return result, nil 55 | } 56 | 57 | func (a *articleService) Update(id int64, article models.Article) (*models.Article, error) { 58 | result, err := a.articleRepository.Update(id, article) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return result, nil 64 | } 65 | 66 | func (a *articleService) Delete(id int64) error { 67 | err := a.articleRepository.Delete(id) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/services/article_service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | 11 | rmock "github.com/geshtng/go-base-backend/internal/mocks/repositories" 12 | "github.com/geshtng/go-base-backend/internal/models" 13 | ) 14 | 15 | func TestNewArticleService(t *testing.T) { 16 | articleRepo := rmock.NewArticleRepository(t) 17 | articleService := NewArticleService(&ArticleServiceConfig{ 18 | ArticleRepository: articleRepo, 19 | }) 20 | 21 | assert.NotNil(t, articleService) 22 | } 23 | 24 | func Test_articleService_GetAll(t *testing.T) { 25 | t.Run("Success", func(t *testing.T) { 26 | tt := struct { 27 | want []*models.Article 28 | wantErr bool 29 | }{ 30 | want: []*models.Article{ 31 | { 32 | ID: 1, 33 | Title: "title", 34 | Description: "description", 35 | }, 36 | }, 37 | wantErr: false, 38 | } 39 | 40 | articleRepo := rmock.NewArticleRepository(t) 41 | s := &articleService{ 42 | articleRepository: articleRepo, 43 | } 44 | 45 | result := []*models.Article{ 46 | { 47 | ID: 1, 48 | Title: "title", 49 | Description: "description", 50 | }, 51 | } 52 | 53 | articleRepo.On("GetAll").Return(result, nil) 54 | 55 | got, err := s.GetAll() 56 | if (err != nil) != tt.wantErr { 57 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 58 | return 59 | } 60 | 61 | if !reflect.DeepEqual(got, tt.want) { 62 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 63 | } 64 | }) 65 | 66 | t.Run("Error Repository.GetAll", func(t *testing.T) { 67 | tt := struct { 68 | want []*models.Article 69 | wantErr bool 70 | }{ 71 | want: nil, 72 | wantErr: true, 73 | } 74 | 75 | articleRepo := rmock.NewArticleRepository(t) 76 | s := &articleService{ 77 | articleRepository: articleRepo, 78 | } 79 | 80 | articleRepo.On("GetAll").Return(nil, errors.New("something error")) 81 | 82 | got, err := s.GetAll() 83 | if (err != nil) != tt.wantErr { 84 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 85 | return 86 | } 87 | 88 | if !reflect.DeepEqual(got, tt.want) { 89 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 90 | } 91 | }) 92 | } 93 | 94 | func Test_articleService_GetByID(t *testing.T) { 95 | t.Run("Success", func(t *testing.T) { 96 | type args struct { 97 | id int64 98 | } 99 | tt := struct { 100 | args args 101 | want *models.Article 102 | wantErr bool 103 | }{ 104 | args: args{ 105 | id: 1, 106 | }, 107 | want: &models.Article{ 108 | ID: 1, 109 | Title: "title", 110 | Description: "description", 111 | }, 112 | wantErr: false, 113 | } 114 | 115 | articleRepo := rmock.NewArticleRepository(t) 116 | s := &articleService{ 117 | articleRepository: articleRepo, 118 | } 119 | 120 | result := &models.Article{ 121 | ID: 1, 122 | Title: "title", 123 | Description: "description", 124 | } 125 | 126 | articleRepo.On("GetByID", mock.Anything).Return(result, nil) 127 | 128 | got, err := s.GetByID(tt.args.id) 129 | if (err != nil) != tt.wantErr { 130 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 131 | return 132 | } 133 | 134 | if !reflect.DeepEqual(got, tt.want) { 135 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 136 | } 137 | }) 138 | 139 | t.Run("Error Repository.GetByID", func(t *testing.T) { 140 | type args struct { 141 | id int64 142 | } 143 | tt := struct { 144 | args args 145 | want *models.Article 146 | wantErr bool 147 | }{ 148 | args: args{ 149 | id: 1, 150 | }, 151 | want: nil, 152 | wantErr: true, 153 | } 154 | 155 | articleRepo := rmock.NewArticleRepository(t) 156 | s := &articleService{ 157 | articleRepository: articleRepo, 158 | } 159 | 160 | articleRepo.On("GetByID", mock.Anything).Return(nil, errors.New("something error")) 161 | 162 | got, err := s.GetByID(tt.args.id) 163 | if (err != nil) != tt.wantErr { 164 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 165 | return 166 | } 167 | 168 | if !reflect.DeepEqual(got, tt.want) { 169 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 170 | } 171 | }) 172 | } 173 | 174 | func Test_articleService_Create(t *testing.T) { 175 | t.Run("Success", func(t *testing.T) { 176 | type args struct { 177 | menu models.Article 178 | } 179 | tt := struct { 180 | args args 181 | want *models.Article 182 | wantErr bool 183 | }{ 184 | args: args{ 185 | menu: models.Article{ 186 | Title: "title", 187 | Description: "description", 188 | }, 189 | }, 190 | want: &models.Article{ 191 | ID: 1, 192 | Title: "title", 193 | Description: "description", 194 | }, 195 | wantErr: false, 196 | } 197 | 198 | articleRepo := rmock.NewArticleRepository(t) 199 | s := &articleService{ 200 | articleRepository: articleRepo, 201 | } 202 | 203 | result := &models.Article{ 204 | ID: 1, 205 | Title: "title", 206 | Description: "description", 207 | } 208 | 209 | articleRepo.On("Insert", mock.Anything).Return(result, nil) 210 | 211 | got, err := s.Create(tt.args.menu) 212 | if (err != nil) != tt.wantErr { 213 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 214 | return 215 | } 216 | 217 | if !reflect.DeepEqual(got, tt.want) { 218 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 219 | } 220 | }) 221 | 222 | t.Run("Error Repository.Insert", func(t *testing.T) { 223 | type args struct { 224 | menu models.Article 225 | } 226 | tt := struct { 227 | args args 228 | want *models.Article 229 | wantErr bool 230 | }{ 231 | args: args{ 232 | menu: models.Article{ 233 | Title: "title", 234 | Description: "description", 235 | }, 236 | }, 237 | want: nil, 238 | wantErr: true, 239 | } 240 | 241 | articleRepo := rmock.NewArticleRepository(t) 242 | s := &articleService{ 243 | articleRepository: articleRepo, 244 | } 245 | 246 | articleRepo.On("Insert", mock.Anything).Return(nil, errors.New("something error")) 247 | 248 | got, err := s.Create(tt.args.menu) 249 | if (err != nil) != tt.wantErr { 250 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 251 | return 252 | } 253 | 254 | if !reflect.DeepEqual(got, tt.want) { 255 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 256 | } 257 | }) 258 | } 259 | 260 | func Test_articleService_Update(t *testing.T) { 261 | t.Run("Success", func(t *testing.T) { 262 | type args struct { 263 | id int64 264 | menu models.Article 265 | } 266 | tt := struct { 267 | args args 268 | want *models.Article 269 | wantErr bool 270 | }{ 271 | args: args{ 272 | id: 1, 273 | menu: models.Article{ 274 | Title: "title update", 275 | Description: "description update", 276 | }, 277 | }, 278 | want: &models.Article{ 279 | ID: 1, 280 | Title: "title update", 281 | Description: "description update", 282 | }, 283 | wantErr: false, 284 | } 285 | 286 | articleRepo := rmock.NewArticleRepository(t) 287 | s := &articleService{ 288 | articleRepository: articleRepo, 289 | } 290 | 291 | result := &models.Article{ 292 | ID: 1, 293 | Title: "title update", 294 | Description: "description update", 295 | } 296 | 297 | articleRepo.On("Update", mock.Anything, mock.Anything).Return(result, nil) 298 | 299 | got, err := s.Update(tt.args.id, tt.args.menu) 300 | if (err != nil) != tt.wantErr { 301 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 302 | return 303 | } 304 | 305 | if !reflect.DeepEqual(got, tt.want) { 306 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 307 | } 308 | }) 309 | 310 | t.Run("Error Repository.Update", func(t *testing.T) { 311 | type args struct { 312 | id int64 313 | menu models.Article 314 | } 315 | tt := struct { 316 | args args 317 | want *models.Article 318 | wantErr bool 319 | }{ 320 | args: args{ 321 | id: 1, 322 | menu: models.Article{ 323 | Title: "title update", 324 | Description: "description update", 325 | }, 326 | }, 327 | want: nil, 328 | wantErr: true, 329 | } 330 | 331 | articleRepo := rmock.NewArticleRepository(t) 332 | s := &articleService{ 333 | articleRepository: articleRepo, 334 | } 335 | 336 | articleRepo.On("Update", mock.Anything, mock.Anything).Return(nil, errors.New("something error")) 337 | 338 | got, err := s.Update(tt.args.id, tt.args.menu) 339 | if (err != nil) != tt.wantErr { 340 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 341 | return 342 | } 343 | 344 | if !reflect.DeepEqual(got, tt.want) { 345 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 346 | } 347 | }) 348 | } 349 | 350 | func Test_articleService_Delete(t *testing.T) { 351 | t.Run("Success", func(t *testing.T) { 352 | type args struct { 353 | id int64 354 | } 355 | tt := struct { 356 | args args 357 | wantErr bool 358 | }{ 359 | args: args{ 360 | id: 1, 361 | }, 362 | wantErr: false, 363 | } 364 | 365 | articleRepo := rmock.NewArticleRepository(t) 366 | s := &articleService{ 367 | articleRepository: articleRepo, 368 | } 369 | 370 | articleRepo.On("Delete", mock.Anything).Return(nil) 371 | 372 | if err := s.Delete(tt.args.id); (err != nil) != tt.wantErr { 373 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 374 | return 375 | } 376 | }) 377 | 378 | t.Run("Error Repository.Delete", func(t *testing.T) { 379 | type args struct { 380 | id int64 381 | } 382 | tt := struct { 383 | args args 384 | wantErr bool 385 | }{ 386 | args: args{ 387 | id: 1, 388 | }, 389 | wantErr: true, 390 | } 391 | 392 | articleRepo := rmock.NewArticleRepository(t) 393 | s := &articleService{ 394 | articleRepository: articleRepo, 395 | } 396 | 397 | articleRepo.On("Delete", mock.Anything).Return(errors.New("something error")) 398 | 399 | if err := s.Delete(tt.args.id); (err != nil) != tt.wantErr { 400 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 401 | return 402 | } 403 | }) 404 | } 405 | -------------------------------------------------------------------------------- /internal/services/auth_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | 6 | "github.com/geshtng/go-base-backend/internal/helpers" 7 | "github.com/geshtng/go-base-backend/internal/models" 8 | r "github.com/geshtng/go-base-backend/internal/repositories" 9 | ) 10 | 11 | type AuthService interface { 12 | Register(auth models.User) (*models.User, *string, error) 13 | Login(username, password string) (*models.User, *string, error) 14 | } 15 | 16 | type authService struct { 17 | userRepository r.UserRepository 18 | } 19 | 20 | type AuthServiceConfig struct { 21 | UserRepository r.UserRepository 22 | } 23 | 24 | func NewAuthService(a *AuthServiceConfig) AuthService { 25 | return &authService{ 26 | userRepository: a.UserRepository, 27 | } 28 | } 29 | 30 | func (a *authService) Register(auth models.User) (*models.User, *string, error) { 31 | encryptedPassword, err := helpers.Encrypt(auth.Password) 32 | if err != nil { 33 | return nil, nil, err 34 | } 35 | 36 | auth.Password = encryptedPassword 37 | 38 | newAuth, err := a.userRepository.Insert(auth) 39 | if err != nil { 40 | return nil, nil, err 41 | } 42 | 43 | dataToJwt := models.User{ 44 | ID: newAuth.ID, 45 | Username: newAuth.Username, 46 | } 47 | 48 | token, err := helpers.GenerateJwtToken(dataToJwt) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | 53 | return newAuth, token, nil 54 | } 55 | 56 | func (a *authService) Login(username, password string) (*models.User, *string, error) { 57 | auth, err := a.userRepository.GetByUsername(username) 58 | if err != nil { 59 | return nil, nil, err 60 | } 61 | 62 | if !helpers.CompareEncrypt(password, auth.Password) { 63 | return nil, nil, gorm.ErrRecordNotFound 64 | } 65 | 66 | dataToJwt := models.User{ 67 | ID: auth.ID, 68 | Username: auth.Username, 69 | } 70 | 71 | token, err := helpers.GenerateJwtToken(dataToJwt) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | 76 | return auth, token, nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/services/auth_service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path" 7 | "reflect" 8 | "runtime" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | 14 | "github.com/geshtng/go-base-backend/internal/helpers" 15 | rmock "github.com/geshtng/go-base-backend/internal/mocks/repositories" 16 | "github.com/geshtng/go-base-backend/internal/models" 17 | ) 18 | 19 | func TestNewAuthService(t *testing.T) { 20 | userRepo := rmock.NewUserRepository(t) 21 | authService := NewAuthService(&AuthServiceConfig{ 22 | UserRepository: userRepo, 23 | }) 24 | 25 | assert.NotNil(t, authService) 26 | } 27 | 28 | func Test_authService_Register(t *testing.T) { 29 | t.Run("Success", func(t *testing.T) { 30 | _, filename, _, _ := runtime.Caller(0) 31 | dir := path.Join(path.Dir(filename), "../../") 32 | err := os.Chdir(dir) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | encryptedPassword, _ := helpers.Encrypt("password") 38 | 39 | type args struct { 40 | auth models.User 41 | } 42 | test := struct { 43 | args args 44 | want *models.User 45 | wantErr bool 46 | }{ 47 | args: args{ 48 | auth: models.User{ 49 | Username: "username", 50 | Password: "password", 51 | }, 52 | }, 53 | want: &models.User{ 54 | Username: "username", 55 | Password: encryptedPassword, 56 | }, 57 | wantErr: false, 58 | } 59 | 60 | userRepo := rmock.NewUserRepository(t) 61 | s := &authService{ 62 | userRepository: userRepo, 63 | } 64 | 65 | expectedUser := models.User{ 66 | Username: "username", 67 | Password: encryptedPassword, 68 | } 69 | 70 | userRepo.On("Insert", mock.Anything).Return(&expectedUser, nil) 71 | 72 | got, _, err := s.Register(test.args.auth) 73 | if (err != nil) != test.wantErr { 74 | t.Errorf("authService.Register() error = %v, wantErr %v", err, test.wantErr) 75 | return 76 | } 77 | 78 | if !reflect.DeepEqual(got, test.want) { 79 | t.Errorf("authService.Register() = %v, want %v", got, test.want) 80 | } 81 | }) 82 | 83 | t.Run("Error Repository.Insert", func(t *testing.T) { 84 | _, filename, _, _ := runtime.Caller(0) 85 | dir := path.Join(path.Dir(filename), "../../") 86 | err := os.Chdir(dir) 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | type args struct { 92 | auth models.User 93 | } 94 | test := struct { 95 | args args 96 | want *models.User 97 | wantErr bool 98 | }{ 99 | args: args{ 100 | auth: models.User{ 101 | Username: "username", 102 | Password: "password", 103 | }, 104 | }, 105 | want: nil, 106 | wantErr: true, 107 | } 108 | 109 | userRepo := rmock.NewUserRepository(t) 110 | s := &authService{ 111 | userRepository: userRepo, 112 | } 113 | 114 | userRepo.On("Insert", mock.Anything).Return(nil, errors.New("something error")) 115 | 116 | got, _, err := s.Register(test.args.auth) 117 | if (err != nil) != test.wantErr { 118 | t.Errorf("authService.Register() error = %v, wantErr %v", err, test.wantErr) 119 | return 120 | } 121 | 122 | if !reflect.DeepEqual(got, test.want) { 123 | t.Errorf("authService.Register() = %v, want %v", got, test.want) 124 | } 125 | }) 126 | } 127 | 128 | func Test_authService_Login(t *testing.T) { 129 | t.Run("Success", func(t *testing.T) { 130 | _, filename, _, _ := runtime.Caller(0) 131 | dir := path.Join(path.Dir(filename), "../../") 132 | err := os.Chdir(dir) 133 | if err != nil { 134 | panic(err) 135 | } 136 | 137 | encryptedPassword, _ := helpers.Encrypt("password") 138 | 139 | type args struct { 140 | username string 141 | password string 142 | } 143 | test := struct { 144 | args args 145 | want *models.User 146 | wantErr bool 147 | }{ 148 | args: args{ 149 | username: "username", 150 | password: "password", 151 | }, 152 | want: &models.User{ 153 | Username: "username", 154 | Password: encryptedPassword, 155 | }, 156 | wantErr: false, 157 | } 158 | 159 | userRepo := rmock.NewUserRepository(t) 160 | s := &authService{ 161 | userRepository: userRepo, 162 | } 163 | 164 | expectedUser := models.User{ 165 | Username: "username", 166 | Password: encryptedPassword, 167 | } 168 | 169 | userRepo.On("GetByUsername", mock.Anything).Return(&expectedUser, nil) 170 | 171 | got, _, err := s.Login(test.args.username, test.args.password) 172 | if (err != nil) != test.wantErr { 173 | t.Errorf("authService.Register() error = %v, wantErr %v", err, test.wantErr) 174 | return 175 | } 176 | 177 | if !reflect.DeepEqual(got, test.want) { 178 | t.Errorf("authService.Register() = %v, want %v", got, test.want) 179 | } 180 | }) 181 | 182 | t.Run("Error Repository.GetByUsername", func(t *testing.T) { 183 | _, filename, _, _ := runtime.Caller(0) 184 | dir := path.Join(path.Dir(filename), "../../") 185 | err := os.Chdir(dir) 186 | if err != nil { 187 | panic(err) 188 | } 189 | 190 | type args struct { 191 | username string 192 | password string 193 | } 194 | test := struct { 195 | args args 196 | want *models.User 197 | wantErr bool 198 | }{ 199 | args: args{ 200 | username: "username", 201 | password: "password", 202 | }, 203 | want: nil, 204 | wantErr: true, 205 | } 206 | 207 | userRepo := rmock.NewUserRepository(t) 208 | s := &authService{ 209 | userRepository: userRepo, 210 | } 211 | 212 | userRepo.On("GetByUsername", mock.Anything).Return(nil, errors.New("something error")) 213 | 214 | got, _, err := s.Login(test.args.username, test.args.password) 215 | if (err != nil) != test.wantErr { 216 | t.Errorf("authService.Register() error = %v, wantErr %v", err, test.wantErr) 217 | return 218 | } 219 | 220 | if !reflect.DeepEqual(got, test.want) { 221 | t.Errorf("authService.Register() = %v, want %v", got, test.want) 222 | } 223 | }) 224 | 225 | t.Run("Error CompareEncrypt", func(t *testing.T) { 226 | _, filename, _, _ := runtime.Caller(0) 227 | dir := path.Join(path.Dir(filename), "../../") 228 | err := os.Chdir(dir) 229 | if err != nil { 230 | panic(err) 231 | } 232 | 233 | encryptedPassword, _ := helpers.Encrypt("password") 234 | 235 | type args struct { 236 | username string 237 | password string 238 | } 239 | test := struct { 240 | args args 241 | want *models.User 242 | wantErr bool 243 | }{ 244 | args: args{ 245 | username: "username", 246 | password: "wrongpassword", 247 | }, 248 | want: nil, 249 | wantErr: true, 250 | } 251 | 252 | userRepo := rmock.NewUserRepository(t) 253 | s := &authService{ 254 | userRepository: userRepo, 255 | } 256 | 257 | expectedUser := models.User{ 258 | Username: "username", 259 | Password: encryptedPassword, 260 | } 261 | 262 | userRepo.On("GetByUsername", mock.Anything).Return(&expectedUser, nil) 263 | 264 | got, _, err := s.Login(test.args.username, test.args.password) 265 | if (err != nil) != test.wantErr { 266 | t.Errorf("authService.Register() error = %v, wantErr %v", err, test.wantErr) 267 | return 268 | } 269 | 270 | if !reflect.DeepEqual(got, test.want) { 271 | t.Errorf("authService.Register() = %v, want %v", got, test.want) 272 | } 273 | }) 274 | } 275 | -------------------------------------------------------------------------------- /internal/services/user_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/geshtng/go-base-backend/internal/models" 5 | r "github.com/geshtng/go-base-backend/internal/repositories" 6 | ) 7 | 8 | type UserService interface { 9 | GetByID(id int64) (*models.User, error) 10 | } 11 | 12 | type userService struct { 13 | userRepository r.UserRepository 14 | } 15 | 16 | type UserServiceConfig struct { 17 | UserRepository r.UserRepository 18 | } 19 | 20 | func NewUserService(u *UserServiceConfig) UserService { 21 | return &userService{ 22 | userRepository: u.UserRepository, 23 | } 24 | } 25 | 26 | func (u *userService) GetByID(id int64) (*models.User, error) { 27 | result, err := u.userRepository.GetByID(id) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return result, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/services/user_service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | 11 | rmock "github.com/geshtng/go-base-backend/internal/mocks/repositories" 12 | "github.com/geshtng/go-base-backend/internal/models" 13 | ) 14 | 15 | func TestNewUserService(t *testing.T) { 16 | userRepo := rmock.NewUserRepository(t) 17 | userService := NewUserService(&UserServiceConfig{ 18 | UserRepository: userRepo, 19 | }) 20 | 21 | assert.NotNil(t, userService) 22 | } 23 | 24 | func Test_userService_GetByID(t *testing.T) { 25 | t.Run("Success", func(t *testing.T) { 26 | type args struct { 27 | id int64 28 | } 29 | tt := struct { 30 | args args 31 | want *models.User 32 | wantErr bool 33 | }{ 34 | args: args{ 35 | id: 1, 36 | }, 37 | want: &models.User{ 38 | ID: 1, 39 | Username: "username", 40 | }, 41 | wantErr: false, 42 | } 43 | 44 | userRepo := rmock.NewUserRepository(t) 45 | s := &userService{ 46 | userRepository: userRepo, 47 | } 48 | 49 | result := &models.User{ 50 | ID: 1, 51 | Username: "username", 52 | } 53 | 54 | userRepo.On("GetByID", mock.Anything).Return(result, nil) 55 | 56 | got, err := s.GetByID(tt.args.id) 57 | if (err != nil) != tt.wantErr { 58 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 59 | return 60 | } 61 | 62 | if !reflect.DeepEqual(got, tt.want) { 63 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 64 | } 65 | }) 66 | 67 | t.Run("Error Repository.GetByID", func(t *testing.T) { 68 | type args struct { 69 | id int64 70 | } 71 | tt := struct { 72 | args args 73 | want *models.User 74 | wantErr bool 75 | }{ 76 | args: args{ 77 | id: 1, 78 | }, 79 | want: nil, 80 | wantErr: true, 81 | } 82 | 83 | userRepo := rmock.NewUserRepository(t) 84 | s := &userService{ 85 | userRepository: userRepo, 86 | } 87 | 88 | userRepo.On("GetByID", mock.Anything).Return(nil, errors.New("something error")) 89 | 90 | got, err := s.GetByID(tt.args.id) 91 | if (err != nil) != tt.wantErr { 92 | t.Errorf("articleService.GetAll() error = %v, wantErr %v", err, tt.wantErr) 93 | return 94 | } 95 | 96 | if !reflect.DeepEqual(got, tt.want) { 97 | t.Errorf("articleService.GetAll() = %v, want %v", got, tt.want) 98 | } 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/geshtng/go-base-backend/config" 7 | "github.com/geshtng/go-base-backend/db" 8 | "github.com/geshtng/go-base-backend/internal/handlers" 9 | "github.com/geshtng/go-base-backend/internal/routes" 10 | ) 11 | 12 | func main() { 13 | port := config.InitConfigPort() 14 | 15 | r := gin.Default() 16 | 17 | err := db.Connect() 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | h := handlers.InitAllHandlers() 23 | 24 | routes.InitAllRoutes(r, h) 25 | 26 | err = r.Run(port) 27 | if err != nil { 28 | panic(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | xrun: 2 | nodemon --exec go run main.go --signal SIGTERM 3 | 4 | run: 5 | go run . --------------------------------------------------------------------------------