├── .gitignore ├── go.work ├── db └── migrations │ ├── 20231215060943_create_table_users.down.sql │ └── 20231215060943_create_table_users.up.sql ├── internal ├── model │ ├── model.go │ ├── auth.go │ ├── mapper │ │ └── user_mapper.go │ └── user_model.go ├── infrastructure │ ├── validator.go │ ├── logrus.go │ ├── fiber.go │ └── gorm.go ├── domain │ └── user.go ├── delivery │ └── http │ │ ├── route │ │ └── route.go │ │ ├── middleware │ │ └── auth_middleware.go │ │ └── handler │ │ └── user_handler.go ├── exception │ └── error_handler.go ├── repository │ └── user_repository.go └── usecase │ └── user_usecase.go ├── .env.example ├── config └── config.go ├── Makefile ├── cmd └── web │ └── main.go ├── test ├── integration │ ├── setup_e2e_test.go │ └── user_test.go └── unit │ ├── mocks │ └── user_repository_mock.go │ └── user_usecase_test.go ├── go.work.sum ├── README.md ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.20 2 | 3 | use . 4 | -------------------------------------------------------------------------------- /db/migrations/20231215060943_create_table_users.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; -------------------------------------------------------------------------------- /internal/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type WebResponse[T any] struct { 4 | Data T 5 | } 6 | -------------------------------------------------------------------------------- /internal/model/auth.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Auth struct { 4 | ID uint 5 | Username string 6 | } 7 | -------------------------------------------------------------------------------- /internal/infrastructure/validator.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func NewValidator(config *viper.Viper) *validator.Validate { 9 | return validator.New() 10 | } -------------------------------------------------------------------------------- /db/migrations/20231215060943_create_table_users.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users( 2 | id INT NOT NULL AUTO_INCREMENT, 3 | name VARCHAR(100) NOT NULL, 4 | username VARCHAR(100) NOT NULL, 5 | password VARCHAR(255) NOT NULL, 6 | created_at BIGINT NOT NULL, 7 | updated_at BIGINT NOT NULL, 8 | PRIMARY KEY(id) 9 | ); -------------------------------------------------------------------------------- /internal/infrastructure/logrus.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func NewLogger(config *viper.Viper) *logrus.Logger { 9 | logger := logrus.New() 10 | 11 | logger.SetLevel(logrus.Level(config.GetInt("LOG_LEVEL"))) 12 | logger.SetFormatter(&logrus.JSONFormatter{}) 13 | 14 | return logger 15 | } 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Golang Rest API 2 | APP_PORT=3000 3 | APP_PREFORK=false 4 | APP_TIMEOUT=10 #in a second 5 | 6 | DB_USER=root 7 | DB_PASSWORD= 8 | DB_HOST=localhost 9 | DB_PORT=3306 10 | DB_NAME=go-rest-api 11 | 12 | # Database Pool 13 | POOL_IDLE=5 14 | POOL_MAX=100 15 | POOL_LIFETIME=3000 #in a second 16 | 17 | LOG_LEVEL=6 #log level using logrus check documentation for level information 18 | 19 | JWT_SECRET_KEY= -------------------------------------------------------------------------------- /internal/domain/user.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type User struct { 4 | ID uint `gorm:"primaryKey;autoIncrement;column:id"` 5 | Name string `gorm:"column:name"` 6 | Username string `gorm:"column:username"` 7 | Password string `gorm:"column:password"` 8 | CreatedAt int64 `gorm:"column:created_at;autoCreateTime:milli"` 9 | UpdatedAt int64 `gorm:"column:updated_at;autoCreateTime:milli;autoUpdateTime:milli"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/model/mapper/user_mapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/domain" 5 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/model" 6 | ) 7 | 8 | func ToUserResponse(user *domain.User) *model.UserResponse { 9 | return &model.UserResponse{ 10 | ID: user.ID, 11 | Name: user.Name, 12 | Username: user.Username, 13 | CreatedAt: user.CreatedAt, 14 | UpdatedAt: user.UpdatedAt, 15 | } 16 | } -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "path/filepath" 7 | "runtime" 8 | 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | func New() *viper.Viper { 13 | _, filename, _, _ := runtime.Caller(0) 14 | currentDir := filepath.Dir(filename) 15 | configFile := path.Join(currentDir, ".." ,".env") 16 | 17 | viper := viper.New() 18 | viper.SetConfigFile(configFile) 19 | viper.AutomaticEnv() //use OS environment variable 20 | 21 | if err := viper.ReadInConfig(); err != nil { 22 | panic(fmt.Errorf("error loading configuration : %+v", err)) 23 | } 24 | 25 | return viper 26 | } 27 | -------------------------------------------------------------------------------- /internal/delivery/http/route/route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/delivery/http/handler" 5 | "github.com/gofiber/fiber/v2" 6 | ) 7 | 8 | func RegisterRoute(app *fiber.App, userHandler *handler.UserHandler, authMiddleware fiber.Handler) { 9 | publicRouter := app.Group("/api") 10 | publicRouter.Post("/users", userHandler.Register) 11 | publicRouter.Post("/users/_login", userHandler.Login) 12 | 13 | protectedRouter := app.Group("/api", authMiddleware) 14 | protectedRouter.Get("/users/_current", userHandler.Current) 15 | protectedRouter.Patch("/users/_current", userHandler.Update) 16 | } 17 | -------------------------------------------------------------------------------- /internal/infrastructure/fiber.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/exception" 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/gofiber/fiber/v2/middleware/recover" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | func NewFiber(config *viper.Viper) *fiber.App { 13 | app := fiber.New(fiber.Config{ 14 | AppName: config.GetString("APP_NAME"), 15 | ErrorHandler: exception.NewErrorHandler(), 16 | Prefork: config.GetBool("APP_PREFORK"), 17 | WriteTimeout: config.GetDuration("APP_TIMEOUT") * time.Second, 18 | ReadTimeout: config.GetDuration("APP_TIMEOUT") * time.Second, 19 | }) 20 | app.Use(recover.New()) 21 | 22 | return app 23 | } 24 | -------------------------------------------------------------------------------- /internal/delivery/http/middleware/auth_middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/model" 7 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/usecase" 8 | "github.com/gofiber/fiber/v2" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func NewAuth(userUsecase usecase.UserUsecase, logger *logrus.Logger) fiber.Handler { 13 | return func(c *fiber.Ctx) error { 14 | authorization := c.Get("Authorization") 15 | 16 | bearerToken := strings.Split(authorization, " ") 17 | if bearerToken[0] != "Bearer" { 18 | return fiber.ErrUnauthorized 19 | } 20 | 21 | auth, err := userUsecase.Verify(c.Context(), &model.VerifyUserRequest{AccessToken: bearerToken[1]}) 22 | if err != nil { 23 | logger.WithError(err).Warn("user not verified") 24 | return err 25 | } 26 | 27 | c.Locals("auth", auth) 28 | 29 | return c.Next() 30 | } 31 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # environment for integration testing 2 | ENV_LOCAL_TEST=\ 3 | APP_NAME="Golang Rest API" \ 4 | APP_PORT=3000 \ 5 | APP_PREFORK=false \ 6 | APP_TIMEOUT=10 \ 7 | DB_USER=root \ 8 | DB_PASSWORD= \ 9 | DB_HOST=localhost \ 10 | DB_PORT=3306 \ 11 | DB_NAME=go-rest-api-test \ 12 | POOL_IDLE=5 \ 13 | POOL_MAX=100 \ 14 | POOL_LIFETIME=3000 \ 15 | LOG_LEVEL=6 \ 16 | JWT_SECRET_KEY=secretkey 17 | 18 | test.unit: 19 | go test ./test/unit -v 20 | 21 | test.integration: 22 | $(ENV_LOCAL_TEST) go test ./test/integration -v 23 | 24 | include .env 25 | 26 | DATABASE_URL="mysql://$(DB_USER):$(DB_PASSWORD)@tcp($(DB_HOST):$(DB_PORT))/$(DB_NAME)" 27 | 28 | migrate.create: 29 | migrate create -ext sql -dir db/migrations $(name) 30 | 31 | migrate.up: 32 | migrate -database $(DATABASE_URL) -path db/migrations up 33 | 34 | migrate.down: 35 | migrate -database $(DATABASE_URL) -path db/migrations down 36 | 37 | migrate.force: 38 | migrate -database $(DATABASE_URL) -path db/migrations force $(version) -------------------------------------------------------------------------------- /internal/infrastructure/gorm.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/spf13/viper" 8 | "gorm.io/driver/mysql" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func NewGorm(config *viper.Viper) *gorm.DB { 13 | user := config.GetString("DB_USER") 14 | password := config.GetString("DB_PASSWORD") 15 | host := config.GetString("DB_HOST") 16 | dbname := config.GetString("DB_NAME") 17 | port := config.GetInt("DB_PORT") 18 | idleConns := config.GetInt("POOL_IDLE") 19 | maxConns := config.GetInt("POOL_MAX") 20 | lifetime := config.GetDuration("POOL_LIFETIME") * time.Second 21 | 22 | dsn := fmt.Sprintf( 23 | "%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", 24 | user, 25 | password, 26 | host, 27 | port, 28 | dbname, 29 | ) 30 | 31 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ 32 | SkipDefaultTransaction: true, 33 | }) 34 | if err != nil { 35 | panic(fmt.Errorf("error connecting database : %+v", err.Error())) 36 | } 37 | 38 | connection, err := db.DB() 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | connection.SetMaxIdleConns(idleConns) 44 | connection.SetMaxOpenConns(maxConns) 45 | connection.SetConnMaxLifetime(lifetime) 46 | 47 | return db 48 | } 49 | -------------------------------------------------------------------------------- /internal/model/user_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type RegisterUserRequest struct { 4 | Name string `json:"name" validate:"required,max=100"` 5 | Username string `json:"username" validate:"required,max=100"` 6 | Password string `json:"password" validate:"required,max=100,min=8"` 7 | } 8 | 9 | type LoginUserRequest struct { 10 | Username string `json:"username" validate:"required,max=100"` 11 | Password string `json:"password" validate:"required,max=100"` 12 | } 13 | 14 | type UpdateUserRequest struct { 15 | Name string `json:"name,omitempty" validate:"max=100"` 16 | Username string `validate:"max=100"` 17 | Password string `json:"password,omitempty" validate:"max=100"` 18 | } 19 | 20 | type GetUserRequest struct { 21 | Username string `json:"username,omitempty"` 22 | } 23 | 24 | type VerifyUserRequest struct { 25 | AccessToken string `json:"access_token,omitempty"` 26 | } 27 | 28 | type UserResponse struct { 29 | ID uint `json:"id,omitempty"` 30 | Name string `json:"name,omitempty"` 31 | Username string `json:"username,omitempty"` 32 | CreatedAt int64 `json:"created_at,omitempty"` 33 | UpdatedAt int64 `json:"updated_at,omitempty"` 34 | } 35 | 36 | type TokenResponse struct { 37 | AccessToken string `json:"access_token,omitempty"` 38 | TokenType string `json:"token_type,omitempty"` 39 | } 40 | -------------------------------------------------------------------------------- /internal/exception/error_handler.go: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-playground/validator/v10" 7 | "github.com/gofiber/fiber/v2" 8 | ) 9 | 10 | // define error here 11 | var ( 12 | // error user domain 13 | ErrUserNotFound = fiber.NewError(fiber.StatusNotFound, "User is not found") 14 | ErrUserAlreadyExist = fiber.NewError(fiber.StatusBadRequest, "username already exist") 15 | ErrUserPasswordNotMatch = fiber.NewError(fiber.StatusBadRequest, "password not match") 16 | ErrUserUnauthorized = fiber.NewError(fiber.StatusUnauthorized, "User unauthorized") 17 | 18 | //error 19 | ErrInternalServerError = fiber.ErrInternalServerError 20 | ) 21 | 22 | func NewErrorHandler() fiber.ErrorHandler { 23 | return func(c *fiber.Ctx, err error) error { 24 | statusCode := fiber.StatusInternalServerError 25 | var message any 26 | 27 | if e, ok := err.(*fiber.Error); ok { 28 | statusCode = e.Code 29 | message = e.Error() 30 | } 31 | 32 | if validationErrors, ok := err.(validator.ValidationErrors); ok { 33 | errorMessages := make([]string, 0) 34 | for _, value := range validationErrors { 35 | errorMessages = append(errorMessages, fmt.Sprintf( 36 | "[%s]: '%v' | needs to implements '%s'", 37 | value.Field(), 38 | value.Value(), 39 | value.ActualTag(), 40 | )) 41 | } 42 | 43 | statusCode = fiber.StatusBadRequest 44 | message = errorMessages 45 | } 46 | 47 | return c.Status(statusCode).JSON(fiber.Map{ 48 | "errors": message, 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/repository/user_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/domain" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type UserRepository interface { 11 | Create(ctx context.Context, user *domain.User) error 12 | Update(ctx context.Context, user *domain.User) error 13 | FindByUsername(ctx context.Context, username string) (*domain.User, error) 14 | CountByUsername(ctx context.Context, username string) (int64, error) 15 | } 16 | 17 | type UserRepositoryImpl struct { 18 | DB *gorm.DB 19 | } 20 | 21 | func NewUserRepository(db *gorm.DB) UserRepository { 22 | return &UserRepositoryImpl{DB: db} 23 | } 24 | 25 | func (r *UserRepositoryImpl) Create(ctx context.Context, user *domain.User) error { 26 | return r.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { 27 | if err := tx.Create(user).Error; err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | }) 33 | } 34 | 35 | func (r *UserRepositoryImpl) Update(ctx context.Context, user *domain.User) error { 36 | return r.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { 37 | if err := tx.Save(user).Error; err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | }) 43 | } 44 | 45 | func (r *UserRepositoryImpl) FindByUsername(ctx context.Context, username string) (*domain.User, error) { 46 | user := new(domain.User) 47 | if err := r.DB.WithContext(ctx).Where("username = ?", username).Take(user).Error; err != nil { 48 | return nil, err 49 | } 50 | return user, nil 51 | } 52 | 53 | func (r *UserRepositoryImpl) CountByUsername(ctx context.Context, username string) (int64, error) { 54 | var countUser int64 55 | if err := r.DB.WithContext(ctx).Model(&domain.User{}).Where("username = ?", username).Count(&countUser).Error; err != nil { 56 | return 0, err 57 | } 58 | return countUser, nil 59 | } -------------------------------------------------------------------------------- /cmd/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/config" 10 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/delivery/http/handler" 11 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/delivery/http/middleware" 12 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/delivery/http/route" 13 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/infrastructure" 14 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/repository" 15 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/usecase" 16 | ) 17 | 18 | func main() { 19 | config := config.New() 20 | 21 | app := infrastructure.NewFiber(config) 22 | port := config.Get("APP_PORT") 23 | 24 | db := infrastructure.NewGorm(config) 25 | logger := infrastructure.NewLogger(config) 26 | validate := infrastructure.NewValidator(config) 27 | userRepository := repository.NewUserRepository(db) 28 | userUsecase := usecase.NewUserUsecase(userRepository, logger, validate, config) 29 | userHandler := handler.NewUserHandler(userUsecase, logger) 30 | 31 | authMiddleware := middleware.NewAuth(userUsecase, logger) 32 | 33 | route.RegisterRoute(app, userHandler, authMiddleware) 34 | 35 | go func() { 36 | if err := app.Listen(fmt.Sprintf(":%v", port)); err != nil { 37 | panic(fmt.Errorf("error running app : %+v", err.Error())) 38 | } 39 | }() 40 | 41 | ch := make(chan os.Signal, 1) // Create channel to signify a signal being sent 42 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM) // When an interrupt or termination signal is sent, notify the channel 43 | 44 | <-ch // This blocks the main thread until an interrupt is received 45 | 46 | // Your cleanup tasks go here 47 | _ = app.Shutdown() 48 | 49 | fmt.Println("App was successful shutdown.") 50 | } 51 | -------------------------------------------------------------------------------- /test/integration/setup_e2e_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/config" 7 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/delivery/http/handler" 8 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/delivery/http/middleware" 9 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/delivery/http/route" 10 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/domain" 11 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/infrastructure" 12 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/repository" 13 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/usecase" 14 | "github.com/go-playground/validator/v10" 15 | "github.com/gofiber/fiber/v2" 16 | "github.com/sirupsen/logrus" 17 | "github.com/spf13/viper" 18 | "github.com/stretchr/testify/suite" 19 | "gorm.io/gorm" 20 | ) 21 | 22 | type e2eTestSuite struct { 23 | suite.Suite 24 | Config *viper.Viper 25 | App *fiber.App 26 | DB *gorm.DB 27 | Log *logrus.Logger 28 | Validate *validator.Validate 29 | UserRepository repository.UserRepository 30 | UserUsecase usecase.UserUsecase 31 | UserHandler *handler.UserHandler 32 | AuthMiddleware fiber.Handler 33 | } 34 | 35 | func TestE2eSuite(t *testing.T) { 36 | suite.Run(t, new(e2eTestSuite)) 37 | } 38 | 39 | func (s *e2eTestSuite) SetupSuite() { 40 | s.Config = config.New() 41 | s.DB = infrastructure.NewGorm(s.Config) 42 | s.Log = infrastructure.NewLogger(s.Config) 43 | s.App = infrastructure.NewFiber(s.Config) 44 | s.Validate = infrastructure.NewValidator(s.Config) 45 | s.UserRepository = repository.NewUserRepository(s.DB) 46 | s.UserUsecase = usecase.NewUserUsecase(s.UserRepository, s.Log, s.Validate, s.Config) 47 | s.UserHandler = handler.NewUserHandler(s.UserUsecase, s.Log) 48 | s.AuthMiddleware = middleware.NewAuth(s.UserUsecase, s.Log) 49 | route.RegisterRoute(s.App, s.UserHandler, s.AuthMiddleware) 50 | } 51 | 52 | func (s *e2eTestSuite) SetupTest() { 53 | s.Require().NoError(s.DB.Migrator().AutoMigrate(&domain.User{})) 54 | } 55 | 56 | func (s *e2eTestSuite) TearDownTest() { 57 | s.Require().NoError(s.DB.Migrator().DropTable("users")) 58 | } 59 | -------------------------------------------------------------------------------- /internal/delivery/http/handler/user_handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/model" 5 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/usecase" 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type UserHandler struct { 11 | UserUsecase usecase.UserUsecase 12 | Logger *logrus.Logger 13 | } 14 | 15 | func NewUserHandler(userUsecase usecase.UserUsecase, log *logrus.Logger) *UserHandler { 16 | return &UserHandler{ 17 | UserUsecase: userUsecase, 18 | Logger: log, 19 | } 20 | } 21 | 22 | func (h *UserHandler) Register(c *fiber.Ctx) error { 23 | registerUserRequest := new(model.RegisterUserRequest) 24 | if err := c.BodyParser(registerUserRequest); err != nil { 25 | h.Logger.WithError(err).Error("error parsing request body") 26 | return err 27 | } 28 | 29 | response, err := h.UserUsecase.Register(c.Context(), registerUserRequest) 30 | if err != nil { 31 | h.Logger.WithError(err).Error("error user register") 32 | return err 33 | } 34 | 35 | return c. 36 | Status(fiber.StatusCreated). 37 | JSON(&model.WebResponse[*model.UserResponse]{ 38 | Data: response, 39 | }) 40 | } 41 | 42 | func (h *UserHandler) Login(c *fiber.Ctx) error { 43 | loginUserReequest := new(model.LoginUserRequest) 44 | if err := c.BodyParser(loginUserReequest); err != nil { 45 | h.Logger.WithError(err).Error("error parsing request body") 46 | return err 47 | } 48 | 49 | response, err := h.UserUsecase.Login(c.Context(), loginUserReequest) 50 | if err != nil { 51 | h.Logger.WithError(err).Error("error user login") 52 | return err 53 | } 54 | 55 | return c. 56 | JSON(&model.WebResponse[*model.TokenResponse]{ 57 | Data: response, 58 | }) 59 | } 60 | 61 | func (h *UserHandler) Current(c *fiber.Ctx) error { 62 | auth := c.Locals("auth").(*model.Auth) 63 | 64 | response, err := h.UserUsecase.Current(c.Context(), &model.GetUserRequest{Username: auth.Username}) 65 | if err != nil { 66 | h.Logger.WithError(err).Error("error get current user") 67 | return err 68 | } 69 | 70 | return c. 71 | JSON(&model.WebResponse[*model.UserResponse]{ 72 | Data: response, 73 | }) 74 | } 75 | 76 | func (h *UserHandler) Update(c *fiber.Ctx) error { 77 | auth := c.Locals("auth").(*model.Auth) 78 | 79 | updateUserRequest := new(model.UpdateUserRequest) 80 | if err := c.BodyParser(updateUserRequest); err != nil { 81 | h.Logger.WithError(err).Error("error parsing request body") 82 | return err 83 | } 84 | 85 | updateUserRequest.Username = auth.Username 86 | response, err := h.UserUsecase.Update(c.Context(), updateUserRequest) 87 | if err != nil { 88 | h.Logger.WithError(err).Error("error update user") 89 | return err 90 | } 91 | 92 | return c. 93 | JSON(&model.WebResponse[*model.UserResponse]{ 94 | Data: response, 95 | }) 96 | 97 | } 98 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 4 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 5 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 6 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 7 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 8 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 9 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 11 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 12 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 15 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 18 | github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 19 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= 20 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= 21 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 22 | github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= 23 | github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= 24 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 25 | go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= 26 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 27 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 28 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 29 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 30 | golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= 31 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 32 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang-clean-architecture-project-structure 2 | This is project structure template following clean architecture and golang standard project layout (not official but commonly used for golang project) 3 | 4 | list library/tool has been used in this project 5 | - [fiber](https://github.com/gofiber/fiber) - web framework 6 | - [viper](https://github.com/spf13/viper) - library for configuration 7 | - [gorm](https://github.com/go-gorm/gorm) - ORM library 8 | - [gomock](https://github.com/uber/mock) - mocking framework 9 | - [testify](https://github.com/stretchr/testify)- toolkit for assertions test 10 | - [validator](https://github.com/go-playground/validator) - library for struct and field validation. 11 | - [migrate](https://github.com/golang-migrate/migrate) - database migration 12 | 13 | and here's the explanation of the project structure: 14 | 15 | - **cmd** : This directory contains the main entry point of the project. Usually, for the place main application file here, which is used to start and run the application. 16 | - **config**: This directory holds configuration files for the project. 17 | - **docs**: This directory contains documentation-related files for the project. 18 | - **internal** : This directory is used to organize internal code of the project. 19 | - **test** : This directory contains tests (unit tests and integration tests). 20 | 21 | other directories : 22 | - **infrastructure** : This directory contains framework or driver layer in clean architecture 23 | - **delivery** : This directory contains code related to data delivery, such as HTTP implementation or RPC. 24 | - **domain**: This directory contains the domain data structure definitions. 25 | - **exception**: This directory contains code related to error handling. 26 | - **model** : This directory contains model data structures and response objects used in the project. 27 | - **mapper** : This directory contains code related to mapping data structures. 28 | - **repository** : This directory contains code related to data access (data access layer). 29 | - **usecase** : This directory contains code related to the use case or business logic that governs how data is processed and used in the application. 30 | 31 | This project structure looks well-organized and follows many best practices in Go application development. It separates concerns clearly, making the code easy to maintain and extend. 32 | 33 | ## Install 34 | ``` 35 | git clone https://github.com/Ikhlashmulya/golang-clean-architecture.git 36 | ``` 37 | 38 | ``` 39 | rm -rf .git/ 40 | ``` 41 | 42 | ``` 43 | cp .env.example .env 44 | ``` 45 | 46 | or using [gonew](https://pkg.go.dev/golang.org/x/tools/cmd/gonew) 47 | 48 | ``` 49 | gonew github.com/Ikhlashmulya/golang-clean-architecture github.com// 50 | ``` 51 | ## Run 52 | ``` 53 | go run cmd/web/main.go 54 | ``` 55 | ## Testing 56 | 57 | ### Run Unit Test 58 | ``` 59 | make test.unit 60 | ``` 61 | 62 | ### Run Integration Test 63 | ``` 64 | make test.integration 65 | ``` 66 | 67 | ## Database Migration 68 | 69 | ### Create 70 | ``` 71 | make migrate.create name=create_table_users 72 | ``` 73 | 74 | ### Up 75 | ``` 76 | make migrate.up 77 | ``` 78 | 79 | ### Down 80 | ``` 81 | make migrate.down 82 | ``` 83 | 84 | ### Force 85 | ``` 86 | make migrate.force version=20231216100 87 | ``` 88 | -------------------------------------------------------------------------------- /test/unit/mocks/user_repository_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/repository/user_repository.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | domain "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/domain" 12 | gomock "go.uber.org/mock/gomock" 13 | ) 14 | 15 | // MockUserRepository is a mock of UserRepository interface. 16 | type MockUserRepository struct { 17 | ctrl *gomock.Controller 18 | recorder *MockUserRepositoryMockRecorder 19 | } 20 | 21 | // MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository. 22 | type MockUserRepositoryMockRecorder struct { 23 | mock *MockUserRepository 24 | } 25 | 26 | // NewMockUserRepository creates a new mock instance. 27 | func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository { 28 | mock := &MockUserRepository{ctrl: ctrl} 29 | mock.recorder = &MockUserRepositoryMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // CountByUsername mocks base method. 39 | func (m *MockUserRepository) CountByUsername(ctx context.Context, username string) (int64, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "CountByUsername", ctx, username) 42 | ret0, _ := ret[0].(int64) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // CountByUsername indicates an expected call of CountByUsername. 48 | func (mr *MockUserRepositoryMockRecorder) CountByUsername(ctx, username interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountByUsername", reflect.TypeOf((*MockUserRepository)(nil).CountByUsername), ctx, username) 51 | } 52 | 53 | // Create mocks base method. 54 | func (m *MockUserRepository) Create(ctx context.Context, user *domain.User) error { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "Create", ctx, user) 57 | ret0, _ := ret[0].(error) 58 | return ret0 59 | } 60 | 61 | // Create indicates an expected call of Create. 62 | func (mr *MockUserRepositoryMockRecorder) Create(ctx, user interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUserRepository)(nil).Create), ctx, user) 65 | } 66 | 67 | // FindByUsername mocks base method. 68 | func (m *MockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "FindByUsername", ctx, username) 71 | ret0, _ := ret[0].(*domain.User) 72 | ret1, _ := ret[1].(error) 73 | return ret0, ret1 74 | } 75 | 76 | // FindByUsername indicates an expected call of FindByUsername. 77 | func (mr *MockUserRepositoryMockRecorder) FindByUsername(ctx, username interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByUsername", reflect.TypeOf((*MockUserRepository)(nil).FindByUsername), ctx, username) 80 | } 81 | 82 | // Update mocks base method. 83 | func (m *MockUserRepository) Update(ctx context.Context, user *domain.User) error { 84 | m.ctrl.T.Helper() 85 | ret := m.ctrl.Call(m, "Update", ctx, user) 86 | ret0, _ := ret[0].(error) 87 | return ret0 88 | } 89 | 90 | // Update indicates an expected call of Update. 91 | func (mr *MockUserRepositoryMockRecorder) Update(ctx, user interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUserRepository)(nil).Update), ctx, user) 94 | } 95 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Ikhlashmulya/golang-clean-architecture-project-structure 2 | 3 | go 1.20 4 | 5 | require github.com/go-playground/validator/v10 v10.14.1 6 | 7 | require ( 8 | github.com/bytedance/sonic v1.8.0 // indirect 9 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 10 | github.com/fsnotify/fsnotify v1.7.0 // indirect 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/gin-gonic/gin v1.9.0 // indirect 13 | github.com/goccy/go-json v0.10.0 // indirect 14 | github.com/golang-jwt/jwt/v5 v5.2.0 // indirect 15 | github.com/hashicorp/errwrap v1.1.0 // indirect 16 | github.com/hashicorp/go-multierror v1.1.1 // indirect 17 | github.com/hashicorp/hcl v1.0.0 // indirect 18 | github.com/json-iterator/go v1.1.12 // indirect 19 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 20 | github.com/kr/pretty v0.3.1 // indirect 21 | github.com/kr/text v0.2.0 // indirect 22 | github.com/magiconair/properties v1.8.7 // indirect 23 | github.com/mitchellh/mapstructure v1.5.0 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/pelletier/go-toml/v2 v2.1.1 // indirect 27 | github.com/rogpeppe/go-internal v1.9.0 // indirect 28 | github.com/sagikazarmark/locafero v0.4.0 // indirect 29 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 30 | github.com/sirupsen/logrus v1.9.3 // indirect 31 | github.com/sourcegraph/conc v0.3.0 // indirect 32 | github.com/spf13/afero v1.11.0 // indirect 33 | github.com/spf13/cast v1.6.0 // indirect 34 | github.com/spf13/pflag v1.0.5 // indirect 35 | github.com/spf13/viper v1.18.1 // indirect 36 | github.com/subosito/gotenv v1.6.0 // indirect 37 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 38 | github.com/ugorji/go/codec v1.2.9 // indirect 39 | go.uber.org/atomic v1.11.0 // indirect 40 | go.uber.org/multierr v1.11.0 // indirect 41 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 42 | golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect 43 | google.golang.org/protobuf v1.31.0 // indirect 44 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 45 | gopkg.in/ini.v1 v1.67.0 // indirect 46 | gorm.io/driver/mysql v1.5.2 // indirect 47 | gorm.io/gorm v1.25.5 // indirect 48 | ) 49 | 50 | require ( 51 | github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect 52 | github.com/KyleBanks/depth v1.2.1 // indirect 53 | github.com/PuerkitoBio/purell v1.2.0 // indirect 54 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 55 | github.com/andybalholm/brotli v1.0.5 // indirect 56 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 57 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 58 | github.com/go-openapi/jsonpointer v0.20.0 // indirect 59 | github.com/go-openapi/jsonreference v0.20.2 // indirect 60 | github.com/go-openapi/spec v0.20.9 // indirect 61 | github.com/go-openapi/swag v0.22.4 // indirect 62 | github.com/go-playground/locales v0.14.1 // indirect 63 | github.com/go-playground/universal-translator v0.18.1 // indirect 64 | github.com/go-sql-driver/mysql v1.7.1 // indirect 65 | github.com/gofiber/fiber/v2 v2.48.0 // indirect 66 | github.com/gofiber/swagger v0.1.12 // indirect 67 | github.com/golang-migrate/migrate/v4 v4.16.2 68 | github.com/google/uuid v1.4.0 // indirect 69 | github.com/google/wire v0.5.0 70 | github.com/jinzhu/inflection v1.0.0 // indirect 71 | github.com/jinzhu/now v1.1.5 // indirect 72 | github.com/joho/godotenv v1.5.1 // indirect 73 | github.com/josharian/intern v1.0.0 // indirect 74 | github.com/klauspost/compress v1.17.0 // indirect 75 | github.com/leodido/go-urn v1.2.4 // indirect 76 | github.com/mailru/easyjson v0.7.7 // indirect 77 | github.com/mattn/go-colorable v0.1.13 // indirect 78 | github.com/mattn/go-isatty v0.0.19 // indirect 79 | github.com/mattn/go-runewidth v0.0.14 // indirect 80 | github.com/philhofer/fwd v1.1.2 // indirect 81 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 82 | github.com/rivo/uniseg v0.4.4 // indirect 83 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect 84 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect 85 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect 86 | github.com/stretchr/objx v0.5.0 // indirect 87 | github.com/stretchr/testify v1.8.4 // indirect 88 | github.com/swaggo/files/v2 v2.0.0 // indirect 89 | github.com/swaggo/gin-swagger v1.6.0 90 | github.com/swaggo/swag v1.16.1 // indirect 91 | github.com/tinylib/msgp v1.1.8 // indirect 92 | github.com/valyala/bytebufferpool v1.0.0 // indirect 93 | github.com/valyala/fasthttp v1.48.0 // indirect 94 | github.com/valyala/tcplisten v1.0.0 // indirect 95 | go.uber.org/mock v0.2.0 // indirect 96 | golang.org/x/crypto v0.16.0 // indirect 97 | golang.org/x/net v0.19.0 // indirect 98 | golang.org/x/sys v0.15.0 // indirect 99 | golang.org/x/text v0.14.0 // indirect 100 | golang.org/x/tools v0.16.0 // indirect 101 | gopkg.in/yaml.v2 v2.4.0 // indirect 102 | gopkg.in/yaml.v3 v3.0.1 // indirect 103 | ) 104 | -------------------------------------------------------------------------------- /internal/usecase/user_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "golang.org/x/crypto/bcrypt" 8 | 9 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/domain" 10 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/exception" 11 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/model" 12 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/model/mapper" 13 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/repository" 14 | "github.com/go-playground/validator/v10" 15 | "github.com/golang-jwt/jwt/v5" 16 | "github.com/sirupsen/logrus" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | type UserUsecase interface { 21 | Register(ctx context.Context, request *model.RegisterUserRequest) (*model.UserResponse, error) 22 | Login(ctx context.Context, request *model.LoginUserRequest) (*model.TokenResponse, error) 23 | Update(ctx context.Context, request *model.UpdateUserRequest) (*model.UserResponse, error) 24 | Current(ctx context.Context, request *model.GetUserRequest) (*model.UserResponse, error) 25 | Verify(ctx context.Context, request *model.VerifyUserRequest) (*model.Auth, error) 26 | } 27 | 28 | type UserUsecaseImpl struct { 29 | UserRepository repository.UserRepository 30 | Logger *logrus.Logger 31 | Validate *validator.Validate 32 | Config *viper.Viper 33 | } 34 | 35 | func NewUserUsecase(userRepo repository.UserRepository, log *logrus.Logger, 36 | validate *validator.Validate, config *viper.Viper) UserUsecase { 37 | return &UserUsecaseImpl{ 38 | UserRepository: userRepo, 39 | Logger: log, 40 | Validate: validate, 41 | Config: config, 42 | } 43 | } 44 | 45 | func (uc *UserUsecaseImpl) Register(ctx context.Context, request *model.RegisterUserRequest) (*model.UserResponse, error) { 46 | if err := uc.Validate.Struct(request); err != nil { 47 | uc.Logger.WithError(err).Error("failed validating request body") 48 | return nil, err 49 | } 50 | 51 | countUser, err := uc.UserRepository.CountByUsername(ctx, request.Username) 52 | if err != nil { 53 | uc.Logger.WithError(err).Error("failed count user by username") 54 | return nil, exception.ErrInternalServerError 55 | } 56 | 57 | if countUser > 0 { 58 | uc.Logger.Warn("user already exists") 59 | return nil, exception.ErrUserAlreadyExist 60 | } 61 | 62 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) 63 | if err != nil { 64 | uc.Logger.WithError(err).Error("failed hashing password") 65 | return nil, exception.ErrInternalServerError 66 | } 67 | 68 | user := new(domain.User) 69 | user.Name = request.Name 70 | user.Username = request.Username 71 | user.Password = string(hashedPassword) 72 | 73 | if err := uc.UserRepository.Create(ctx, user); err != nil { 74 | uc.Logger.WithError(err).Error("failed create user to database") 75 | return nil, exception.ErrInternalServerError 76 | } 77 | 78 | return mapper.ToUserResponse(user), nil 79 | } 80 | 81 | func (uc *UserUsecaseImpl) Login(ctx context.Context, request *model.LoginUserRequest) (*model.TokenResponse, error) { 82 | if err := uc.Validate.Struct(request); err != nil { 83 | uc.Logger.WithError(err).Error("failed validating request body") 84 | return nil, err 85 | } 86 | 87 | user, err := uc.UserRepository.FindByUsername(ctx, request.Username) 88 | if err != nil { 89 | uc.Logger.WithError(err).Error("failed find user by username") 90 | return nil, exception.ErrUserNotFound 91 | } 92 | 93 | if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password)); err != nil { 94 | uc.Logger.WithError(err).Error("failed to compare hashedPassword and password") 95 | return nil, exception.ErrUserPasswordNotMatch 96 | } 97 | 98 | claims := jwt.MapClaims{ 99 | "id": user.ID, 100 | "username": user.Username, 101 | "exp": time.Now().Add(2 * time.Hour).Unix(), 102 | } 103 | 104 | token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(uc.Config.GetString("JWT_SECRET_KEY"))) 105 | if err != nil { 106 | uc.Logger.WithError(err).Error("failed sign token") 107 | return nil, exception.ErrInternalServerError 108 | } 109 | 110 | tokenResponse := &model.TokenResponse{ 111 | AccessToken: token, 112 | TokenType: "Bearer", 113 | } 114 | 115 | return tokenResponse, nil 116 | } 117 | 118 | func (uc *UserUsecaseImpl) Update(ctx context.Context, request *model.UpdateUserRequest) (*model.UserResponse, error) { 119 | if err := uc.Validate.Struct(request); err != nil { 120 | uc.Logger.WithError(err).Error("failed validating request body") 121 | return nil, err 122 | } 123 | 124 | user, err := uc.UserRepository.FindByUsername(ctx, request.Username) 125 | if err != nil { 126 | uc.Logger.WithError(err).Error("failed find user by username") 127 | return nil, exception.ErrUserNotFound 128 | } 129 | 130 | if request.Name != "" { 131 | user.Name = request.Name 132 | } 133 | 134 | if request.Password != "" { 135 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) 136 | if err != nil { 137 | uc.Logger.WithError(err).Error("failed hashing password") 138 | return nil, exception.ErrInternalServerError 139 | } 140 | 141 | user.Password = string(hashedPassword) 142 | } 143 | 144 | if err := uc.UserRepository.Update(ctx, user); err != nil { 145 | uc.Logger.WithError(err).Error("failed update user to database") 146 | return nil, exception.ErrInternalServerError 147 | } 148 | 149 | return mapper.ToUserResponse(user), nil 150 | } 151 | 152 | func (uc *UserUsecaseImpl) Current(ctx context.Context, request *model.GetUserRequest) (*model.UserResponse, error) { 153 | if err := uc.Validate.Struct(request); err != nil { 154 | uc.Logger.WithError(err).Error("failed validating request body") 155 | return nil, err 156 | } 157 | 158 | user, err := uc.UserRepository.FindByUsername(ctx, request.Username) 159 | if err != nil { 160 | uc.Logger.WithError(err).Error("failed find user by username") 161 | return nil, exception.ErrUserNotFound 162 | } 163 | 164 | return mapper.ToUserResponse(user), nil 165 | } 166 | 167 | func (uc *UserUsecaseImpl) Verify(ctx context.Context, request *model.VerifyUserRequest) (*model.Auth, error) { 168 | token, err := jwt.Parse(request.AccessToken, func(t *jwt.Token) (interface{}, error) { 169 | return []byte(uc.Config.GetString("JWT_SECRET_KEY")), nil 170 | }) 171 | if err != nil { 172 | uc.Logger.WithError(err).Error("user unauthorized") 173 | return nil, exception.ErrUserUnauthorized 174 | } 175 | 176 | claims, ok := token.Claims.(jwt.MapClaims) 177 | if !ok { 178 | return nil, exception.ErrUserUnauthorized 179 | } 180 | 181 | countUser, err := uc.UserRepository.CountByUsername(ctx, claims["username"].(string)) 182 | if err != nil { 183 | uc.Logger.WithError(err).Error("failed count user by username") 184 | return nil, exception.ErrInternalServerError 185 | } 186 | 187 | if countUser == 0 { 188 | return nil, exception.ErrUserUnauthorized 189 | } 190 | 191 | return &model.Auth{ 192 | Username: claims["username"].(string), 193 | ID: uint(claims["id"].(float64)), 194 | }, nil 195 | } 196 | -------------------------------------------------------------------------------- /test/unit/user_usecase_test.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/config" 8 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/domain" 9 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/exception" 10 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/model" 11 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/usecase" 12 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/test/unit/mocks" 13 | "github.com/go-playground/validator/v10" 14 | "github.com/sirupsen/logrus" 15 | "github.com/stretchr/testify/assert" 16 | "go.uber.org/mock/gomock" 17 | "golang.org/x/crypto/bcrypt" 18 | "gorm.io/gorm" 19 | ) 20 | 21 | var ( 22 | ctx = context.Background() 23 | ) 24 | 25 | func TestRegisterUser(t *testing.T) { 26 | ctrl := gomock.NewController(t) 27 | userRepository := mocks.NewMockUserRepository(ctrl) 28 | userUsecase := usecase.NewUserUsecase(userRepository, logrus.New(), validator.New(), config.New()) 29 | 30 | t.Run("success", func(t *testing.T) { 31 | userRepository.EXPECT().CountByUsername(ctx, "johndoe").Return(int64(0), nil) 32 | userRepository.EXPECT().Create(ctx, gomock.Any()).Return(nil) 33 | 34 | request := &model.RegisterUserRequest{ 35 | Name: "John Doe", 36 | Username: "johndoe", 37 | Password: "password", 38 | } 39 | 40 | response, err := userUsecase.Register(ctx, request) 41 | assert.NoError(t, err) 42 | assert.Equal(t, request.Name, response.Name) 43 | assert.Equal(t, request.Username, response.Username) 44 | }) 45 | 46 | t.Run("failed validation", func(t *testing.T) { 47 | request := &model.RegisterUserRequest{ 48 | Name: "", 49 | Username: "", 50 | Password: "", 51 | } 52 | 53 | _, err := userUsecase.Register(ctx, request) 54 | assert.Error(t, err) 55 | }) 56 | 57 | t.Run("failed username already exist", func(t *testing.T) { 58 | userRepository.EXPECT().CountByUsername(ctx, "johndoe").Return(int64(1), nil) 59 | 60 | request := &model.RegisterUserRequest{ 61 | Name: "John Doe", 62 | Username: "johndoe", 63 | Password: "password", 64 | } 65 | 66 | _, err := userUsecase.Register(ctx, request) 67 | assert.Error(t, err) 68 | }) 69 | } 70 | 71 | func TestLoginUser(t *testing.T) { 72 | ctrl := gomock.NewController(t) 73 | userRepository := mocks.NewMockUserRepository(ctrl) 74 | userUsecase := usecase.NewUserUsecase(userRepository, logrus.New(), validator.New(), config.New()) 75 | 76 | user := createUser(t) 77 | 78 | t.Run("success", func(t *testing.T) { 79 | userRepository.EXPECT().FindByUsername(ctx, "johndoe").Return(user, nil) 80 | 81 | response, err := userUsecase.Login(ctx, &model.LoginUserRequest{ 82 | Username: "johndoe", 83 | Password: "password", 84 | }) 85 | 86 | assert.NoError(t, err) 87 | assert.Equal(t, "Bearer", response.TokenType) 88 | assert.NotEmpty(t, response.TokenType) 89 | }) 90 | 91 | t.Run("failed user not found", func(t *testing.T) { 92 | userRepository.EXPECT().FindByUsername(ctx, "johndoe").Return(nil, gorm.ErrRecordNotFound) 93 | 94 | _, err := userUsecase.Login(ctx, &model.LoginUserRequest{ 95 | Username: "johndoe", 96 | Password: "password", 97 | }) 98 | 99 | assert.Error(t, err) 100 | assert.ErrorIs(t, exception.ErrUserNotFound, err) 101 | }) 102 | 103 | t.Run("failed password not match", func(t *testing.T) { 104 | userRepository.EXPECT().FindByUsername(ctx, "johndoe").Return(user, nil) 105 | 106 | _, err := userUsecase.Login(ctx, &model.LoginUserRequest{ 107 | Username: "johndoe", 108 | Password: "wrongPassword", 109 | }) 110 | 111 | assert.Error(t, err) 112 | assert.ErrorIs(t, exception.ErrUserPasswordNotMatch, err) 113 | }) 114 | 115 | t.Run("failed validation", func(t *testing.T) { 116 | _, err := userUsecase.Login(ctx, &model.LoginUserRequest{ 117 | Username: "", 118 | Password: "", 119 | }) 120 | 121 | assert.Error(t, err) 122 | }) 123 | } 124 | 125 | func TestUpdateUser(t *testing.T) { 126 | ctrl := gomock.NewController(t) 127 | userRepository := mocks.NewMockUserRepository(ctrl) 128 | userUsecase := usecase.NewUserUsecase(userRepository, logrus.New(), validator.New(), config.New()) 129 | 130 | user := createUser(t) 131 | 132 | t.Run("success", func(t *testing.T) { 133 | userRepository.EXPECT().FindByUsername(ctx, "johndoe").Return(user, nil) 134 | userRepository.EXPECT().Update(ctx, gomock.Any()).Return(nil) 135 | 136 | request := &model.UpdateUserRequest{ 137 | Username: "johndoe", 138 | Name: "John Doe edited", 139 | } 140 | 141 | response, err := userUsecase.Update(ctx, request) 142 | assert.NoError(t, err) 143 | assert.Equal(t, request.Name, response.Name) 144 | }) 145 | 146 | t.Run("failed user not found", func(t *testing.T) { 147 | userRepository.EXPECT().FindByUsername(ctx, "johndoe").Return(nil, gorm.ErrRecordNotFound) 148 | 149 | request := &model.UpdateUserRequest{ 150 | Username: "johndoe", 151 | Name: "John Doe edited", 152 | } 153 | 154 | _, err := userUsecase.Update(ctx, request) 155 | assert.Error(t, err) 156 | assert.ErrorIs(t, exception.ErrUserNotFound, err) 157 | 158 | }) 159 | } 160 | 161 | func TestCurrentUser(t *testing.T) { 162 | ctrl := gomock.NewController(t) 163 | userRepository := mocks.NewMockUserRepository(ctrl) 164 | userUsecase := usecase.NewUserUsecase(userRepository, logrus.New(), validator.New(), config.New()) 165 | 166 | user := createUser(t) 167 | 168 | t.Run("success", func(t *testing.T) { 169 | userRepository.EXPECT().FindByUsername(ctx, "johndoe").Return(user, nil) 170 | 171 | response, err := userUsecase.Current(ctx, &model.GetUserRequest{Username: "johndoe"}) 172 | assert.NoError(t, err) 173 | assert.Equal(t, user.ID, response.ID) 174 | assert.Equal(t, user.Name, response.Name) 175 | assert.Equal(t, user.Username, response.Username) 176 | assert.Equal(t, user.CreatedAt, response.CreatedAt) 177 | assert.Equal(t, user.UpdatedAt, response.UpdatedAt) 178 | }) 179 | 180 | t.Run("failed not found", func(t *testing.T) { 181 | userRepository.EXPECT().FindByUsername(ctx, "johndoe").Return(nil, gorm.ErrRecordNotFound) 182 | 183 | _, err := userUsecase.Current(ctx, &model.GetUserRequest{Username: "johndoe"}) 184 | assert.Error(t, err) 185 | assert.ErrorIs(t, exception.ErrUserNotFound, err) 186 | }) 187 | } 188 | 189 | func TestVerifyUser(t *testing.T) { 190 | ctrl := gomock.NewController(t) 191 | userRepository := mocks.NewMockUserRepository(ctrl) 192 | userUsecase := usecase.NewUserUsecase(userRepository, logrus.New(), validator.New(), config.New()) 193 | 194 | user := createUser(t) 195 | 196 | t.Run("success", func(t *testing.T) { 197 | userRepository.EXPECT().FindByUsername(ctx, "johndoe").Return(user, nil) 198 | 199 | response, err := userUsecase.Login(ctx, &model.LoginUserRequest{ 200 | Username: "johndoe", 201 | Password: "password", 202 | }) 203 | assert.NoError(t, err) 204 | 205 | userRepository.EXPECT().CountByUsername(ctx, "johndoe").Return(int64(1), nil) 206 | 207 | auth, err := userUsecase.Verify(ctx, &model.VerifyUserRequest{AccessToken: response.AccessToken}) 208 | assert.NoError(t, err) 209 | assert.Equal(t, user.Username, auth.Username) 210 | }) 211 | 212 | t.Run("failed invalid token", func(t *testing.T) { 213 | _, err := userUsecase.Verify(ctx, &model.VerifyUserRequest{AccessToken: "wrongToken"}) 214 | assert.Error(t, err) 215 | assert.ErrorIs(t, exception.ErrUserUnauthorized, err) 216 | }) 217 | } 218 | 219 | func createUser(t *testing.T) *domain.User { 220 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost) 221 | assert.NoError(t, err) 222 | 223 | user := &domain.User{ 224 | ID: 1, 225 | Name: "John Doe", 226 | Username: "johndoe", 227 | Password: string(hashedPassword), 228 | CreatedAt: 12345, 229 | UpdatedAt: 12345, 230 | } 231 | 232 | return user 233 | } 234 | -------------------------------------------------------------------------------- /test/integration/user_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | 11 | "github.com/Ikhlashmulya/golang-clean-architecture-project-structure/internal/model" 12 | ) 13 | 14 | func (s *e2eTestSuite) TestUserRegisterSuccess() { 15 | requestBody := &model.RegisterUserRequest{ 16 | Name: "John Doe", 17 | Username: "johndoe", 18 | Password: "johndoe123", 19 | } 20 | 21 | bodyJSON, err := json.Marshal(requestBody) 22 | s.Assert().NoError(err) 23 | 24 | request := httptest.NewRequest(http.MethodPost, "/api/users", strings.NewReader(string(bodyJSON))) 25 | request.Header.Add("content-type", "application/json") 26 | 27 | response, err := s.App.Test(request) 28 | s.Assert().NoError(err) 29 | s.Assert().Equal(http.StatusCreated, response.StatusCode) 30 | 31 | bytes, err := io.ReadAll(response.Body) 32 | s.Assert().NoError(err) 33 | 34 | responseBody := new(model.WebResponse[*model.UserResponse]) 35 | err = json.Unmarshal(bytes, responseBody) 36 | s.Assert().NoError(err) 37 | 38 | s.Assert().Equal(requestBody.Name, responseBody.Data.Name) 39 | } 40 | 41 | func (s *e2eTestSuite) TestUserRegisterFailedValidation() { 42 | requestBody := &model.RegisterUserRequest{ 43 | Name: "", 44 | Username: "", 45 | Password: "", 46 | } 47 | 48 | bodyJSON, err := json.Marshal(requestBody) 49 | s.Assert().NoError(err) 50 | 51 | request := httptest.NewRequest(http.MethodPost, "/api/users", strings.NewReader(string(bodyJSON))) 52 | request.Header.Add("content-type", "application/json") 53 | 54 | response, err := s.App.Test(request) 55 | s.Assert().NoError(err) 56 | s.Assert().Equal(http.StatusBadRequest, response.StatusCode) 57 | 58 | bytes, err := io.ReadAll(response.Body) 59 | s.Assert().NoError(err) 60 | 61 | responseBody := make(map[string]any) 62 | err = json.Unmarshal(bytes, &responseBody) 63 | s.Assert().NoError(err) 64 | 65 | s.Assert().NotEmpty(responseBody["errors"]) 66 | } 67 | 68 | func (s *e2eTestSuite) TestUserRegisterFailedUserAlreadyExists() { 69 | s.TestUserRegisterSuccess() 70 | 71 | requestBody := &model.RegisterUserRequest{ 72 | Name: "John Doe", 73 | Username: "johndoe", 74 | Password: "johndoe123", 75 | } 76 | 77 | bodyJSON, err := json.Marshal(requestBody) 78 | s.Assert().NoError(err) 79 | 80 | request := httptest.NewRequest(http.MethodPost, "/api/users", strings.NewReader(string(bodyJSON))) 81 | request.Header.Add("content-type", "application/json") 82 | 83 | response, err := s.App.Test(request) 84 | s.Assert().NoError(err) 85 | s.Assert().Equal(http.StatusBadRequest, response.StatusCode) 86 | 87 | bytes, err := io.ReadAll(response.Body) 88 | s.Assert().NoError(err) 89 | 90 | responseBody := make(map[string]any) 91 | err = json.Unmarshal(bytes, &responseBody) 92 | s.Assert().NoError(err) 93 | 94 | s.Assert().NotEmpty(responseBody["errors"]) 95 | } 96 | 97 | func (s *e2eTestSuite) TestUserLoginSuccess() { 98 | s.TestUserRegisterSuccess() 99 | 100 | requestBody := &model.LoginUserRequest{ 101 | Username: "johndoe", 102 | Password: "johndoe123", 103 | } 104 | 105 | bodyJSON, err := json.Marshal(requestBody) 106 | s.Assert().NoError(err) 107 | 108 | request := httptest.NewRequest(http.MethodPost, "/api/users/_login", strings.NewReader(string(bodyJSON))) 109 | request.Header.Add("content-type", "application/json") 110 | 111 | response, err := s.App.Test(request) 112 | s.Assert().NoError(err) 113 | s.Assert().Equal(http.StatusOK, response.StatusCode) 114 | 115 | bytes, err := io.ReadAll(response.Body) 116 | s.Assert().NoError(err) 117 | 118 | responseBody := new(model.WebResponse[*model.TokenResponse]) 119 | err = json.Unmarshal(bytes, responseBody) 120 | s.Assert().NoError(err) 121 | 122 | s.Assert().NotEmpty(responseBody.Data.AccessToken) 123 | s.Assert().Equal("Bearer", responseBody.Data.TokenType) 124 | } 125 | 126 | func (s *e2eTestSuite) TestUserLoginFailedUserNotFound() { 127 | s.TestUserRegisterSuccess() 128 | 129 | requestBody := &model.LoginUserRequest{ 130 | Username: "wrongjohndoe", 131 | Password: "johndoe123", 132 | } 133 | 134 | bodyJSON, err := json.Marshal(requestBody) 135 | s.Assert().NoError(err) 136 | 137 | request := httptest.NewRequest(http.MethodPost, "/api/users/_login", strings.NewReader(string(bodyJSON))) 138 | request.Header.Add("content-type", "application/json") 139 | 140 | response, err := s.App.Test(request) 141 | s.Assert().NoError(err) 142 | s.Assert().Equal(http.StatusNotFound, response.StatusCode) 143 | 144 | bytes, err := io.ReadAll(response.Body) 145 | s.Assert().NoError(err) 146 | 147 | responseBody := make(map[string]any) 148 | err = json.Unmarshal(bytes, &responseBody) 149 | s.Assert().NoError(err) 150 | 151 | s.Assert().NotEmpty(responseBody["errors"]) 152 | } 153 | 154 | func (s *e2eTestSuite) TestUserLoginFailedPasswordNotMatch() { 155 | s.TestUserRegisterSuccess() 156 | 157 | requestBody := &model.LoginUserRequest{ 158 | Username: "johndoe", 159 | Password: "wrongpassword", 160 | } 161 | 162 | bodyJSON, err := json.Marshal(requestBody) 163 | s.Assert().NoError(err) 164 | 165 | request := httptest.NewRequest(http.MethodPost, "/api/users/_login", strings.NewReader(string(bodyJSON))) 166 | request.Header.Add("content-type", "application/json") 167 | 168 | response, err := s.App.Test(request) 169 | s.Assert().NoError(err) 170 | s.Assert().Equal(http.StatusBadRequest, response.StatusCode) 171 | 172 | bytes, err := io.ReadAll(response.Body) 173 | s.Assert().NoError(err) 174 | 175 | responseBody := make(map[string]any) 176 | err = json.Unmarshal(bytes, &responseBody) 177 | s.Assert().NoError(err) 178 | 179 | s.Assert().NotEmpty(responseBody["errors"]) 180 | } 181 | 182 | func (s *e2eTestSuite) TestUserLoginFailedValidation() { 183 | s.TestUserRegisterSuccess() 184 | 185 | requestBody := &model.LoginUserRequest{ 186 | Username: "", 187 | Password: "", 188 | } 189 | 190 | bodyJSON, err := json.Marshal(requestBody) 191 | s.Assert().NoError(err) 192 | 193 | request := httptest.NewRequest(http.MethodPost, "/api/users/_login", strings.NewReader(string(bodyJSON))) 194 | request.Header.Add("content-type", "application/json") 195 | 196 | response, err := s.App.Test(request) 197 | s.Assert().NoError(err) 198 | s.Assert().Equal(http.StatusBadRequest, response.StatusCode) 199 | 200 | bytes, err := io.ReadAll(response.Body) 201 | s.Assert().NoError(err) 202 | 203 | responseBody := make(map[string]any) 204 | err = json.Unmarshal(bytes, &responseBody) 205 | s.Assert().NoError(err) 206 | 207 | s.Assert().NotEmpty(responseBody["errors"]) 208 | } 209 | 210 | func (s *e2eTestSuite) TestUserCurrentSuccess() { 211 | token := s.GetTokenUser() 212 | 213 | request := httptest.NewRequest(http.MethodGet, "/api/users/_current", nil) 214 | request.Header.Add("content-type", "application/json") 215 | request.Header.Add("Authorization", "Bearer "+token) 216 | 217 | response, err := s.App.Test(request) 218 | s.Assert().NoError(err) 219 | s.Assert().Equal(http.StatusOK, response.StatusCode) 220 | 221 | bytes, err := io.ReadAll(response.Body) 222 | s.Assert().NoError(err) 223 | 224 | responseBody := new(model.WebResponse[*model.UserResponse]) 225 | err = json.Unmarshal(bytes, responseBody) 226 | s.Assert().NoError(err) 227 | 228 | s.Assert().Equal("John Doe", responseBody.Data.Name) 229 | s.Assert().Equal("johndoe", responseBody.Data.Username) 230 | s.Assert().NotEmpty(responseBody.Data.CreatedAt) 231 | s.Assert().NotEmpty(responseBody.Data.UpdatedAt) 232 | } 233 | 234 | func (s *e2eTestSuite) TestUserCurrentFailedUnauthorized() { 235 | request := httptest.NewRequest(http.MethodGet, "/api/users/_current", nil) 236 | request.Header.Add("content-type", "application/json") 237 | 238 | response, err := s.App.Test(request) 239 | s.Assert().NoError(err) 240 | s.Assert().Equal(http.StatusUnauthorized, response.StatusCode) 241 | 242 | bytes, err := io.ReadAll(response.Body) 243 | s.Assert().NoError(err) 244 | 245 | responseBody := make(map[string]any) 246 | err = json.Unmarshal(bytes, &responseBody) 247 | s.Assert().NoError(err) 248 | 249 | s.Assert().NotEmpty(responseBody["errors"]) 250 | } 251 | 252 | func (s *e2eTestSuite) TestUserUpdateSuccess() { 253 | token := s.GetTokenUser() 254 | 255 | requestBody := &model.UpdateUserRequest{ 256 | Name: "John Doe Update", 257 | Password: "johndoeupdate", 258 | } 259 | 260 | bodyJSON, err := json.Marshal(requestBody) 261 | s.Assert().NoError(err) 262 | 263 | request := httptest.NewRequest(http.MethodPatch, "/api/users/_current", strings.NewReader(string(bodyJSON))) 264 | request.Header.Add("content-type", "application/json") 265 | request.Header.Add("Authorization", "Bearer "+token) 266 | 267 | response, err := s.App.Test(request) 268 | s.Assert().NoError(err) 269 | s.Assert().Equal(http.StatusOK, response.StatusCode) 270 | 271 | bytes, err := io.ReadAll(response.Body) 272 | s.Assert().NoError(err) 273 | 274 | responseBody := new(model.WebResponse[*model.UserResponse]) 275 | err = json.Unmarshal(bytes, responseBody) 276 | s.Assert().NoError(err) 277 | 278 | s.Assert().Equal(requestBody.Name, responseBody.Data.Name) 279 | } 280 | 281 | func (s *e2eTestSuite) TestUserUpdateFailedUnauthorized() { 282 | requestBody := &model.UpdateUserRequest{ 283 | Name: "John Doe Update", 284 | Password: "johndoeupdate", 285 | } 286 | 287 | bodyJSON, err := json.Marshal(requestBody) 288 | s.Assert().NoError(err) 289 | 290 | request := httptest.NewRequest(http.MethodPatch, "/api/users/_current", strings.NewReader(string(bodyJSON))) 291 | request.Header.Add("content-type", "application/json") 292 | 293 | response, err := s.App.Test(request) 294 | s.Assert().NoError(err) 295 | s.Assert().Equal(http.StatusUnauthorized, response.StatusCode) 296 | 297 | bytes, err := io.ReadAll(response.Body) 298 | s.Assert().NoError(err) 299 | 300 | responseBody := make(map[string]any) 301 | err = json.Unmarshal(bytes, &responseBody) 302 | s.Assert().NoError(err) 303 | 304 | s.Assert().NotEmpty(responseBody["errors"]) 305 | } 306 | 307 | func (s *e2eTestSuite) GetTokenUser() string { 308 | s.TestUserRegisterSuccess() 309 | tokenResponse, err := s.UserUsecase.Login(context.Background(), &model.LoginUserRequest{Username: "johndoe", Password: "johndoe123"}) 310 | s.Assert().NoError(err) 311 | 312 | return tokenResponse.AccessToken 313 | } 314 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 3 | github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 4 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 5 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 6 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 7 | github.com/PuerkitoBio/purell v1.2.0 h1:/Jdm5QfyM8zdlqT6WVZU4cfP23sot6CEHA4CS49Ezig= 8 | github.com/PuerkitoBio/purell v1.2.0/go.mod h1:OhLRTaaIzhvIyofkJfB24gokC7tM42Px5UhoT32THBk= 9 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 10 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 11 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 12 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 13 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 14 | github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= 15 | github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 16 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 17 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 18 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 20 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 26 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 27 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 28 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 29 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 30 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 31 | github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= 32 | github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= 33 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 34 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 35 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 36 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 37 | github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= 38 | github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= 39 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 40 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 41 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 42 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 43 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 44 | github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= 45 | github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 46 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 47 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 48 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 49 | github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= 50 | github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 51 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 52 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 53 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 54 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 55 | github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= 56 | github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 57 | github.com/go-rel/rel v0.39.0 h1:2zmK8kazM82iRRfWX7+mm1MxDkGKDj2W+xJLjguli5U= 58 | github.com/go-rel/rel v0.39.0/go.mod h1:yN6+aimHyRIzbuWFe5DaxiZPuVuPfd7GlLpy/YTqTUg= 59 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 60 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 61 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 62 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 63 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 64 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 65 | github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc= 66 | github.com/gofiber/fiber/v2 v2.47.0 h1:EN5lHVCc+Pyqh5OEsk8fzRiifgwpbrP0rulQ4iNf3fs= 67 | github.com/gofiber/fiber/v2 v2.47.0/go.mod h1:mbFMVN1lQuzziTkkakgtKKdjfsXSw9BKR5lmcNksUoU= 68 | github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0= 69 | github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8= 70 | github.com/gofiber/swagger v0.1.12 h1:1Son/Nc1teiIftsVu6UHqXnJ3uf31pUzZO6XQDx3QYs= 71 | github.com/gofiber/swagger v0.1.12/go.mod h1:iOCNEt1gNTtlvCEKoxYX4agnZNtxlAjhujMKG6pmG74= 72 | github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= 73 | github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 74 | github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= 75 | github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= 76 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 77 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 78 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 79 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 80 | github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 81 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 82 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 83 | github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 84 | github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= 85 | github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= 86 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 87 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 88 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 89 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 90 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 91 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 92 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 93 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 94 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 95 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 96 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 97 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 98 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 99 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 100 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 101 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 102 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 103 | github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= 104 | github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 105 | github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= 106 | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 107 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 108 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 109 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 110 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 111 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 112 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 113 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 114 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 115 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 116 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 117 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 118 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 119 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 120 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 121 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 122 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 123 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 124 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 125 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 126 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 127 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 128 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 129 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 130 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 131 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 132 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 133 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 134 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 135 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 136 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 137 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 138 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 139 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 140 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 141 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 142 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 143 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 144 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 145 | github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= 146 | github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 147 | github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 148 | github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= 149 | github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 150 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 151 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 152 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 153 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 154 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 155 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 156 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 157 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 158 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 159 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 160 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 161 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 162 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 163 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 164 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 165 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 166 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= 167 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8= 168 | github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= 169 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= 170 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= 171 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE= 172 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= 173 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 174 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 175 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 176 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 177 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 178 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 179 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 180 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 181 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 182 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 183 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 184 | github.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM= 185 | github.com/spf13/viper v1.18.1/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 186 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 187 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 188 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 189 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 190 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 191 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 192 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 193 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 194 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 195 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 196 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 197 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 198 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 199 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 200 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 201 | github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= 202 | github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= 203 | github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= 204 | github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= 205 | github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= 206 | github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto= 207 | github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= 208 | github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= 209 | github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= 210 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 211 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 212 | github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= 213 | github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 214 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 215 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 216 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 217 | github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c= 218 | github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 219 | github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc= 220 | github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 221 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 222 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 223 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 224 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 225 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 226 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 227 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 228 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 229 | go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU= 230 | go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= 231 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 232 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 233 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= 234 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 235 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 236 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 237 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 238 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 239 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 240 | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= 241 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 242 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 243 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 244 | golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= 245 | golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 246 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 247 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 248 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 249 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 250 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 251 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 252 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 253 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 254 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 255 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 256 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 257 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 258 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 259 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 260 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 261 | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= 262 | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 263 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 264 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 265 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 266 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 267 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 268 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 269 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 270 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 271 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 272 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 273 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 274 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 275 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 276 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 277 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 278 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 279 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 280 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 281 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 282 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 283 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= 284 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 285 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 286 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 287 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 288 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 289 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 290 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 291 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 292 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 293 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 294 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 295 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 296 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 297 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 298 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 299 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 300 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 301 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 302 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 303 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 304 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 305 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 306 | golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 307 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 308 | golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 309 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 310 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 311 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 312 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 313 | golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= 314 | golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= 315 | golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 316 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 317 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 318 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 319 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 320 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 321 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 322 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 323 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 324 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 325 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 326 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 327 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 328 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 329 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 330 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 331 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 332 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 333 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 334 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 335 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 336 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 337 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 338 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 339 | gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= 340 | gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= 341 | gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= 342 | gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= 343 | gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 344 | gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 345 | gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= 346 | gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 347 | gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= 348 | gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 349 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 350 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 351 | --------------------------------------------------------------------------------