From e408ce73ddae5b646b331ff02638396ffbab79ab Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Tue, 23 Jan 2024 13:49:00 +0700 Subject: [PATCH 01/28] init -1 --- application/.gitignore | 4 - application/app.go | 122 -- application/development_router.go | 44 - application/role_permission_router.go | 54 - application/user_management_router.go | 33 - application/user_router.go | 50 - controller/development/dev_controller.go | 238 --- controller/development/dev_controller_test.go | 103 -- .../permission/permission_controller.go | 170 -- .../permission/permission_controller_test.go | 553 ------ controller/role/role_controller.go | 219 --- controller/role/role_controller_test.go | 760 --------- controller/user/user_controller.go | 339 ---- controller/user/user_controller_test.go | 1508 ----------------- .../user_management_controller.go | 167 -- .../user_management_controller_test.go | 554 ------ database/migration/main.go | 32 +- .../Gost Project Docs.postman_collection.json | 1419 ---------------- docs/service.txt | 12 - domain/base/request.go | 9 - domain/base/response.go | 13 - domain/base/time_test.go | 47 - domain/entity/all_entities.go | 20 +- domain/entity/all_entities_test.go | 48 - domain/{base/time.go => entity/base.go} | 13 +- domain/entity/role.go | 15 + domain/entity/role_permission.go | 48 - domain/entity/user.go | 23 +- domain/model/base.go | 21 + domain/model/role.go | 21 + domain/model/role_permission.go | 41 - domain/model/user.go | 16 +- domain/model/user_management.go | 21 - go.mod | 5 - go.sum | 16 - internal/helper/helper.go | 17 - internal/helper/helper_test.go | 12 - internal/rbac/permission.go | 78 - internal/rbac/rbac_test.go | 48 - internal/rbac/role.go | 36 - internal/role/role.go | 26 + main.go | 9 - .../permission/permission_repository.go | 136 -- .../permission/permission_repository_test.go | 396 ----- repository/role/role_repository.go | 177 -- repository/role/role_repository_test.go | 454 ----- repository/user/user_repository.go | 183 -- repository/user/user_repository_test.go | 550 ------ scripts/generate_keys.sh | 7 - scripts/update_module.sh | 9 - service/email/email_service.go | 69 - service/email/email_service_test.go | 37 - service/file/file_service.go | 242 --- service/permission/permission_service.go | 162 -- service/permission/permission_service_test.go | 146 -- service/role/role_service.go | 193 --- service/role/role_service_test.go | 214 --- service/user/user_service.go | 502 ------ service/user/user_service_test.go | 393 ----- .../user_management_service.go | 191 --- .../user_management_service_test.go | 139 -- 61 files changed, 127 insertions(+), 11057 deletions(-) delete mode 100644 application/.gitignore delete mode 100644 application/app.go delete mode 100644 application/development_router.go delete mode 100644 application/role_permission_router.go delete mode 100644 application/user_management_router.go delete mode 100644 application/user_router.go delete mode 100644 controller/development/dev_controller.go delete mode 100644 controller/development/dev_controller_test.go delete mode 100644 controller/permission/permission_controller.go delete mode 100644 controller/permission/permission_controller_test.go delete mode 100644 controller/role/role_controller.go delete mode 100644 controller/role/role_controller_test.go delete mode 100644 controller/user/user_controller.go delete mode 100644 controller/user/user_controller_test.go delete mode 100644 controller/user_management/user_management_controller.go delete mode 100644 controller/user_management/user_management_controller_test.go delete mode 100644 docs/Gost Project Docs.postman_collection.json delete mode 100644 docs/service.txt delete mode 100644 domain/base/request.go delete mode 100644 domain/base/response.go delete mode 100644 domain/base/time_test.go delete mode 100644 domain/entity/all_entities_test.go rename domain/{base/time.go => entity/base.go} (63%) create mode 100644 domain/entity/role.go delete mode 100644 domain/entity/role_permission.go create mode 100644 domain/model/base.go create mode 100644 domain/model/role.go delete mode 100644 domain/model/role_permission.go delete mode 100644 domain/model/user_management.go delete mode 100644 internal/rbac/permission.go delete mode 100644 internal/rbac/rbac_test.go delete mode 100644 internal/rbac/role.go create mode 100644 internal/role/role.go delete mode 100644 main.go delete mode 100644 repository/permission/permission_repository.go delete mode 100644 repository/permission/permission_repository_test.go delete mode 100644 repository/role/role_repository.go delete mode 100644 repository/role/role_repository_test.go delete mode 100644 repository/user/user_repository.go delete mode 100644 repository/user/user_repository_test.go delete mode 100644 scripts/generate_keys.sh delete mode 100644 scripts/update_module.sh delete mode 100644 service/email/email_service.go delete mode 100644 service/email/email_service_test.go delete mode 100644 service/file/file_service.go delete mode 100644 service/permission/permission_service.go delete mode 100644 service/permission/permission_service_test.go delete mode 100644 service/role/role_service.go delete mode 100644 service/role/role_service_test.go delete mode 100644 service/user/user_service.go delete mode 100644 service/user/user_service_test.go delete mode 100644 service/user_management/user_management_service.go delete mode 100644 service/user_management/user_management_service_test.go diff --git a/application/.gitignore b/application/.gitignore deleted file mode 100644 index e160c35..0000000 --- a/application/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/log -/logs -*.log -*.logs \ No newline at end of file diff --git a/application/app.go b/application/app.go deleted file mode 100644 index 191558e..0000000 --- a/application/app.go +++ /dev/null @@ -1,122 +0,0 @@ -// 📌 Origin Github Repository: https://github.com/Lukmanerngost - -// 🔍 README -// Application package configures middleware, error management, and -// handles OS signals for gracefully stopping the server when receiving -// an interrupt signal. This package provides routes related to user -// management and role-based access control (RBAC). And so on. - -package application - -import ( - "errors" - "fmt" - "log" - "os" - "os/signal" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/logger" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/internal/env" -) - -var ( - port int - - // Create a new fiber instance with custom config - router = fiber.New(fiber.Config{ - AppName: "Gost Project", - // Override default error handler - ErrorHandler: func(ctx *fiber.Ctx, err error) error { - // Status code defaults to 500 - code := fiber.StatusInternalServerError - - // Retrieve the custom status code - // if it's a *fiber.Error - var e *fiber.Error - if errors.As(err, &e) { - code = e.Code - } - - // Send custom error page - err = ctx.Status(code).JSON(fiber.Map{ - "message": e.Message, - }) - if err != nil { - return ctx.Status(fiber.StatusInternalServerError). - SendString("Internal Server Error") - } - return nil - }, - // memory management - // ReduceMemoryUsage: true, - // ReadBufferSize: 5120, - }) -) - -func setup() { - // Check env and database - env.ReadConfig("./.env") - config := env.Configuration() - privKey := config.GetPrivateKey() - pubKey := config.GetPublicKey() - if privKey == nil || pubKey == nil { - log.Fatal("private and public keys are not valid or not found") - } - port = config.AppPort - - connector.LoadDatabase() - connector.LoadRedisCache() -} - -func RunApp() { - setup() - router.Use(cors.New(cors.Config{ - AllowCredentials: true, - })) - router.Use(logger.New()) - // Custom File Writer - _ = os.MkdirAll("./log", os.ModePerm) - fileName := fmt.Sprintf("./log/%s.log", time.Now().Format("20060102")) - file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - log.Fatalf("error opening file: %v", err) - } - defer file.Close() - router.Use(logger.New(logger.Config{ - Output: file, - })) - - // Create channel for idle connections. - idleConnsClosed := make(chan struct{}) - - go func() { - sigint := make(chan os.Signal, 1) - signal.Notify(sigint, os.Interrupt) // Catch OS signals. - <-sigint - - // Received an interrupt signal, shutdown. - // ctrl+c - if err := router.Shutdown(); err != nil { - // Error from closing listeners, or context timeout: - log.Printf("Oops... Server is not shutting down! Reason: %v", err) - } - - close(idleConnsClosed) - }() - - getUserManagementRoutes(router) // user CRUD without auth ⚠️ - getDevopmentRouter(router) // experimental without auth ⚠️ - getUserRoutes(router) // user with auth - getRolePermissionRoutes(router) // RBAC CRUD with auth - - if err := router.Listen(fmt.Sprintf(":%d", port)); err != nil { - log.Printf("Oops... Server is not running! Reason: %v", err) - } - - <-idleConnsClosed -} diff --git a/application/development_router.go b/application/development_router.go deleted file mode 100644 index a79253e..0000000 --- a/application/development_router.go +++ /dev/null @@ -1,44 +0,0 @@ -// 📌 Origin Github Repository: https://github.com/Lukmanerngost - -// 🔍 README -// Development Routes provides experimental/ developing/ testing -// for routes, middleware, connection and many more without JWT -// authentication in header. ⚠️ So, don't forget to commented -// on the line of code that routes getDevopmentRouter -// in the app.go file. - -package application - -import ( - "github.com/gofiber/fiber/v2" - - controller "github.com/Lukmanern/gost/controller/development" -) - -var ( - devController controller.DevController -) - -func getDevopmentRouter(router fiber.Router) { - devController = controller.NewDevControllerImpl() - // Developement 'helper' Process - devRouter := router.Group("development") - devRouter.Get("ping/db", devController.PingDatabase) - devRouter.Get("ping/redis", devController.PingRedis) - devRouter.Get("panic", devController.Panic) - devRouter.Get("storing-to-redis", devController.StoringToRedis) - devRouter.Get("get-from-redis", devController.GetFromRedis) - devRouter.Post("upload-file", devController.UploadFile) - devRouter.Post("get-files-list", devController.GetFilesList) - devRouter.Delete("remove-file", devController.RemoveFile) - devRouter.Get("test-new", devController.FakeHandler) - - // you should create new role named new-role-001 and new permission - // named new-permission-001 from RBAC-endpoints to test these endpoints - // jwtHandler := middleware.NewJWTHandler() - // devRouterAuth := devRouter.Use(jwtHandler.IsAuthenticated) - // devRouterAuth.Get("test-new-role", - // jwtHandler.CheckHasRole("new-role-001"), devController.CheckNewRole) - // devRouterAuth.Get("test-new-permission", - // jwtHandler.CheckHasPermission(21), devController.CheckNewPermission) -} diff --git a/application/role_permission_router.go b/application/role_permission_router.go deleted file mode 100644 index 168df60..0000000 --- a/application/role_permission_router.go +++ /dev/null @@ -1,54 +0,0 @@ -// 📌 Origin Github Repository: https://github.com/Lukmanerngost - -// 🔍 README -// Role-Permission Routes provides des create, read (get & getAll), update, and -// delete functionalities for Role and Permission entities including connecting -// both of them. This routes can be access by user that has admin-role (see database/migration). - -package application - -import ( - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/internal/middleware" - "github.com/Lukmanern/gost/internal/rbac" - - permCtr "github.com/Lukmanern/gost/controller/permission" - roleCtr "github.com/Lukmanern/gost/controller/role" - permSvc "github.com/Lukmanern/gost/service/permission" - roleSvc "github.com/Lukmanern/gost/service/role" -) - -var ( - roleService roleSvc.RoleService - roleController roleCtr.RoleController - - permissionService permSvc.PermissionService - permissionController permCtr.PermissionController -) - -func getRolePermissionRoutes(router fiber.Router) { - jwtHandler := middleware.NewJWTHandler() - - permissionService = permSvc.NewPermissionService() - permissionController = permCtr.NewPermissionController(permissionService) - permissionRouter := router.Group("permission").Use(jwtHandler.IsAuthenticated) - - // create-permission is unused - permissionRouter.Post("", jwtHandler.CheckHasPermission(rbac.PermCreatePermission.ID), permissionController.Create) - permissionRouter.Get("", jwtHandler.CheckHasPermission(rbac.PermViewPermission.ID), permissionController.GetAll) - permissionRouter.Get(":id", jwtHandler.CheckHasPermission(rbac.PermViewPermission.ID), permissionController.Get) - permissionRouter.Put(":id", jwtHandler.CheckHasPermission(rbac.PermUpdatePermission.ID), permissionController.Update) - permissionRouter.Delete(":id", jwtHandler.CheckHasPermission(rbac.PermDeletePermission.ID), permissionController.Delete) - - roleService = roleSvc.NewRoleService(permissionService) - roleController = roleCtr.NewRoleController(roleService) - roleRouter := router.Group("role").Use(jwtHandler.IsAuthenticated) - - roleRouter.Post("", jwtHandler.CheckHasPermission(rbac.PermCreateRole.ID), roleController.Create) - roleRouter.Post("connect", jwtHandler.CheckHasPermission(rbac.PermCreateRole.ID), roleController.Connect) - roleRouter.Get("", jwtHandler.CheckHasPermission(rbac.PermViewRole.ID), roleController.GetAll) - roleRouter.Get(":id", jwtHandler.CheckHasPermission(rbac.PermViewRole.ID), roleController.Get) - roleRouter.Put(":id", jwtHandler.CheckHasPermission(rbac.PermUpdateRole.ID), roleController.Update) - roleRouter.Delete(":id", jwtHandler.CheckHasPermission(rbac.PermDeleteRole.ID), roleController.Delete) -} diff --git a/application/user_management_router.go b/application/user_management_router.go deleted file mode 100644 index 9110d57..0000000 --- a/application/user_management_router.go +++ /dev/null @@ -1,33 +0,0 @@ -// 📌 Origin Github Repository: https://github.com/Lukmanerngost - -// 🔍 README -// User Management Routes provides create, read (get & getAll), update, and -// delete functionalities for user data management without JWT authentication -// in header. ⚠️ So, don't forget to commented on the line of code that routes -// getUserManagementRoutes in the app.go file. - -package application - -import ( - "github.com/gofiber/fiber/v2" - - controller "github.com/Lukmanern/gost/controller/user_management" - service "github.com/Lukmanern/gost/service/user_management" -) - -var ( - userDevService service.UserManagementService - userDevController controller.UserManagementController -) - -func getUserManagementRoutes(router fiber.Router) { - userDevService = service.NewUserManagementService() - userDevController = controller.NewUserManagementController(userDevService) - - userDevRoute := router.Group("user-management") - userDevRoute.Post("create", userDevController.Create) - userDevRoute.Get("", userDevController.GetAll) - userDevRoute.Get(":id", userDevController.Get) - userDevRoute.Put(":id", userDevController.Update) - userDevRoute.Delete(":id", userDevController.Delete) -} diff --git a/application/user_router.go b/application/user_router.go deleted file mode 100644 index 66bc972..0000000 --- a/application/user_router.go +++ /dev/null @@ -1,50 +0,0 @@ -// 📌 Origin Github Repository: https://github.com/Lukmanerngost - -// 🔍 README -// User Routes provides some features and action that user can use. -// User Routes provide the typical web application authentication flow, -// such as registration, sending verification codes, and verifying accounts -// with a verification code. - -package application - -import ( - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/internal/middleware" - - controller "github.com/Lukmanern/gost/controller/user" - service "github.com/Lukmanern/gost/service/user" - - permSvc "github.com/Lukmanern/gost/service/permission" - roleSvc "github.com/Lukmanern/gost/service/role" -) - -var ( - userPermService permSvc.PermissionService - userRoleService roleSvc.RoleService - userService service.UserService - userController controller.UserController -) - -func getUserRoutes(router fiber.Router) { - userPermService = permSvc.NewPermissionService() - userRoleService = roleSvc.NewRoleService(userPermService) - userService = service.NewUserService(userRoleService) - userController = controller.NewUserController(userService) - jwtHandler := middleware.NewJWTHandler() - - userRoute := router.Group("user") - userRoute.Post("login", userController.Login) - userRoute.Post("register", userController.Register) - userRoute.Post("verification", userController.AccountActivation) - userRoute.Post("request-delete", userController.DeleteAccountActivation) - userRoute.Post("forget-password", userController.ForgetPassword) - userRoute.Post("reset-password", userController.ResetPassword) - - userRouteAuth := userRoute.Use(jwtHandler.IsAuthenticated) - userRouteAuth.Post("logout", userController.Logout) - userRouteAuth.Get("my-profile", userController.MyProfile) - userRouteAuth.Put("profile-update", userController.UpdateProfile) - userRouteAuth.Post("update-password", userController.UpdatePassword) -} diff --git a/controller/development/dev_controller.go b/controller/development/dev_controller.go deleted file mode 100644 index 724d038..0000000 --- a/controller/development/dev_controller.go +++ /dev/null @@ -1,238 +0,0 @@ -package controller - -import ( - "fmt" - "sync" - "time" - - "github.com/go-playground/validator/v10" - "github.com/go-redis/redis" - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/response" - - fileService "github.com/Lukmanern/gost/service/file" -) - -type DevController interface { - // PingDatabase func Ping database 5 times - PingDatabase(c *fiber.Ctx) error - - // PingRedis func Ping redis 5 times - PingRedis(c *fiber.Ctx) error - - // Panic func handles panic with defer func - Panic(c *fiber.Ctx) error - - // StoringToRedis func stores data{key:value} to redis - StoringToRedis(c *fiber.Ctx) error - - // GetFromRedis func gets data from redis - GetFromRedis(c *fiber.Ctx) error - - // CheckNewRole func gives result for checking - // middleware for new role - CheckNewRole(c *fiber.Ctx) error - - // CheckNewPermission func gives result for - // checking middleware for new permission - CheckNewPermission(c *fiber.Ctx) error - - // UploadFile func uploads a new file into Supabase Bucket - // See : https://supabase.com/docs/guides/storage - UploadFile(c *fiber.Ctx) error - - // RemoveFile func removes file from Supabase Bucket - // See : https://supabase.com/docs/guides/storage - RemoveFile(c *fiber.Ctx) error - - // GetFilesList func gets list file/s from Supabase Bucket - // See : https://supabase.com/docs/guides/storage - GetFilesList(c *fiber.Ctx) error - - // FakeHandler sends string to client - FakeHandler(c *fiber.Ctx) error -} - -type DevControllerImpl struct { - fileSvc fileService.FileService - redis *redis.Client - db *gorm.DB -} - -var ( - devImpl *DevControllerImpl - devImplOnce sync.Once -) - -func NewDevControllerImpl() DevController { - devImplOnce.Do(func() { - devImpl = &DevControllerImpl{ - fileSvc: fileService.NewFileService(), - redis: connector.LoadRedisCache(), - db: connector.LoadDatabase(), - } - }) - - return devImpl -} - -func (ctr *DevControllerImpl) PingDatabase(c *fiber.Ctx) error { - db := ctr.db - if db == nil { - return response.Error(c, "failed db is nil") - } - sqldb, sqlErr := db.DB() - if sqlErr != nil { - return response.Error(c, "failed get sql-db") - } - for i := 0; i < 5; i++ { - pingErr := sqldb.Ping() - if pingErr != nil { - return response.Error(c, "failed to ping-sql-db") - } - } - - return response.CreateResponse(c, fiber.StatusOK, true, "success ping-sql-db", nil) -} - -func (ctr *DevControllerImpl) PingRedis(c *fiber.Ctx) error { - redis := ctr.redis - if redis == nil { - return response.Error(c, constants.RedisNil) - } - for i := 0; i < 5; i++ { - status := redis.Ping() - if status.Err() != nil { - return response.Error(c, "failed to ping-redis") - } - } - - return response.CreateResponse(c, fiber.StatusOK, true, "success ping-redis", nil) -} - -func (ctr *DevControllerImpl) Panic(c *fiber.Ctx) error { - defer func() error { - r := recover() - if r != nil { - message := "message panic: " + r.(string) - return response.Error(c, message) - } - return nil - }() - panic("your panic message") // should string -} - -func (ctr *DevControllerImpl) StoringToRedis(c *fiber.Ctx) error { - redis := ctr.redis - if redis == nil { - return response.Error(c, constants.RedisNil) - } - redisStatus := redis.Set("example-key", "example-value", 50*time.Minute) - if redisStatus.Err() != nil { - message := fmt.Sprintf("redis status error (%s)", redisStatus.Err().Error()) - return response.Error(c, message) - } - - return response.SuccessCreated(c, nil) -} - -func (ctr *DevControllerImpl) GetFromRedis(c *fiber.Ctx) error { - redis := ctr.redis - if redis == nil { - return response.Error(c, constants.RedisNil) - } - redisStatus := redis.Get("example-key") - if redisStatus.Err() != nil { - message := fmt.Sprintf("redis status error (%s)", redisStatus.Err().Error()) - return response.Error(c, message) - } - res, resErr := redisStatus.Result() - if resErr != nil { - message := fmt.Sprintf("redis result error (%s)", resErr.Error()) - return response.Error(c, message) - } - - return response.SuccessLoaded(c, res) -} - -func (ctr *DevControllerImpl) CheckNewRole(c *fiber.Ctx) error { - return response.CreateResponse(c, fiber.StatusOK, true, "success check new role", nil) -} - -func (ctr *DevControllerImpl) CheckNewPermission(c *fiber.Ctx) error { - return response.CreateResponse(c, fiber.StatusOK, true, "success check new permission", nil) -} - -func (ctr *DevControllerImpl) UploadFile(c *fiber.Ctx) error { - file, err := c.FormFile("file") - if err != nil { - return response.BadRequest(c, "failed to parse form file: "+err.Error()) - } - if file == nil { - return response.BadRequest(c, "file is nil or not found") - } - mimeType := file.Header.Get(fiber.HeaderContentType) - if mimeType != "application/pdf" { - return response.BadRequest(c, "only PDF file are allowed for upload") - } - maxSize := int64(3 * 1024 * 1024) // 3MB in bytes - if file.Size > maxSize { - return response.BadRequest(c, "file size exceeds the maximum allowed (3MB)") - } - - fileURL, uploadErr := ctr.fileSvc.UploadFile(file) - if uploadErr != nil { - fiberErr, ok := uploadErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+uploadErr.Error()) - } - return response.SuccessCreated(c, map[string]any{ - "file_url": fileURL, - }) -} - -func (ctr *DevControllerImpl) RemoveFile(c *fiber.Ctx) error { - var fileName struct { - FileName string `validate:"required,min=4,max=150" json:"file_name"` - } - if err := c.BodyParser(&fileName); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&fileName); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - removeErr := ctr.fileSvc.RemoveFile(fileName.FileName) - if removeErr != nil { - fiberErr, ok := removeErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+removeErr.Error()) - } - return response.SuccessNoContent(c) -} - -func (ctr *DevControllerImpl) GetFilesList(c *fiber.Ctx) error { - resp, getErr := ctr.fileSvc.GetFilesList() - if getErr != nil { - fiberErr, ok := getErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+getErr.Error()) - } - return response.SuccessLoaded(c, resp) -} - -func (ctr *DevControllerImpl) FakeHandler(c *fiber.Ctx) error { - message := "success create one new endpoint" - return response.CreateResponse(c, fiber.StatusOK, true, message, nil) -} diff --git a/controller/development/dev_controller_test.go b/controller/development/dev_controller_test.go deleted file mode 100644 index ea08ea7..0000000 --- a/controller/development/dev_controller_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Don't run test per file without -p 1 -// or simply run test per func or run -// project test using make test command -// check Makefile file -package controller - -import ( - "net/http" - "testing" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" - "github.com/gofiber/fiber/v2" -) - -type handlerF = func(c *fiber.Ctx) error - -func init() { - // Check env and database - env.ReadConfig("./../../.env") - - connector.LoadDatabase() - connector.LoadRedisCache() -} - -func TestNewDevControllerImpl(t *testing.T) { - ctr := NewDevControllerImpl() - c := helper.NewFiberCtx() - if ctr == nil || c == nil { - t.Error("should not error") - } - - pingDbErr := ctr.PingDatabase(c) - if pingDbErr != nil { - t.Error("err: ", pingDbErr) - } - - pingRedisErr := ctr.PingRedis(c) - if pingRedisErr != nil { - t.Error("err: ", pingRedisErr) - } - - panicErr := ctr.Panic(c) - if panicErr != nil { - t.Error("err: ", panicErr) - } - - storingErr := ctr.StoringToRedis(c) - if storingErr != nil { - t.Error("err: ", storingErr) - } - - getErr := ctr.GetFromRedis(c) - if getErr != nil { - t.Error("err: ", getErr) - } - - checkRoleErr := ctr.CheckNewRole(c) - if checkRoleErr != nil { - t.Error("err: ", checkRoleErr) - } - - checkPermErr := ctr.CheckNewPermission(c) - if checkPermErr != nil { - t.Error("err: ", checkPermErr) - } -} - -func TestMethods(t *testing.T) { - c := helper.NewFiberCtx() - ctr := NewDevControllerImpl() - if ctr == nil || c == nil { - t.Error("should not nil") - } - - testCases := []struct { - caseName string - method handlerF - respCode int - }{ - {"PingDatabase", ctr.PingDatabase, http.StatusOK}, - {"PingRedis", ctr.PingRedis, http.StatusOK}, - {"Panic", ctr.Panic, http.StatusInternalServerError}, - {"StoringToRedis", ctr.StoringToRedis, http.StatusCreated}, - {"GetFromRedis", ctr.GetFromRedis, http.StatusOK}, - {"CheckNewRole", ctr.CheckNewRole, http.StatusOK}, - {"CheckNewPermission", ctr.CheckNewPermission, http.StatusOK}, - {"UploadFile", ctr.UploadFile, http.StatusBadRequest}, - {"RemoveFile", ctr.RemoveFile, http.StatusBadRequest}, - {"GetFilesList", ctr.GetFilesList, http.StatusOK}, - } - - for _, tc := range testCases { - c := helper.NewFiberCtx() - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - tc.method(c) - resp := c.Response() - if resp.StatusCode() != tc.respCode { - t.Errorf("Expected response code %d, but got %d on: %s", tc.respCode, resp.StatusCode(), tc.caseName) - } - } -} diff --git a/controller/permission/permission_controller.go b/controller/permission/permission_controller.go deleted file mode 100644 index 1a54d0c..0000000 --- a/controller/permission/permission_controller.go +++ /dev/null @@ -1,170 +0,0 @@ -package controller - -import ( - "math" - "sync" - - "github.com/go-playground/validator/v10" - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/response" - service "github.com/Lukmanern/gost/service/permission" -) - -type PermissionController interface { - // Create func creates a new permission - Create(c *fiber.Ctx) error - - // Get func gets a permission - Get(c *fiber.Ctx) error - - // GetAll func gets some permissions - GetAll(c *fiber.Ctx) error - - // Update func updates a permission - Update(c *fiber.Ctx) error - - // Delete func deletes a permission - Delete(c *fiber.Ctx) error -} - -type PermissionControllerImpl struct { - service service.PermissionService -} - -var ( - permissionControllerImpl *PermissionControllerImpl - permissionControllerImplOnce sync.Once -) - -func NewPermissionController(service service.PermissionService) PermissionController { - permissionControllerImplOnce.Do(func() { - permissionControllerImpl = &PermissionControllerImpl{ - service: service, - } - }) - return permissionControllerImpl -} - -func (ctr *PermissionControllerImpl) Create(c *fiber.Ctx) error { - var permission model.PermissionCreate - if err := c.BodyParser(&permission); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&permission); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - id, createErr := ctr.service.Create(ctx, permission) - if createErr != nil { - fiberErr, ok := createErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+createErr.Error()) - } - data := map[string]any{ - "id": id, - } - return response.SuccessCreated(c, data) -} - -func (ctr *PermissionControllerImpl) Get(c *fiber.Ctx) error { - id, err := c.ParamsInt("id") - if err != nil || id <= 0 { - return response.BadRequest(c, constants.InvalidID) - } - - ctx := c.Context() - permission, getErr := ctr.service.GetByID(ctx, id) - if getErr != nil { - fiberErr, ok := getErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+getErr.Error()) - } - return response.SuccessLoaded(c, permission) -} - -func (ctr *PermissionControllerImpl) GetAll(c *fiber.Ctx) error { - request := base.RequestGetAll{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 20), - Keyword: c.Query("search"), - Sort: c.Query("sort"), - } - if request.Page <= 0 || request.Limit <= 0 { - return response.BadRequest(c, "invalid page or limit value") - } - - ctx := c.Context() - permissions, total, getErr := ctr.service.GetAll(ctx, request) - if getErr != nil { - return response.Error(c, constants.ServerErr+getErr.Error()) - } - - data := make([]interface{}, len(permissions)) - for i := range permissions { - data[i] = permissions[i] - } - responseData := base.GetAllResponse{ - Meta: base.PageMeta{ - Total: total, - Pages: int(math.Ceil(float64(total) / float64(request.Limit))), - Page: request.Page, - }, - Data: data, - } - return response.SuccessLoaded(c, responseData) -} - -func (ctr *PermissionControllerImpl) Update(c *fiber.Ctx) error { - id, err := c.ParamsInt("id") - if err != nil || id <= 0 { - return response.BadRequest(c, constants.InvalidID) - } - var permission model.PermissionUpdate - permission.ID = id - if err := c.BodyParser(&permission); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&permission); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - updateErr := ctr.service.Update(ctx, permission) - if updateErr != nil { - fiberErr, ok := updateErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+updateErr.Error()) - } - return response.SuccessNoContent(c) -} - -func (ctr *PermissionControllerImpl) Delete(c *fiber.Ctx) error { - id, err := c.ParamsInt("id") - if err != nil || id <= 0 { - return response.BadRequest(c, constants.InvalidID) - } - - ctx := c.Context() - deleteErr := ctr.service.Delete(ctx, id) - if deleteErr != nil { - fiberErr, ok := deleteErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+deleteErr.Error()) - } - return response.SuccessNoContent(c) -} diff --git a/controller/permission/permission_controller_test.go b/controller/permission/permission_controller_test.go deleted file mode 100644 index 285fa0c..0000000 --- a/controller/permission/permission_controller_test.go +++ /dev/null @@ -1,553 +0,0 @@ -// Don't run test per file without -p 1 -// or simply run test per func or run -// project test using make test command -// check Makefile file -package controller - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" - "github.com/Lukmanern/gost/internal/middleware" - "github.com/Lukmanern/gost/internal/response" - - userController "github.com/Lukmanern/gost/controller/user" - userRepository "github.com/Lukmanern/gost/repository/user" - service "github.com/Lukmanern/gost/service/permission" - roleService "github.com/Lukmanern/gost/service/role" - userService "github.com/Lukmanern/gost/service/user" -) - -var ( - userRepo userRepository.UserRepository - permService service.PermissionService - permController PermissionController - appURL string -) - -func init() { - env.ReadConfig("./../../.env") - config := env.Configuration() - appURL = config.AppURL - - connector.LoadDatabase() - connector.LoadRedisCache() - - userRepo = userRepository.NewUserRepository() - permService = service.NewPermissionService() - permController = NewPermissionController(permService) -} - -func TestPermNewPermissionController(t *testing.T) { - permSvc := service.NewPermissionService() - permCtr := NewPermissionController(permSvc) - - if permSvc == nil || permCtr == nil { - t.Error(constants.ShouldNotNil) - } -} - -func TestPermCreate(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - userID, userToken := createUserAndToken() - if userID < 1 || len(userToken) < 2 { - t.Error("should more") - } - - defer func() { - userRepo.Delete(ctx, userID) - }() - - testCases := []struct { - caseName string - respCode int - payload model.PermissionCreate - token string // jwt into claims (fake claims) - }{ - { - caseName: "success create", - respCode: http.StatusCreated, - payload: model.PermissionCreate{ - Name: "example-permission-001", - Description: "example-description-of-permission-001", - }, - token: userToken, - }, - { - caseName: "failed create: name already used", - respCode: http.StatusBadRequest, - payload: model.PermissionCreate{ - Name: "example-permission-001", - Description: "example-description-of-permission-001", - }, - token: userToken, - }, - { - caseName: "failed create: name/desc too short", - respCode: http.StatusBadRequest, - payload: model.PermissionCreate{}, - token: userToken, - }, - } - - createdIDs := make([]float64, 0) - jwtHandler := middleware.NewJWTHandler() - for _, tc := range testCases { - c := helper.NewFiberCtx() - c.Request().Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.token)) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - requestBody, err := json.Marshal(tc.payload) - if err != nil { - t.Fatal("Error while serializing payload to request body") - } - c.Request().SetBody(requestBody) - - fakeClaims := jwtHandler.GenerateClaims(tc.token) - if fakeClaims != nil { - c.Locals("claims", fakeClaims) - } - ctr.Create(c) - resp := c.Response() - if resp.StatusCode() != tc.respCode { - t.Error("should equal, but got", resp.StatusCode()) - } - - if resp.StatusCode() == http.StatusCreated { - respBody := c.Response().Body() - respString := string(respBody) - respStruct := struct { - Message string `json:"message"` - Success bool `json:"success"` - Data map[string]any `json:"data"` - }{} - - err := json.Unmarshal([]byte(respString), &respStruct) - if err != nil { - t.Errorf("Failed to parse response JSON: %v", err) - } - if !respStruct.Success { - t.Error("Expected success") - } - if respStruct.Message != response.MessageSuccessCreated { - t.Error("Expected message to be equal") - } - if id, ok := respStruct.Data["id"].(float64); !ok || id < 1 { - t.Error("should be a positive integer") - } else { - createdIDs = append(createdIDs, id) - } - } - } - - for _, id := range createdIDs { - err := permService.Delete(ctx, int(id)) - if err != nil { - t.Error("deletingc created permission/s should not error") - } - } -} - -func TestPermGet(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - testCases := []struct { - caseName string - respCode int - permID int - }{ - { - caseName: "success get -1", - respCode: http.StatusOK, - permID: 1, - }, - { - caseName: "success get -2", - respCode: http.StatusOK, - permID: 1, - }, - { - caseName: "failed get: invalid id", - respCode: http.StatusBadRequest, - permID: -10, - }, - { - caseName: "failed get: data not found", - respCode: http.StatusNotFound, - permID: 9999, - }, - } - - for _, tc := range testCases { - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/permission/%d", tc.permID), nil) - app := fiber.New() - app.Get("/permission/:id", permController.Get) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, want", tc.respCode, "but got", resp.StatusCode) - } - if resp.StatusCode == http.StatusOK { - var respStruct struct { - Message string `json:"message"` - Success bool `json:"success"` - Data base.GetAllResponse `json:"data"` - } - err := json.NewDecoder(resp.Body).Decode(&respStruct) - if err != nil { - t.Errorf("Failed to parse response JSON: %v", err) - } - if !respStruct.Success { - t.Error("Expected success") - } - if respStruct.Message != response.MessageSuccessLoaded { - t.Error("Expected message to be equal") - } - } - } -} - -func TestPermGetAll(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - testCases := []struct { - caseName string - respCode int - payload base.RequestGetAll - }{ - { - caseName: "success get -1", - respCode: http.StatusOK, - payload: base.RequestGetAll{ - Limit: 10, - Page: 1, - }, - }, - { - caseName: "success get -2", - respCode: http.StatusOK, - payload: base.RequestGetAll{ - Limit: 100, - Page: 2, - }, - }, - { - caseName: "failed get: invalid payload", - respCode: http.StatusBadRequest, - payload: base.RequestGetAll{ - Limit: -1, - Page: -1, - }, - }, - } - - for _, tc := range testCases { - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/permission?page=%d&limit=%d", tc.payload.Page, tc.payload.Limit), nil) - app := fiber.New() - app.Get("/permission", permController.GetAll) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal") - } - if resp.StatusCode == http.StatusOK { - var respStruct struct { - Message string `json:"message"` - Success bool `json:"success"` - Data base.GetAllResponse `json:"data"` - } - err := json.NewDecoder(resp.Body).Decode(&respStruct) - if err != nil { - t.Errorf("Failed to parse response JSON: %v", err) - } - if !respStruct.Success { - t.Error("Expected success") - } - if respStruct.Message != response.MessageSuccessLoaded { - t.Error("Expected message to be equal") - } - if len(respStruct.Data.Data.([]any)) > tc.payload.Limit { - t.Error("should less or equal", len(respStruct.Data.Data.([]any))) - } - } - } -} - -func TestPermUpdate(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - // create 1 permission - permID, createErr := permService.Create(ctx, model.PermissionCreate{ - Name: strings.ToLower(helper.RandomString(12)), - Description: "description-of-example-permission-001", - }) - if createErr != nil || permID < 1 { - t.Fatal("should not error while creating permission") - } - - defer func() { - permService.Delete(ctx, permID) - }() - - testCases := []struct { - caseName string - respCode int - permID int - payload model.PermissionUpdate - }{ - { - caseName: "success update -1", - respCode: http.StatusNoContent, - permID: permID, - payload: model.PermissionUpdate{ - ID: permID, - Name: helper.RandomString(12), - Description: helper.RandomString(20), - }, - }, - { - caseName: "success update -2", - respCode: http.StatusNoContent, - permID: permID, - payload: model.PermissionUpdate{ - ID: permID, - Name: helper.RandomString(12), - Description: helper.RandomString(20), - }, - }, - { - caseName: "failed update: invalid name/description", - respCode: http.StatusBadRequest, - permID: permID, - payload: model.PermissionUpdate{ - ID: permID, - Name: "", - Description: "", - }, - }, - { - caseName: "failed update: invalid id", - respCode: http.StatusBadRequest, - permID: -10, - }, - { - caseName: "failed update: data not found", - respCode: http.StatusNotFound, - permID: permID + 99, - payload: model.PermissionUpdate{ - ID: permID + 99, - Name: helper.RandomString(12), - Description: helper.RandomString(20), - }, - }, - } - - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := fmt.Sprintf(appURL+"permission/%d", tc.permID) - req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(jsonObject)) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - app := fiber.New() - app.Put("/permission/:id", permController.Update) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, want", tc.respCode, "but got", resp.StatusCode) - } - if resp.StatusCode != http.StatusNoContent { - var data response.Response - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - t.Fatal("failed to decode JSON:", err) - } - } - if resp.StatusCode == http.StatusNoContent { - perm, getErr := permService.GetByID(ctx, permID) - if getErr != nil || perm == nil { - t.Error("should not error while get permission") - } - if perm.Name != strings.ToLower(tc.payload.Name) || - perm.Description != tc.payload.Description { - t.Error("should equal") - } - } - } -} - -func TestPermDelete(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - // create 1 permission - permID, createErr := permService.Create(ctx, model.PermissionCreate{ - Name: strings.ToLower(helper.RandomString(12)), - Description: "description-of-example-permission-001", - }) - if createErr != nil || permID < 1 { - t.Fatal("should not error while creating permission") - } - - defer func() { - permService.Delete(ctx, permID) - }() - - testCases := []struct { - caseName string - respCode int - permID int - }{ - { - caseName: "success get -1", - respCode: http.StatusNoContent, - permID: permID, - }, - { - caseName: "failed: not found / already deleted", - respCode: http.StatusNotFound, - permID: permID, - }, - { - caseName: "failed: not found", - respCode: http.StatusNotFound, - permID: permID + 100, - }, - { - caseName: "failed: invalid id", - respCode: http.StatusBadRequest, - permID: -10, - }, - } - - for _, tc := range testCases { - url := appURL + "permission/" + strconv.Itoa(tc.permID) - req, httpReqErr := http.NewRequest(http.MethodDelete, url, nil) - if httpReqErr != nil || req == nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - app := fiber.New() - app.Delete("/permission/:id", permController.Delete) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, want", tc.respCode, "but got", resp.StatusCode) - } - } -} - -func createUserAndToken() (userID int, token string) { - permService := service.NewPermissionService() - roleService := roleService.NewRoleService(permService) - userSvc := userService.NewUserService(roleService) - userCtr := userController.NewUserController(userSvc) - - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - log.Fatal(constants.ShouldNotNil) - } - - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - log.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - log.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - log.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - - verifyErr := userSvc.Verification(ctx, model.UserVerificationCode{ - Code: *userByID.VerificationCode, - Email: userByID.Email, - }) - if verifyErr != nil { - log.Fatal(constants.ShouldNotErr) - } - userByID = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - log.Fatal("should success get user by id") - } - if userByID.VerificationCode != nil || userByID.ActivatedAt == nil { - log.Fatal("user should active for now, verification code should nil") - } - - userToken, loginErr := userSvc.Login(ctx, model.UserLogin{ - Email: createdUser.Email, - Password: createdUser.Password, - IP: helper.RandomIPAddress(), - }) - if userToken == "" || loginErr != nil { - log.Fatal("login should success") - } - - return userID, userToken -} diff --git a/controller/role/role_controller.go b/controller/role/role_controller.go deleted file mode 100644 index f794326..0000000 --- a/controller/role/role_controller.go +++ /dev/null @@ -1,219 +0,0 @@ -package controller - -import ( - "math" - "sync" - - "github.com/go-playground/validator/v10" - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/response" - service "github.com/Lukmanern/gost/service/role" -) - -type RoleController interface { - - // Create func creates a new role - Create(c *fiber.Ctx) error - - // Connect func connects a role with some permissions - // and storing data in role_has_permissions table - Connect(c *fiber.Ctx) error - - // Get func gets a role - Get(c *fiber.Ctx) error - - // GetAll func gets some roles - GetAll(c *fiber.Ctx) error - - // Update func updates a role - Update(c *fiber.Ctx) error - - // Delete func deletes a role - Delete(c *fiber.Ctx) error -} - -type RoleControllerImpl struct { - service service.RoleService -} - -var ( - roleControllerImpl *RoleControllerImpl - roleControllerImplOnce sync.Once -) - -func NewRoleController(service service.RoleService) RoleController { - roleControllerImplOnce.Do(func() { - roleControllerImpl = &RoleControllerImpl{ - service: service, - } - }) - return roleControllerImpl -} - -func (ctr *RoleControllerImpl) Create(c *fiber.Ctx) error { - var role model.RoleCreate - if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - // hashmap - idCheckers := make(map[int]bool) - for _, id := range role.PermissionsID { - if id < 1 { - return response.BadRequest(c, "One of the permission IDs is invalid") - } - if idCheckers[id] { - return response.BadRequest(c, "Permission IDs contain the same value") - } - idCheckers[id] = true - } - validate := validator.New() - if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - id, createErr := ctr.service.Create(ctx, role) - if createErr != nil { - fiberErr, ok := createErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+createErr.Error()) - } - data := map[string]any{ - "id": id, - } - return response.SuccessCreated(c, data) -} - -func (ctr *RoleControllerImpl) Connect(c *fiber.Ctx) error { - var role model.RoleConnectToPermissions - if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - // hashmap - idCheckers := make(map[int]bool) - for _, id := range role.PermissionsID { - if id < 1 { - return response.BadRequest(c, "One of the permission IDs is invalid") - } - if idCheckers[id] { - return response.BadRequest(c, "Permission IDs contain the same value") - } - idCheckers[id] = true - } - validate := validator.New() - if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - connectErr := ctr.service.ConnectPermissions(ctx, role) - if connectErr != nil { - fiberErr, ok := connectErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+connectErr.Error()) - } - return response.SuccessCreated(c, "role and permissions success connected") -} - -func (ctr *RoleControllerImpl) Get(c *fiber.Ctx) error { - id, err := c.ParamsInt("id") - if err != nil || id <= 0 { - return response.BadRequest(c, constants.InvalidID) - } - - ctx := c.Context() - role, getErr := ctr.service.GetByID(ctx, id) - if getErr != nil { - fiberErr, ok := getErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+getErr.Error()) - } - return response.SuccessLoaded(c, role) -} - -func (ctr *RoleControllerImpl) GetAll(c *fiber.Ctx) error { - request := base.RequestGetAll{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 20), - Keyword: c.Query("search"), - Sort: c.Query("sort"), - } - if request.Page <= 0 || request.Limit <= 0 { - return response.BadRequest(c, "invalid page or limit value") - } - - ctx := c.Context() - roles, total, getErr := ctr.service.GetAll(ctx, request) - if getErr != nil { - return response.Error(c, constants.ServerErr+getErr.Error()) - } - - data := make([]interface{}, len(roles)) - for i := range roles { - data[i] = roles[i] - } - responseData := base.GetAllResponse{ - Meta: base.PageMeta{ - Total: total, - Pages: int(math.Ceil(float64(total) / float64(request.Limit))), - Page: request.Page, - }, - Data: data, - } - return response.SuccessLoaded(c, responseData) -} - -func (ctr *RoleControllerImpl) Update(c *fiber.Ctx) error { - id, err := c.ParamsInt("id") - if err != nil || id <= 0 { - return response.BadRequest(c, constants.InvalidID) - } - var role model.RoleUpdate - role.ID = id - if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - updateErr := ctr.service.Update(ctx, role) - if updateErr != nil { - fiberErr, ok := updateErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+updateErr.Error()) - } - return response.SuccessNoContent(c) -} - -func (ctr *RoleControllerImpl) Delete(c *fiber.Ctx) error { - id, err := c.ParamsInt("id") - if err != nil || id <= 0 { - return response.BadRequest(c, constants.InvalidID) - } - - ctx := c.Context() - deleteErr := ctr.service.Delete(ctx, id) - if deleteErr != nil { - fiberErr, ok := deleteErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+deleteErr.Error()) - } - return response.SuccessNoContent(c) -} diff --git a/controller/role/role_controller_test.go b/controller/role/role_controller_test.go deleted file mode 100644 index 1df10d4..0000000 --- a/controller/role/role_controller_test.go +++ /dev/null @@ -1,760 +0,0 @@ -// Don't run test per file without -p 1 -// or simply run test per func or run -// project test using make test command -// check Makefile file -package controller - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "net/http" - "strings" - "testing" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" - "github.com/Lukmanern/gost/internal/response" - "github.com/gofiber/fiber/v2" - - permissionController "github.com/Lukmanern/gost/controller/permission" - permSvc "github.com/Lukmanern/gost/service/permission" - service "github.com/Lukmanern/gost/service/role" -) - -var ( - permService permSvc.PermissionService - roleService service.RoleService - roleController RoleController - permController permissionController.PermissionController - appURL string -) - -func init() { - env.ReadConfig("./../../.env") - config := env.Configuration() - appURL = config.AppURL - - connector.LoadDatabase() - connector.LoadRedisCache() - - permService = permSvc.NewPermissionService() - permController = permissionController.NewPermissionController(permService) - - roleService = service.NewRoleService(permService) - roleController = NewRoleController(roleService) -} - -func TestRoleCreate(t *testing.T) { - t.Parallel() - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - permIDs := make([]int, 0) - for i := 0; i < 4; i++ { - // create 1 permission - permID, createErr := permService.Create(ctx, model.PermissionCreate{ - Name: helper.RandomString(11), - Description: helper.RandomString(30), - }) - if createErr != nil || permID < 1 { - t.Fatal("should not error while creating permission") - } - defer func() { - permService.Delete(ctx, permID) - }() - - permIDs = append(permIDs, permID) - } - - createdRole := model.RoleCreate{ - Name: helper.RandomString(9), - Description: helper.RandomString(30), - PermissionsID: permIDs, - } - roleID, createErr := roleService.Create(ctx, createdRole) - if createErr != nil || roleID <= 0 { - t.Fatal("should not error while creating new Role") - } - roleByID, getErr := roleService.GetByID(ctx, roleID) - if getErr != nil || roleByID == nil { - t.Fatal("should not error while getting Role") - } - if len(roleByID.Permissions) != 4 { - t.Error("the length should equal") - } - defer func() { - roleService.Delete(ctx, roleID) - }() - - testCases := []struct { - caseName string - respCode int - payload model.RoleCreate - }{ - { - caseName: "success create -1", - respCode: http.StatusCreated, - payload: model.RoleCreate{ - Name: helper.RandomString(10), - Description: helper.RandomString(30), - }, - }, - { - caseName: "success create -2", - respCode: http.StatusCreated, - payload: model.RoleCreate{ - Name: helper.RandomString(10), - Description: helper.RandomString(30), - }, - }, - { - caseName: "failed create: permissions not found", - respCode: http.StatusNotFound, - payload: model.RoleCreate{ - Name: helper.RandomString(10), - Description: helper.RandomString(30), - PermissionsID: []int{permIDs[0] + 90}, - }, - }, - { - caseName: "failed create: invalid name, too short", - respCode: http.StatusBadRequest, - payload: model.RoleCreate{ - Name: "", - Description: helper.RandomString(30), - PermissionsID: []int{permIDs[0] - 90}, - }, - }, - { - caseName: "failed create: invalid description, too short", - respCode: http.StatusBadRequest, - payload: model.RoleCreate{ - Name: helper.RandomString(10), - Description: "", - PermissionsID: []int{permIDs[0]}, - }, - }, - } - - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + "role" - req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - app := fiber.New() - app.Post("/role", roleController.Create) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, want", tc.respCode, "but got", resp.StatusCode) - } - var data response.Response - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - t.Fatal("failed to decode JSON:", err) - } - - if resp.StatusCode == http.StatusCreated { - data, ok1 := data.Data.(map[string]interface{}) - if !ok1 { - t.Error("should ok1") - } - anyID, ok2 := data["id"] - if !ok2 { - t.Error("should ok2") - } - intID, ok3 := anyID.(float64) - if !ok3 { - t.Error("should ok3") - } - deleteErr := roleService.Delete(ctx, int(intID)) - if deleteErr != nil { - t.Error(constants.ShouldNotErr) - } - } - } -} - -func TestRoleConnect(t *testing.T) { - t.Parallel() - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - permIDs := make([]int, 0) - for i := 0; i < 4; i++ { - // create 1 permission - permID, createErr := permService.Create(ctx, model.PermissionCreate{ - Name: helper.RandomString(11), - Description: helper.RandomString(30), - }) - if createErr != nil || permID < 1 { - t.Fatal("should not error while creating permission") - } - defer func() { - permService.Delete(ctx, permID) - }() - - permIDs = append(permIDs, permID) - } - - createdRole := model.RoleCreate{ - Name: helper.RandomString(9), - Description: helper.RandomString(30), - PermissionsID: permIDs, - } - roleID, createErr := roleService.Create(ctx, createdRole) - if createErr != nil || roleID <= 0 { - t.Fatal("should not error while creating new Role") - } - roleByID, getErr := roleService.GetByID(ctx, roleID) - if getErr != nil || roleByID == nil { - t.Fatal("should not error while getting Role") - } - if len(roleByID.Permissions) != 4 { - t.Error("the length should equal") - } - defer func() { - roleService.Delete(ctx, roleID) - }() - - testCases := []struct { - caseName string - respCode int - payload model.RoleConnectToPermissions - }{ - { - caseName: "success connect -1", - respCode: http.StatusCreated, - payload: model.RoleConnectToPermissions{ - RoleID: roleID, - PermissionsID: permIDs, - }, - }, - { - caseName: "success connect -2", - respCode: http.StatusCreated, - payload: model.RoleConnectToPermissions{ - RoleID: roleID, - PermissionsID: permIDs, - }, - }, - { - caseName: "failed connect: status not found", - respCode: http.StatusNotFound, - payload: model.RoleConnectToPermissions{ - RoleID: roleID + 99, - PermissionsID: permIDs, - }, - }, - { - caseName: "failed connect: invalid role id", - respCode: http.StatusBadRequest, - payload: model.RoleConnectToPermissions{ - RoleID: -1, - PermissionsID: permIDs, - }, - }, - { - caseName: "failed connect: invalid id", - respCode: http.StatusBadRequest, - payload: model.RoleConnectToPermissions{ - RoleID: roleID, - PermissionsID: []int{-1, 2, 3}, - }, - }, - } - - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + "role/connect" - req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - app := fiber.New() - app.Post("/role/connect", roleController.Connect) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, want", tc.respCode, "but got", resp.StatusCode) - } - var data response.Response - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - t.Fatal("failed to decode JSON:", err) - } - if resp.StatusCode == http.StatusOK { - role, getErr := roleService.GetByID(ctx, tc.payload.RoleID) - if getErr != nil || role == nil { - t.Fatal("should not error while getting role") - } - - if len(role.Permissions) != len(tc.payload.PermissionsID) { - t.Error("should equal") - } - } - } -} - -func TestRoleGet(t *testing.T) { - t.Parallel() - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - permIDs := make([]int, 0) - for i := 0; i < 4; i++ { - // create 1 permission - permID, createErr := permService.Create(ctx, model.PermissionCreate{ - Name: helper.RandomString(11), - Description: helper.RandomString(30), - }) - if createErr != nil || permID < 1 { - t.Fatal("should not error while creating permission") - } - defer func() { - permService.Delete(ctx, permID) - }() - - permIDs = append(permIDs, permID) - } - - createdRole := model.RoleCreate{ - Name: helper.RandomString(9), - Description: helper.RandomString(30), - PermissionsID: permIDs, - } - roleID, createErr := roleService.Create(ctx, createdRole) - if createErr != nil || roleID <= 0 { - t.Fatal("should not error while creating new Role") - } - roleByID, getErr := roleService.GetByID(ctx, roleID) - if getErr != nil || roleByID == nil { - t.Fatal("should not error while getting Role") - } - if len(roleByID.Permissions) != 4 { - t.Error("the length should equal") - } - defer func() { - roleService.Delete(ctx, roleID) - }() - - testCases := []struct { - caseName string - respCode int - roleID int - }{ - { - caseName: "success get -1", - respCode: http.StatusOK, - roleID: roleID, - }, - { - caseName: "success get -2", - respCode: http.StatusOK, - roleID: roleID, - }, - { - caseName: "failed get: status not found", - respCode: http.StatusNotFound, - roleID: roleID + 99, - }, - { - caseName: "failed get: invalid id", - respCode: http.StatusBadRequest, - roleID: -10, - }, - } - - for _, tc := range testCases { - url := fmt.Sprintf(appURL+"role/%d", tc.roleID) - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - app := fiber.New() - app.Get("/role/:id", roleController.Get) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, want", tc.respCode, "but got", resp.StatusCode) - } - var data response.Response - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - t.Fatal("failed to decode JSON:", err) - } - } -} - -func TestRoleGetAll(t *testing.T) { - t.Parallel() - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - permIDs := make([]int, 0) - for i := 0; i < 4; i++ { - // create 1 permission - permID, createErr := permService.Create(ctx, model.PermissionCreate{ - Name: helper.RandomString(11), - Description: helper.RandomString(30), - }) - if createErr != nil || permID < 1 { - t.Fatal("should not error while creating permission") - } - defer func() { - permService.Delete(ctx, permID) - }() - - permIDs = append(permIDs, permID) - } - - createdRole := model.RoleCreate{ - Name: helper.RandomString(9), - Description: helper.RandomString(30), - PermissionsID: permIDs, - } - roleID, createErr := roleService.Create(ctx, createdRole) - if createErr != nil || roleID <= 0 { - t.Fatal("should not error while creating new Role") - } - roleByID, getErr := roleService.GetByID(ctx, roleID) - if getErr != nil || roleByID == nil { - t.Fatal("should not error while getting Role") - } - if len(roleByID.Permissions) != 4 { - t.Error("the length should equal") - } - defer func() { - roleService.Delete(ctx, roleID) - }() - - testCases := []struct { - caseName string - respCode int - params string - }{ - { - caseName: "success getAll -1", - respCode: http.StatusOK, - params: "limit=10&page=1", - }, - { - caseName: "success getAll -2", - respCode: http.StatusOK, - params: "limit=100&page=1", - }, - { - caseName: "failed getAll: invalid limit/page", - respCode: http.StatusBadRequest, - params: "limit=-10&page=-1", - }, - } - - for _, tc := range testCases { - url := appURL + "role?" + tc.params - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - app := fiber.New() - app.Get("/role", roleController.GetAll) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, want", tc.respCode, "but got", resp.StatusCode) - } - var data response.Response - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - t.Fatal("failed to decode JSON:", err) - } - } -} - -func TestRoleUpdate(t *testing.T) { - t.Parallel() - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - permIDs := make([]int, 0) - for i := 0; i < 4; i++ { - // create 1 permission - permID, createErr := permService.Create(ctx, model.PermissionCreate{ - Name: helper.RandomString(11), - Description: helper.RandomString(30), - }) - if createErr != nil || permID < 1 { - t.Fatal("should not error while creating permission") - } - defer func() { - permService.Delete(ctx, permID) - }() - - permIDs = append(permIDs, permID) - } - - createdRole := model.RoleCreate{ - Name: helper.RandomString(9), - Description: helper.RandomString(30), - PermissionsID: permIDs, - } - roleID, createErr := roleService.Create(ctx, createdRole) - if createErr != nil || roleID <= 0 { - t.Fatal("should not error while creating new Role") - } - roleByID, getErr := roleService.GetByID(ctx, roleID) - if getErr != nil || roleByID == nil { - t.Fatal("should not error while getting Role") - } - if len(roleByID.Permissions) != 4 { - t.Error("the length should equal") - } - defer func() { - roleService.Delete(ctx, roleID) - }() - - testCases := []struct { - caseName string - respCode int - roleID int - payload model.RoleUpdate - }{ - { - caseName: "success update -1", - respCode: http.StatusNoContent, - roleID: roleID, - payload: model.RoleUpdate{ - ID: roleID, - Name: helper.RandomString(12), - Description: helper.RandomString(20), - }, - }, - { - caseName: "success update -2", - respCode: http.StatusNoContent, - roleID: roleID, - payload: model.RoleUpdate{ - ID: roleID, - Name: helper.RandomString(12), - Description: helper.RandomString(20), - }, - }, - { - caseName: "failed update: invalid name/description", - respCode: http.StatusBadRequest, - roleID: roleID, - payload: model.RoleUpdate{ - ID: roleID, - Name: "", - Description: "", - }, - }, - { - caseName: "failed update: invalid id", - respCode: http.StatusBadRequest, - roleID: -10, - }, - { - caseName: "failed update: data not found", - respCode: http.StatusNotFound, - roleID: roleID + 99, - payload: model.RoleUpdate{ - ID: roleID + 99, - Name: helper.RandomString(12), - Description: helper.RandomString(20), - }, - }, - } - - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := fmt.Sprintf(appURL+"role/%d", tc.roleID) - req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(jsonObject)) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - app := fiber.New() - app.Put("/role/:id", roleController.Update) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, want", tc.respCode, "but got", resp.StatusCode) - } - if resp.StatusCode != http.StatusNoContent { - var data response.Response - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - t.Fatal("failed to decode JSON:", err) - } - } - if resp.StatusCode == http.StatusNoContent { - perm, getErr := roleService.GetByID(ctx, roleID) - if getErr != nil || perm == nil { - t.Error("should not nil while get permission") - } - if perm.Name != strings.ToLower(tc.payload.Name) || - perm.Description != tc.payload.Description { - t.Error("should equal") - } - } - } -} - -func TestRoleDelete(t *testing.T) { - t.Parallel() - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := permController - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - permIDs := make([]int, 0) - for i := 0; i < 4; i++ { - // create 1 permission - permID, createErr := permService.Create(ctx, model.PermissionCreate{ - Name: helper.RandomString(11), - Description: helper.RandomString(30), - }) - if createErr != nil || permID < 1 { - t.Fatal("should not error while creating permission") - } - defer func() { - permService.Delete(ctx, permID) - }() - - permIDs = append(permIDs, permID) - } - - createdRole := model.RoleCreate{ - Name: helper.RandomString(9), - Description: helper.RandomString(30), - PermissionsID: permIDs, - } - roleID, createErr := roleService.Create(ctx, createdRole) - if createErr != nil || roleID <= 0 { - t.Fatal("should not error while creating new Role") - } - roleByID, getErr := roleService.GetByID(ctx, roleID) - if getErr != nil || roleByID == nil { - t.Fatal("should not error while getting Role") - } - if len(roleByID.Permissions) != 4 { - t.Error("the length should equal") - } - defer func() { - roleService.Delete(ctx, roleID) - }() - - testCases := []struct { - caseName string - respCode int - roleID int - }{ - { - caseName: "success update -1", - respCode: http.StatusNoContent, - roleID: roleID, - }, - { - caseName: "success update -2", - respCode: http.StatusNotFound, - roleID: roleID, - }, - { - caseName: "failed update: invalid id", - respCode: http.StatusBadRequest, - roleID: -10, - }, - { - caseName: "failed update: data not found", - respCode: http.StatusNotFound, - roleID: roleID + 99, - }, - } - - for _, tc := range testCases { - url := fmt.Sprintf(appURL+"role/%d", tc.roleID) - req, err := http.NewRequest(http.MethodDelete, url, nil) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - app := fiber.New() - app.Delete("/role/:id", roleController.Delete) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, want", tc.respCode, "but got", resp.StatusCode) - } - if resp.StatusCode != http.StatusNoContent { - var data response.Response - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - t.Fatal("failed to decode JSON:", err) - } - } - if resp.StatusCode == http.StatusNoContent { - role, getErr := roleService.GetByID(ctx, tc.roleID) - if getErr == nil || role != nil { - t.Error("should error while get role") - } - } - } -} diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go deleted file mode 100644 index 915d8b3..0000000 --- a/controller/user/user_controller.go +++ /dev/null @@ -1,339 +0,0 @@ -package controller - -import ( - "net" - "strings" - "sync" - - "github.com/go-playground/validator/v10" - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/middleware" - "github.com/Lukmanern/gost/internal/response" - service "github.com/Lukmanern/gost/service/user" -) - -type UserController interface { - // Register function register user account, - // than send verification-code to email - Register(c *fiber.Ctx) error - - // AccountActivation function activates user account with - // verification code that has been sended to the user's email - AccountActivation(c *fiber.Ctx) error - - // DeleteUserByVerification function deletes user data if the - // user account is not yet verified. This implies that the email - // owner hasn't actually registered the email, indicating that - // the user who registered may be making typing errors or may - // be a hacker attempting to get the verification code. - DeleteAccountActivation(c *fiber.Ctx) error - - // ForgetPassword function send - // verification code into user's email - ForgetPassword(c *fiber.Ctx) error - - // ResetPassword func resets password by creating - // new password by email and verification code - ResetPassword(c *fiber.Ctx) error - - // Login func gives token and access to user - Login(c *fiber.Ctx) error - - // Logout func stores user's token into Redis - Logout(c *fiber.Ctx) error - - // UpdatePassword func updates user's password - UpdatePassword(c *fiber.Ctx) error - - // UpdateProfile func updates user's profile data - UpdateProfile(c *fiber.Ctx) error - - // MyProfile func shows user's profile data - MyProfile(c *fiber.Ctx) error -} - -type UserControllerImpl struct { - service service.UserService -} - -var ( - userController *UserControllerImpl - userControllerOnce sync.Once -) - -func NewUserController(service service.UserService) UserController { - userControllerOnce.Do(func() { - userController = &UserControllerImpl{ - service: service, - } - }) - - return userController -} - -func (ctr *UserControllerImpl) Register(c *fiber.Ctx) error { - var user model.UserRegister - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - user.Email = strings.ToLower(user.Email) - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - id, regisErr := ctr.service.Register(ctx, user) - if regisErr != nil { - fiberErr, ok := regisErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+regisErr.Error()) - } - - message := "Account success created. please check " + user.Email + " " - message += "inbox, our system has sended verification code or link." - data := map[string]any{ - "id": id, - } - return response.CreateResponse(c, fiber.StatusCreated, true, message, data) -} - -func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { - var user model.UserVerificationCode - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - ctx := c.Context() - err := ctr.service.Verification(ctx, user) - if err != nil { - fiberErr, ok := err.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+err.Error()) - } - - message := "Thank you for your confirmation. Your account is active now, you can login." - return response.CreateResponse(c, fiber.StatusOK, true, message, nil) -} - -func (ctr *UserControllerImpl) DeleteAccountActivation(c *fiber.Ctx) error { - var verifyData model.UserVerificationCode - if err := c.BodyParser(&verifyData); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&verifyData); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - ctx := c.Context() - err := ctr.service.DeleteUserByVerification(ctx, verifyData) - if err != nil { - fiberErr, ok := err.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+err.Error()) - } - - message := "Your data is already deleted, thank you for your confirmation." - return response.CreateResponse(c, fiber.StatusOK, true, message, nil) -} - -func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { - var user model.UserLogin - // user.IP = c.IP() // Note : uncomment this line in production - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - userIP := net.ParseIP(user.IP) - if userIP == nil { - return response.BadRequest(c, constants.InvalidBody+"invalid user ip address") - } - counter, _ := ctr.service.FailedLoginCounter(userIP.String(), false) - ipBlockMsg := "Your IP has been blocked by system. Please try again in 1 or 2 Hour" - if counter >= 5 { - return response.CreateResponse(c, fiber.StatusBadRequest, false, ipBlockMsg, nil) - } - - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - token, loginErr := ctr.service.Login(ctx, user) - if loginErr != nil { - counter, _ := ctr.service.FailedLoginCounter(userIP.String(), true) - if counter >= 5 { - return response.CreateResponse(c, fiber.StatusBadRequest, false, ipBlockMsg, nil) - } - fiberErr, ok := loginErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+loginErr.Error()) - } - - data := map[string]any{ - "token": token, - "token-length": len(token), - } - return response.CreateResponse(c, fiber.StatusOK, true, "success login", data) -} - -func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { - userClaims, ok := c.Locals("claims").(*middleware.Claims) - if !ok || userClaims == nil { - return response.Unauthorized(c) - } - logoutErr := ctr.service.Logout(c) - if logoutErr != nil { - return response.Error(c, constants.ServerErr+logoutErr.Error()) - } - return response.CreateResponse(c, fiber.StatusOK, true, "success logout", nil) -} - -func (ctr *UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { - var user model.UserForgetPassword - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - forgetErr := ctr.service.ForgetPassword(ctx, user) - if forgetErr != nil { - fiberErr, ok := forgetErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+forgetErr.Error()) - } - - message := "success sending link for reset password to email, check your email inbox" - return response.CreateResponse(c, fiber.StatusAccepted, true, message, nil) -} - -func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { - var user model.UserResetPassword - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - if user.NewPassword != user.NewPasswordConfirm { - return response.BadRequest(c, "password confirmation not match") - } - - ctx := c.Context() - resetErr := ctr.service.ResetPassword(ctx, user) - if resetErr != nil { - fiberErr, ok := resetErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+resetErr.Error()) - } - - message := "your password already updated, you can login with your new password, thank you" - return response.CreateResponse(c, fiber.StatusAccepted, true, message, nil) -} - -func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { - userClaims, ok := c.Locals("claims").(*middleware.Claims) - if !ok || userClaims == nil { - return response.Unauthorized(c) - } - - var user model.UserPasswordUpdate - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - user.ID = userClaims.ID - - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - if user.NewPassword != user.NewPasswordConfirm { - return response.BadRequest(c, "new password confirmation is wrong") - } - if user.NewPassword == user.OldPassword { - return response.BadRequest(c, "no new password, try another new password") - } - - ctx := c.Context() - updateErr := ctr.service.UpdatePassword(ctx, user) - if updateErr != nil { - fiberErr, ok := updateErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+updateErr.Error()) - } - - return response.SuccessNoContent(c) -} - -func (ctr *UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { - userClaims, ok := c.Locals("claims").(*middleware.Claims) - if !ok || userClaims == nil { - return response.Unauthorized(c) - } - - var user model.UserProfileUpdate - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - user.ID = userClaims.ID - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - updateErr := ctr.service.UpdateProfile(ctx, user) - if updateErr != nil { - fiberErr, ok := updateErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+updateErr.Error()) - } - - return response.SuccessNoContent(c) -} - -func (ctr *UserControllerImpl) MyProfile(c *fiber.Ctx) error { - userClaims, ok := c.Locals("claims").(*middleware.Claims) - if !ok || userClaims == nil { - return response.Unauthorized(c) - } - - ctx := c.Context() - userProfile, getErr := ctr.service.MyProfile(ctx, userClaims.ID) - if getErr != nil { - fiberErr, ok := getErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+getErr.Error()) - } - return response.SuccessLoaded(c, userProfile) -} diff --git a/controller/user/user_controller_test.go b/controller/user/user_controller_test.go deleted file mode 100644 index 97685a9..0000000 --- a/controller/user/user_controller_test.go +++ /dev/null @@ -1,1508 +0,0 @@ -package controller - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "net/http" - "strings" - "testing" - - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/entity" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" - "github.com/Lukmanern/gost/internal/middleware" - "github.com/Lukmanern/gost/internal/response" - repository "github.com/Lukmanern/gost/repository/user" - permService "github.com/Lukmanern/gost/service/permission" - roleService "github.com/Lukmanern/gost/service/role" - service "github.com/Lukmanern/gost/service/user" -) - -var ( - userSvc service.UserService - userCtr UserController - userRepo repository.UserRepository - appURL string -) - -func init() { - env.ReadConfig("./../../.env") - config := env.Configuration() - appURL = config.AppURL - - connector.LoadDatabase() - r := connector.LoadRedisCache() - r.FlushAll() // clear all key:value in redis - - permService := permService.NewPermissionService() - roleService := roleService.NewRoleService(permService) - userSvc = service.NewUserService(roleService) - userCtr = NewUserController(userSvc) - userRepo = repository.NewUserRepository() -} - -func TestNewUserController(t *testing.T) { - t.Parallel() - permService := permService.NewPermissionService() - roleService := roleService.NewRoleService(permService) - userService := service.NewUserService(roleService) - userController := NewUserController(userService) - - if userController == nil || userService == nil || roleService == nil || permService == nil { - t.Error(constants.ShouldNotNil) - } -} - -func TestRegister(t *testing.T) { - t.Parallel() - // unaudit - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - c.Method(http.MethodPost) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - response response.Response - payload *model.UserRegister - }{ - { - caseName: "success register -1", - respCode: http.StatusCreated, - response: response.Response{ - Message: response.MessageSuccessCreated, - Success: true, - Data: nil, - }, - payload: &model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - }, - }, - { - caseName: "success register -2", - respCode: http.StatusCreated, - response: response.Response{ - Message: response.MessageSuccessCreated, - Success: true, - Data: nil, - }, - payload: &model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - }, - }, - { - caseName: "success register -3", - respCode: http.StatusCreated, - response: response.Response{ - Message: response.MessageSuccessCreated, - Success: true, - Data: nil, - }, - payload: &model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - }, - }, - { - caseName: "failed register: email already used", - respCode: http.StatusBadRequest, - response: response.Response{ - Message: "", - Success: false, - Data: nil, - }, - payload: &model.UserRegister{ - Name: helper.RandomString(10), - Email: createdUser.Email, - Password: helper.RandomString(10), - RoleID: 1, // admin - }, - }, - { - caseName: "failed register: name too short", - respCode: http.StatusBadRequest, - response: response.Response{ - Message: "", - Success: false, - Data: nil, - }, - payload: &model.UserRegister{ - Name: "", - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - }, - }, - { - caseName: "failed register: password too short", - respCode: http.StatusBadRequest, - response: response.Response{ - Message: "", - Success: false, - Data: nil, - }, - payload: &model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: "", - RoleID: 1, // admin - }, - }, - } - - endp := "user/register" - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(&tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + endp - req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) - if httpReqErr != nil || req == nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - app := fiber.New() - app.Post(endp, ctr.Register) - req.Close = true - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, but got", resp.StatusCode) - } - - if tc.payload != nil { - respModel := response.Response{} - decodeErr := json.NewDecoder(resp.Body).Decode(&respModel) - if decodeErr != nil { - t.Error(constants.ShouldNotErr, decodeErr) - } - } - - if tc.response.Success { - userByEmail, getErr := userRepo.GetByEmail(ctx, tc.payload.Email) - if getErr != nil || userByEmail == nil { - t.Fatal("should success whilte create and get user") - } - if userByEmail.Name != helper.ToTitle(tc.payload.Name) { - t.Error("name should equal") - } - - deleteErr := userRepo.Delete(ctx, userByEmail.ID) - if deleteErr != nil { - t.Fatal("should success whilte delete user by ID") - } - } - } - -} - -func TestAccountActivation(t *testing.T) { - t.Parallel() - // unaudit - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - c.Method(http.MethodPost) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - payload *model.UserVerificationCode - }{ - { - caseName: "success verify", - respCode: http.StatusOK, - payload: &model.UserVerificationCode{ - Code: *vCode, - Email: createdUser.Email, - }, - }, - { - caseName: "failed verify: code not found", - respCode: http.StatusNotFound, - payload: &model.UserVerificationCode{ - Code: *vCode, - Email: createdUser.Email, - }, - }, - { - caseName: "failed verify: code/email too short", - respCode: http.StatusBadRequest, - payload: &model.UserVerificationCode{ - Code: "", - Email: "", - }, - }, - } - - endp := "user/verification" - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(&tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + endp - req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) - if httpReqErr != nil || req == nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - app := fiber.New() - app.Post(endp, ctr.AccountActivation) - req.Close = true - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, but got", resp.StatusCode) - } - - // if success - if resp.StatusCode == http.StatusOK { - userByEmail, getErr := userRepo.GetByEmail(ctx, createdUser.Email) - if getErr != nil || userByEmail == nil { - t.Error("should not error and user not nil") - } - if userByEmail.VerificationCode != nil { - t.Fatal("verif code should nil after activation") - } - if userByEmail.ActivatedAt == nil { - t.Fatal("activated_at should not nil after activation") - } - } - } -} - -func TestDeleteAccountActivation(t *testing.T) { - t.Parallel() - // unaudit - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - c.Method(http.MethodPost) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - payload *model.UserVerificationCode - }{ - { - caseName: "success delete account", - respCode: http.StatusOK, - payload: &model.UserVerificationCode{ - Code: *vCode, - Email: createdUser.Email, - }, - }, - { - caseName: "failed delete account: code not found", - respCode: http.StatusNotFound, - payload: &model.UserVerificationCode{ - Code: *vCode, - Email: createdUser.Email, - }, - }, - { - caseName: "failed delete account: code/email too short", - respCode: http.StatusBadRequest, - payload: &model.UserVerificationCode{ - Code: "", - Email: "", - }, - }, - } - - endp := "user/request-delete" - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(&tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + endp - req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) - if httpReqErr != nil || req == nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - app := fiber.New() - app.Post(endp, ctr.DeleteAccountActivation) - req.Close = true - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, but got", resp.StatusCode, "on", tc.caseName) - } - - // if success - if resp.StatusCode == http.StatusOK { - userByConds, getErr1 := userRepo.GetByConditions(ctx, map[string]any{ - "verification_code =": tc.payload.Code, - }) - if getErr1 == nil || userByConds != nil { - t.Error("should error and user should nil") - } - - userByID, getErr2 := userRepo.GetByID(ctx, userID) - if getErr2 == nil || userByID != nil { - t.Error("should error and user should nil") - } - } - } -} - -func TestForgetPassword(t *testing.T) { - t.Parallel() - // unaudit - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - c.Method(http.MethodPost) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - - verifyErr := userSvc.Verification(ctx, model.UserVerificationCode{ - Code: *vCode, - Email: userByID.Email, - }) - if verifyErr != nil { - t.Fatal("verification should not error") - } - - // value reset - userByID = nil - getErr = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - if userByID.VerificationCode != nil || userByID.ActivatedAt == nil { - t.Fatal("user should active for now, but its get inactive") - } - - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - payload *model.UserForgetPassword - }{ - { - caseName: "success forget password", - respCode: http.StatusAccepted, - payload: &model.UserForgetPassword{ - Email: createdUser.Email, - }, - }, - { - caseName: "faield forget password: email not found", - respCode: http.StatusNotFound, - payload: &model.UserForgetPassword{ - Email: helper.RandomEmail(), - }, - }, - { - caseName: "faield forget password: invalid email", - respCode: http.StatusBadRequest, - payload: &model.UserForgetPassword{ - Email: "invalid-email", - }, - }, - } - - endp := "user/forget-password" - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(&tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + endp - req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) - if httpReqErr != nil || req == nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - app := fiber.New() - app.Post(endp, ctr.ForgetPassword) - req.Close = true - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error("should equal, but got", resp.StatusCode) - } - } -} - -func TestResetPassword(t *testing.T) { - t.Parallel() - // unaudit - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - c.Method(http.MethodPost) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - verifyErr := userSvc.Verification(ctx, model.UserVerificationCode{ - Code: *vCode, - Email: userByID.Email, - }) - if verifyErr != nil { - t.Error(constants.ShouldNotErr) - } - - // value reset - userByID = nil - getErr = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - if userByID.VerificationCode != nil || userByID.ActivatedAt == nil { - t.Fatal("user should active for now, but its get inactive") - } - - userForgetPasswd := model.UserForgetPassword{ - Email: userByID.Email, - } - forgetPassErr := userSvc.ForgetPassword(ctx, userForgetPasswd) - if forgetPassErr != nil { - t.Error(constants.ShouldNotErr) - } - - // value reset - userByID = nil - getErr = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - if userByID.VerificationCode == nil || userByID.ActivatedAt == nil { - t.Fatal("user should active for now, and verification code should not nil") - } - - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - payload *model.UserResetPassword - }{ - { - caseName: "success reset password", - respCode: http.StatusAccepted, - payload: &model.UserResetPassword{ - Email: userByID.Email, - Code: *userByID.VerificationCode, - NewPassword: "newPassword", - NewPasswordConfirm: "newPassword", - }, - }, - { - caseName: "failed reset password: password not match", - respCode: http.StatusBadRequest, - payload: &model.UserResetPassword{ - Email: userByID.Email, - Code: *userByID.VerificationCode, - NewPassword: "newPassword", - NewPasswordConfirm: "newPasswordNotMatch", - }, - }, - { - caseName: "failed reset password: verification code too short", - respCode: http.StatusBadRequest, - payload: &model.UserResetPassword{ - Email: helper.RandomEmail(), - Code: "short", - NewPassword: "newPassword", - NewPasswordConfirm: "newPasswordNotMatch", - }, - }, - } - - endp := "user/reset-password" - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(&tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + endp - req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) - if httpReqErr != nil || req == nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - app := fiber.New() - app.Post(endp, ctr.ResetPassword) - req.Close = true - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error(tc.caseName, "should equal, but got", resp.StatusCode, "want", tc.respCode) - } - - if resp.StatusCode == http.StatusAccepted { - // proofing that password has changed - token, loginErr := userSvc.Login(ctx, model.UserLogin{ - Email: userByID.Email, - Password: tc.payload.NewPassword, - IP: helper.RandomIPAddress(), - }) - if token == "" || loginErr != nil { - t.Error("should success login, got failed login") - } - } - } -} - -func TestLogin(t *testing.T) { - t.Parallel() - // unaudit - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - c.Method(http.MethodPost) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - // create inactive user - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - // create active user - createdActiveUser := entity.User{} - func() { - createdUser2 := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser2) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - - verifyErr := userSvc.Verification(ctx, model.UserVerificationCode{ - Code: *vCode, - Email: userByID.Email, - }) - if verifyErr != nil { - t.Error(constants.ShouldNotErr) - } - userByID = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - - createdActiveUser = *userByID - createdActiveUser.Password = createdUser2.Password - }() - - defer userRepo.Delete(ctx, createdActiveUser.ID) - - testCases := []struct { - caseName string - respCode int - payload *model.UserLogin - }{ - { - caseName: "success login", - respCode: http.StatusOK, - payload: &model.UserLogin{ - Email: createdActiveUser.Email, - Password: createdActiveUser.Password, - IP: helper.RandomIPAddress(), - }, - }, - { - caseName: "failed login -1: account is inactive", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Email: strings.ToLower(createdUser.Email), - Password: createdUser.Password, - IP: helper.RandomIPAddress(), - }, - }, - { - caseName: "failed login -2: account is inactive", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Email: strings.ToLower(createdUser.Email), - Password: createdUser.Password, - IP: helper.RandomIPAddress(), - }, - }, - { - caseName: "failed login: wrong passwd", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Password: "wrongPass11", - Email: createdUser.Email, - IP: helper.RandomIPAddress(), - }, - }, - { - caseName: "failed login: invalid ip", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Password: "wrongPass11", - Email: createdUser.Email, - IP: "invalid-ip", - }, - }, - { - caseName: "faield login: email not found", - respCode: http.StatusNotFound, - payload: &model.UserLogin{ - Password: "secret123", - Email: helper.RandomEmail(), - IP: helper.RandomIPAddress(), - }, - }, - { - caseName: "faield login: invalid email", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Password: "secret", - Email: "invalid-email", - IP: helper.RandomIPAddress(), - }, - }, - { - caseName: "faield login: payload too short", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Password: "", - Email: "", - IP: helper.RandomIPAddress(), - }, - }, - } - - endp := "user/login" - for _, tc := range testCases { - log.Println(":::::::" + tc.caseName) - jsonObject, err := json.Marshal(&tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + endp - req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) - if httpReqErr != nil || req == nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - app := fiber.New() - app.Post(endp, ctr.Login) - req.Close = true - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error(tc.caseName, "should equal, but got", resp.StatusCode, "want", tc.respCode) - } - } - - // try blocking IP feature - clientIP := "127.0.0.3" - testCase := struct { - caseName string - respCode int - payload *model.UserLogin - }{ - caseName: "failed login: stacking redis", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Email: createdActiveUser.Email, - Password: "validpassword", - IP: clientIP, // keep the ip same - }, - } - for i := 0; i < 7; i++ { - log.Println(":::::::" + testCase.caseName) - jsonObject, err := json.Marshal(&testCase.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + endp - req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) - if httpReqErr != nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - app := fiber.New() - app.Post(endp, ctr.Login) - req.Close = true - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != testCase.respCode { - t.Error(testCase.caseName, "should equal, but got", resp.StatusCode, "want", testCase.respCode) - } - } - - redis := connector.LoadRedisCache() - if redis == nil { - t.Fatal(constants.ShouldNotNil) - } - value := redis.Get("failed-login-" + clientIP).Val() - if value != "5" { - t.Error("should 5, get", value) - } -} - -func TestLogout(t *testing.T) { - t.Parallel() - // unaudit - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - // create inactive user - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - - verifyErr := userSvc.Verification(ctx, model.UserVerificationCode{ - Code: *vCode, - Email: userByID.Email, - }) - if verifyErr != nil { - t.Error(constants.ShouldNotErr) - } - userByID = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - if userByID.VerificationCode != nil || userByID.ActivatedAt == nil { - t.Fatal("user should active for now, verification code should nil") - } - - userToken, loginErr := userSvc.Login(ctx, model.UserLogin{ - Email: createdUser.Email, - Password: createdUser.Password, - IP: helper.RandomIPAddress(), - }) - if userToken == "" || loginErr != nil { - t.Error("login should success") - } - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - token string - }{ - { - caseName: "success", - respCode: http.StatusOK, - token: userToken, - }, - { - caseName: "failed: fake claims", - respCode: http.StatusUnauthorized, - token: "fake-token", - }, - { - caseName: "failed: payload nil, token nil", - respCode: http.StatusUnauthorized, - token: "", - }, - } - - jwtHandler := middleware.NewJWTHandler() - for _, tc := range testCases { - c := helper.NewFiberCtx() - c.Request().Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", userToken)) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - fakeClaims := jwtHandler.GenerateClaims(tc.token) - if fakeClaims != nil { - c.Locals("claims", fakeClaims) - } - ctr.Logout(c) - resp := c.Response() - if resp.StatusCode() != tc.respCode { - t.Error("should equal, but got", resp.StatusCode()) - } - - if resp.StatusCode() == http.StatusOK { - respBody := c.Response().Body() - respString := string(respBody) - respStruct := struct { - Message string `json:"message"` - Success bool `json:"success"` - }{} - - err := json.Unmarshal([]byte(respString), &respStruct) - if err != nil { - t.Errorf("Failed to parse response JSON: %v", err) - } - - if !respStruct.Success { - t.Error("Expected success") - } - } - } -} - -func TestUpdatePassword(t *testing.T) { - t.Parallel() - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - // create inactive user - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - - verifyErr := userSvc.Verification(ctx, model.UserVerificationCode{ - Code: *vCode, - Email: userByID.Email, - }) - if verifyErr != nil { - t.Error(constants.ShouldNotErr) - } - userByID = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - if userByID.VerificationCode != nil || userByID.ActivatedAt == nil { - t.Fatal("user should active for now, verification code should nil") - } - - userToken, loginErr := userSvc.Login(ctx, model.UserLogin{ - Email: createdUser.Email, - Password: createdUser.Password, - IP: helper.RandomIPAddress(), - }) - if userToken == "" || loginErr != nil { - t.Error("login should success") - } - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - token string - payload *model.UserPasswordUpdate - }{ - { - caseName: "success", - respCode: http.StatusNoContent, - token: userToken, - payload: &model.UserPasswordUpdate{ - OldPassword: createdUser.Password, - NewPassword: "passwordNew123", - NewPasswordConfirm: "passwordNew123", - }, - }, - { - caseName: "success", - respCode: http.StatusNoContent, - token: userToken, - payload: &model.UserPasswordUpdate{ - OldPassword: "passwordNew123", - NewPassword: "passwordNew12345", - NewPasswordConfirm: "passwordNew12345", - }, - }, - { - caseName: "failed: no new password", - respCode: http.StatusBadRequest, - token: userToken, - payload: &model.UserPasswordUpdate{ - OldPassword: "noNewPassword", - NewPassword: "noNewPassword", - NewPasswordConfirm: "noNewPassword", - }, - }, - { - caseName: "failed: payload nil", - respCode: http.StatusBadRequest, - token: userToken, - }, - { - caseName: "failed: fake claims", - respCode: http.StatusUnauthorized, - token: "fake-token", - }, - { - caseName: "failed: payload nil, token nil", - respCode: http.StatusUnauthorized, - token: "", - }, - } - - jwtHandler := middleware.NewJWTHandler() - for _, tc := range testCases { - c := helper.NewFiberCtx() - c.Request().Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", userToken)) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - if tc.payload != nil { - requestBody, err := json.Marshal(tc.payload) - if err != nil { - t.Fatal("Error while serializing payload to request body") - } - c.Request().SetBody(requestBody) - } - fakeClaims := jwtHandler.GenerateClaims(tc.token) - if fakeClaims != nil { - c.Locals("claims", fakeClaims) - } - ctr.UpdatePassword(c) - resp := c.Response() - if resp.StatusCode() != tc.respCode { - t.Error("should equal, but got", resp.StatusCode(), "want", tc.respCode) - } - - if resp.StatusCode() == http.StatusNoContent { - token, loginErr := userSvc.Login(ctx, model.UserLogin{ - Email: userByID.Email, - Password: tc.payload.NewPassword, - IP: helper.RandomIPAddress(), - }) - if loginErr != nil || token == "" { - t.Error("login should success with new password") - } - } - } -} - -func TestUpdateProfile(t *testing.T) { - t.Parallel() - // unaudit - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - // create inactive user - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - - verifyErr := userSvc.Verification(ctx, model.UserVerificationCode{ - Code: *vCode, - Email: userByID.Email, - }) - if verifyErr != nil { - t.Error(constants.ShouldNotErr) - } - userByID = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - if userByID.VerificationCode != nil || userByID.ActivatedAt == nil { - t.Fatal("user should active for now, verification code should nil") - } - - userToken, loginErr := userSvc.Login(ctx, model.UserLogin{ - Email: createdUser.Email, - Password: createdUser.Password, - IP: helper.RandomIPAddress(), - }) - if userToken == "" || loginErr != nil { - t.Error("login should success") - } - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - token string - payload *model.UserProfileUpdate - }{ - { - caseName: "success", - respCode: http.StatusNoContent, - token: userToken, - payload: &model.UserProfileUpdate{ - Name: helper.RandomString(11), - }, - }, - { - caseName: "success", - respCode: http.StatusNoContent, - token: userToken, - payload: &model.UserProfileUpdate{ - Name: helper.RandomString(11), - }, - }, - { - caseName: "failed: payload nil", - respCode: http.StatusBadRequest, - token: userToken, - }, - { - caseName: "failed: fake claims", - respCode: http.StatusUnauthorized, - token: "fake-token", - }, - { - caseName: "failed: payload nil, token nil", - respCode: http.StatusUnauthorized, - token: "", - }, - } - - jwtHandler := middleware.NewJWTHandler() - for _, tc := range testCases { - c := helper.NewFiberCtx() - c.Request().Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", userToken)) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - if tc.payload != nil { - requestBody, err := json.Marshal(tc.payload) - if err != nil { - t.Fatal("Error while serializing payload to request body") - } - c.Request().SetBody(requestBody) - } - fakeClaims := jwtHandler.GenerateClaims(tc.token) - if fakeClaims != nil { - c.Locals("claims", fakeClaims) - } - ctr.UpdateProfile(c) - resp := c.Response() - if resp.StatusCode() != tc.respCode { - t.Error("should equal, but got", resp.StatusCode(), "want", tc.respCode) - } - - if resp.StatusCode() == http.StatusNoContent { - userByID, err := userRepo.GetByID(ctx, userID) - if err != nil || userByID == nil { - t.Error(constants.ShouldNotErr) - } - - if userByID.Name != helper.ToTitle(tc.payload.Name) { - t.Error("shoudl equal") - } - } - } -} - -func TestMyProfile(t *testing.T) { - t.Parallel() - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - // create inactive user - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin - } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") - } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") - } - - verifyErr := userSvc.Verification(ctx, model.UserVerificationCode{ - Code: *vCode, - Email: userByID.Email, - }) - if verifyErr != nil { - t.Error(constants.ShouldNotErr) - } - userByID = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") - } - if userByID.VerificationCode != nil || userByID.ActivatedAt == nil { - t.Fatal("user should active for now, verification code should nil") - } - - userToken, loginErr := userSvc.Login(ctx, model.UserLogin{ - Email: createdUser.Email, - Password: createdUser.Password, - IP: helper.RandomIPAddress(), - }) - if userToken == "" || loginErr != nil { - t.Error("login should success") - } - defer func() { - userRepo.Delete(ctx, userID) - - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - token string - }{ - { - caseName: "success", - respCode: http.StatusOK, - token: userToken, - }, - { - caseName: "failed: fake claims", - respCode: http.StatusUnauthorized, - token: "fake-token", - }, - { - caseName: "failed: payload nil, token nil", - respCode: http.StatusUnauthorized, - token: "", - }, - } - - jwtHandler := middleware.NewJWTHandler() - for _, tc := range testCases { - c := helper.NewFiberCtx() - c.Request().Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", userToken)) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - fakeClaims := jwtHandler.GenerateClaims(tc.token) - if fakeClaims != nil { - c.Locals("claims", fakeClaims) - } - ctr.MyProfile(c) - resp := c.Response() - if resp.StatusCode() != tc.respCode { - t.Error("should equal, but got", resp.StatusCode()) - } - - if resp.StatusCode() == http.StatusOK { - respBody := c.Response().Body() - respString := string(respBody) - respStruct := struct { - Message string `json:"message"` - Success bool `json:"success"` - Data model.UserProfile `json:"data"` - }{} - - err := json.Unmarshal([]byte(respString), &respStruct) - if err != nil { - t.Errorf("Failed to parse response JSON: %v", err) - } - - if !respStruct.Success { - t.Error("Expected success") - } - if respStruct.Message != response.MessageSuccessLoaded { - t.Error("Expected message to be equal") - } - if respStruct.Data.Email != createdUser.Email || respStruct.Data.Role.ID != createdUser.RoleID { - t.Error("email and other should equal") - } - } - } -} diff --git a/controller/user_management/user_management_controller.go b/controller/user_management/user_management_controller.go deleted file mode 100644 index 8032c72..0000000 --- a/controller/user_management/user_management_controller.go +++ /dev/null @@ -1,167 +0,0 @@ -// don't use this for production -// use this file just for testing -// and testing management. - -package controller - -import ( - "math" - "strings" - - "github.com/go-playground/validator/v10" - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/response" - service "github.com/Lukmanern/gost/service/user_management" -) - -type UserManagementController interface { - // Create func creates a new user - Create(c *fiber.Ctx) error - - // Get func gets a user - Get(c *fiber.Ctx) error - - // GetAll func gets some users - GetAll(c *fiber.Ctx) error - - // Update func updates a user - Update(c *fiber.Ctx) error - - // Delete func deletes a user - Delete(c *fiber.Ctx) error -} - -type UserManagementControllerImpl struct { - service service.UserManagementService -} - -func NewUserManagementController(userService service.UserManagementService) UserManagementController { - return &UserManagementControllerImpl{ - service: userService, - } -} - -func (ctr *UserManagementControllerImpl) Create(c *fiber.Ctx) error { - var user model.UserCreate - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - user.Email = strings.ToLower(user.Email) - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - id, createErr := ctr.service.Create(ctx, user) - if createErr != nil { - fiberErr, ok := createErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+createErr.Error()) - } - data := map[string]any{ - "id": id, - } - return response.SuccessCreated(c, data) -} - -func (ctr *UserManagementControllerImpl) Get(c *fiber.Ctx) error { - id, err := c.ParamsInt("id") - if err != nil || id <= 0 { - return response.BadRequest(c, constants.InvalidID) - } - - ctx := c.Context() - userProfile, getErr := ctr.service.GetByID(ctx, id) - if getErr != nil { - fiberErr, ok := getErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+getErr.Error()) - } - return response.SuccessLoaded(c, userProfile) -} - -func (ctr *UserManagementControllerImpl) GetAll(c *fiber.Ctx) error { - request := base.RequestGetAll{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 20), - Keyword: c.Query("search"), - Sort: c.Query("sort"), - } - if request.Page <= 0 || request.Limit <= 0 { - return response.BadRequest(c, "invalid page or limit value") - } - - ctx := c.Context() - users, total, getErr := ctr.service.GetAll(ctx, request) - if getErr != nil { - return response.Error(c, constants.ServerErr+getErr.Error()) - } - - data := make([]interface{}, len(users)) - for i := range users { - data[i] = users[i] - } - responseData := base.GetAllResponse{ - Meta: base.PageMeta{ - Total: total, - Pages: int(math.Ceil(float64(total) / float64(request.Limit))), - Page: request.Page, - }, - Data: data, - } - return response.SuccessLoaded(c, responseData) -} - -func (ctr *UserManagementControllerImpl) Update(c *fiber.Ctx) error { - id, err := c.ParamsInt("id") - if err != nil || id <= 0 { - return response.BadRequest(c, constants.InvalidID) - } - var user model.UserProfileUpdate - user.ID = id - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) - } - - ctx := c.Context() - updateErr := ctr.service.Update(ctx, user) - if updateErr != nil { - fiberErr, ok := updateErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+updateErr.Error()) - } - return response.SuccessNoContent(c) -} - -func (ctr *UserManagementControllerImpl) Delete(c *fiber.Ctx) error { - id, err := c.ParamsInt("id") - if err != nil || id <= 0 { - return response.BadRequest(c, constants.InvalidID) - } - - ctx := c.Context() - deleteErr := ctr.service.Delete(ctx, id) - if deleteErr != nil { - fiberErr, ok := deleteErr.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, constants.ServerErr+deleteErr.Error()) - } - return response.SuccessNoContent(c) -} diff --git a/controller/user_management/user_management_controller_test.go b/controller/user_management/user_management_controller_test.go deleted file mode 100644 index aa5fd96..0000000 --- a/controller/user_management/user_management_controller_test.go +++ /dev/null @@ -1,554 +0,0 @@ -// Don't run test per file without -p 1 -// or simply run test per func or run -// project test using make test command -// check Makefile file -package controller_test - -import ( - "bytes" - "encoding/json" - "io" - "log" - "net/http" - "net/http/httptest" - "strconv" - "testing" - - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" - "github.com/Lukmanern/gost/internal/response" - - controller "github.com/Lukmanern/gost/controller/user_management" - service "github.com/Lukmanern/gost/service/user_management" -) - -var ( - userDevService service.UserManagementService - userDevController controller.UserManagementController - appURL string -) - -func init() { - env.ReadConfig("./../../.env") - config := env.Configuration() - appURL = config.AppURL - - connector.LoadDatabase() - r := connector.LoadRedisCache() - r.FlushAll() // clear all key:value in redis - - userDevService = service.NewUserManagementService() - userDevController = controller.NewUserManagementController(userDevService) -} - -func TestCreate(t *testing.T) { - c := helper.NewFiberCtx() - ctr := userDevController - if ctr == nil || c == nil { - t.Error(constants.ShouldNotNil) - } - c.Method(http.MethodPost) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - createdUser := model.UserCreate{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - } - createdUserID, createErr := userDevService.Create(c.Context(), createdUser) - if createErr != nil || createdUserID < 1 { - t.Fatal("should not error and userID should more tha zero") - } - defer func() { - userDevService.Delete(c.Context(), createdUserID) - r := recover() - if r != nil { - t.Error("panic ::", r) - } - }() - - testCases := []struct { - caseName string - payload *model.UserCreate - wantErr bool - }{ - { - caseName: "success create user -1", - payload: &model.UserCreate{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - IsAdmin: true, - }, - wantErr: false, - }, - { - caseName: "success create user -2", - payload: &model.UserCreate{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - IsAdmin: true, - }, - wantErr: false, - }, - { - caseName: "success create user -3", - payload: &model.UserCreate{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - IsAdmin: true, - }, - wantErr: false, - }, - { - caseName: "failed create user: invalid email address", - payload: &model.UserCreate{ - Name: helper.RandomString(10), - Email: "invalid-email-address", - Password: helper.RandomString(11), - IsAdmin: true, - }, - wantErr: true, - }, - { - caseName: "failed create user: email already used", - payload: &model.UserCreate{ - Name: helper.RandomString(10), - Email: createdUser.Email, - Password: helper.RandomString(11), - IsAdmin: true, - }, - wantErr: true, - }, - { - caseName: "failed create user: password too short", - payload: &model.UserCreate{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: "short", - IsAdmin: true, - }, - wantErr: true, - }, - { - caseName: "failed create user: nil payload, validate failed", - payload: nil, - wantErr: true, - }, - } - - for _, tc := range testCases { - jsonObject, marshalErr := json.Marshal(&tc.payload) - if marshalErr != nil { - t.Error(constants.ShouldNotErr, marshalErr.Error()) - } - c.Request().SetBody(jsonObject) - - createErr := ctr.Create(c) - if createErr != nil { - t.Error(constants.ShouldNotErr, createErr) - } else if tc.payload == nil { - continue - } - - ctx := c.Context() - userByEMail, getErr := userDevService.GetByEmail(ctx, tc.payload.Email) - // if wantErr is false and user is not found - // there is test failed - if getErr != nil && !tc.wantErr { - t.Fatal("test fail", getErr) - } - if !tc.wantErr { - if userByEMail == nil { - t.Fatal(constants.ShouldNotNil) - } else { - deleteErr := userDevService.Delete(ctx, userByEMail.ID) - if deleteErr != nil { - t.Error(constants.ShouldNotErr) - } - } - if userByEMail.Name != helper.ToTitle(tc.payload.Name) { - t.Error(constants.ShouldEqual) - } - } - } -} - -func TestGet(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - if c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - createdUser := model.UserCreate{ - Name: helper.RandomString(11), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - IsAdmin: true, - } - createdUserID, createErr := userDevService.Create(ctx, createdUser) - if createErr != nil || createdUserID <= 0 { - t.Error("should not error and more than zero") - } - defer func() { - userDevService.Delete(ctx, createdUserID) - r := recover() - if r != nil { - t.Error("panic ::", r) - } - }() - - testCases := []struct { - caseName string - userID string - respCode int - wantErr bool - response response.Response - }{ - { - caseName: "success get user", - userID: strconv.Itoa(createdUserID), - respCode: http.StatusOK, - wantErr: false, - response: response.Response{ - Message: response.MessageSuccessLoaded, - Success: true, - }, - }, - { - caseName: "failed get user: negatif user id", - userID: "-10", - respCode: http.StatusBadRequest, - wantErr: true, - }, - { - caseName: "failed get user: user not found", - userID: "9999", - respCode: http.StatusNotFound, - wantErr: true, - }, - { - caseName: "failed get user: failed convert id to int", - userID: "not-number", - respCode: http.StatusBadRequest, - wantErr: true, - }, - } - - for _, tc := range testCases { - req := httptest.NewRequest(http.MethodGet, "/user-management/"+tc.userID, nil) - app := fiber.New() - app.Get("/user-management/:id", userDevController.Get) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error(constants.ShouldEqual) - } - if !tc.wantErr { - respModel := response.Response{} - decodeErr := json.NewDecoder(resp.Body).Decode(&respModel) - if decodeErr != nil { - t.Error(constants.ShouldNotErr, decodeErr) - } - - if tc.response.Message != respModel.Message && tc.response.Message != "" { - t.Error(constants.ShouldEqual) - } - if respModel.Success != tc.response.Success { - t.Error(constants.ShouldEqual) - } - } - } -} - -func TestGetAll(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - if c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - userIDs := make([]int, 0) - for i := 0; i < 10; i++ { - createdUser := model.UserCreate{ - Name: helper.RandomString(11), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - IsAdmin: true, - } - createdUserID, createErr := userDevService.Create(ctx, createdUser) - if createErr != nil || createdUserID <= 0 { - t.Error("should not error and more than zero") - } - userIDs = append(userIDs, createdUserID) - } - - defer func() { - for _, id := range userIDs { - userDevService.Delete(ctx, id) - } - r := recover() - if r != nil { - t.Error("panic ::", r) - } - }() - - testCases := []struct { - caseName string - payload string - respCode int - wantErr bool - }{ - { - caseName: "success getall", - payload: "page=1&limit=100&search=", - respCode: http.StatusOK, - wantErr: false, - }, - { - caseName: "failed getall", - payload: "page=-1&limit=-100&search=", - respCode: http.StatusBadRequest, - wantErr: true, - }, - } - - for _, tc := range testCases { - req := httptest.NewRequest(http.MethodGet, "/user-management?"+tc.payload, nil) - app := fiber.New() - app.Get("/user-management", userDevController.GetAll) - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr, err.Error()) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error(constants.ShouldEqual) - } - if !tc.wantErr { - body := response.Response{} - bytes, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(constants.ShouldNotErr, err.Error()) - } - err = json.Unmarshal(bytes, &body) - if err != nil { - t.Fatal(constants.ShouldNotErr, err.Error()) - } - if !body.Success { - t.Fatal("should be success") - } - if len(bytes) <= 2 { - t.Error("len of bytes should much") - } - } - } -} - -func TestUpdate(t *testing.T) { - c := helper.NewFiberCtx() - ctr := userDevController - ctx := c.Context() - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - c.Method(http.MethodPut) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - createdUser := model.UserCreate{ - Name: helper.RandomString(11), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - IsAdmin: true, - } - createdUserID, createErr := userDevService.Create(ctx, createdUser) - if createErr != nil || createdUserID <= 0 { - t.Error("should not error and more than zero") - } - defer func() { - userDevService.Delete(ctx, createdUserID) - r := recover() - if r != nil { - t.Error("panic ::", r) - } - }() - - testCases := []struct { - caseName string - payload *model.UserProfileUpdate - respCode int - }{ - { - caseName: "success update user -1", - payload: &model.UserProfileUpdate{ - ID: createdUserID, - Name: helper.RandomString(6), - }, - respCode: http.StatusNoContent, - }, - { - caseName: "success update user -2", - payload: &model.UserProfileUpdate{ - ID: createdUserID, - Name: helper.RandomString(8), - }, - respCode: http.StatusNoContent, - }, - { - caseName: "success update user -3", - payload: &model.UserProfileUpdate{ - ID: createdUserID, - Name: helper.RandomString(10), - }, - respCode: http.StatusNoContent, - }, - { - caseName: "failed update: invalid id", - respCode: http.StatusBadRequest, - payload: &model.UserProfileUpdate{ - ID: -10, - Name: "valid-name", - }, - }, - { - caseName: "failed update: invalid name, too short", - respCode: http.StatusBadRequest, - payload: &model.UserProfileUpdate{ - ID: 11, - Name: "", - }, - }, - { - caseName: "failed update: not found", - respCode: http.StatusNotFound, - payload: &model.UserProfileUpdate{ - ID: createdUserID + 10, - Name: "valid-name", - }, - }, - } - - for _, tc := range testCases { - log.Println(tc.caseName) - jsonObject, err := json.Marshal(&tc.payload) - if err != nil { - t.Error(constants.ShouldNotErr, err.Error()) - } - url := appURL + "user-management/" + strconv.Itoa(tc.payload.ID) - req, httpReqErr := http.NewRequest(http.MethodPut, url, bytes.NewReader(jsonObject)) - if httpReqErr != nil || req == nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - app := fiber.New() - app.Put("/user-management/:id", userDevController.Update) - req.Close = true - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error(constants.ShouldEqual, resp.StatusCode) - } - if tc.payload != nil { - respModel := response.Response{} - decodeErr := json.NewDecoder(resp.Body).Decode(&respModel) - if decodeErr != nil && decodeErr != io.EOF { - t.Error(constants.ShouldNotErr, decodeErr) - } - } - } -} - -func TestDelete(t *testing.T) { - c := helper.NewFiberCtx() - ctr := userDevController - ctx := c.Context() - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - c.Method(http.MethodPut) - c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - createdUser := model.UserCreate{ - Name: helper.RandomString(11), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - IsAdmin: true, - } - createdUserID, createErr := userDevService.Create(ctx, createdUser) - if createErr != nil || createdUserID <= 0 { - t.Error("should not error and more than zero") - } - defer func() { - userDevService.Delete(ctx, createdUserID) - r := recover() - if r != nil { - t.Error("panic ::", r) - } - }() - - testCases := []struct { - caseName string - wantErr bool - respCode int - paramID int - response response.Response - }{ - { - caseName: "success delete user", - respCode: http.StatusNoContent, - paramID: createdUserID, - }, - { - caseName: "failed delete: invalid id", - respCode: http.StatusBadRequest, - paramID: -100, - }, - { - caseName: "failed delete: not found", - respCode: http.StatusNotFound, - paramID: createdUserID + 100, - }, - } - - for _, tc := range testCases { - log.Println(tc.caseName) - url := appURL + "user-management/" + strconv.Itoa(tc.paramID) - req, httpReqErr := http.NewRequest(http.MethodDelete, url, nil) - if httpReqErr != nil || req == nil { - t.Fatal(constants.ShouldNotNil) - } - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - app := fiber.New() - app.Delete("/user-management/:id", userDevController.Delete) - req.Close = true - resp, err := app.Test(req, -1) - if err != nil { - t.Fatal(constants.ShouldNotErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.respCode { - t.Error(constants.ShouldEqual, resp.StatusCode) - } - } - - userByID, err := userDevService.GetByID(ctx, createdUserID) - if err == nil || userByID != nil { - t.Error("should error and user should nil") - } -} diff --git a/database/migration/main.go b/database/migration/main.go index 95d6396..b8de018 100644 --- a/database/migration/main.go +++ b/database/migration/main.go @@ -7,7 +7,7 @@ import ( "github.com/Lukmanern/gost/database/connector" "github.com/Lukmanern/gost/domain/entity" "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/rbac" + "github.com/Lukmanern/gost/internal/role" "gorm.io/gorm" ) @@ -91,39 +91,13 @@ func seeding() { } // Seeding permission and role - for _, data := range rbac.AllRoles() { + for _, data := range role.AllRoles() { + data.SetCreateTime() if createErr := tx.Create(&data).Error; createErr != nil { tx.Rollback() log.Panicf("Error while creating Roles: %s", createErr) } } - for _, perm := range rbac.AllPermissions() { - perm.SetCreateTime() - perm.ID = 0 - if createErr := tx.Create(&perm).Error; createErr != nil { - tx.Rollback() - log.Panicf("Error while creating Permissions: %s", createErr) - } - - if perm.ID <= 20 { - if createErr := tx.Create(&entity.RoleHasPermission{ - RoleID: 1, // admin - PermissionID: perm.ID, - }).Error; createErr != nil { - tx.Rollback() - log.Panicf("Error while creating Roles: %s", createErr) - } - } - if perm.ID > 10 { - if createErr := tx.Create(&entity.RoleHasPermission{ - RoleID: 2, // user - PermissionID: perm.ID, - }).Error; createErr != nil { - tx.Rollback() - log.Panicf("Error while creating Roles: %s", createErr) - } - } - } // Commit the transaction for seeding if commitErr := tx.Commit().Error; commitErr != nil { diff --git a/docs/Gost Project Docs.postman_collection.json b/docs/Gost Project Docs.postman_collection.json deleted file mode 100644 index ee57c4e..0000000 --- a/docs/Gost Project Docs.postman_collection.json +++ /dev/null @@ -1,1419 +0,0 @@ -{ - "info": { - "_postman_id": "269648ff-f4c8-4685-8a64-211226f94ad6", - "name": "Gost Project Docs", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "16420382" - }, - "item": [ - { - "name": "User Management", - "item": [ - { - "name": "Create", - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"John Doe\",\r\n \"email\": \"your_valid_active@gmail.com\",\r\n \"password\": \"password00\",\r\n \"is_admin\": true // if false -> user\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user-management/create", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user-management", - "create" - ] - } - }, - "response": [] - }, - { - "name": "Get", - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://localhost:9009/user-management/1", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "9009", - "path": [ - "user-management", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Get All", - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/user-management/?page=1&limit=100&search=", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user-management", - "" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "100" - }, - { - "key": "search", - "value": "" - } - ] - } - }, - "response": [] - }, - { - "name": "Update", - "request": { - "auth": { - "type": "noauth" - }, - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"John Doe Key\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user-management/1", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user-management", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Delete", - "request": { - "auth": { - "type": "noauth" - }, - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user-management/1", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user-management", - "1" - ] - } - }, - "response": [] - } - ], - "description": "CRUD User without authentication." - }, - { - "name": "Development", - "item": [ - { - "name": "Ping SQL DB", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/development/ping/db", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "ping", - "db" - ] - } - }, - "response": [] - }, - { - "name": "Ping Redis", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/development/ping/redis", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "ping", - "redis" - ] - } - }, - "response": [] - }, - { - "name": "Test Panic Handler", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/development/panic", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "panic" - ] - } - }, - "response": [] - }, - { - "name": "Set to Redis", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/development/storing-to-redis", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "storing-to-redis" - ] - } - }, - "response": [] - }, - { - "name": "Get from Redis", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/development/get-from-redis", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "get-from-redis" - ] - } - }, - "response": [] - }, - { - "name": "_Test New Role (unused)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJ1bnN1cmx1a21hbkBnbWFpbC5jb20iLCJyb2xlIjoibmV3LXJvbGUtMDAyIiwicGVybWlzc2lvbnMiOnsiMyI6Mjh9LCJleHAiOjE3MDA0NjAyMDIsIm5iZiI6MTY5OTYwMTAwMn0.WO9OnVLct8Ta-u3fuC5TZPzgwviFiEWvtALplsF_KNpH_LoGujjB_Aa55_eHaIQwg0b1Wycb4ntaojSG40Wiut7LN9Uizb17Je1ewUw7HZmZp-HU7dWvRDYi68FvCP1ra0VZ1BnFzs8d8gYzPJ0oUtRBs3oySJTULZaW_zN07M8", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/development/test-new-role", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "test-new-role" - ] - } - }, - "response": [] - }, - { - "name": "_Test New Permission (unused)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJ1bnN1cmx1a21hbkBnbWFpbC5jb20iLCJyb2xlIjoibmV3LXJvbGUtMDAyIiwicGVybWlzc2lvbnMiOnsiMyI6Mjh9LCJleHAiOjE3MDA0NjAyMDIsIm5iZiI6MTY5OTYwMTAwMn0.WO9OnVLct8Ta-u3fuC5TZPzgwviFiEWvtALplsF_KNpH_LoGujjB_Aa55_eHaIQwg0b1Wycb4ntaojSG40Wiut7LN9Uizb17Je1ewUw7HZmZp-HU7dWvRDYi68FvCP1ra0VZ1BnFzs8d8gYzPJ0oUtRBs3oySJTULZaW_zN07M8", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/development/test-new-permission", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "test-new-permission" - ] - } - }, - "response": [] - }, - { - "name": "List All Files", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"limit\": 9999,\r\n \"offset\": 1,\r\n \"prefix\": \"\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://SECRET-UNIQUE-STRING.supabase.co/storage/v1/object/list/user_upload_public", - "protocol": "https", - "host": [ - "SECRET-UNIQUE-STRING", - "supabase", - "co" - ], - "path": [ - "storage", - "v1", - "object", - "list", - "user_upload_public" - ] - } - }, - "response": [] - }, - { - "name": "List All Files API", - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "http://127.0.0.1:9009/development/get-files-list", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "get-files-list" - ] - } - }, - "response": [] - }, - { - "name": "Upload File", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "file", - "type": "file", - "src": "/C:/Users/Lenovo/OneDrive/Desktop/Sec/Best Practices for MITRE ATT&CK Mapping.pdf" - }, - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ] - }, - "url": { - "raw": "https://tntrwzoefhqqauzrttpe.supabase.co/storage/v1/object/user_upload_public/Best Practices for MITRE ATT&CK Mapping.pdf", - "protocol": "https", - "host": [ - "tntrwzoefhqqauzrttpe", - "supabase", - "co" - ], - "path": [ - "storage", - "v1", - "object", - "user_upload_public", - "Best Practices for MITRE ATT&CK Mapping.pdf" - ] - } - }, - "response": [] - }, - { - "name": "Upload File API", - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "file", - "type": "file", - "src": "/C:/Users/Lenovo/OneDrive/Desktop/Sec/Hack Book/cyber-security.pdf" - }, - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ] - }, - "url": { - "raw": "http://127.0.0.1:9009/development/upload-file", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "upload-file" - ] - } - }, - "response": [] - }, - { - "name": "Remove File", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"prefixes\": \"Best Practices for MITRE ATT&CK Mapping.pdf\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://tntrwzoefhqqauzrttpe.supabase.co/storage/v1/object/user_upload_public", - "protocol": "https", - "host": [ - "tntrwzoefhqqauzrttpe", - "supabase", - "co" - ], - "path": [ - "storage", - "v1", - "object", - "user_upload_public" - ] - } - }, - "response": [] - }, - { - "name": "Remove File API", - "request": { - "auth": { - "type": "noauth" - }, - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"file_name\": \"uploaded-file.pdf\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/development/remove-file", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "development", - "remove-file" - ] - } - }, - "response": [] - } - ], - "description": "StartFragmentDevelopment Routes provides experimental/ developing/ testingfor routes, middleware, connection and many more without JWT authentication in header.\n\nSo, don't forget to commentedon the line of code that routes **getDevopmentRouterin the app.go.**" - }, - { - "name": "User", - "item": [ - { - "name": "Register", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"John Doe\",\r\n \"email\": \"your_valid_email@gmail.com\", // your valid & active email\r\n \"password\": \"password00\",\r\n \"role_id\": 1\r\n // role_id 1 == admin, 2 == user, see at database/migration golang-code.\r\n}\r\n\r\n", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user/register", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "register" - ] - } - }, - "response": [] - }, - { - "name": "Verification / Account Activation", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"email\": \"your_valid_email@gmail.com\", // your valid & active email\r\n \"code\": \"QQFjl5ZNSEmrfGK6OOoF1\" // check your email inbox/ spam\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user/verification", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "verification" - ] - } - }, - "response": [] - }, - { - "name": "Req. Delete Account from Activation", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"email\": \"lukmanernandi16@gmail.com\",\r\n \"code\": \"qJtHrTP1Yzy6N9UcgL8e0\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user/request-delete", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "request-delete" - ] - } - }, - "response": [] - }, - { - "name": "Login", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"email\": \"your_valid_email@gmail.com\", // your valid & active email\r\n \"password\": \"password00\",\r\n \"ip\": \"128.0.0.18\"\r\n // After login you can copy-paste the token to https://jwt.io/ too see \r\n // and understand the structure of claims struct (see internal/middleware).\r\n}\r\n", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user/login", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "login" - ] - } - }, - "response": [] - }, - { - "name": "Forgot Password", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"email\": \"your_valid_email@gmail.com\", // your valid & active email\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user/forget-password", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "forget-password" - ] - } - }, - "response": [] - }, - { - "name": "Reset Password", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"email\": \"your_valid_email@gmail.com\", // your valid & active email\r\n \"code\": \"uH7b9bKO4kxY96NYwuQPQ\", // check your email inbox / spam\r\n \"new_password\": \"password99\",\r\n \"new_password_confirm\": \"password99\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user/reset-password", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "reset-password" - ] - } - }, - "response": [] - }, - { - "name": "My Profile", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/user/my-profile", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "my-profile" - ] - } - }, - "response": [] - }, - { - "name": "Logout", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/user/logout", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "logout" - ] - } - }, - "response": [] - }, - { - "name": "Profile Update", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"John Doe (updated)\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user/profile-update", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "profile-update" - ] - } - }, - "response": [] - }, - { - "name": "Update Password", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"old_password\": \"password00\",\r\n \"new_password\": \"password000\", // should different with old passwd\r\n \"new_password_confirm\": \"password000\" // should equal with new passwd\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/user/update-password", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "user", - "update-password" - ] - } - }, - "response": [] - } - ], - "description": "StartFragmentUser Management Routes provides create, read (get & getAll), update, anddelete functionalities for user data management without JWT authenticationin header. So, don't forget to commented on the line of code that routesgetUserManagementRoutes in the app.go." - }, - { - "name": "Role and Permission", - "item": [ - { - "name": "Create Role", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"new-role-001\",\r\n \"description\": \"new-role-001 description\",\r\n \"permissions_id\": [1, 2, 3]\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/role", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "role" - ] - } - }, - "response": [] - }, - { - "name": "Connect Role to Permissions", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"role_id\": 3,\r\n \"permissions_id\": [19,20,21]\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/role/connect", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "role", - "connect" - ] - } - }, - "response": [] - }, - { - "name": "Get Role", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/role/1", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "role", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Get All Roles", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJsdWttYW5lcm5hbmRpMTZAZ21haWwuY29tIiwicm9sZSI6ImFkbWluIiwicGVybWlzc2lvbnMiOnsiMSI6MjU1LCIyIjoyNTUsIjMiOjE1fSwiZXhwIjoxNzAwNDYwNTY1LCJuYmYiOjE2OTk2MDEzNjV9.XdcZVnEmedlaYhWgNjGYPJn9tm9H4jEbkleIKCoE5bDqGj5Zcv4UjSebkCWOCSzNpiCEMVsePuXgW8bRn9r6JDgCAXgsneUvVJHIXwh5FmtOghqXU8E06PsJ2ajkkiSqR9yFh_xI2AsQ6TKk-RM_-DfeRZKt9uryZ5Iur_gaL9M", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/role?page=1&limit=20&search=", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "role" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "search", - "value": "" - } - ] - } - }, - "response": [] - }, - { - "name": "Update Role", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJsdWttYW5lcm5hbmRpMTZAZ21haWwuY29tIiwicm9sZSI6ImFkbWluIiwicGVybWlzc2lvbnMiOnsiMSI6MjU1LCIyIjoyNTUsIjMiOjE1fSwiZXhwIjoxNzAwNDYwNTY1LCJuYmYiOjE2OTk2MDEzNjV9.XdcZVnEmedlaYhWgNjGYPJn9tm9H4jEbkleIKCoE5bDqGj5Zcv4UjSebkCWOCSzNpiCEMVsePuXgW8bRn9r6JDgCAXgsneUvVJHIXwh5FmtOghqXU8E06PsJ2ajkkiSqR9yFh_xI2AsQ6TKk-RM_-DfeRZKt9uryZ5Iur_gaL9M", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\" : \"admin-update\",\r\n \"description\": \"description menyusul saja ya ...\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/role/1", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "role", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Delete Role", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJsdWttYW5lcm5hbmRpMTZAZ21haWwuY29tIiwicm9sZSI6ImFkbWluIiwicGVybWlzc2lvbnMiOnsiMSI6MjU1LCIyIjoyNTUsIjMiOjE1fSwiZXhwIjoxNzAwNDYwNTY1LCJuYmYiOjE2OTk2MDEzNjV9.XdcZVnEmedlaYhWgNjGYPJn9tm9H4jEbkleIKCoE5bDqGj5Zcv4UjSebkCWOCSzNpiCEMVsePuXgW8bRn9r6JDgCAXgsneUvVJHIXwh5FmtOghqXU8E06PsJ2ajkkiSqR9yFh_xI2AsQ6TKk-RM_-DfeRZKt9uryZ5Iur_gaL9M", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"test update\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/role/3", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "role", - "3" - ] - } - }, - "response": [] - }, - { - "name": "Create Permission", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"new-permission-001\",\r\n \"description\": \"new-permission-001 Description\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/permission", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "permission" - ] - } - }, - "response": [] - }, - { - "name": "Get Permission", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJleGFtcGxlMkBleGFtcGxlLmNvbSIsInJvbGUiOiJhZG1pbiIsInBlcm1pc3Npb25zIjpbImNyZWF0ZS11c2VyIiwidmlldy11c2VyIiwidXBkYXRlLXVzZXIiLCJkZWxldGUtdXNlciIsImNyZWF0ZS1yb2xlIiwidmlldy1yb2xlIiwidXBkYXRlLXJvbGUiLCJkZWxldGUtcm9sZSIsImNyZWF0ZS11c2VyLWhhcy1yb2xlIiwidmlldy11c2VyLWhhcy1yb2xlIiwidXBkYXRlLXVzZXItaGFzLXJvbGUiLCJkZWxldGUtdXNlci1oYXMtcm9sZSIsImNyZWF0ZS1wZXJtaXNzaW9uIiwicmVhZC1wZXJtaXNzaW9uIiwidXBkYXRlLXBlcm1pc3Npb24iLCJkZWxldGUtcGVybWlzc2lvbiIsImNyZWF0ZS1yb2xlLWhhcy1wZXJtaXNzaW9ucyIsInZpZXctcm9sZS1oYXMtcGVybWlzc2lvbnMiLCJ1cGRhdGUtcm9sZS1oYXMtcGVybWlzc2lvbnMiLCJkZWxldGUtcm9sZS1oYXMtcGVybWlzc2lvbnMiLCJjcmVhdGUtb25lIiwidmlldy1vbmUiLCJ1cGRhdGUtb25lIiwiZGVsZXRlLW9uZSIsImNyZWF0ZS10d28iLCJ2aWV3LXR3byIsInVwZGF0ZS10d28iLCJkZWxldGUtdHdvIiwiY3JlYXRlLXRocmVlIiwidmlldy10aHJlZSIsInVwZGF0ZS10aHJlZSIsImRlbGV0ZS10aHJlZSIsImNyZWF0ZS1mb3VyIiwidmlldy1mb3VyIiwidXBkYXRlLWZvdXIiLCJkZWxldGUtZm91ciIsImNyZWF0ZS1maXZlIiwidmlldy1maXZlIiwidXBkYXRlLWZpdmUiLCJkZWxldGUtZml2ZSIsImNyZWF0ZS1zaXgiLCJ2aWV3LXNpeCIsInVwZGF0ZS1zaXgiLCJkZWxldGUtc2l4IiwiY3JlYXRlLXNldmVuIiwidmlldy1zZXZlbiIsInVwZGF0ZS1zZXZlbiIsImRlbGV0ZS1zZXZlbiJdLCJsYWJlbCI6bnVsbCwiZXhwIjoxNjk3OTQyNDkxLCJuYmYiOjE2OTcwODMyOTF9.ZMVHIXp9-e5JgTQxT2xf-exZlUc3b04gTAJy-kBIylAFHf92tmDrRvYt6o4z8UKGvu5OiUskSEplXPQ4h1cOAjV6g3JE4Zs8GBdj4t453rt7G54j7a2eDAd1Wv6bhLQvx-LtX_vGQHFWcW4ivz-p0FvUe5BP57S3ONXgb1P8ORk", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/permission/1", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "permission", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Get All Permissions", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://127.0.0.1:9009/permission?page=1&limit=20&search=", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "permission" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "search", - "value": "" - } - ] - } - }, - "response": [] - }, - { - "name": "Update Permission", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"test update\",\r\n \"description\": \"xxx\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/permission/1", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "permission", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Delete Permission", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"name\": \"test update\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://127.0.0.1:9009/permission/1", - "protocol": "http", - "host": [ - "127", - "0", - "0", - "1" - ], - "port": "9009", - "path": [ - "permission", - "1" - ] - } - }, - "response": [] - } - ], - "description": "StartFragmentRole-Permission Routes provides des create, read (get & getAll), update, anddelete functionalities for Role and Permission entities including connectingboth of them. This routes can be access by user that has admin-role (see database/migration)." - } - ] -} \ No newline at end of file diff --git a/docs/service.txt b/docs/service.txt deleted file mode 100644 index 317ca89..0000000 --- a/docs/service.txt +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=GostProject - -[Service] -Type=simple -Restart=always -RestartSec=5s -WorkingDirectory=/home//gost -ExecStart=/home//gost/main - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/domain/base/request.go b/domain/base/request.go deleted file mode 100644 index fad61e6..0000000 --- a/domain/base/request.go +++ /dev/null @@ -1,9 +0,0 @@ -package base - -// RequestGetAll struct used for request getAll controller funcs -type RequestGetAll struct { - Page int `query:"page"` - Limit int `query:"limit"` - Keyword string `query:"search"` - Sort string `query:"sort"` -} diff --git a/domain/base/response.go b/domain/base/response.go deleted file mode 100644 index e6f4717..0000000 --- a/domain/base/response.go +++ /dev/null @@ -1,13 +0,0 @@ -package base - -type PageMeta struct { - Total int `json:"total"` - Pages int `json:"pages"` - Page int `json:"page"` -} - -// GetAllResponse struct used for response getAll controller funcs -type GetAllResponse struct { - Meta PageMeta `json:"meta"` - Data interface{} `json:"data"` -} diff --git a/domain/base/time_test.go b/domain/base/time_test.go deleted file mode 100644 index 73354b5..0000000 --- a/domain/base/time_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package base - -import ( - "testing" -) - -func TestTimeFields_SetCreateTime(t *testing.T) { - tests := []struct { - name string - att *TimeFields - }{ - { - name: "Test SetTimes", - att: &TimeFields{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.att.SetCreateTime() - if tt.att.CreatedAt == nil || tt.att.UpdatedAt == nil { - t.Errorf("Expected CreatedAt and UpdatedAt to be set, but one or both are nil") - } - }) - } -} - -func TestTimeFields_SetUpdateTime(t *testing.T) { - tests := []struct { - name string - att *TimeFields - }{ - { - name: "Test SetUpdateTime", - att: &TimeFields{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.att.SetUpdateTime() - if tt.att.UpdatedAt == nil { - t.Errorf("Expected UpdatedAt to be set, but it is nil") - } - }) - } -} diff --git a/domain/entity/all_entities.go b/domain/entity/all_entities.go index 0105969..737a775 100644 --- a/domain/entity/all_entities.go +++ b/domain/entity/all_entities.go @@ -15,17 +15,17 @@ type Table interface { // AllTables func serve/ return all structs that // developer has been created. This func used in // database migration scripts. -func AllTables() []any { - allTables := []any{ - &User{}, - &UserHasRoles{}, - &Role{}, - &RoleHasPermission{}, - &Permission{}, - // ... - // Add more tables/structs - } +var allTables = []any{ + &User{}, + &UserHasRoles{}, + &Role{}, + + // ... + // Add more tables/structs +} + +func AllTables() []any { for _, table := range allTables { _, ok := table.(Table) if !ok { diff --git a/domain/entity/all_entities_test.go b/domain/entity/all_entities_test.go deleted file mode 100644 index 4aac3e5..0000000 --- a/domain/entity/all_entities_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package entity - -import ( - "testing" - "time" - - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/internal/constants" -) - -func TestAllTablesName(t *testing.T) { - type tableNamer interface { - TableName() string - } - allTables := AllTables() - - for _, table := range allTables { - strct, ok := table.(tableNamer) - if !ok { - t.Error("error while getting tableNamer") - } - name := strct.TableName() - if name == "" { - t.Errorf("TableName for %T should not be empty: " + name) - } - } -} - -func TestUserSetActivateAccount(t *testing.T) { - timeNow := time.Now() - code := "example-code" - user := User{ - VerificationCode: &code, - ActivatedAt: nil, - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - - user.SetActivateAccount() - if user.VerificationCode != nil { - t.Error(constants.ShouldNil) - } - if user.ActivatedAt == nil { - t.Error(constants.ShouldNil) - } -} diff --git a/domain/base/time.go b/domain/entity/base.go similarity index 63% rename from domain/base/time.go rename to domain/entity/base.go index acbe6f2..3eddd2e 100644 --- a/domain/base/time.go +++ b/domain/entity/base.go @@ -1,4 +1,4 @@ -package base +package entity import "time" @@ -8,6 +8,7 @@ import "time" type TimeFields struct { CreatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"created_at"` UpdatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"updated_at"` + DeletedAt *time.Time `gorm:"type:timestamp null;default:null" json:"deleted_at"` } // SetCreateTime func fills created_at and updated_at fields @@ -19,9 +20,15 @@ func (att *TimeFields) SetCreateTime() { } // SetUpdateTime func fills updated_at fields -// This struct prevents developers from forgets -// or any common mistake. +// This struct prevents developers from forgets or any common mistake. func (att *TimeFields) SetUpdateTime() { timeNow := time.Now() att.UpdatedAt = &timeNow } + +// SetDeleteTime func fills deleted_at fields and for status in softdelete feature +// This struct prevents developers from forgets or any common mistake. +func (att *TimeFields) SetDeleteTime() { + timeNow := time.Now() + att.DeletedAt = &timeNow +} diff --git a/domain/entity/role.go b/domain/entity/role.go new file mode 100644 index 0000000..0e53e96 --- /dev/null +++ b/domain/entity/role.go @@ -0,0 +1,15 @@ +// ⚠️ Don't forget to Add Your new Table +// at AllTables func at all_entities.go file + +package entity + +type Role struct { + ID int `gorm:"type:serial;primaryKey" json:"id"` + Name string `gorm:"type:varchar(255) not null unique" json:"name"` + Description string `gorm:"type:varchar(255) not null" json:"description"` + TimeFields +} + +func (e *Role) TableName() string { + return "roles" +} diff --git a/domain/entity/role_permission.go b/domain/entity/role_permission.go deleted file mode 100644 index cacd618..0000000 --- a/domain/entity/role_permission.go +++ /dev/null @@ -1,48 +0,0 @@ -// ⚠️ Don't forget to Add Your new Table -// at AllTables func at all_entities.go file - -package entity - -import "github.com/Lukmanern/gost/domain/base" - -// This vars used in userServiceLayer -const ( - ADMIN = 1 - USER = 2 - // ... - // Add your own roleID -) - -type Role struct { - ID int `gorm:"type:serial;primaryKey" json:"id"` - Name string `gorm:"type:varchar(255) not null unique" json:"name"` - Description string `gorm:"type:varchar(255) not null" json:"description"` - Permissions []Permission `gorm:"many2many:role_has_permissions" json:"permissions"` - base.TimeFields -} - -func (e *Role) TableName() string { - return "roles" -} - -type RoleHasPermission struct { - RoleID int `json:"role_id"` - Role Role `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"role"` - PermissionID int `json:"permission_id"` - Permission Permission `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"permission"` -} - -func (e *RoleHasPermission) TableName() string { - return "role_has_permissions" -} - -type Permission struct { - ID int `gorm:"type:serial;primaryKey" json:"id"` - Name string `gorm:"type:varchar(255) not null unique" json:"name"` - Description string `gorm:"type:varchar(255) not null" json:"description"` - base.TimeFields -} - -func (e *Permission) TableName() string { - return "permissions" -} diff --git a/domain/entity/user.go b/domain/entity/user.go index fb7b47c..31052a5 100644 --- a/domain/entity/user.go +++ b/domain/entity/user.go @@ -5,25 +5,16 @@ package entity import ( "time" - - "github.com/Lukmanern/gost/domain/base" ) type User struct { - ID int `gorm:"type:serial;primaryKey" json:"id"` - Name string `gorm:"type:varchar(100) not null" json:"name"` - Email string `gorm:"type:varchar(100) not null unique" json:"email"` - Password string `gorm:"type:varchar(255) not null" json:"password"` - VerificationCode *string `gorm:"type:varchar(100) null" json:"verification_code"` - ActivatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"activated_at"` - Roles []Role `gorm:"many2many:user_has_roles" json:"roles"` - base.TimeFields -} - -func (e *User) SetActivateAccount() { - timeNow := time.Now() - e.ActivatedAt = &timeNow - e.VerificationCode = nil + ID int `gorm:"type:bigserial;primaryKey" json:"id"` + Name string `gorm:"type:varchar(100) not null" json:"name"` + Email string `gorm:"type:varchar(100) not null unique" json:"email"` + Password string `gorm:"type:varchar(255) not null" json:"password"` + ActivatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"activated_at"` + Roles []Role `gorm:"many2many:user_has_roles" json:"roles"` + TimeFields } func (e *User) TableName() string { diff --git a/domain/model/base.go b/domain/model/base.go new file mode 100644 index 0000000..cd3bef5 --- /dev/null +++ b/domain/model/base.go @@ -0,0 +1,21 @@ +package model + +type PageMeta struct { + TotalData int `json:"total_data"` + TotalPages int `json:"total_pages"` + AtPage int `json:"at_page"` +} + +// GetAllResponse struct used for response getAll controller funcs +type GetAllResponse struct { + Meta PageMeta `json:"meta"` + Data interface{} `json:"data"` +} + +// RequestGetAll struct used for request getAll controller funcs +type RequestGetAll struct { + Page int `query:"page"` + Limit int `query:"limit"` + Keyword string `query:"search"` + Sort string `query:"sort"` +} diff --git a/domain/model/role.go b/domain/model/role.go new file mode 100644 index 0000000..efd7171 --- /dev/null +++ b/domain/model/role.go @@ -0,0 +1,21 @@ +package model + +import "github.com/Lukmanern/gost/domain/entity" + +type RoleResponse struct { + ID int `validate:"required,numeric,min=1" json:"id"` + Name string `validate:"required" json:"name"` + Description string `validate:"required" json:"description"` + entity.TimeFields +} + +type RoleCreate struct { + Name string `validate:"required,max=60" json:"name"` + Description string `validate:"required,max=100" json:"description"` +} + +type RoleUpdate struct { + ID int `validate:"required,numeric,min=1"` + Name string `validate:"required,max=60" json:"name"` + Description string `validate:"required,max=100" json:"description"` +} diff --git a/domain/model/role_permission.go b/domain/model/role_permission.go deleted file mode 100644 index fd23d2f..0000000 --- a/domain/model/role_permission.go +++ /dev/null @@ -1,41 +0,0 @@ -package model - -type RoleCreate struct { - Name string `validate:"required,min=1,max=60" json:"name"` - Description string `validate:"required,min=1,max=100" json:"description"` - PermissionsID []int `json:"permissions_id"` // can null -} - -type RoleResponse struct { - ID int `validate:"required,numeric,min=1" json:"id"` - Name string `validate:"required,min=1" json:"name"` - Description string `validate:"required,min=1" json:"description"` -} - -type RoleUpdate struct { - ID int `validate:"required,numeric,min=1"` - Name string `validate:"required,min=1,max=60" json:"name"` - Description string `validate:"required,min=1,max=100" json:"description"` -} - -type RoleConnectToPermissions struct { - RoleID int `validate:"required,numeric,min=1" json:"role_id"` - PermissionsID []int `validate:"required" json:"permissions_id"` -} - -type PermissionCreate struct { - Name string `validate:"required,min=1,max=60" json:"name"` - Description string `validate:"required,min=1,max=100" json:"description"` -} - -type PermissionUpdate struct { - ID int `validate:"required,numeric,min=1"` - Name string `validate:"required,min=1,max=60" json:"name"` - Description string `validate:"required,min=1,max=100" json:"description"` -} - -type PermissionResponse struct { - ID int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` -} diff --git a/domain/model/user.go b/domain/model/user.go index 80f0457..8710966 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -9,7 +9,7 @@ type UserRegister struct { RoleID int `validate:"required,numeric,min=1" json:"role_id"` } -type UserVerificationCode struct { +type UserActivation struct { Code string `validate:"required,min=21,max=60" json:"code"` Email string `validate:"required,email,min=5,max=60" json:"email"` } @@ -41,5 +41,17 @@ type UserPasswordUpdate struct { type UserProfile struct { Email string Name string - Role entity.Role + Roles []string +} + +type UserResponse struct { + ID int + Name string +} + +type UserResponseDetail struct { + ID int + Email string + Name string + Roles []entity.Role } diff --git a/domain/model/user_management.go b/domain/model/user_management.go deleted file mode 100644 index c6d6f38..0000000 --- a/domain/model/user_management.go +++ /dev/null @@ -1,21 +0,0 @@ -package model - -type UserCreate struct { - Name string `validate:"required,min=5,max=60" json:"name"` - Email string `validate:"required,email,min=5,max=60" json:"email"` - Password string `validate:"required,min=8,max=30" json:"password"` - IsAdmin bool `validate:"boolean" json:"is_admin"` -} - -type UserResponse struct { - ID int `validate:"required,numeric,min=1" json:"id"` - Name string `validate:"required,min=5,max=60" json:"name"` - Email string `validate:"required,email,min=5,max=60" json:"email"` -} - -type UserProfileUpdate struct { - ID int `validate:"required,numeric,min=1"` - Name string `validate:"required,min=5,max=60" json:"name"` - // ... - // add more fields -} diff --git a/go.mod b/go.mod index b64cabd..236fe21 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/Lukmanern/gost go 1.20 require ( - github.com/go-playground/validator/v10 v10.15.5 github.com/go-redis/redis v6.15.9+incompatible github.com/gofiber/fiber/v2 v2.50.0 github.com/golang-jwt/jwt/v5 v5.0.0 @@ -20,9 +19,6 @@ require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect github.com/google/uuid v1.3.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -32,7 +28,6 @@ require ( github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/kr/text v0.2.0 // indirect - github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect diff --git a/go.sum b/go.sum index 2a19284..4581fec 100644 --- a/go.sum +++ b/go.sum @@ -9,15 +9,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= -github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -58,8 +49,6 @@ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -85,14 +74,9 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/internal/helper/helper.go b/internal/helper/helper.go index ac7fa44..e6ac12b 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -27,23 +27,6 @@ func RandomString(n uint) string { return string(b) } -// RandomEmails func return some emails -// used for testing and any needs. -func RandomEmails(n uint) []string { - emailsMap := make(map[string]int) - for uint(len(emailsMap)) < n { - body := strings.ToLower(RandomString(7) + RandomString(7) + RandomString(7)) - randEmail := body + "@gost.project" - emailsMap[randEmail]++ - } - - emails := make([]string, 0, len(emailsMap)) - for email := range emailsMap { - emails = append(emails, email) - } - return emails -} - // RandomEmail func return a email // used for testing and any needs. func RandomEmail() string { diff --git a/internal/helper/helper_test.go b/internal/helper/helper_test.go index 2ec1c68..90a137d 100644 --- a/internal/helper/helper_test.go +++ b/internal/helper/helper_test.go @@ -2,7 +2,6 @@ package helper import ( "net" - "strings" "testing" "github.com/Lukmanern/gost/internal/constants" @@ -16,17 +15,6 @@ func TestRandomString(t *testing.T) { } } -func TestRandomEmails(t *testing.T) { - for i := 1; i <= 20; i++ { - emails := RandomEmails(uint(i)) - assert.Len(t, emails, i, "total of emails should equal") - for _, email := range emails { - assert.GreaterOrEqual(t, len(email), 10, "length of an email should not less than 10") - assert.Equal(t, email, strings.ToLower(email), "email should be lowercase") - } - } -} - func TestRandomEmail(t *testing.T) { for i := 0; i < 25; i++ { email := RandomEmail() diff --git a/internal/rbac/permission.go b/internal/rbac/permission.go deleted file mode 100644 index 0e55c0e..0000000 --- a/internal/rbac/permission.go +++ /dev/null @@ -1,78 +0,0 @@ -package rbac - -import ( - "log" - - "github.com/Lukmanern/gost/domain/entity" -) - -// AllPermissions func return all permissions entities -// that has been created by developer. This func run self -// audit that check for name and id should be unique value. -// ⚠️ Do not forget to run TestAllPermissions to audit -func AllPermissions() []entity.Permission { - permissions := []entity.Permission{ - // user - PermCreateUser, PermViewUser, PermUpdateUser, PermDeleteUser, - // user has role - PermCreateUserHasRole, PermViewUserHasRole, PermUpdateUserHasRole, PermDeleteUserHasRole, - // role - PermCreateRole, PermViewRole, PermUpdateRole, PermDeleteRole, - // role has permissions - PermCreateRoleHasPermissions, PermViewRoleHasPermissions, PermUpdateRoleHasPermissions, PermDeleteRoleHasPermissions, - // permission - PermCreatePermission, PermViewPermission, PermUpdatePermission, PermDeletePermission, - // ... - // add more permissions - } - - // self tested check id and name - // id and name should unique - checkIDs := make(map[int]int) - checkNames := make(map[string]int) - for _, perm := range permissions { - if perm.ID < 1 || len(perm.Name) <= 1 { - log.Fatal("permission name too short or invalid id at:", perm) - } - checkIDs[perm.ID]++ - checkNames[perm.Name]++ - if checkIDs[perm.ID] > 1 || checkNames[perm.Name] > 1 { - log.Fatal("permission name or id should unique, but got:", perm) - } - } - - return permissions -} - -var ( - PermCreateUser = entity.Permission{ID: 1, Name: "create-user", Description: "CRUD for User Entity"} - PermViewUser = entity.Permission{ID: 2, Name: "view-user", Description: "CRUD for User Entity"} - PermUpdateUser = entity.Permission{ID: 3, Name: "update-user", Description: "CRUD for User Entity"} - PermDeleteUser = entity.Permission{ID: 4, Name: "delete-user", Description: "CRUD for User Entity"} - - PermCreateUserHasRole = entity.Permission{ID: 5, Name: "create-user-has-role", Description: "CRUD for User-Has-Role entity"} - PermViewUserHasRole = entity.Permission{ID: 6, Name: "view-user-has-role", Description: "CRUD for User-Has-Role entity"} - PermUpdateUserHasRole = entity.Permission{ID: 7, Name: "update-user-has-role", Description: "CRUD for User-Has-Role entity"} - PermDeleteUserHasRole = entity.Permission{ID: 8, Name: "delete-user-has-role", Description: "CRUD for User-Has-Role entity"} - - PermCreateRole = entity.Permission{ID: 9, Name: "create-role", Description: "CRUD for Role Entity"} - PermViewRole = entity.Permission{ID: 10, Name: "view-role", Description: "CRUD for Role Entity"} - PermUpdateRole = entity.Permission{ID: 11, Name: "update-role", Description: "CRUD for Role Entity"} - PermDeleteRole = entity.Permission{ID: 12, Name: "delete-role", Description: "CRUD for Role Entity"} - - PermCreateRoleHasPermissions = entity.Permission{ID: 13, Name: "create-role-has-permissions", Description: "CRUD for Role-Has-Permission Entity"} - PermViewRoleHasPermissions = entity.Permission{ID: 14, Name: "view-role-has-permissions", Description: "CRUD for Role-Has-Permission Entity"} - PermUpdateRoleHasPermissions = entity.Permission{ID: 15, Name: "update-role-has-permissions", Description: "CRUD for Role-Has-Permission Entity"} - PermDeleteRoleHasPermissions = entity.Permission{ID: 16, Name: "delete-role-has-permissions", Description: "CRUD for Role-Has-Permission Entity"} - - PermCreatePermission = entity.Permission{ID: 17, Name: "create-permission", Description: "CRUD for Role Entity"} - PermViewPermission = entity.Permission{ID: 18, Name: "read-permission", Description: "CRUD for Role Entity"} - PermUpdatePermission = entity.Permission{ID: 19, Name: "update-permission", Description: "CRUD for Role Entity"} - PermDeletePermission = entity.Permission{ID: 20, Name: "delete-permission", Description: "CRUD for Role Entity"} - - // ... - // add more permissions - // Rule : - // Name should unique - // ID +1 from the ID before -) diff --git a/internal/rbac/rbac_test.go b/internal/rbac/rbac_test.go deleted file mode 100644 index 1de87d2..0000000 --- a/internal/rbac/rbac_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package rbac - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAllPermissions(t *testing.T) { - defer func() { - r := recover() - assert.Nil(t, r, "should not panic, but got:", r) - }() - - permissions := AllPermissions() - for _, permission := range permissions { - assert.NotEmpty(t, permission.Name, "permission name should not be empty") - } -} - -func TestCountPermissions(t *testing.T) { - hashMapPermissions := make(map[string]int, 0) - - for _, permission := range AllPermissions() { - hashMapPermissions[permission.Name]++ - assert.LessOrEqual(t, hashMapPermissions[permission.Name], 1, "should be 1, not more; non-unique permission detected: %s", permission.Name) - } - - assert.Equal(t, len(hashMapPermissions), len(AllPermissions()), "should have equal length; non-unique permission detected") -} - -func TestAllRoles(t *testing.T) { - roles := AllRoles() - for _, role := range roles { - assert.NotEmpty(t, role.Name, "name should not be empty") - } -} - -func TestCountRoles(t *testing.T) { - hashMapRoles := make(map[string]int, 0) - - for _, role := range AllRoles() { - hashMapRoles[role.Name]++ - assert.LessOrEqual(t, hashMapRoles[role.Name], 1, "should be 1, not more; non-unique role (role:name) detected: %s", role.Name) - } - - assert.Equal(t, len(hashMapRoles), len(AllRoles()), "should have equal length; non-unique role detected") -} diff --git a/internal/rbac/role.go b/internal/rbac/role.go deleted file mode 100644 index 0953d81..0000000 --- a/internal/rbac/role.go +++ /dev/null @@ -1,36 +0,0 @@ -package rbac - -import "github.com/Lukmanern/gost/domain/entity" - -// AllRoles func return all roles entities that has been -// created by developer. This func run self audit that -// check for name should be unique value. -// ⚠️ Do not forget to put new role here. -func AllRoles() []entity.Role { - roleNames := []string{ - RoleAdmin, - RoleUser, - - // ... - // add more here - } - - roles := []entity.Role{} - for _, name := range roleNames { - newRoleEntity := entity.Role{ - Name: name, - } - newRoleEntity.SetCreateTime() - roles = append(roles, newRoleEntity) - } - - return roles -} - -const ( - RoleAdmin = "admin" - RoleUser = "user" - - // ... - // add more here -) diff --git a/internal/role/role.go b/internal/role/role.go new file mode 100644 index 0000000..f23b2af --- /dev/null +++ b/internal/role/role.go @@ -0,0 +1,26 @@ +package role + +import "github.com/Lukmanern/gost/domain/entity" + +const ( + RoleSuperAdmin = "super-admin" + RoleAdmin = "admin" + RoleUser = "user" +) + +func AllRoles() []entity.Role { + return []entity.Role{ + { + Name: RoleSuperAdmin, + Description: "description for super-admin role", + }, + { + Name: RoleAdmin, + Description: "description for admin role", + }, + { + Name: RoleUser, + Description: "description for user role", + }, + } +} diff --git a/main.go b/main.go deleted file mode 100644 index a2558ed..0000000 --- a/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/Lukmanern/gost/application" -) - -func main() { - application.RunApp() -} diff --git a/repository/permission/permission_repository.go b/repository/permission/permission_repository.go deleted file mode 100644 index fb1fc94..0000000 --- a/repository/permission/permission_repository.go +++ /dev/null @@ -1,136 +0,0 @@ -package repository - -import ( - "context" - "sync" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/entity" - "gorm.io/gorm" -) - -type PermissionRepository interface { - // Create adds a new permission to the repository. - Create(ctx context.Context, permission entity.Permission) (id int, err error) - - // GetByID retrieves a permission by its unique identifier. - GetByID(ctx context.Context, id int) (permission *entity.Permission, err error) - - // GetByName retrieves a permission by its name. - GetByName(ctx context.Context, name string) (permission *entity.Permission, err error) - - // GetAll retrieves all permissions based on a filter for pagination. - GetAll(ctx context.Context, filter base.RequestGetAll) (permissions []entity.Permission, total int, err error) - - // Update modifies permission information in the repository. - Update(ctx context.Context, permission entity.Permission) (err error) - - // Delete removes a permission from the repository by its ID. - Delete(ctx context.Context, id int) (err error) -} - -type PermissionRepositoryImpl struct { - db *gorm.DB -} - -var ( - permissionRepositoryImpl *PermissionRepositoryImpl - permissionRepositoryImplOnce sync.Once -) - -func NewPermissionRepository() PermissionRepository { - permissionRepositoryImplOnce.Do(func() { - permissionRepositoryImpl = &PermissionRepositoryImpl{ - db: connector.LoadDatabase(), - } - }) - return permissionRepositoryImpl -} - -func (repo *PermissionRepositoryImpl) Create(ctx context.Context, permission entity.Permission) (id int, err error) { - err = repo.db.Transaction(func(tx *gorm.DB) error { - res := tx.Create(&permission) - if res.Error != nil { - tx.Rollback() - return res.Error - } - id = permission.ID - return nil - }) - if err != nil { - return 0, err - } - - return id, nil -} - -func (repo *PermissionRepositoryImpl) GetByID(ctx context.Context, id int) (permission *entity.Permission, err error) { - permission = &entity.Permission{} - result := repo.db.Where("id = ?", id).First(&permission) - if result.Error != nil { - return nil, result.Error - } - return permission, nil -} - -func (repo *PermissionRepositoryImpl) GetByName(ctx context.Context, name string) (permission *entity.Permission, err error) { - permission = &entity.Permission{} - result := repo.db.Where("name = ?", name).First(&permission) - if result.Error != nil { - return nil, result.Error - } - return permission, nil -} - -func (repo *PermissionRepositoryImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (permissions []entity.Permission, total int, err error) { - var count int64 - args := []interface{}{"%" + filter.Keyword + "%"} - cond := "name LIKE ?" - result := repo.db.Where(cond, args...).Find(&permissions) - count = result.RowsAffected - if result.Error != nil { - return nil, 0, result.Error - } - permissions = []entity.Permission{} - skip := int64(filter.Limit * (filter.Page - 1)) - limit := int64(filter.Limit) - result = repo.db.Where(cond, args...).Limit(int(limit)).Offset(int(skip)).Find(&permissions) - if result.Error != nil { - return nil, 0, result.Error - } - total = int(count) - return permissions, total, nil -} - -func (repo *PermissionRepositoryImpl) Update(ctx context.Context, permission entity.Permission) (err error) { - err = repo.db.Transaction(func(tx *gorm.DB) error { - var oldData entity.Permission - result := tx.Where("id = ?", permission.ID).First(&oldData) - if result.Error != nil { - tx.Rollback() - return result.Error - } - - oldData.Name = permission.Name - oldData.Description = permission.Description - oldData.UpdatedAt = permission.UpdatedAt - result = tx.Save(&oldData) - if result.Error != nil { - tx.Rollback() - return result.Error - } - return nil - }) - - return err -} - -func (repo *PermissionRepositoryImpl) Delete(ctx context.Context, id int) (err error) { - deleted := entity.Permission{} - result := repo.db.Where("id = ?", id).Delete(&deleted) - if result.Error != nil { - return result.Error - } - return nil -} diff --git a/repository/permission/permission_repository_test.go b/repository/permission/permission_repository_test.go deleted file mode 100644 index 2493a8b..0000000 --- a/repository/permission/permission_repository_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package repository - -import ( - "context" - "strconv" - "testing" - "time" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/entity" - "github.com/Lukmanern/gost/internal/env" -) - -var ( - permissionRepoImpl PermissionRepositoryImpl - timeNow time.Time - ctx context.Context -) - -func init() { - filePath := "./../../.env" - env.ReadConfig(filePath) - timeNow = time.Now() - ctx = context.Background() - permissionRepoImpl = PermissionRepositoryImpl{ - db: connector.LoadDatabase(), - } - -} - -func createOnePermission(t *testing.T, namePrefix string) *entity.Permission { - permission := entity.Permission{ - Name: "valid-permission-name-" + namePrefix, - Description: "valid-permission-description-" + namePrefix, - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - id, createErr := permissionRepoImpl.Create(ctx, permission) - if createErr != nil { - t.Errorf("error while creating permission") - } - permission.ID = id - return &permission -} - -func TestNewPermissionRepository(t *testing.T) { - permRepo := NewPermissionRepository() - if permRepo == nil { - t.Error("should not nil") - } -} - -func TestPermissionRepositoryImplCreate(t *testing.T) { - permission := createOnePermission(t, "create-same-name") - if permission == nil { - t.Error("failed creating permission : permission is nil") - } - defer func() { - permissionRepoImpl.Delete(ctx, permission.ID) - }() - - type args struct { - ctx context.Context - permission entity.Permission - } - tests := []struct { - name string - repo PermissionRepositoryImpl - args args - wantErr bool - wantPanic bool - }{ - { - name: "error while creating with the same name", - wantErr: true, - wantPanic: true, - args: args{ - ctx: ctx, - permission: entity.Permission{ - Name: permission.Name, - Description: "", - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - defer func() { - if r := recover(); r == nil && tt.wantPanic { - t.Errorf("create() do not panic") - } - }() - gotID, err := tt.repo.Create(tt.args.ctx, tt.args.permission) - if (err != nil) != tt.wantErr { - t.Errorf("PermissionRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotID <= 0 { - t.Errorf("ID should be positive") - } - }) - } -} - -func TestPermissionRepositoryImplGetByID(t *testing.T) { - permission := createOnePermission(t, "TestGetByID") - if permission == nil { - t.Error("failed creating permission : permission is nil") - } - defer func() { - permissionRepoImpl.Delete(ctx, permission.ID) - }() - - type args struct { - ctx context.Context - id int - } - tests := []struct { - name string - repo PermissionRepositoryImpl - args args - wantErr bool - }{ - { - name: "success get permission by valid id", - repo: permissionRepoImpl, - args: args{ - ctx: ctx, - id: permission.ID, - }, - wantErr: false, - }, - { - name: "failed get permission by invalid id", - repo: permissionRepoImpl, - args: args{ - ctx: ctx, - id: -10, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotPermission, err := tt.repo.GetByID(tt.args.ctx, tt.args.id) - if (err != nil) != tt.wantErr { - t.Errorf("PermissionRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr && gotPermission == nil { - t.Error("permission should not nil") - } - }) - } -} - -func TestPermissionRepositoryImplGetByName(t *testing.T) { - permission := createOnePermission(t, "TestGetByName") - if permission == nil { - t.Error("failed creating permission : permission is nil") - } - defer func() { - permissionRepoImpl.Delete(ctx, permission.ID) - }() - - type args struct { - ctx context.Context - name string - } - tests := []struct { - name string - repo PermissionRepositoryImpl - args args - wantPermission *entity.Permission - wantErr bool - }{ - { - name: "success get permission by valid id", - repo: permissionRepoImpl, - args: args{ - ctx: ctx, - name: permission.Name, - }, - wantErr: false, - }, - { - name: "failed get permission by invalid id", - repo: permissionRepoImpl, - args: args{ - ctx: ctx, - name: "unknown name", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotPermission, err := tt.repo.GetByName(tt.args.ctx, tt.args.name) - if (err != nil) != tt.wantErr { - t.Errorf("PermissionRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr && gotPermission == nil { - t.Error("permission should not nil") - } - }) - } -} - -func TestPermissionRepositoryImplGetAll(t *testing.T) { - permissions := make([]entity.Permission, 0) - for i := 0; i < 10; i++ { - permission := createOnePermission(t, "TestGetAll-"+strconv.Itoa(i)) - if permission == nil { - continue - } - defer func() { - permissionRepoImpl.Delete(ctx, permission.ID) - }() - - permissions = append(permissions, *permission) - } - lenPermissions := len(permissions) - - type args struct { - ctx context.Context - filter base.RequestGetAll - } - tests := []struct { - name string - repo PermissionRepositoryImpl - args args - wantErr bool - }{ - { - name: "success get all", - repo: permissionRepoImpl, - args: args{ - ctx: ctx, - filter: base.RequestGetAll{ - Limit: 1000, - Page: 1, - }, - }, - wantErr: false, - }, - { - name: "success get all", - repo: permissionRepoImpl, - args: args{ - ctx: ctx, - filter: base.RequestGetAll{ - Limit: 1, - Page: 1, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotPermissions, gotTotal, err := tt.repo.GetAll(tt.args.ctx, tt.args.filter) - if (err != nil) != tt.wantErr { - t.Errorf("PermissionRepositoryImpl.GetAll() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.args.filter.Limit > lenPermissions && len(gotPermissions) < lenPermissions { - t.Error("permissions should be $lenPermissions or more") - } - if tt.args.filter.Limit > lenPermissions && gotTotal < lenPermissions { - t.Error("total permissions should be $lenPermissions or more") - } - if tt.args.filter.Limit < lenPermissions && len(gotPermissions) > lenPermissions { - t.Error("permissions should be less than $lenPermission") - } - }) - } -} - -func TestPermissionRepositoryImplUpdate(t *testing.T) { - permission := createOnePermission(t, "TestUpdateByID") - if permission == nil { - t.Error("failed creating permission : permission is nil") - } - defer func() { - permissionRepoImpl.Delete(ctx, permission.ID) - }() - - type args struct { - ctx context.Context - permission entity.Permission - } - tests := []struct { - name string - repo PermissionRepositoryImpl - wantErr bool - args args - }{ - { - name: "success update name and desc", - repo: permissionRepoImpl, - wantErr: false, - args: args{ - ctx: ctx, - permission: entity.Permission{ - ID: permission.ID, - Name: "updated name", - Description: "updated description", - }, - }, - }, - { - name: "failed update name and desc with invalid id", - repo: permissionRepoImpl, - wantErr: true, - args: args{ - ctx: ctx, - permission: entity.Permission{ - ID: -10, - Name: "updated name", - Description: "updated description", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.repo.Update(tt.args.ctx, tt.args.permission); (err != nil) != tt.wantErr { - t.Errorf("PermissionRepositoryImpl.Update() error = %v, wantErr %v", err, tt.wantErr) - return - } - - p, err := tt.repo.GetByID(tt.args.ctx, permission.ID) - if err != nil { - t.Error("error while getting permission") - } - if p.Name != tt.args.permission.Name || p.Description != tt.args.permission.Description { - t.Error("name and description failed to update") - } - }) - } -} - -func TestPermissionRepositoryImplDelete(t *testing.T) { - permission := createOnePermission(t, "TestDeleteByID") - if permission == nil { - t.Error("failed creating permission : permission is nil") - } - defer func() { - permissionRepoImpl.Delete(ctx, permission.ID) - }() - - type args struct { - ctx context.Context - id int - } - tests := []struct { - name string - repo PermissionRepositoryImpl - args args - wantErr bool - }{ - { - name: "success update permission", - repo: permissionRepoImpl, - wantErr: false, - args: args{ - ctx: ctx, - id: permission.ID, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.repo.Delete(tt.args.ctx, tt.args.id); (err != nil) != tt.wantErr { - t.Errorf("PermissionRepositoryImpl.Delete() error = %v, wantErr %v", err, tt.wantErr) - return - } - - permission, err := tt.repo.GetByID(tt.args.ctx, tt.args.id) - if !tt.wantErr && err == nil { - t.Error("should error") - } - if !tt.wantErr && permission != nil { - t.Error("permission should nil") - } - }) - } -} diff --git a/repository/role/role_repository.go b/repository/role/role_repository.go deleted file mode 100644 index c1f4d96..0000000 --- a/repository/role/role_repository.go +++ /dev/null @@ -1,177 +0,0 @@ -package repository - -import ( - "context" - "sync" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/entity" - "gorm.io/gorm" -) - -type RoleRepository interface { - // Create adds a new role to the repository with specified permissions. - Create(ctx context.Context, role entity.Role, permissionsID []int) (id int, err error) - - // ConnectToPermission associates a role with specified permissions. - ConnectToPermission(ctx context.Context, roleID int, permissionsID []int) (err error) - - // GetByID retrieves a role by its unique identifier. - GetByID(ctx context.Context, id int) (role *entity.Role, err error) - - // GetByName retrieves a role by its name. - GetByName(ctx context.Context, name string) (role *entity.Role, err error) - - // GetAll retrieves all roles based on a filter for pagination. - GetAll(ctx context.Context, filter base.RequestGetAll) (roles []entity.Role, total int, err error) - - // Update modifies role information in the repository. - Update(ctx context.Context, role entity.Role) (err error) - - // Delete removes a role from the repository by its ID. - Delete(ctx context.Context, id int) (err error) -} - -type RoleRepositoryImpl struct { - db *gorm.DB -} - -var ( - roleRepositoryImpl *RoleRepositoryImpl - roleRepositoryImplOnce sync.Once -) - -func NewRoleRepository() RoleRepository { - roleRepositoryImplOnce.Do(func() { - roleRepositoryImpl = &RoleRepositoryImpl{ - db: connector.LoadDatabase(), - } - }) - return roleRepositoryImpl -} - -func (repo *RoleRepositoryImpl) Create(ctx context.Context, role entity.Role, permissionsID []int) (id int, err error) { - err = repo.db.Transaction(func(tx *gorm.DB) error { - res := tx.Create(&role) - if res.Error != nil { - tx.Rollback() - return res.Error - } - id = role.ID - - for _, permissionID := range permissionsID { - roleHasPermissionEntity := entity.RoleHasPermission{ - RoleID: id, - PermissionID: permissionID, - } - if err := tx.Create(&roleHasPermissionEntity).Error; err != nil { - tx.Rollback() - return err - } - } - return nil - }) - if err != nil { - return 0, err - } - - return id, nil -} - -func (repo *RoleRepositoryImpl) ConnectToPermission(ctx context.Context, roleID int, permissionsID []int) (err error) { - err = repo.db.Transaction(func(tx *gorm.DB) error { - deleted := entity.RoleHasPermission{} - result := tx.Where("role_id = ?", roleID).Delete(&deleted) - if result.Error != nil { - tx.Rollback() - return result.Error - } - - for _, permissionID := range permissionsID { - roleHasPermissionEntity := entity.RoleHasPermission{ - RoleID: roleID, - PermissionID: permissionID, - } - if err := tx.Create(&roleHasPermissionEntity).Error; err != nil { - tx.Rollback() - return err - } - } - return nil - }) - if err != nil { - return err - } - return nil -} - -func (repo *RoleRepositoryImpl) GetByID(ctx context.Context, id int) (role *entity.Role, err error) { - role = &entity.Role{} - result := repo.db.Where("id = ?", id).Preload("Permissions").First(&role) - if result.Error != nil { - return nil, result.Error - } - return role, nil -} - -func (repo *RoleRepositoryImpl) GetByName(ctx context.Context, name string) (role *entity.Role, err error) { - role = &entity.Role{} - result := repo.db.Where("name = ?", name).Preload("Permissions").First(&role) - if result.Error != nil { - return nil, result.Error - } - return role, nil -} - -func (repo *RoleRepositoryImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (roles []entity.Role, total int, err error) { - var count int64 - args := []interface{}{"%" + filter.Keyword + "%"} - cond := "name LIKE ?" - result := repo.db.Where(cond, args...).Find(&roles) - count = result.RowsAffected - if result.Error != nil { - return nil, 0, result.Error - } - roles = []entity.Role{} - skip := int64(filter.Limit * (filter.Page - 1)) - limit := int64(filter.Limit) - result = repo.db.Where(cond, args...).Limit(int(limit)).Offset(int(skip)).Find(&roles) - if result.Error != nil { - return nil, 0, result.Error - } - total = int(count) - return roles, total, nil -} - -func (repo *RoleRepositoryImpl) Update(ctx context.Context, role entity.Role) (err error) { - err = repo.db.Transaction(func(tx *gorm.DB) error { - var oldData entity.Role - result := tx.Where("id = ?", role.ID).First(&oldData) - if result.Error != nil { - tx.Rollback() - return result.Error - } - - oldData.Name = role.Name - oldData.Description = role.Description - oldData.UpdatedAt = role.UpdatedAt - result = tx.Save(&oldData) - if result.Error != nil { - tx.Rollback() - return result.Error - } - return nil - }) - - return err -} - -func (repo *RoleRepositoryImpl) Delete(ctx context.Context, id int) (err error) { - deleted := entity.Role{} - result := repo.db.Where("id = ?", id).Delete(&deleted) - if result.Error != nil { - return result.Error - } - return nil -} diff --git a/repository/role/role_repository_test.go b/repository/role/role_repository_test.go deleted file mode 100644 index 8835806..0000000 --- a/repository/role/role_repository_test.go +++ /dev/null @@ -1,454 +0,0 @@ -package repository - -import ( - "context" - "reflect" - "strconv" - "testing" - "time" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/entity" - "github.com/Lukmanern/gost/internal/env" -) - -var ( - roleRepoImpl RoleRepositoryImpl - permissionsID []int - timeNow time.Time - ctx context.Context -) - -func init() { - filePath := "./../../.env" - env.ReadConfig(filePath) - - timeNow = time.Now() - ctx = context.Background() - - roleRepoImpl = RoleRepositoryImpl{ - db: connector.LoadDatabase(), - } - permissionsID = []int{1, 2, 3, 4, 5} - -} - -func createOneRole(t *testing.T, namePrefix string) *entity.Role { - role := entity.Role{ - Name: "valid-role-name-" + namePrefix, - Description: "valid-role-description-" + namePrefix, - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - id, createErr := roleRepoImpl.Create(ctx, role, permissionsID) - if createErr != nil { - t.Error("error while creating role : ", createErr.Error()) - } - role.ID = id - return &role -} - -func TestNewRoleRepository(t *testing.T) { - roleRepo := NewRoleRepository() - if roleRepo == nil { - t.Error("should not nil") - } -} - -func TestCreate(t *testing.T) { - role := createOneRole(t, "create-same-name") - if role == nil { - t.Error("failed creating role : role is nil") - } - defer func() { - roleRepoImpl.Delete(ctx, role.ID) - }() - - type args struct { - ctx context.Context - role entity.Role - permissionsID []int - } - tests := []struct { - name string - repo RoleRepositoryImpl - args args - wantErr bool - wantPanic bool - }{ - { - name: "error while creating with the same name", - wantErr: true, - args: args{ - ctx: ctx, - role: entity.Role{ - Name: role.Name, - Description: "", - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - defer func() { - if r := recover(); r == nil && tt.wantPanic { - t.Errorf("create() do not panic") - } - }() - gotID, err := tt.repo.Create(tt.args.ctx, tt.args.role, tt.args.permissionsID) - if (err != nil) != tt.wantErr { - t.Errorf("RoleRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotID <= 0 { - t.Errorf("ID should be positive") - } - }) - } -} - -func TestConnectToPermission(t *testing.T) { - role := createOneRole(t, "TestRoleConnectToPermission") - if role == nil || role.ID == 0 { - t.Error("failed creating role : role is nil") - } - defer func() { - roleRepoImpl.Delete(ctx, role.ID) - }() - - ctxBg := context.Background() - testCases := []struct { - name string - roleID int - permissionsID []int - wantErr bool - }{ - { - name: "Success Case", - roleID: role.ID, - permissionsID: []int{2, 3, 4}, - wantErr: false, - }, - { - name: "Failed Case", - roleID: role.ID, - permissionsID: []int{-2, 3, 4}, - wantErr: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := roleRepoImpl.ConnectToPermission(ctxBg, tc.roleID, tc.permissionsID) - if err != nil && !tc.wantErr { - t.Errorf("Expected error: %v, got error: %v", tc.wantErr, err) - } - - roleByID, getErr := roleRepoImpl.GetByID(ctx, role.ID) - if getErr != nil { - t.Errorf("Expect no error, got error: %v", getErr) - } - - if !tc.wantErr { - perms := roleByID.Permissions - permsID := []int{} - for _, perm := range perms { - permsID = append(permsID, perm.ID) - } - - if !reflect.DeepEqual(tc.permissionsID, permsID) { - t.Error("permsID should equal") - } - } - }) - } -} - -func TestGetByID(t *testing.T) { - role := createOneRole(t, "TestGetByID") - if role == nil { - t.Error("failed creating role : role is nil") - } - defer func() { - roleRepoImpl.Delete(ctx, role.ID) - }() - - type args struct { - ctx context.Context - id int - } - tests := []struct { - name string - repo RoleRepositoryImpl - args args - wantErr bool - }{ - { - name: "success get permission by valid id", - repo: roleRepoImpl, - args: args{ - ctx: ctx, - id: role.ID, - }, - wantErr: false, - }, - { - name: "failed get permission by invalid id", - repo: roleRepoImpl, - args: args{ - ctx: ctx, - id: -10, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotRole, err := tt.repo.GetByID(tt.args.ctx, tt.args.id) - if (err != nil) != tt.wantErr { - t.Errorf("RoleRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr && gotRole == nil { - t.Error("role should not nil") - } - }) - } -} - -func TestGetByName(t *testing.T) { - role := createOneRole(t, "TestGetByName") - if role == nil { - t.Error("failed creating role : role is nil") - } - defer func() { - roleRepoImpl.Delete(ctx, role.ID) - }() - - type args struct { - ctx context.Context - name string - } - tests := []struct { - name string - repo RoleRepositoryImpl - args args - wantErr bool - }{ - { - name: "success get permission by valid id", - repo: roleRepoImpl, - args: args{ - ctx: ctx, - name: role.Name, - }, - wantErr: false, - }, - { - name: "failed get permission by invalid id", - repo: roleRepoImpl, - args: args{ - ctx: ctx, - name: "unknown name", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotRole, err := tt.repo.GetByName(tt.args.ctx, tt.args.name) - if (err != nil) != tt.wantErr { - t.Errorf("RoleRepositoryImpl.GetByName() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr && gotRole == nil { - t.Error("role should not nil") - } - }) - } -} - -func TestGetAll(t *testing.T) { - roles := make([]entity.Role, 0) - for i := 0; i < 10; i++ { - role := createOneRole(t, "TestGetAll"+strconv.Itoa(i)) - if role == nil { - continue - } - defer func() { - roleRepoImpl.Delete(ctx, role.ID) - }() - - roles = append(roles, *role) - } - lenRoles := len(roles) - type args struct { - ctx context.Context - filter base.RequestGetAll - } - tests := []struct { - name string - repo RoleRepositoryImpl - args args - wantErr bool - }{ - { - name: "success get all", - repo: roleRepoImpl, - args: args{ - ctx: ctx, - filter: base.RequestGetAll{ - Limit: 1000, - Page: 1, - }, - }, - wantErr: false, - }, - { - name: "success get all", - repo: roleRepoImpl, - args: args{ - ctx: ctx, - filter: base.RequestGetAll{ - Limit: 1, - Page: 1, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotRoles, gotTotal, err := tt.repo.GetAll(tt.args.ctx, tt.args.filter) - if (err != nil) != tt.wantErr { - t.Errorf("RoleRepositoryImpl.GetAll() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.args.filter.Limit > lenRoles && len(gotRoles) < lenRoles { - t.Error("permissions should be $lenRoles or more") - } - if tt.args.filter.Limit > lenRoles && gotTotal < lenRoles { - t.Error("total permissions should be $lenRoles or more") - } - if tt.args.filter.Limit < lenRoles && len(gotRoles) > lenRoles { - t.Error("permissions should be less than $lenPermission") - } - }) - } -} - -func TestUpdate(t *testing.T) { - role := createOneRole(t, "TestUpdateByID") - if role == nil { - t.Error("failed creating role : role is nil") - } - defer func() { - roleRepoImpl.Delete(ctx, role.ID) - }() - - type args struct { - ctx context.Context - role entity.Role - } - tests := []struct { - name string - repo RoleRepositoryImpl - args args - wantErr bool - }{ - { - name: "success update name and desc", - repo: roleRepoImpl, - wantErr: false, - args: args{ - ctx: ctx, - role: entity.Role{ - ID: role.ID, - Name: "updated name", - Description: "updated description", - }, - }, - }, - { - name: "failed update name and desc with invalid id", - repo: roleRepoImpl, - wantErr: true, - args: args{ - ctx: ctx, - role: entity.Role{ - ID: -10, - Name: "updated name", - Description: "updated description", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.repo.Update(tt.args.ctx, tt.args.role); (err != nil) != tt.wantErr { - t.Errorf("RoleRepositoryImpl.Update() error = %v, wantErr %v", err, tt.wantErr) - } - - p, err := tt.repo.GetByID(tt.args.ctx, role.ID) - if err != nil { - t.Error("error while getting role") - } - if p.Name != tt.args.role.Name || p.Description != tt.args.role.Description { - t.Error("name and description failed to update") - } - }) - } -} - -func TestDelete(t *testing.T) { - role := createOneRole(t, "TestDeleteByID") - if role == nil { - t.Error("failed creating role : role is nil") - } - defer func() { - roleRepoImpl.Delete(ctx, role.ID) - }() - - type args struct { - ctx context.Context - id int - } - tests := []struct { - name string - repo RoleRepositoryImpl - args args - wantErr bool - }{ - { - name: "success update permission", - repo: roleRepoImpl, - wantErr: false, - args: args{ - ctx: ctx, - id: role.ID, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.repo.Delete(tt.args.ctx, tt.args.id); (err != nil) != tt.wantErr { - t.Errorf("RoleRepositoryImpl.Delete() error = %v, wantErr %v", err, tt.wantErr) - } - - role, err := tt.repo.GetByID(tt.args.ctx, tt.args.id) - if !tt.wantErr && err == nil { - t.Error("should error") - } - if !tt.wantErr && role != nil { - t.Error("role should nil") - } - }) - } -} diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go deleted file mode 100644 index 51e36f8..0000000 --- a/repository/user/user_repository.go +++ /dev/null @@ -1,183 +0,0 @@ -// used by user auth service - -package repository - -import ( - "context" - "sync" - - "gorm.io/gorm" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/entity" -) - -type UserRepository interface { - // Create adds a new user to the repository with a specified role. - Create(ctx context.Context, user entity.User, roleID int) (id int, err error) - - // GetByID retrieves a user by their unique identifier. - GetByID(ctx context.Context, id int) (user *entity.User, err error) - - // GetByEmail retrieves a user by their email address. - GetByEmail(ctx context.Context, email string) (user *entity.User, err error) - - // GetByConditions retrieves a user based on specified conditions. - GetByConditions(ctx context.Context, conds map[string]any) (user *entity.User, err error) - - // GetAll retrieves all users based on a filter for pagination. - GetAll(ctx context.Context, filter base.RequestGetAll) (users []entity.User, total int, err error) - - // Update modifies user information in the repository. - Update(ctx context.Context, user entity.User) (err error) - - // Delete removes a user from the repository by their ID. - Delete(ctx context.Context, id int) (err error) - - // UpdatePassword updates a user's password in the repository. - UpdatePassword(ctx context.Context, id int, passwordHashed string) (err error) -} - -type UserRepositoryImpl struct { - db *gorm.DB -} - -var ( - userRepositoryImpl *UserRepositoryImpl - userRepositoryImplOnce sync.Once -) - -func NewUserRepository() UserRepository { - userRepositoryImplOnce.Do(func() { - userRepositoryImpl = &UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - }) - return userRepositoryImpl -} - -func (repo *UserRepositoryImpl) Create(ctx context.Context, user entity.User, roleID int) (id int, err error) { - err = repo.db.Transaction(func(tx *gorm.DB) error { - if res := tx.Create(&user); res.Error != nil { - tx.Rollback() - return res.Error - } - id = user.ID - - if res := tx.Create(&entity.UserHasRoles{ - UserID: id, - RoleID: roleID, - }); res.Error != nil { - tx.Rollback() - return res.Error - } - return nil - }) - if err != nil { - return 0, err - } - return id, nil -} - -func (repo *UserRepositoryImpl) GetByID(ctx context.Context, id int) (user *entity.User, err error) { - user = &entity.User{} - result := repo.db.Where("id = ?", id).Preload("Roles.Permissions").First(&user) - if result.Error != nil { - return nil, result.Error - } - return user, nil -} - -func (repo *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (user *entity.User, err error) { - user = &entity.User{} - result := repo.db.Where("email = ?", email).Preload("Roles.Permissions").First(&user) - if result.Error != nil { - return nil, result.Error - } - return user, nil -} - -func (repo *UserRepositoryImpl) GetByConditions(ctx context.Context, conds map[string]any) (user *entity.User, err error) { - // this func is easy-contain-vunarable by default - user = &entity.User{} - query := repo.db - for con, val := range conds { - query = query.Where(con+" ?", val) - } - result := query.Preload("Roles.Permissions").First(&user) - if result.Error != nil { - return nil, result.Error - } - return user, nil -} - -func (repo *UserRepositoryImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (users []entity.User, total int, err error) { - var count int64 - args := []interface{}{"%" + filter.Keyword + "%"} - cond := "name LIKE ?" - result := repo.db.Where(cond, args...).Find(&users) - count = result.RowsAffected - if result.Error != nil { - return nil, 0, result.Error - } - users = []entity.User{} - skip := int64(filter.Limit * (filter.Page - 1)) - limit := int64(filter.Limit) - result = repo.db.Where(cond, args...).Limit(int(limit)).Offset(int(skip)).Find(&users) - if result.Error != nil { - return nil, 0, result.Error - } - total = int(count) - return users, total, nil -} - -func (repo *UserRepositoryImpl) Update(ctx context.Context, user entity.User) (err error) { - err = repo.db.Transaction(func(tx *gorm.DB) error { - var oldData entity.User - result := tx.Where("id = ?", user.ID).First(&oldData) - if result.Error != nil { - return result.Error - } - - oldData.Name = user.Name - oldData.ActivatedAt = user.ActivatedAt - oldData.VerificationCode = user.VerificationCode - oldData.UpdatedAt = user.UpdatedAt - result = tx.Save(&oldData) - if result.Error != nil { - return result.Error - } - return nil - }) - - return err -} - -func (repo *UserRepositoryImpl) Delete(ctx context.Context, id int) (err error) { - deleted := entity.User{} - result := repo.db.Where("id = ?", id).Delete(&deleted) - if result.Error != nil { - return result.Error - } - return nil -} - -func (repo *UserRepositoryImpl) UpdatePassword(ctx context.Context, id int, passwordHashed string) (err error) { - err = repo.db.Transaction(func(tx *gorm.DB) error { - var user entity.User - result := tx.Where("id = ?", id).First(&user) - if result.Error != nil { - return result.Error - } - user.Password = passwordHashed - user.SetUpdateTime() - result = tx.Save(&user) - if result.Error != nil { - return result.Error - } - return nil - }) - - return err -} diff --git a/repository/user/user_repository_test.go b/repository/user/user_repository_test.go deleted file mode 100644 index 7d2a7f3..0000000 --- a/repository/user/user_repository_test.go +++ /dev/null @@ -1,550 +0,0 @@ -package repository - -import ( - "context" - "testing" - "time" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/entity" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" -) - -var ( - timeNow time.Time - ctx context.Context -) - -func init() { - filePath := "./../../.env" - env.ReadConfig(filePath) - timeNow = time.Now() - ctx = context.Background() -} - -func TestNewUserRepository(t *testing.T) { - userRepository := NewUserRepository() - if userRepository == nil { - t.Error("should not nil") - } -} - -func TestUserRepositoryImplCreate(t *testing.T) { - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - - type args struct { - ctx context.Context - user entity.User - } - tests := []struct { - name string - repo UserRepositoryImpl - wantErr bool - wantPanic bool - args args - }{ - { - name: "success create new user", - repo: userRepositoryImpl, - wantErr: false, - wantPanic: false, - args: args{ - ctx: context.Background(), - user: entity.User{ - Name: "validname", - Email: "valid1@email.com", - Password: "example-password", - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - }, - }, - }, - { - name: "success create new user with void data", - repo: userRepositoryImpl, - wantErr: false, - wantPanic: false, - }, - { - name: "failed create new user with void data and nil repository", - repo: UserRepositoryImpl{ - db: nil, - }, - wantErr: true, - wantPanic: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if !tt.wantPanic { - gotID, err := tt.repo.Create(tt.args.ctx, tt.args.user, 1) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - gotID2, err2 := tt.repo.Create(tt.args.ctx, tt.args.user, 1) - if err2 == nil || gotID2 != 0 { - t.Error("should be error, couse email is already used") - } - tt.repo.Delete(tt.args.ctx, gotID) - - return - } - // want panic - defer func() { - if r := recover(); r == nil { - t.Errorf("create() do not panic") - } - }() - userID, err := tt.repo.Create(tt.args.ctx, tt.args.user, 1) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - userRepositoryImpl.Delete(ctx, userID) - }) - } -} - -func TestUserRepositoryImplGetByID(t *testing.T) { - // create user - user := entity.User{ - Name: "validname", - Email: "valid2@email.com", - Password: "example-password", - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") - } - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - - type args struct { - ctx context.Context - id int - } - tests := []struct { - name string - repo UserRepositoryImpl - wantErr bool - wantUser bool - args args - }{ - { - name: "Success get user by id", - repo: userRepositoryImpl, - wantErr: false, - wantUser: true, - args: args{ - ctx: ctx, - id: id, - }, - }, - { - name: "Failed get user by negative id", - repo: userRepositoryImpl, - wantErr: true, - wantUser: false, - args: args{ - ctx: ctx, - id: -10, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUser, err := tt.repo.GetByID(tt.args.ctx, tt.args.id) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantUser { - if gotUser == nil { - t.Error("error user shouldn't nil") - } - } - }) - } -} - -func TestUserRepositoryImplGetByEmail(t *testing.T) { - // create user - user := entity.User{ - Name: "validname", - Email: "valid3@email.com", - Password: "example-password", - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") - } - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - - type args struct { - ctx context.Context - email string - } - tests := []struct { - name string - repo UserRepositoryImpl - wantUser bool - wantErr bool - args args - }{ - { - name: "Success get user by valid email", - repo: userRepositoryImpl, - wantErr: false, - wantUser: true, - args: args{ - ctx: ctx, - email: user.Email, - }, - }, - { - name: "Failed get user by invalid-email", - repo: userRepositoryImpl, - wantErr: true, - wantUser: false, - args: args{ - ctx: ctx, - email: "invalid-email", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUser, err := tt.repo.GetByEmail(tt.args.ctx, tt.args.email) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantUser { - if gotUser == nil { - t.Error("error user shouldn't nil") - } - } - }) - } -} - -func TestUserRepositoryImplGetAll(t *testing.T) { - // create user - allUsersID := make([]int, 0) - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - for _, id := range []string{"4", "5", "6", "7", "8"} { - user := entity.User{ - Name: "validname", - Email: "valid" + id + "@email.com", // email is unique - Password: "example-password", - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - newUserID, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user :" + id) - } - allUsersID = append(allUsersID, newUserID) - } - defer func() { - for _, userID := range allUsersID { - userRepositoryImpl.Delete(ctx, userID) - } - }() - - type args struct { - ctx context.Context - filter base.RequestGetAll - } - tests := []struct { - name string - repo UserRepositoryImpl - wantErr bool - args args - }{ - { - name: "success get 5 or more users", - repo: userRepositoryImpl, - wantErr: false, - args: args{ - ctx: ctx, - filter: base.RequestGetAll{ - Page: 1, - Limit: 1000, - Keyword: "", - }, - }, - }, - { - name: "success get less than 5", - repo: userRepositoryImpl, - wantErr: false, - args: args{ - ctx: ctx, - filter: base.RequestGetAll{ - Page: 1, - Limit: 1, - Keyword: "", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUsers, gotTotal, err := tt.repo.GetAll(tt.args.ctx, tt.args.filter) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.GetAll() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.args.filter.Limit > 5 && len(gotUsers) < 5 { - t.Error("users should be 5 or more") - } - if tt.args.filter.Limit > 5 && gotTotal < 5 { - t.Error("total users should be 5 or more") - } - if tt.args.filter.Limit < 5 && len(gotUsers) > 5 { - t.Error("users should be less than 5") - } - }) - } -} - -func TestUserRepositoryImplUpdate(t *testing.T) { - // create user - user := entity.User{ - Name: "validname", - Email: "valid9@email.com", - Password: "example-password", - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") - } - // add id to user - user.ID = id - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - - type args struct { - ctx context.Context - user entity.User - } - tests := []struct { - name string - repo UserRepositoryImpl - wantErr bool - newUserName string - args args - }{ - { - name: "success update user's name", - repo: userRepositoryImpl, - wantErr: false, - newUserName: "test-update-001", - args: args{ - ctx: ctx, - user: user, - }, - }, - } - for _, tt := range tests { - tt.args.user.Name = tt.newUserName - t.Run(tt.name, func(t *testing.T) { - if err := tt.repo.Update(tt.args.ctx, tt.args.user); (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.Update() error = %v, wantErr %v", err, tt.wantErr) - } - getUser, getErr := tt.repo.GetByID(tt.args.ctx, id) - if getErr != nil { - t.Error("error while getting user") - } - if getUser.Name != tt.newUserName { - t.Error("update name failed") - } - }) - } -} - -func TestUserRepositoryImplDelete(t *testing.T) { - userRepository := NewUserRepository() - if userRepository == nil { - t.Error("shouldn't nil") - } - - ctx := context.Background() - err := userRepository.Delete(ctx, -2) - if err != nil { - t.Error("delete shouldn't error") - } - if ctx.Err() != nil { - t.Error("delete shouldn't error") - } -} - -func TestUserRepositoryImplUpdatePassword(t *testing.T) { - // create user - user := entity.User{ - Name: "validname", - Email: helper.RandomEmail(), - Password: "example-password", - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") - } - // add id to user - user.ID = id - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - - type args struct { - ctx context.Context - id int - passwordHashed string - } - tests := []struct { - name string - repo UserRepositoryImpl - args args - wantErr bool - }{ - { - name: "success update user's password", - repo: userRepositoryImpl, - wantErr: false, - args: args{ - ctx: ctx, - id: id, - passwordHashed: "new-password-hashed", - }, - }, - { - name: "failed getting user with negative id", - repo: userRepositoryImpl, - wantErr: true, - args: args{ - ctx: ctx, - id: -100, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.repo.UpdatePassword(tt.args.ctx, tt.args.id, tt.args.passwordHashed); (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.UpdatePassword() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr { - getUser, getErr := tt.repo.GetByID(tt.args.ctx, id) - if getErr != nil { - t.Error("error while getting user") - } - if getUser.Password != tt.args.passwordHashed { - t.Error("failed to update user's password") - } - } - }) - } -} - -func TestUserRepositoryImplGetByConditions(t *testing.T) { - user := entity.User{ - Name: "validname", - Email: helper.RandomEmail(), - Password: "example-password", - TimeFields: base.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") - } - // add id to user - user.ID = id - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - - type args struct { - ctx context.Context - conds map[string]any - } - tests := []struct { - name string - repo UserRepositoryImpl - args args - wantErr bool - }{ - { - name: "success get data", - repo: userRepositoryImpl, - args: args{ - ctx: ctx, - conds: map[string]any{ - "name =": user.Name, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUser, err := tt.repo.GetByConditions(tt.args.ctx, tt.args.conds) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.GetByConditions() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotUser.ID != user.ID || gotUser.Email != user.Email || gotUser.Password != user.Password { - t.Error("should got same ID/ Email/ Password") - } - }) - } -} diff --git a/scripts/generate_keys.sh b/scripts/generate_keys.sh deleted file mode 100644 index d771f74..0000000 --- a/scripts/generate_keys.sh +++ /dev/null @@ -1,7 +0,0 @@ -# unix -# openssl req -x509 -newkey rsa:4096 -keyout keys/private.key -out keys/publickey.crt -days 365 -nodes -subj "/CN=localhost" - -# windows -# "C:\Program Files\Git\usr\bin\openssl.exe" req -x509 -newkey rsa:4096 -keyout keys/private.key -out keys/publickey.crt -days 365 -nodes -subj "/CN=localhost" - -echo "Open this file and You should run this scripts manually, choose between windows/ unix." \ No newline at end of file diff --git a/scripts/update_module.sh b/scripts/update_module.sh deleted file mode 100644 index 73e853f..0000000 --- a/scripts/update_module.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# Step 1: Remove .git directory -rm -rf .git - -# Step 2: Search and replace "github.com/Lukmanern/gost" with "github.com/YourUsername/YourRepoName" -find . -type f -exec sed -i 's/github\.com\/Lukmanern\/gost/github\.com\/YourUsername\/YourRepoName/g' {} + - -echo "Finish! .git directory removed and search/replace completed." diff --git a/service/email/email_service.go b/service/email/email_service.go deleted file mode 100644 index e503d93..0000000 --- a/service/email/email_service.go +++ /dev/null @@ -1,69 +0,0 @@ -package service - -import ( - "fmt" - "net/smtp" - "strings" - "sync" - - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" -) - -type EmailService interface { - // SendMail func sends message with subject to some emails address. - SendMail(emails []string, subject, message string) error -} - -// EmailServiceImpl struct contains all the -// SMTP needs for sending emails. -// SMTP => Simple Mail Transfer Protocol -type EmailServiceImpl struct { - Server string - Port int - Email string - Password string - SmptAuth smtp.Auth - SmptMime string - SmptAddr string -} - -var ( - emailService *EmailServiceImpl - emailServiceOnce sync.Once -) - -func NewEmailService() EmailService { - emailServiceOnce.Do(func() { - config := env.Configuration() - emailService = &EmailServiceImpl{ - Server: config.SMTPServer, - Port: config.SMTPPort, - Email: config.SMTPEmail, - Password: config.SMTPPassword, - } - - emailService.SmptAuth = smtp.PlainAuth("", emailService.Email, emailService.Password, emailService.Server) - emailService.SmptAddr = fmt.Sprintf("%s:%d", emailService.Server, emailService.Port) - emailService.SmptMime = "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\r\n" - }) - - return emailService -} - -func (svc *EmailServiceImpl) SendMail(emails []string, subject, message string) error { - validateErr := helper.ValidateEmails(emails...) - if validateErr != nil { - return validateErr - } - body := "From: " + "CONFIG_SENDER_NAME" + "\n" + - "To: " + strings.Join(emails, ",") + "\n" + - "Subject: " + subject + "\n" + svc.SmptMime + "\n\n" + - message - - err := smtp.SendMail(svc.SmptAddr, svc.SmptAuth, svc.Email, emails, []byte(body)) - if err != nil { - return err - } - return nil -} diff --git a/service/email/email_service_test.go b/service/email/email_service_test.go deleted file mode 100644 index ee4553b..0000000 --- a/service/email/email_service_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package service - -import ( - "testing" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/internal/env" -) - -func init() { - // Check env and database - env.ReadConfig("./../../.env") - - connector.LoadDatabase() - connector.LoadRedisCache() -} - -func TestSendEmail(t *testing.T) { - emailService := NewEmailService() - if emailService == nil { - t.Error("should not nil") - } - invalidEmail := []string{"invalid-email-address"} - subject := "valid-subject" - message := "simple-example-message" - sendErr := emailService.SendMail(invalidEmail, subject, message) - if sendErr == nil { - t.Error("should error, because invalid email") - } - // reset value - sendErr = nil - validEmail := []string{"your_valid_email_001@gost.project"} // enter your valid email address - sendErr = emailService.SendMail(validEmail, subject, message) - if sendErr != nil { - t.Error("should not error, but got error:", sendErr.Error()) - } -} diff --git a/service/file/file_service.go b/service/file/file_service.go deleted file mode 100644 index 88f8aa3..0000000 --- a/service/file/file_service.go +++ /dev/null @@ -1,242 +0,0 @@ -package service - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/http" - "strconv" - "sync" - - "github.com/Lukmanern/gost/internal/env" - "github.com/gofiber/fiber/v2" -) - -type FileReponse struct { - Name string `json:"name"` - CreatedAt string `json:"created_at"` - Metadata struct { - Size int64 `json:"size"` - } `json:"metadata"` -} - -type FileService interface { - // UploadFile func uploads file to supabase bucket. - UploadFile(fileHeader *multipart.FileHeader) (fileURL string, err error) - - // RemoveFile func deletes a file from supabase bucket. - RemoveFile(fileName string) (err error) - - // GetFilesList func get list of files from supabase bucket. - GetFilesList() (files []map[string]any, err error) -} - -type FileServiceImpl struct { - PublicURL string - ListFilesURL string - UploadURL string - DeleteURL string - Token string -} - -var ( - fileService *FileServiceImpl - fileServiceOnce sync.Once -) - -func NewFileService() FileService { - fileServiceOnce.Do(func() { - config := env.Configuration() - baseURL := config.BucketURL + "/storage/v1/object/" - fileService = &FileServiceImpl{ - PublicURL: baseURL + "public/" + config.BucketName + "/", - ListFilesURL: baseURL + "list/" + config.BucketName, - UploadURL: baseURL + config.BucketName + "/", - DeleteURL: baseURL + config.BucketName, - Token: config.BucketToken, - } - }) - return fileService -} - -func (c FileServiceImpl) UploadFile(fileHeader *multipart.FileHeader) (fileURL string, err error) { - fileName := fileHeader.Filename - file, headerErr := fileHeader.Open() - if headerErr != nil { - return "", headerErr - } - defer file.Close() - - requestBody := &bytes.Buffer{} - writer := multipart.NewWriter(requestBody) - fileField, formErr := writer.CreateFormFile("file", fileName) - if formErr != nil { - return "", formErr - } - _, copyErr := io.Copy(fileField, file) - if copyErr != nil { - return "", copyErr - } - writer.Close() - url := c.UploadURL + fileName - request, newReqErr := http.NewRequest(http.MethodPost, url, requestBody) - if newReqErr != nil { - return "", newReqErr - } - - request.Header.Set(fiber.HeaderAuthorization, "Bearer "+c.Token) - request.Header.Set(fiber.HeaderContentType, writer.FormDataContentType()) - FileServiceImpl := &http.Client{} - respUpload, doErr := FileServiceImpl.Do(request) - if doErr != nil { - return "", doErr - } - defer respUpload.Body.Close() - - if respUpload.StatusCode != http.StatusOK { - return "", responseErrHandler(respUpload) - } - - link := c.PublicURL + fileName - reqTestGet, reqErr := http.NewRequest(http.MethodGet, link, nil) - if reqErr != nil { - return "", reqErr - } - respTestGet, doErr := FileServiceImpl.Do(reqTestGet) - if doErr != nil { - return "", doErr - } - defer respTestGet.Body.Close() - if respTestGet.StatusCode != http.StatusOK { - return "", responseErrHandler(respTestGet) - } - return link, nil -} - -func (c FileServiceImpl) RemoveFile(fileName string) (err error) { - body := map[string]interface{}{ - "prefixes": fileName, - } - reqBody, err := json.Marshal(body) - if err != nil { - return err - } - request, err := http.NewRequest(http.MethodDelete, c.DeleteURL, bytes.NewBuffer(reqBody)) - if err != nil { - return err - } - - request.Header.Set(fiber.HeaderAuthorization, "Bearer "+c.Token) - request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - FileServiceImpl := &http.Client{} - response, err := FileServiceImpl.Do(request) - if err != nil { - return responseErrHandler(response) - } - defer response.Body.Close() - if response.StatusCode != http.StatusOK { - return responseErrHandler(response) - } - - respBody, readErr := io.ReadAll(response.Body) - if readErr != nil { - return readErr - } - var resp []FileReponse - if unmarshalErr := json.Unmarshal(respBody, &resp); unmarshalErr != nil { - return unmarshalErr - } - if len(resp) < 1 { - return fiber.NewError(fiber.StatusNotFound, "file/s not found") - } - return nil -} - -func (c FileServiceImpl) GetFilesList() (files []map[string]any, err error) { - type sortBy struct { - Column string `json:"column"` - Order string `json:"order"` - } - type listReqBody struct { - Limit int `json:"limit"` - Offset int `json:"offset"` - Prefix string `json:"prefix"` - SortByOptions sortBy `json:"sortBy"` - } - - body := listReqBody{ - Limit: 999, - Offset: 1, - Prefix: "", - SortByOptions: sortBy{ - Column: "name", - Order: "asc", - }, - } - reqBody, err := json.Marshal(body) - if err != nil { - return nil, err - } - listFileURL := c.ListFilesURL - request, err := http.NewRequest(http.MethodPost, listFileURL, bytes.NewBuffer(reqBody)) - if err != nil { - return nil, err - } - - request.Header.Set(fiber.HeaderAuthorization, "Bearer "+c.Token) - request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - FileServiceImpl := &http.Client{} - response, err := FileServiceImpl.Do(request) - if err != nil { - return nil, responseErrHandler(response) - } - defer response.Body.Close() - if response.StatusCode != http.StatusOK { - return nil, responseErrHandler(response) - } - - respBody, readErr := io.ReadAll(response.Body) - if readErr != nil { - return nil, readErr - } - var listResp []FileReponse - - if unmarshalErr := json.Unmarshal(respBody, &listResp); unmarshalErr != nil { - return nil, unmarshalErr - } - for _, list := range listResp { - // Calculate the size in megabytes - sizeInMB := float64(list.Metadata.Size) / 1024 / 1024 - formattedSize := fmt.Sprintf("%.4f MB", sizeInMB) - file := map[string]interface{}{ - "name": list.Name, - "uploaded_at": list.CreatedAt, - "size_mb": formattedSize, - } - files = append(files, file) - } - - return files, nil -} - -func responseErrHandler(resp *http.Response) (err error) { - respBody, readErr := io.ReadAll(resp.Body) - if readErr != nil { - return readErr - } - var errResp struct { - Message string `json:"message"` - Error string `json:"error"` - StatusCode string `json:"statusCode"` - } - if unmarshalErr := json.Unmarshal(respBody, &errResp); unmarshalErr != nil { - return unmarshalErr - } - statusCode, convErr := strconv.Atoi(errResp.StatusCode) - if convErr != nil { - return fiber.NewError(fiber.StatusInternalServerError, "failed conv: "+convErr.Error()) - } - return fiber.NewError(statusCode, errResp.Message+", "+errResp.Error) -} diff --git a/service/permission/permission_service.go b/service/permission/permission_service.go deleted file mode 100644 index 31e742d..0000000 --- a/service/permission/permission_service.go +++ /dev/null @@ -1,162 +0,0 @@ -package service - -import ( - "context" - "strings" - "sync" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/entity" - "github.com/Lukmanern/gost/domain/model" - repository "github.com/Lukmanern/gost/repository/permission" -) - -type PermissionService interface { - // Create func create one permission. - Create(ctx context.Context, permission model.PermissionCreate) (id int, err error) - - // GetByID func get one permission by ID. - GetByID(ctx context.Context, id int) (permission *model.PermissionResponse, err error) - - // GetAll func get some permissions with payload. - GetAll(ctx context.Context, filter base.RequestGetAll) (permissions []model.PermissionResponse, total int, err error) - - // Update func update one permission by ID and payload. - Update(ctx context.Context, permission model.PermissionUpdate) (err error) - - // Delete func delete one permission by ID. - Delete(ctx context.Context, id int) (err error) -} - -type PermissionServiceImpl struct { - repository repository.PermissionRepository -} - -var ( - permissionServiceImpl *PermissionServiceImpl - permissionServiceImplOnce sync.Once -) - -const permNotFound = "permission/s not found" - -func NewPermissionService() PermissionService { - permissionServiceImplOnce.Do(func() { - permissionServiceImpl = &PermissionServiceImpl{ - repository: repository.NewPermissionRepository(), - } - }) - return permissionServiceImpl -} - -func (svc *PermissionServiceImpl) Create(ctx context.Context, permission model.PermissionCreate) (id int, err error) { - permission.Name = strings.ToLower(permission.Name) - - checkPermission, getErr := svc.repository.GetByName(ctx, permission.Name) - if getErr == nil || checkPermission != nil { - return 0, fiber.NewError(fiber.StatusBadRequest, "permission name has been used") - } - entityPermission := entity.Permission{ - Name: permission.Name, - Description: permission.Description, - } - entityPermission.SetCreateTime() - id, err = svc.repository.Create(ctx, entityPermission) - if err != nil { - return 0, err - } - return id, nil -} - -func (svc *PermissionServiceImpl) GetByID(ctx context.Context, id int) (permission *model.PermissionResponse, err error) { - permissionEntity, getErr := svc.repository.GetByID(ctx, id) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return nil, fiber.NewError(fiber.StatusNotFound, permNotFound) - } - return nil, getErr - } - if permissionEntity == nil { - return nil, fiber.NewError(fiber.StatusNotFound, permNotFound) - } - - permission = &model.PermissionResponse{ - ID: permissionEntity.ID, - Name: permissionEntity.Name, - Description: permissionEntity.Description, - } - return permission, nil -} - -func (svc *PermissionServiceImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (permissions []model.PermissionResponse, total int, err error) { - permissionEntities, total, err := svc.repository.GetAll(ctx, filter) - if err != nil { - return nil, 0, err - } - - permissions = []model.PermissionResponse{} - for _, permissionEntity := range permissionEntities { - newPermission := model.PermissionResponse{ - ID: permissionEntity.ID, - Name: permissionEntity.Name, - Description: permissionEntity.Description, - } - - permissions = append(permissions, newPermission) - } - return permissions, total, nil -} - -func (svc *PermissionServiceImpl) Update(ctx context.Context, data model.PermissionUpdate) (err error) { - data.Name = strings.ToLower(data.Name) - permissionByName, getErr := svc.repository.GetByName(ctx, data.Name) - if getErr != nil && getErr != gorm.ErrRecordNotFound { - return getErr - } - if permissionByName != nil && permissionByName.ID != data.ID { - return fiber.NewError(fiber.StatusBadRequest, "permission name has been used") - } - - permissionByID, getErr := svc.repository.GetByID(ctx, data.ID) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, permNotFound) - } - return getErr - } - if permissionByID == nil { - return fiber.NewError(fiber.StatusNotFound, permNotFound) - } - - entityRole := entity.Permission{ - ID: data.ID, - Name: data.Name, - Description: data.Description, - } - entityRole.SetUpdateTime() - err = svc.repository.Update(ctx, entityRole) - if err != nil { - return err - } - return nil -} - -func (svc *PermissionServiceImpl) Delete(ctx context.Context, id int) (err error) { - permission, getErr := svc.repository.GetByID(ctx, id) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, permNotFound) - } - return getErr - } - if permission == nil { - return fiber.NewError(fiber.StatusNotFound, permNotFound) - } - err = svc.repository.Delete(ctx, id) - if err != nil { - return err - } - return nil -} diff --git a/service/permission/permission_service_test.go b/service/permission/permission_service_test.go deleted file mode 100644 index e62aff0..0000000 --- a/service/permission/permission_service_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// Don't run test per file without -p 1 -// or simply run test per func or run -// project test using make test command -// check Makefile file -package service - -import ( - "strings" - "testing" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" -) - -func init() { - // Check env and database - env.ReadConfig("./../../.env") - - connector.LoadDatabase() - connector.LoadRedisCache() -} - -func TestNewPermissionService(t *testing.T) { - svc := NewPermissionService() - if svc == nil { - t.Error(constants.ShouldNotNil) - } -} - -// Create 1 role -// -> get by id -// -> get all and check >= 1 -// -> update -// -> delete -// -> get by id - -func TestSuccessCrudPermission(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - svc := NewPermissionService() - if svc == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - modelPerm := model.PermissionCreate{ - Name: strings.ToLower(helper.RandomString(10)), - Description: helper.RandomString(30), - } - permID, createErr := svc.Create(ctx, modelPerm) - if createErr != nil || permID < 1 { - t.Error("should not error and permID should more than one, but got", createErr.Error()) - } - defer func() { - svc.Delete(ctx, permID) - }() - - permByID, getErr := svc.GetByID(ctx, permID) - if getErr != nil || permByID == nil { - t.Error("should not error and permByID should not nil") - } - if permByID.Name != modelPerm.Name || permByID.Description != modelPerm.Description { - t.Error("name and desc should same") - } - - perms, total, getAllErr := svc.GetAll(ctx, base.RequestGetAll{Limit: 10, Page: 1}) - if len(perms) < 1 || total < 1 || getAllErr != nil { - t.Error("should more than or equal one and not error at all") - } - - updatePermModel := model.PermissionUpdate{ - ID: permID, - Name: strings.ToLower(helper.RandomString(11)), - Description: helper.RandomString(31), - } - updateErr := svc.Update(ctx, updatePermModel) - if updateErr != nil { - t.Error(constants.ShouldNotErr) - } - - // value reset - permByID = nil - getErr = nil - permByID, getErr = svc.GetByID(ctx, permID) - if getErr != nil || permByID == nil { - t.Error("should not error and permByID should not nil") - } - if permByID.Name != updatePermModel.Name || permByID.Description != updatePermModel.Description { - t.Error("name and desc should same") - } - - deleteErr := svc.Delete(ctx, permID) - if deleteErr != nil { - t.Error(constants.ShouldNotErr) - } - - // value reset - permByID = nil - getErr = nil - permByID, getErr = svc.GetByID(ctx, permID) - if getErr == nil || permByID != nil { - t.Error("should error and permByID should nil") - } -} - -func TestFailedCrudPermission(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - svc := NewPermissionService() - if svc == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - modelPerm := model.PermissionCreate{ - Name: strings.ToLower(helper.RandomString(10)), - Description: helper.RandomString(30), - } - permID, createErr := svc.Create(ctx, modelPerm) - if createErr != nil || permID < 1 { - t.Error("should not error and permID should more than one") - } - defer func() { - svc.Delete(ctx, permID) - }() - - permByID, getErr := svc.GetByID(ctx, -10) - if getErr == nil || permByID != nil { - t.Error("should error and permByID should nil") - } - - updatePermModel := model.PermissionUpdate{ - ID: -10, - Name: strings.ToLower(helper.RandomString(11)), - Description: helper.RandomString(31), - } - updateErr := svc.Update(ctx, updatePermModel) - if updateErr == nil { - t.Error(constants.ShouldErr) - } - - deleteErr := svc.Delete(ctx, -10) - if deleteErr == nil { - t.Error(constants.ShouldErr) - } -} diff --git a/service/role/role_service.go b/service/role/role_service.go deleted file mode 100644 index 5969f5b..0000000 --- a/service/role/role_service.go +++ /dev/null @@ -1,193 +0,0 @@ -package service - -import ( - "context" - "strings" - "sync" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/entity" - "github.com/Lukmanern/gost/domain/model" - repository "github.com/Lukmanern/gost/repository/role" - permService "github.com/Lukmanern/gost/service/permission" -) - -type RoleService interface { - - // Create func create one role. - Create(ctx context.Context, data model.RoleCreate) (id int, err error) - - // ConnectPermissions func connect one role with one or more permissions. - ConnectPermissions(ctx context.Context, data model.RoleConnectToPermissions) (err error) - - // GetByID func get one role. - GetByID(ctx context.Context, id int) (role *entity.Role, err error) - - // GetAll func get some roles. - GetAll(ctx context.Context, filter base.RequestGetAll) (roles []model.RoleResponse, total int, err error) - - // Update func update one role. - Update(ctx context.Context, data model.RoleUpdate) (err error) - - // Delete func delete one role. - Delete(ctx context.Context, id int) (err error) -} - -type RoleServiceImpl struct { - repository repository.RoleRepository - servicePermission permService.PermissionService -} - -var ( - roleServiceImpl *RoleServiceImpl - roleServiceImplOnce sync.Once -) - -const roleNotFound = "role/s not found" - -func NewRoleService(servicePermission permService.PermissionService) RoleService { - roleServiceImplOnce.Do(func() { - roleServiceImpl = &RoleServiceImpl{ - repository: repository.NewRoleRepository(), - servicePermission: servicePermission, - } - }) - return roleServiceImpl -} - -func (svc *RoleServiceImpl) Create(ctx context.Context, data model.RoleCreate) (id int, err error) { - data.Name = strings.ToLower(data.Name) - for _, id := range data.PermissionsID { - permission, getErr := svc.servicePermission.GetByID(ctx, id) - if getErr != nil || permission == nil { - return 0, fiber.NewError(fiber.StatusNotFound, "one of permissions isn't found") - } - } - role, getErr := svc.repository.GetByName(ctx, data.Name) - if getErr == nil || role != nil { - return 0, fiber.NewError(fiber.StatusBadRequest, "role name has been used") - } - - entityRole := entity.Role{ - Name: data.Name, - Description: data.Description, - } - entityRole.SetCreateTime() - id, err = svc.repository.Create(ctx, entityRole, data.PermissionsID) - if err != nil { - return 0, err - } - return id, nil -} - -func (svc *RoleServiceImpl) ConnectPermissions(ctx context.Context, data model.RoleConnectToPermissions) (err error) { - role, getErr := svc.repository.GetByID(ctx, data.RoleID) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, roleNotFound) - } - return getErr - } - if role == nil { - return fiber.NewError(fiber.StatusNotFound, roleNotFound) - } - for _, id := range data.PermissionsID { - permission, getErr := svc.servicePermission.GetByID(ctx, id) - if getErr != nil || permission == nil { - return fiber.NewError(fiber.StatusNotFound, "one of permissions isn't found") - } - } - - connectErr := svc.repository.ConnectToPermission(ctx, data.RoleID, data.PermissionsID) - if connectErr != nil { - return connectErr - } - return nil -} - -func (svc *RoleServiceImpl) GetByID(ctx context.Context, id int) (role *entity.Role, err error) { - role, err = svc.repository.GetByID(ctx, id) - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fiber.NewError(fiber.StatusNotFound, roleNotFound) - } - return nil, err - } - if role == nil { - return nil, fiber.NewError(fiber.StatusNotFound, roleNotFound) - } - return role, nil -} - -func (svc *RoleServiceImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (roles []model.RoleResponse, total int, err error) { - roleEntities, total, err := svc.repository.GetAll(ctx, filter) - if err != nil { - return nil, 0, err - } - - roles = []model.RoleResponse{} - for _, roleEntity := range roleEntities { - newRole := model.RoleResponse{ - ID: roleEntity.ID, - Name: roleEntity.Name, - Description: roleEntity.Description, - } - roles = append(roles, newRole) - } - return roles, total, nil -} - -func (svc *RoleServiceImpl) Update(ctx context.Context, data model.RoleUpdate) (err error) { - data.Name = strings.ToLower(data.Name) - roleByName, getErr := svc.repository.GetByName(ctx, data.Name) - if getErr != nil && getErr != gorm.ErrRecordNotFound { - return getErr - } - if roleByName != nil && roleByName.ID != data.ID { - return fiber.NewError(fiber.StatusBadRequest, "role name has been used") - } - - roleByID, getErr := svc.repository.GetByID(ctx, data.ID) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, roleNotFound) - } - return getErr - } - if roleByID == nil { - return fiber.NewError(fiber.StatusNotFound, roleNotFound) - } - - entityRole := entity.Role{ - ID: data.ID, - Name: data.Name, - Description: data.Description, - } - entityRole.SetUpdateTime() - err = svc.repository.Update(ctx, entityRole) - if err != nil { - return err - } - return nil -} - -func (svc *RoleServiceImpl) Delete(ctx context.Context, id int) (err error) { - role, getErr := svc.repository.GetByID(ctx, id) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, roleNotFound) - } - return getErr - } - if role == nil { - return fiber.NewError(fiber.StatusNotFound, roleNotFound) - } - err = svc.repository.Delete(ctx, id) - if err != nil { - return err - } - return nil -} diff --git a/service/role/role_service_test.go b/service/role/role_service_test.go deleted file mode 100644 index 1b54a19..0000000 --- a/service/role/role_service_test.go +++ /dev/null @@ -1,214 +0,0 @@ -// Don't run test per file without -p 1 -// or simply run test per func or run -// project test using make test command -// check Makefile file -package service - -import ( - "strings" - "testing" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" - permService "github.com/Lukmanern/gost/service/permission" -) - -func init() { - // Check env and database - env.ReadConfig("./../../.env") - - connector.LoadDatabase() - connector.LoadRedisCache() -} - -func TestNewRoleService(t *testing.T) { - permSvc := permService.NewPermissionService() - svc := NewRoleService(permSvc) - if svc == nil { - t.Error(constants.ShouldNotNil) - } -} - -// create 1 role, create 4 permissions -// trying to connect -func TestSuccessCrudRole(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - permSvc := permService.NewPermissionService() - if permSvc == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - svc := NewRoleService(permSvc) - if svc == nil { - t.Error(constants.ShouldNotNil) - } - - modelRole := model.RoleCreate{ - Name: strings.ToLower(helper.RandomString(10)), - Description: helper.RandomString(30), - } - roleID, createErr := svc.Create(ctx, modelRole) - if createErr != nil || roleID < 1 { - t.Error("should not error and id should more than zero") - } - - // Save the ID for deleting the permissions - permsID := make([]int, 0) - for i := 0; i < 3; i++ { - modelPerm := model.PermissionCreate{ - Name: strings.ToLower(helper.RandomString(10)), - Description: helper.RandomString(30), - } - permID, createErr := permSvc.Create(ctx, modelPerm) - if createErr != nil || permID < 1 { - t.Error("should not error and permID should be more than one") - } - - permsID = append(permsID, permID) - } - - defer func() { - svc.Delete(ctx, roleID) - for _, id := range permsID { - permSvc.Delete(ctx, id) - } - }() - - // Success connect - modelConnect := model.RoleConnectToPermissions{ - RoleID: roleID, - PermissionsID: permsID, - } - connectErr := svc.ConnectPermissions(ctx, modelConnect) - if connectErr != nil { - t.Error(constants.ShouldNotErr) - } - - roleByID, getErr := svc.GetByID(ctx, roleID) - if getErr != nil || roleByID == nil { - t.Error("should not error and role not nil") - } - if len(roleByID.Permissions) != len(permsID) { - t.Error("total of permissions connected by role should be equal") - } - - roles, total, getAllErr := svc.GetAll(ctx, base.RequestGetAll{Limit: 10, Page: 1}) - if len(roles) < 1 || total < 1 || getAllErr != nil { - t.Error("should be more than or equal to one and not error at all") - } - - updateRoleModel := model.RoleUpdate{ - ID: roleID, - Name: strings.ToLower(helper.RandomString(11)), - Description: helper.RandomString(31), - } - updateErr := svc.Update(ctx, updateRoleModel) - if updateErr != nil { - t.Error(constants.ShouldNotErr) - } - - // Value reset - roleByID = nil - getErr = nil - roleByID, getErr = svc.GetByID(ctx, roleID) - if getErr != nil || roleByID == nil { - t.Error("should not error and roleByID should not be nil") - } - if roleByID.Name != updateRoleModel.Name || roleByID.Description != updateRoleModel.Description { - t.Error("name and description should be the same") - } - - deleteErr := svc.Delete(ctx, roleID) - if deleteErr != nil { - t.Error(constants.ShouldNotErr) - } - - // Value reset - roleByID = nil - getErr = nil - roleByID, getErr = svc.GetByID(ctx, roleID) - if getErr == nil || roleByID != nil { - t.Error("should error and roleByID should be nil") - } -} - -func TestFailedCrudRoles(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - permSvc := permService.NewPermissionService() - if permSvc == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - svc := NewRoleService(permSvc) - if svc == nil { - t.Error(constants.ShouldNotNil) - } - - // failed create: permissions not found - func() { - modelRole := model.RoleCreate{ - Name: strings.ToLower(helper.RandomString(10)), - Description: helper.RandomString(30), - PermissionsID: []int{-1, -2, -3}, - } - roleID, createErr := svc.Create(ctx, modelRole) - if createErr == nil || roleID != 0 { - t.Error("should error and id should zero") - } - }() - - // success create - modelRole := model.RoleCreate{ - Name: strings.ToLower(helper.RandomString(10)), - Description: helper.RandomString(30), - } - roleID, createErr := svc.Create(ctx, modelRole) - if createErr != nil || roleID < 1 { - t.Error("should not error and id should more than zero") - } - - defer func() { - svc.Delete(ctx, roleID) - }() - - // failed connect - modelConnectFailed := model.RoleConnectToPermissions{ - RoleID: roleID, - PermissionsID: []int{-3, -2, -1}, - } - connectErr := svc.ConnectPermissions(ctx, modelConnectFailed) - if connectErr == nil { - t.Error(constants.ShouldErr) - } - - modelConnectFailed = model.RoleConnectToPermissions{ - RoleID: -1, - PermissionsID: []int{}, - } - connectErr = nil - connectErr = svc.ConnectPermissions(ctx, modelConnectFailed) - if connectErr == nil { - t.Error(constants.ShouldErr) - } - - // failed update - updateRoleModel := model.RoleUpdate{ - ID: -1, - Name: strings.ToLower(helper.RandomString(11)), - Description: helper.RandomString(31), - } - updateErr := svc.Update(ctx, updateRoleModel) - if updateErr == nil { - t.Error(constants.ShouldErr) - } - - // failed delete - deleteErr := svc.Delete(ctx, -1) - if deleteErr == nil { - t.Error(constants.ShouldErr) - } -} diff --git a/service/user/user_service.go b/service/user/user_service.go deleted file mode 100644 index 3010183..0000000 --- a/service/user/user_service.go +++ /dev/null @@ -1,502 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "strconv" - "sync" - "time" - - "github.com/go-redis/redis" - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/entity" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/hash" - "github.com/Lukmanern/gost/internal/helper" - "github.com/Lukmanern/gost/internal/middleware" - repository "github.com/Lukmanern/gost/repository/user" - emailService "github.com/Lukmanern/gost/service/email" - roleService "github.com/Lukmanern/gost/service/role" -) - -type UserService interface { - - // Register function register user account, than send verification-code to email - Register(ctx context.Context, user model.UserRegister) (id int, err error) - - // Verification function activates user account with - // verification code that has been sended to the user's email - Verification(ctx context.Context, verifyData model.UserVerificationCode) (err error) - - // DeleteUserByVerification function deletes user data if the user account is not yet verified. - // This implies that the email owner hasn't actually registered the email, indicating that - // the user who registered may be making typing errors or may be a hacker attempting to get - // the verification code. - DeleteUserByVerification(ctx context.Context, verifyData model.UserVerificationCode) (err error) - - // FailedLoginCounter function counts failed login attempts and stores them in Redis. - // After the N-th attempt to log in with the same IP address results in continuous failures, - // the system will impose a 50-minute ban. During this period, login requests (refer to - // the login function in the user controller) will not be processed. - FailedLoginCounter(userIP string, increment bool) (counter int, err error) - - // Login func give user token/ jwt for auth header. - Login(ctx context.Context, user model.UserLogin) (token string, err error) - - // Logout function stores the user's active token in Redis, effectively - // blacklisting the token. This ensures that the token cannot be reused - // for authentication (refer to the IsBlacklisted function in internal/middleware). - Logout(c *fiber.Ctx) (err error) - - // ForgetPassword func send verification code into user's email - ForgetPassword(ctx context.Context, user model.UserForgetPassword) (err error) - - // ResetPassword func resets password by creating - // new password by email and verification code - ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) - - // UpdatePassword func updates user's password - UpdatePassword(ctx context.Context, user model.UserPasswordUpdate) (err error) - - // UpdateProfile func updates user's profile data - UpdateProfile(ctx context.Context, user model.UserProfileUpdate) (err error) - - // MyProfile func shows user's profile data - MyProfile(ctx context.Context, id int) (profile model.UserProfile, err error) -} - -type UserServiceImpl struct { - repository repository.UserRepository - roleService roleService.RoleService - emailService emailService.EmailService - jwtHandler *middleware.JWTHandler - redis *redis.Client -} - -var ( - userService *UserServiceImpl - userServiceOnce sync.Once -) - -func NewUserService(roleService roleService.RoleService) UserService { - userServiceOnce.Do(func() { - userService = &UserServiceImpl{ - roleService: roleService, - repository: repository.NewUserRepository(), - emailService: emailService.NewEmailService(), - jwtHandler: middleware.NewJWTHandler(), - redis: connector.LoadRedisCache(), - } - }) - - return userService -} - -func (svc *UserServiceImpl) Register(ctx context.Context, user model.UserRegister) (id int, err error) { - // search user by email - // if exist, return error - userByEmail, getUserErr := svc.repository.GetByEmail(ctx, user.Email) - if getUserErr == nil || userByEmail != nil { - return 0, fiber.NewError(fiber.StatusBadRequest, "email has been used") - } - - // search role, if not exist return error - roleByID, getRoleErr := svc.roleService.GetByID(ctx, user.RoleID) - if getRoleErr != nil || roleByID == nil { - return 0, fiber.NewError(fiber.StatusNotFound, "role not found") - } - - // create verification code - // for user (must unique) - var ( - verifCode string - passwordHashed string - hashErr error - counter int = 0 - ) - for { - verifCode = "" - verifCode = helper.RandomString(7) + helper.RandomString(7) + helper.RandomString(7) // total = 21 - userGetByCode, getByCodeErr := svc.repository.GetByConditions(ctx, map[string]any{ - "verification_code =": verifCode, - }) - if getByCodeErr != nil || userGetByCode == nil { - break - } - counter++ - if counter >= 150 { - return 0, errors.New("failed generating verification code") - } - } - // generate password hashed - counter = 0 - for { - passwordHashed, hashErr = hash.Generate(user.Password) - if hashErr == nil { - break - } - counter++ - if counter >= 150 { - return 0, errors.New("failed hashing user password") - } - } - - userEntity := entity.User{ - Name: helper.ToTitle(user.Name), - Email: user.Email, - Password: passwordHashed, - VerificationCode: &verifCode, - ActivatedAt: nil, - } - // set created_at and updated_at equal to now - userEntity.SetCreateTime() - id, err = svc.repository.Create(ctx, userEntity, user.RoleID) - if err != nil { - return 0, err - } - - toEmail := []string{user.Email} - subject := "Gost Project: Activation Account" - message := "Hello, My name is Bot001 from Project Gost: Golang Starter By Lukmanern." - message += "
Your account has already been created but is not yet active. To activate your account," - message += " you can click on the Activation Link. If you do not registering for an account or any activity" - message += " on Project Gost, you can request data deletion too." - message += "
Thank You, Best Regards Bot001." - message += "


Code : " + verifCode - - sendingErr := svc.emailService.SendMail(toEmail, subject, message) - if sendingErr != nil { - message := "account is created, but confimation email failed sending: " - message += sendingErr.Error() - return 0, errors.New(message) - } - return id, nil -} - -func (svc *UserServiceImpl) Verification(ctx context.Context, verifyData model.UserVerificationCode) (err error) { - // search user by code, if not exist return error - userEntity, getByCodeErr := svc.repository.GetByConditions(ctx, map[string]any{ - "verification_code =": verifyData.Code, - "email =": verifyData.Email, - }) - if getByCodeErr != nil || userEntity == nil { - return fiber.NewError(fiber.StatusNotFound, "verification code not found") - } - if userEntity.ActivatedAt != nil { - return fiber.NewError(fiber.StatusBadRequest, "your account already activated") - } - // set updated_at, activated_at and - // nulling verification code - userEntity.SetActivateAccount() - userEntity.SetUpdateTime() - updateErr := svc.repository.Update(ctx, *userEntity) - if updateErr != nil { - return updateErr - } - return nil -} - -func (svc *UserServiceImpl) DeleteUserByVerification(ctx context.Context, verifyData model.UserVerificationCode) (err error) { - // search user by code, if not exist return error - userEntity, getByCodeErr := svc.repository.GetByConditions(ctx, map[string]any{ - "verification_code =": verifyData.Code, - "email =": verifyData.Email, - }) - if getByCodeErr != nil || userEntity == nil { - return fiber.NewError(fiber.StatusNotFound, "verification code not found") - } - // check if account active or inactive - if userEntity.ActivatedAt != nil { - return fiber.NewError(fiber.StatusBadRequest, "can not delete your account, your account is active") - } - deleteErr := svc.repository.Delete(ctx, userEntity.ID) - if deleteErr != nil { - return deleteErr - } - return nil -} - -func (svc *UserServiceImpl) FailedLoginCounter(userIP string, increment bool) (counter int, err error) { - // set key for banned counter - key := "failed-login-" + userIP - getStatus := svc.redis.Get(key) - counter, _ = strconv.Atoi(getStatus.Val()) - if increment { - counter++ - setStatus := svc.redis.Set(key, counter, 50*time.Minute) - if setStatus.Err() != nil { - return 0, errors.New("storing data to redis") - } - } - return counter, nil -} - -func (svc *UserServiceImpl) Login(ctx context.Context, user model.UserLogin) (token string, err error) { - // search user by email - // if not exist/found, return error - userEntity, err := svc.repository.GetByEmail(ctx, user.Email) - if err != nil { - if err == gorm.ErrRecordNotFound { - return "", fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - return "", err - } - if userEntity == nil { - return "", fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - - res, verfiryErr := hash.Verify(userEntity.Password, user.Password) - if verfiryErr != nil { - return "", verfiryErr - } - if !res { - return "", fiber.NewError(fiber.StatusBadRequest, "wrong password") - } - // if exist but not activated - // return error - if userEntity.ActivatedAt == nil { - message := "Your account is already exist in our system, but it's still inactive, " - message += "please check Your email inbox to activated-it" - return "", fiber.NewError(fiber.StatusBadRequest, message) - } - - userRole := userEntity.Roles[0] - permIDs := make([]int, 0) - for _, perm := range userRole.Permissions { - permIDs = append(permIDs, perm.ID) - } - bitGroups := middleware.BuildBitGroups(permIDs...) - config := env.Configuration() - expired := time.Now().Add(config.AppAccessTokenTTL) - token, generetaErr := svc.jwtHandler.GenerateJWT(userEntity.ID, user.Email, userRole.Name, bitGroups, expired) - if generetaErr != nil { - message := fmt.Sprintf("system error while generating token (%s)", generetaErr.Error()) - return "", fiber.NewError(fiber.StatusInternalServerError, message) - } - if len(token) > 2800 { - message := "token is too large, more than 2800 characters (too large for http header)" - return "", errors.New(message) - } - return token, nil -} - -func (svc *UserServiceImpl) Logout(c *fiber.Ctx) (err error) { - err = svc.jwtHandler.InvalidateToken(c) - if err != nil { - return errors.New("problem invalidating token") - } - - return nil -} - -func (svc *UserServiceImpl) ForgetPassword(ctx context.Context, user model.UserForgetPassword) (err error) { - userEntity, err := svc.repository.GetByEmail(ctx, user.Email) - if err != nil { - if err == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - return err - } - if userEntity == nil { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - if userEntity.ActivatedAt == nil { - message := "your account has not been activated since register, please check your inbox/ spam mail." - return fiber.NewError(fiber.StatusBadRequest, message) - } - - var ( - verifCode string - counter int // max retry - ) - for { - verifCode = helper.RandomString(7) + helper.RandomString(7) + helper.RandomString(7) // total = 21 - userGetByCode, getByCodeErr := svc.repository.GetByConditions(ctx, map[string]any{ - "verification_code =": verifCode, - }) - if getByCodeErr != nil || userGetByCode == nil { - break - } - counter++ - if counter >= 150 { - return errors.New("failed generating verification code") - } - } - userEntity.VerificationCode = &verifCode - userEntity.SetUpdateTime() - - err = svc.repository.Update(ctx, *userEntity) - if err != nil { - return err - } - - // Todo : refactor - toEmail := []string{user.Email} - subject := "Gost Project: Reset Password" - message := "Hello, My name is Bot001 from Project Gost: Golang Starter By Lukmanern." - message += " Your account has already been created but is not yet active. To activate your account," - message += " you can click on the Activation Link. If you do not registering for an account or any activity" - message += " on Project Gost, you can request data deletion by clicking the Link Request Delete." - message += "

Thank You, Best Regards Bot001." - message += "

Code : " + verifCode - - sendingErr := svc.emailService.SendMail(toEmail, subject, message) - if sendingErr != nil { - message := "token forget password is created, but confimation email failed sending: " - message += sendingErr.Error() - return errors.New(message) - } - return nil -} - -func (svc *UserServiceImpl) ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) { - userByCode, err := svc.repository.GetByConditions(ctx, map[string]any{ - "email =": user.Email, - "verification_code =": user.Code, - }) - if err != nil { - if err == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - return err - } - if userByCode == nil { - return fiber.NewError(fiber.StatusNotFound, "user not found") - } - if userByCode.ActivatedAt == nil { - message := "Your account is already exist in our system, but it's still " - message += "inactive, please check Your email inbox to activated-it" - return fiber.NewError(fiber.StatusBadRequest, message) - } - - var ( - hashErr error - passwdHashed string - counter int // max retry - ) - for { - passwdHashed, hashErr = hash.Generate(user.NewPassword) - if hashErr == nil { - break - } - counter++ - if counter >= 150 { - return errors.New("failed hashing user password") - } - } - - userByCode.VerificationCode = nil - userByCode.SetUpdateTime() - updateErr := svc.repository.Update(ctx, *userByCode) - if updateErr != nil { - return updateErr - } - - updatePasswdErr := svc.repository.UpdatePassword(ctx, userByCode.ID, passwdHashed) - if updatePasswdErr != nil { - return updatePasswdErr - } - return nil -} - -func (svc *UserServiceImpl) UpdatePassword(ctx context.Context, user model.UserPasswordUpdate) (err error) { - userByID, err := svc.repository.GetByID(ctx, user.ID) - if err != nil { - if err == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - return err - } - if userByID == nil { - return fiber.NewError(fiber.StatusNotFound, "user not found") - } - - res, verfiryErr := hash.Verify(userByID.Password, user.OldPassword) - if verfiryErr != nil { - return verfiryErr - } - if !res { - return fiber.NewError(fiber.StatusBadRequest, "wrong password") - } - - var ( - hashErr error - passwdHashed string - counter int - ) - for { - passwdHashed, hashErr = hash.Generate(user.NewPassword) - if hashErr == nil { - break - } - counter++ - if counter >= 150 { - return errors.New("failed hashing user password") - } - } - - updatePwErr := svc.repository.UpdatePassword(ctx, userByID.ID, passwdHashed) - if updatePwErr != nil { - return updatePwErr - } - return nil -} - -func (svc *UserServiceImpl) MyProfile(ctx context.Context, id int) (profile model.UserProfile, err error) { - // search profile by ID - user, err := svc.repository.GetByID(ctx, id) - if err != nil { - if err == gorm.ErrRecordNotFound { - return profile, fiber.NewError(fiber.StatusNotFound, "user not found") - } - - return profile, err - } - if user == nil { - return profile, fiber.NewError(fiber.StatusInternalServerError, "error while checking user") - } - - // set response - profile = model.UserProfile{ - Name: user.Name, - Email: user.Email, - } - if len(user.Roles) > 0 { - profile.Role = user.Roles[0] - } - return profile, nil -} - -func (svc *UserServiceImpl) UpdateProfile(ctx context.Context, user model.UserProfileUpdate) (err error) { - // search profile by ID - userByID, getErr := svc.repository.GetByID(ctx, user.ID) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - return err - } - if userByID == nil { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - - userEntity := entity.User{ - ID: user.ID, - Name: helper.ToTitle(user.Name), - VerificationCode: userByID.VerificationCode, - ActivatedAt: userByID.ActivatedAt, - } - userEntity.SetUpdateTime() - - err = svc.repository.Update(ctx, userEntity) - if err != nil { - return err - } - return nil -} diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go deleted file mode 100644 index 00aa5c0..0000000 --- a/service/user/user_service_test.go +++ /dev/null @@ -1,393 +0,0 @@ -// Don't run test per file without -p 1 -// or simply run test per func or run -// project test using make test command -// check Makefile file -package service - -import ( - "testing" - - "github.com/gofiber/fiber/v2" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" - "github.com/Lukmanern/gost/internal/middleware" - repository "github.com/Lukmanern/gost/repository/user" - permService "github.com/Lukmanern/gost/service/permission" - roleService "github.com/Lukmanern/gost/service/role" -) - -func init() { - // Check env and database - env.ReadConfig("./../../.env") - - connector.LoadDatabase() - connector.LoadRedisCache() -} - -func TestNewUserService(t *testing.T) { - permSvc := permService.NewPermissionService() - roleSvc := roleService.NewRoleService(permSvc) - svc := NewUserService(roleSvc) - if svc == nil { - t.Error(constants.ShouldNotNil) - } -} - -func TestSuccessRegister(t *testing.T) { - defer func() { - connector.LoadRedisCache().FlushAll() - }() - permSvc := permService.NewPermissionService() - roleSvc := roleService.NewRoleService(permSvc) - svc := NewUserService(roleSvc) - c := helper.NewFiberCtx() - ctx := c.Context() - if svc == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - userRepo := repository.NewUserRepository() - if userRepo == nil { - t.Error(constants.ShouldNotNil) - } - - modelUserRegis := model.UserRegister{ - Name: helper.RandomString(12), - Email: helper.RandomEmail(), - Password: helper.RandomString(12), - RoleID: 1, // admin - } - userID, regisErr := svc.Register(ctx, modelUserRegis) - if regisErr != nil || userID < 1 { - t.Error("should not error and id should more than zero") - } - - defer func() { - userRepo.Delete(ctx, userID) - }() - - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Error("should not error and id should not nil") - } - if userByID.Name != helper.ToTitle(modelUserRegis.Name) || - userByID.Email != modelUserRegis.Email || - userByID.Roles[0].ID != modelUserRegis.RoleID { - t.Error("should equal") - } - if userByID.VerificationCode == nil { - t.Error(constants.ShouldNotNil) - } - if userByID.ActivatedAt != nil { - t.Error(constants.ShouldNil) - } - - // failed login : account is created, - // but account is inactive - modelUserLogin := model.UserLogin{ - Email: modelUserRegis.Email, - Password: modelUserRegis.Password, - IP: helper.RandomIPAddress(), - } - token, loginErr := svc.Login(ctx, modelUserLogin) - if loginErr == nil || token != "" { - t.Error("should error login and token should nil-string") - } - fiberErr, ok := loginErr.(*fiber.Error) - if ok { - if fiberErr.Code != fiber.StatusBadRequest { - t.Error("should error 400BadReq") - } - } - - // failed forget password : account is created, - // but account is inactive - forgetPassErr := svc.ForgetPassword(ctx, model.UserForgetPassword{Email: modelUserRegis.Email}) - if forgetPassErr == nil { - t.Error("should error login and token should nil-string") - } - fiberErr, ok = forgetPassErr.(*fiber.Error) - if ok { - if fiberErr.Code != fiber.StatusBadRequest { - t.Error("should error 400BadReq") - } - } - - // failed forget password : account is created, - // but account is inactive - resetPasswdErr := svc.ResetPassword(ctx, model.UserResetPassword{Code: "wrongCode"}) - if resetPasswdErr == nil { - t.Error("should error login and token should nil-string") - } - - vCode := userByID.VerificationCode - - verifErr := svc.Verification(ctx, model.UserVerificationCode{ - Code: *vCode, - Email: userByID.Email, - }) - if verifErr != nil { - t.Error(constants.ShouldNotNil) - } - - // value reset - userByID = nil - getErr = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Error("should not error and id should not nil") - } - if userByID.VerificationCode != nil { - t.Error(constants.ShouldNotNil) - } - if userByID.ActivatedAt == nil { - t.Error(constants.ShouldNil) - } - - // reset value - token = "" - loginErr = nil - modelUserLogin = model.UserLogin{ - Email: modelUserRegis.Email, - Password: modelUserRegis.Password, - IP: helper.RandomIPAddress(), - } - token, loginErr = svc.Login(ctx, modelUserLogin) - if loginErr != nil || token == "" { - t.Error("should not error login and token should not nil-string") - } - - jwtHandler := middleware.NewJWTHandler() - if jwtHandler.IsBlacklisted(token) { - t.Error("should not in black-list") - } - - modelUserForgetPasswd := model.UserForgetPassword{ - Email: modelUserLogin.Email, - } - forgetPwErr := svc.ForgetPassword(ctx, modelUserForgetPasswd) - if forgetPwErr != nil { - t.Error(constants.ShouldNotErr) - } - - // value reset - userByID = nil - getErr = nil - userByID, getErr = userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Error("should not error and id should not nil") - } - if userByID.VerificationCode == nil { - t.Error(constants.ShouldNotNil) - } - if userByID.ActivatedAt == nil { - t.Error(constants.ShouldNotNil) - } - - passwd := helper.RandomString(12) - modelUserResetPasswd := model.UserResetPassword{ - Email: userByID.Email, - Code: *userByID.VerificationCode, - NewPassword: passwd, - NewPasswordConfirm: passwd, - } - resetErr := svc.ResetPassword(ctx, modelUserResetPasswd) - if resetErr != nil { - t.Error(constants.ShouldNotErr) - } - - // reset value, login failed - token = "" - loginErr = nil - modelUserLogin = model.UserLogin{ - Email: modelUserRegis.Email, - Password: modelUserRegis.Password, - IP: helper.RandomIPAddress(), - } - token, loginErr = svc.Login(ctx, modelUserLogin) - if loginErr == nil || token != "" { - t.Error("should error login and token should nil-string") - } - - // reset value, login success - token = "" - loginErr = nil - modelUserLogin = model.UserLogin{ - Email: modelUserRegis.Email, - Password: modelUserResetPasswd.NewPassword, - IP: helper.RandomIPAddress(), - } - token, loginErr = svc.Login(ctx, modelUserLogin) - if loginErr != nil || token == "" { - t.Error("should not error login and token should not nil-string") - } - - passwd = helper.RandomString(14) - modelUserUpdatePasswd := model.UserPasswordUpdate{ - ID: userID, - OldPassword: modelUserResetPasswd.NewPassword, - NewPassword: passwd, - NewPasswordConfirm: passwd, - } - updatePasswdErr := svc.UpdatePassword(ctx, modelUserUpdatePasswd) - if updatePasswdErr != nil { - t.Error(constants.ShouldNotErr) - } - - // reset value, login success - token = "" - loginErr = nil - modelUserLogin = model.UserLogin{ - Email: modelUserRegis.Email, - Password: modelUserUpdatePasswd.NewPassword, - IP: helper.RandomIPAddress(), - } - token, loginErr = svc.Login(ctx, modelUserLogin) - if loginErr != nil || token == "" { - t.Error("should not error login and token should not nil-string") - } - - modelUserUpdate := model.UserProfileUpdate{ - ID: userID, - Name: helper.RandomString(10), - } - updateProfileErr := svc.UpdateProfile(ctx, modelUserUpdate) - if updateProfileErr != nil { - t.Error(constants.ShouldNotErr) - } - - profile, getErr := svc.MyProfile(ctx, userID) - if getErr != nil { - t.Error(constants.ShouldNotErr) - } - if profile.Name != helper.ToTitle(modelUserUpdate.Name) { - t.Error("should equal") - } - - // success logout - cForLogout := helper.NewFiberCtx() - logoutErr := svc.Logout(cForLogout) - if logoutErr != nil { - t.Error("should no error") - } -} - -func TestFailedRegister(t *testing.T) { - defer func() { - connector.LoadRedisCache().FlushAll() - }() - permSvc := permService.NewPermissionService() - roleSvc := roleService.NewRoleService(permSvc) - svc := NewUserService(roleSvc) - c := helper.NewFiberCtx() - ctx := c.Context() - if svc == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - userRepo := repository.NewUserRepository() - if userRepo == nil { - t.Error(constants.ShouldNotNil) - } - - modelUserRegis := model.UserRegister{ - Name: helper.RandomString(12), - Email: helper.RandomEmail(), - Password: helper.RandomString(12), - RoleID: -10, // failed - } - userID, regisErr := svc.Register(ctx, modelUserRegis) - if regisErr == nil || userID != 0 { - t.Error("should error and id should zero") - } - - defer func() { - userRepo.Delete(ctx, userID) - }() - - verifErr := svc.Verification(ctx, model.UserVerificationCode{ - Code: "wrongCode", - Email: "wrongEmail", - }) - if verifErr == nil { - t.Error(constants.ShouldErr) - } - fiberErr, ok := verifErr.(*fiber.Error) - if ok { - if fiberErr.Code != fiber.StatusNotFound { - t.Error("should error 404") - } - } - - deleteUserErr := svc.DeleteUserByVerification(ctx, model.UserVerificationCode{ - Code: "wrongCode", - Email: "wrongEmail", - }) - if deleteUserErr == nil { - t.Error(constants.ShouldErr) - } - fiberErr, ok = deleteUserErr.(*fiber.Error) - if ok { - if fiberErr.Code != fiber.StatusNotFound { - t.Error("should error 404") - } - } - - // failed login - _, loginErr := svc.Login(ctx, model.UserLogin{ - IP: helper.RandomIPAddress(), - }) - if loginErr == nil { - t.Error(constants.ShouldErr) - } - - forgetErr := svc.ForgetPassword(ctx, model.UserForgetPassword{Email: "wrong_email@gost.project"}) - if forgetErr == nil { - t.Error(constants.ShouldErr) - } - - verifyErr := svc.ResetPassword(ctx, model.UserResetPassword{Code: "wrong-code"}) - if verifyErr == nil { - t.Error(constants.ShouldErr) - } - - updatePasswdErr := svc.UpdatePassword(ctx, model.UserPasswordUpdate{ID: -1}) - if updatePasswdErr == nil { - t.Error(constants.ShouldErr) - } - - _, getErr := svc.MyProfile(ctx, -10) - if getErr == nil { - t.Error(constants.ShouldErr) - } -} - -func TestBannedIPAddress(t *testing.T) { - defer func() { - connector.LoadRedisCache().FlushAll() - }() - permSvc := permService.NewPermissionService() - roleSvc := roleService.NewRoleService(permSvc) - svc := NewUserService(roleSvc) - c := helper.NewFiberCtx() - ctx := c.Context() - if svc == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - for i := 1; i <= 15; i++ { - counter, err := svc.FailedLoginCounter(helper.RandomIPAddress(), true) - if err != nil { - t.Error(constants.ShouldNotErr) - } - if i >= 4 { - if counter == i { - t.Error("counter should error") - } - } - } -} diff --git a/service/user_management/user_management_service.go b/service/user_management/user_management_service.go deleted file mode 100644 index e70ae6d..0000000 --- a/service/user_management/user_management_service.go +++ /dev/null @@ -1,191 +0,0 @@ -// don't use this for production -// use this file just for testing -// and testing management. - -package service - -import ( - "context" - "errors" - "strings" - "sync" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/entity" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/hash" - "github.com/Lukmanern/gost/internal/helper" - repository "github.com/Lukmanern/gost/repository/user" -) - -type UserManagementService interface { - - // Create func create one user. - Create(ctx context.Context, user model.UserCreate) (id int, err error) - - // GetByID func get one user by ID. - GetByID(ctx context.Context, id int) (user *model.UserResponse, err error) - - // GetByEmail func get one user by Email. - GetByEmail(ctx context.Context, email string) (user *model.UserResponse, err error) - - // GetAll func get some users - GetAll(ctx context.Context, filter base.RequestGetAll) (users []model.UserResponse, total int, err error) - - // Update func update one user data. - Update(ctx context.Context, user model.UserProfileUpdate) (err error) - - // Delete func delete one user. - Delete(ctx context.Context, id int) (err error) -} - -type UserManagementServiceImpl struct { - repository repository.UserRepository -} - -var ( - userManagementService *UserManagementServiceImpl - userManagementServiceOnce sync.Once -) - -func NewUserManagementService() UserManagementService { - userManagementServiceOnce.Do(func() { - userManagementService = &UserManagementServiceImpl{ - repository: repository.NewUserRepository(), - } - }) - - return userManagementService -} - -func (svc *UserManagementServiceImpl) Create(ctx context.Context, user model.UserCreate) (id int, err error) { - userCheck, getErr := svc.GetByEmail(ctx, user.Email) - if getErr == nil || userCheck != nil { - return 0, fiber.NewError(fiber.StatusBadRequest, "email has been used") - } - - passwordHashed, hashErr := hash.Generate(user.Password) - if hashErr != nil { - message := "something failed while hashing data, please try again" - return 0, errors.New(message) - } - - userEntity := entity.User{ - Name: helper.ToTitle(user.Name), - Email: user.Email, - Password: passwordHashed, - } - userEntity.SetCreateTime() - - roleID := entity.USER - if user.IsAdmin { - roleID = entity.ADMIN - } - - id, err = svc.repository.Create(ctx, userEntity, roleID) - if err != nil { - return 0, err - } - return id, nil -} - -func (svc *UserManagementServiceImpl) GetByID(ctx context.Context, id int) (user *model.UserResponse, err error) { - userEntity, err := svc.repository.GetByID(ctx, id) - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - return nil, err - } - user = &model.UserResponse{ - ID: userEntity.ID, - Name: userEntity.Name, - Email: userEntity.Email, - } - return user, nil -} - -func (svc *UserManagementServiceImpl) GetByEmail(ctx context.Context, email string) (user *model.UserResponse, err error) { - email = strings.ToLower(email) - userEntity, getErr := svc.repository.GetByEmail(ctx, email) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return nil, fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - return nil, getErr - } - user = &model.UserResponse{ - ID: userEntity.ID, - Name: userEntity.Name, - Email: userEntity.Email, - } - return user, nil -} - -func (svc *UserManagementServiceImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (users []model.UserResponse, total int, err error) { - userEntities, total, err := svc.repository.GetAll(ctx, filter) - if err != nil { - return nil, 0, err - } - - users = []model.UserResponse{} - for _, userEntity := range userEntities { - newUserResponse := model.UserResponse{ - ID: userEntity.ID, - Name: userEntity.Name, - Email: userEntity.Email, - } - users = append(users, newUserResponse) - } - return users, total, nil -} - -func (svc *UserManagementServiceImpl) Update(ctx context.Context, user model.UserProfileUpdate) (err error) { - getUser, getErr := svc.repository.GetByID(ctx, user.ID) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - return getErr - } - if getUser == nil { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - - userEntity := entity.User{ - ID: user.ID, - Name: helper.ToTitle(user.Name), - // ... - // add more fields - } - userEntity.SetUpdateTime() - - err = svc.repository.Update(ctx, userEntity) - if err != nil { - return err - } - return nil -} - -func (svc *UserManagementServiceImpl) Delete(ctx context.Context, id int) (err error) { - getUser, getErr := svc.repository.GetByID(ctx, id) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - return getErr - } - if getUser == nil { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) - } - - err = svc.repository.Delete(ctx, id) - if err != nil { - return err - } - return nil -} diff --git a/service/user_management/user_management_service_test.go b/service/user_management/user_management_service_test.go deleted file mode 100644 index 71b601b..0000000 --- a/service/user_management/user_management_service_test.go +++ /dev/null @@ -1,139 +0,0 @@ -// Don't run test per file without -p 1 -// or simply run test per func or run -// project test using make test command -// check Makefile file -package service - -import ( - "testing" - - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" - "github.com/gofiber/fiber/v2" -) - -func init() { - // Check env and database - env.ReadConfig("./../../.env") - - connector.LoadDatabase() - connector.LoadRedisCache() -} - -func TestNewUserManagementService(t *testing.T) { - svc := NewUserManagementService() - if svc == nil { - t.Error(constants.ShouldNotNil) - } -} - -// Create 1 user -// -> get by id -// -> get by email -// -> get all and check >= 1 -// -> update -// -> delete -// -> get by id (checking) -// -> get by email (checking) - -func TestSuccessCrud(t *testing.T) { - c := helper.NewFiberCtx() - ctx := c.Context() - svc := NewUserManagementService() - if svc == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } - - userModel := model.UserCreate{ - Name: "John Doe", - Email: helper.RandomEmail(), - Password: "password", - IsAdmin: true, - } - userID, createErr := svc.Create(ctx, userModel) - if createErr != nil || userID < 1 { - t.Error("should not error or id should more than or equal one") - } - defer func() { - svc.Delete(ctx, userID) - }() - - userByID, getByIDErr := svc.GetByID(ctx, userID) - if getByIDErr != nil || userByID == nil { - t.Error("should not error or user should not nil") - } - if userByID.Name != userModel.Name || userByID.Email != userModel.Email { - t.Error("name and email should same") - } - - userByEmail, getByEmailErr := svc.GetByEmail(ctx, userModel.Email) - if getByEmailErr != nil || userByEmail == nil { - t.Error("should not error or user should not nil") - } - if userByEmail.Name != userModel.Name || userByEmail.Email != userModel.Email { - t.Error("name and email should same") - } - - users, total, getAllErr := svc.GetAll(ctx, base.RequestGetAll{Limit: 10, Page: 1}) - if len(users) < 1 || total < 1 || getAllErr != nil { - t.Error("should more than or equal one and not error at all") - } - - updateUserData := model.UserProfileUpdate{ - ID: userID, - Name: "John Doe Update", - } - updateErr := svc.Update(ctx, updateUserData) - if updateErr != nil { - t.Error(constants.ShouldNotErr) - } - - // reset value - getByIDErr = nil - userByID = nil - userByID, getByIDErr = svc.GetByID(ctx, userID) - if getByIDErr != nil || userByID == nil { - t.Error("should not error or user should not nil") - } - if userByID.Name != updateUserData.Name || userByID.Email != userModel.Email { - t.Error("name and email should same") - } - - deleteErr := svc.Delete(ctx, userID) - if deleteErr != nil { - t.Error(constants.ShouldNotErr) - } - - // reset value - getByIDErr = nil - userByID = nil - userByID, getByIDErr = svc.GetByID(ctx, userID) - if getByIDErr == nil || userByID != nil { - t.Error("should error and user should nil") - } - fiberErr, ok := getByIDErr.(*fiber.Error) - if ok { - if fiberErr.Code != fiber.StatusNotFound { - t.Error("should error 404") - } - } - - // reset value - userByEmail = nil - getByEmailErr = nil - userByEmail, getByEmailErr = svc.GetByEmail(ctx, userModel.Email) - if getByEmailErr == nil || userByEmail != nil { - t.Error("should error or user should nil") - } - - fiberErr, ok = getByEmailErr.(*fiber.Error) - if ok { - if fiberErr.Code != fiber.StatusNotFound { - t.Error("should error 404") - } - } -} From 84c5b7774a65498ca4b1271d2d0a569c65f7b22e Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Tue, 23 Jan 2024 14:00:47 +0700 Subject: [PATCH 02/28] add main, remove v19x --- .github/workflows/build.yml | 2 +- main.go | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 main.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f3c8a53..bc01442 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ["1.19.x", "1.20.x", "1.21.x"] + go-version: ["1.20.x", "1.21.x"] steps: - uses: actions/checkout@v3 diff --git a/main.go b/main.go new file mode 100644 index 0000000..47125b9 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("hello world") +} From b4158a5a3ec9c1b9ab04b15989c1b81b0b9e5168 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Tue, 23 Jan 2024 15:30:13 +0700 Subject: [PATCH 03/28] update internal pkg --- domain/entity/all_entities.go | 3 +- domain/model/user.go | 27 +++++---- internal/constants/constants.go | 19 ------- internal/consts/consts.go | 31 ++++++++++ internal/helper/helper_test.go | 12 ++-- internal/middleware/middleware.go | 48 +++------------- internal/middleware/middleware_test.go | 46 ++++----------- internal/response/response.go | 79 ++++++++++++++++---------- internal/response/response_test.go | 10 +++- 9 files changed, 131 insertions(+), 144 deletions(-) delete mode 100644 internal/constants/constants.go create mode 100644 internal/consts/consts.go diff --git a/domain/entity/all_entities.go b/domain/entity/all_entities.go index 737a775..9ff3910 100644 --- a/domain/entity/all_entities.go +++ b/domain/entity/all_entities.go @@ -21,8 +21,7 @@ var allTables = []any{ &UserHasRoles{}, &Role{}, - // ... - // Add more tables/structs + // add more tables/structs } func AllTables() []any { diff --git a/domain/model/user.go b/domain/model/user.go index 8710966..45d0a1f 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -1,6 +1,10 @@ package model -import "github.com/Lukmanern/gost/domain/entity" +import ( + "time" + + "github.com/Lukmanern/gost/domain/entity" +) type UserRegister struct { Name string `validate:"required,min=2,max=60" json:"name"` @@ -39,19 +43,22 @@ type UserPasswordUpdate struct { } type UserProfile struct { - Email string - Name string - Roles []string + Email string + Name string + ActivatedAt *time.Time + Roles []string } type UserResponse struct { - ID int - Name string + ID int + Name string + ActivatedAt *time.Time } type UserResponseDetail struct { - ID int - Email string - Name string - Roles []entity.Role + ID int + Email string + Name string + ActivatedAt *time.Time + Roles []entity.Role } diff --git a/internal/constants/constants.go b/internal/constants/constants.go deleted file mode 100644 index 94ab46a..0000000 --- a/internal/constants/constants.go +++ /dev/null @@ -1,19 +0,0 @@ -// Constants used for error messages and testing assertions. - -package constants - -const ( - Unauthorized = "should unauthorized" - RedisNil = "redis nil value" - NotFound = "data not found" - ServerErr = "internal server error: " - InvalidID = "invalid id" - InvalidBody = "invalid json body: " - - ShouldErr = "should error" - ShouldNotErr = "should not error" - ShouldNil = "should nil" - ShouldNotNil = "should not nil" - ShouldEqual = "should equal" - ShouldNotEqual = "should not equal" -) diff --git a/internal/consts/consts.go b/internal/consts/consts.go new file mode 100644 index 0000000..9ad102a --- /dev/null +++ b/internal/consts/consts.go @@ -0,0 +1,31 @@ +package consts + +const ( + SuccessCreated = "data successfully created" + SuccessLoaded = "data successfully loaded" + Unauthorized = "unauthorized" + BadRequest = "bad request, please check your request and try again" + NotFound = "data not found" +) + +const ( + InvalidJSONBody = "invalid JSON body" + InvalidUserID = "invalid user ID" + InvalidID = "invalid ID" + + RedisNil = "redis nil value" + + ErrGetIDFromJWT = "error while getting user ID from JWT-Claims" + ErrHashing = "error while hashing password, please try again" +) + +const ( + ShouldErr = "should error" + ShouldNotErr = "should not error" + ShouldNil = "should nil" + ShouldNotNil = "should not nil" + ShouldEqual = "should equal" + ShouldNotEqual = "should not equal" + + LoginShouldSuccess = "login should success" +) diff --git a/internal/helper/helper_test.go b/internal/helper/helper_test.go index 90a137d..ea3b019 100644 --- a/internal/helper/helper_test.go +++ b/internal/helper/helper_test.go @@ -4,7 +4,7 @@ import ( "net" "testing" - "github.com/Lukmanern/gost/internal/constants" + "github.com/Lukmanern/gost/internal/consts" "github.com/stretchr/testify/assert" ) @@ -27,27 +27,27 @@ func TestRandomIPAddress(t *testing.T) { for i := 0; i < 20; i++ { ipRand := RandomIPAddress() ip := net.ParseIP(ipRand) - assert.NotNil(t, ip, constants.ShouldNotNil) + assert.NotNil(t, ip, consts.ShouldNotNil) } } func TestValidateEmails(t *testing.T) { err1 := ValidateEmails("f", "a") - assert.Error(t, err1, constants.ShouldErr) + assert.Error(t, err1, consts.ShouldErr) err2 := ValidateEmails("validemail098@gmail.com") assert.NoError(t, err2, "should not error") err3 := ValidateEmails("validemail0911@gmail.com", "invalidemail0987@.gmail.com") - assert.Error(t, err3, constants.ShouldErr) + assert.Error(t, err3, consts.ShouldErr) err4 := ValidateEmails("validemail0987@gmail.com", "valid_email0987@gmail.com", "invalidemail0987@gmail.com.") - assert.Error(t, err4, constants.ShouldErr) + assert.Error(t, err4, consts.ShouldErr) } func TestNewFiberCtx(t *testing.T) { c := NewFiberCtx() - assert.NotNil(t, c, constants.ShouldNotNil) + assert.NotNil(t, c, consts.ShouldNotNil) } func TestToTitle(t *testing.T) { diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 323d52a..c57812c 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -2,7 +2,6 @@ package middleware import ( "crypto/rsa" - "errors" "fmt" "log" "strings" @@ -28,11 +27,11 @@ type JWTHandler struct { // Claims struct will be generated as token,contains // user data like ID, email, role and permissions. +// You can add new field if you want. type Claims struct { - ID int `json:"id"` - Email string `json:"email"` - Role string `json:"role"` - Permissions map[int]int `json:"permissions"` + ID int `json:"id"` + Email string `json:"email"` + Role string `json:"role"` jwt.RegisteredClaims } @@ -67,16 +66,12 @@ func NewJWTHandler() *JWTHandler { } // GenerateJWT func generate new token with expire time for user -func (j *JWTHandler) GenerateJWT(id int, email, role string, permissions map[int]int, expired time.Time) (t string, err error) { - if email == "" || role == "" || len(permissions) < 1 { - return "", errors.New("email/ role/ permission too short or void") - } +func (j *JWTHandler) GenerateJWT(id int, email, role string, expired time.Time) (t string, err error) { // Create Claims claims := Claims{ - ID: id, - Email: email, - Role: role, - Permissions: permissions, + ID: id, + Email: email, + Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: &jwt.NumericDate{Time: expired}, NotBefore: &jwt.NumericDate{Time: time.Now()}, @@ -211,25 +206,6 @@ func CheckHasPermission(requirePermID int, userPermissions map[int]int) bool { return true } -// HasPermission func extracts and checks for claims from fiber Ctx -func (j JWTHandler) HasPermission(c *fiber.Ctx, endpointPermID int) error { - claims, ok := c.Locals("claims").(*Claims) - if !ok { - return response.Unauthorized(c) - } - userPermissions := claims.Permissions - endpointBits := BuildBitGroups(endpointPermID) - // it seems O(n), but it's actually O(1) - // because length of $endpointBits is 1 - for key, requiredBits := range endpointBits { - userBits, ok := userPermissions[key] - if !ok || requiredBits&userBits == 0 { - return response.Unauthorized(c) - } - } - return c.Next() -} - // HasRole func check claims-role equal or not with require role func (j JWTHandler) HasRole(c *fiber.Ctx, role string) error { claims, ok := c.Locals("claims").(*Claims) @@ -239,14 +215,6 @@ func (j JWTHandler) HasRole(c *fiber.Ctx, role string) error { return c.Next() } -// CheckHasPermission func is handler/middleware that -// called before the controller for checks the fiber ctx -func (j JWTHandler) CheckHasPermission(endpointPermID int) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - return j.HasPermission(c, endpointPermID) - } -} - // CheckHasRole func is handler/middleware that // called before the controller for checks the fiber ctx func (j JWTHandler) CheckHasRole(role string) func(c *fiber.Ctx) error { diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index 0e51b7d..e73e0aa 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/Lukmanern/gost/internal/constants" + "github.com/Lukmanern/gost/internal/consts" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" "github.com/gofiber/fiber/v2" @@ -63,7 +63,7 @@ func TestNewJWTHandler(t *testing.T) { func TestGenerateClaims(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(1, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(1, params.Email, params.Role, params.Exp) if err != nil || token == "" { t.Fatal("should not error") } @@ -95,7 +95,7 @@ func TestGenerateClaims(t *testing.T) { func TestJWTHandlerInvalidateToken(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("error while generating token") } @@ -119,7 +119,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { jwtHandler := NewJWTHandler() cookie, err := jwtHandler.GenerateJWT(1000, helper.RandomEmail(), "example-role", - params.Per, time.Now().Add(1*time.Hour)) + time.Now().Add(1*time.Hour)) if err != nil { t.Error("generate cookie/token should not error") } @@ -151,7 +151,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { func TestJWTHandlerIsAuthenticated(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("error while generating token") } @@ -187,26 +187,9 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { }() } -func TestJWTHandlerHasPermission(t *testing.T) { - jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) - if err != nil { - t.Error("Error while generating token:", err) - } - if token == "" { - t.Error("Error: Token is empty") - } - c := helper.NewFiberCtx() - c.Request().Header.Add(fiber.HeaderAuthorization, "Bearer "+token) - jwtHandler.HasPermission(c, 25) - if c.Response().Header.StatusCode() != fiber.StatusUnauthorized { - t.Error("Should authorized") - } -} - func TestJWTHandlerHasRole(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("Error while generating token:", err) } @@ -217,29 +200,24 @@ func TestJWTHandlerHasRole(t *testing.T) { c.Request().Header.Add(fiber.HeaderAuthorization, "Bearer "+token) jwtHandler.HasRole(c, "test-role") if c.Response().Header.StatusCode() != fiber.StatusUnauthorized { - t.Error(constants.Unauthorized) + t.Error(consts.Unauthorized) } } func TestJWTHandlerCheckHasPermission(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("Error while generating token:", err) } if token == "" { t.Error("Error: Token is empty") } - - err2 := jwtHandler.CheckHasPermission(9999) - if err2 == nil { - t.Error(constants.Unauthorized) - } } func TestJWTHandlerCheckHasRole(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) if err != nil { t.Error("Error while generating token:", err) } @@ -247,9 +225,9 @@ func TestJWTHandlerCheckHasRole(t *testing.T) { t.Error("Error: Token is empty") } - err2 := jwtHandler.CheckHasRole("permission-1") - if err2 == nil { - t.Error(constants.Unauthorized) + checkErr := jwtHandler.CheckHasRole("permission-1") + if checkErr == nil { + t.Error(consts.Unauthorized) } } diff --git a/internal/response/response.go b/internal/response/response.go index eb7447d..826525a 100644 --- a/internal/response/response.go +++ b/internal/response/response.go @@ -3,23 +3,26 @@ package response import ( "strings" + "github.com/Lukmanern/gost/internal/consts" "github.com/gofiber/fiber/v2" ) -// Response struct is standart JSON structure that this project used. -// You can change if you want. This also prevents developers make -// common mistake to make messsage to frontend developer. type Response struct { Message string `json:"message"` Success bool `json:"success"` Data interface{} `json:"data"` } -const ( - MessageSuccessCreated = "data successfully created" - MessageSuccessLoaded = "data successfully loaded" - MessageUnauthorized = "unauthorized" -) +// CreateResponse generates a new response +// with the given parameters. +func CreateResponse(c *fiber.Ctx, statusCode int, response Response) error { + c.Status(statusCode) + return c.JSON(Response{ + Message: strings.ToLower(response.Message), + Success: response.Success, + Data: response.Data, + }) +} // SuccessNoContent formats a successful // response with HTTP status 204. @@ -28,56 +31,72 @@ func SuccessNoContent(c *fiber.Ctx) error { return c.Send(nil) } -// CreateResponse generates a new response -// with the given parameters. -func CreateResponse(c *fiber.Ctx, statusCode int, success bool, message string, data interface{}) error { - c.Status(statusCode) - return c.JSON(Response{ - Message: strings.ToLower(message), - Success: success, - Data: data, - }) -} - // SuccessLoaded formats a successful response // with HTTP status 200 and the provided data. func SuccessLoaded(c *fiber.Ctx, data interface{}) error { - return CreateResponse(c, fiber.StatusOK, true, MessageSuccessLoaded, data) + return CreateResponse(c, fiber.StatusOK, Response{ + Message: strings.ToLower(consts.SuccessLoaded), + Success: true, + Data: data, + }) } // SuccessCreated formats a successful response // with HTTP status 201 and the provided data. func SuccessCreated(c *fiber.Ctx, data interface{}) error { - return CreateResponse(c, fiber.StatusCreated, true, MessageSuccessCreated, data) + return CreateResponse(c, fiber.StatusCreated, Response{ + Message: strings.ToLower(consts.SuccessCreated), + Success: true, + Data: data, + }) } -// BadRequest formats a response with HTTP -// status 400 and the specified message. -func BadRequest(c *fiber.Ctx, message string) error { - return CreateResponse(c, fiber.StatusBadRequest, false, message, nil) +// BadRequest formats a response with HTTP status 400. +func BadRequest(c *fiber.Ctx) error { + return CreateResponse(c, fiber.StatusBadRequest, Response{ + Message: consts.BadRequest, + Success: false, + Data: nil, + }) } // Unauthorized formats a response with // HTTP status 401 indicating unauthorized access. func Unauthorized(c *fiber.Ctx) error { - return CreateResponse(c, fiber.StatusUnauthorized, false, MessageUnauthorized, nil) + return CreateResponse(c, fiber.StatusUnauthorized, Response{ + Message: consts.Unauthorized, + Success: false, + Data: nil, + }) } // DataNotFound formats a response with // HTTP status 404 and the specified message. -func DataNotFound(c *fiber.Ctx, message string) error { - return CreateResponse(c, fiber.StatusNotFound, false, message, nil) +func DataNotFound(c *fiber.Ctx) error { + return CreateResponse(c, fiber.StatusNotFound, Response{ + Message: consts.NotFound, + Success: false, + Data: nil, + }) } // Error formats an error response // with HTTP status 500 and the specified message. func Error(c *fiber.Ctx, message string) error { - return CreateResponse(c, fiber.StatusInternalServerError, false, message, nil) + return CreateResponse(c, fiber.StatusInternalServerError, Response{ + Message: message, + Success: false, + Data: nil, + }) } // ErrorWithData formats an error response // with HTTP status 500 and the specified // message and data. func ErrorWithData(c *fiber.Ctx, message string, data interface{}) error { - return CreateResponse(c, fiber.StatusInternalServerError, false, message, data) + return CreateResponse(c, fiber.StatusInternalServerError, Response{ + Message: message, + Success: false, + Data: data, + }) } diff --git a/internal/response/response_test.go b/internal/response/response_test.go index dc4700a..4679d99 100644 --- a/internal/response/response_test.go +++ b/internal/response/response_test.go @@ -71,7 +71,11 @@ func TestCreateResponse(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := CreateResponse(tt.args.c, tt.args.statusCode, tt.args.success, tt.args.message, tt.args.data); (err != nil) != tt.wantErr { + if err := CreateResponse(tt.args.c, tt.args.statusCode, Response{ + Message: tt.args.message, + Success: tt.args.success, + Data: tt.args.data, + }); (err != nil) != tt.wantErr { t.Errorf("CreateResponse() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -181,7 +185,7 @@ func TestBadRequest(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := BadRequest(tt.args.c, tt.args.message); (err != nil) != tt.wantErr { + if err := BadRequest(tt.args.c); (err != nil) != tt.wantErr { t.Errorf("BadRequest() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -244,7 +248,7 @@ func TestDataNotFound(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := DataNotFound(tt.args.c, tt.args.message); (err != nil) != tt.wantErr { + if err := DataNotFound(tt.args.c); (err != nil) != tt.wantErr { t.Errorf("DataNotFound() error = %v, wantErr %v", err, tt.wantErr) } }) From 8bcdb99d8410ff0a9e0b896b4fc36e846f64f4bc Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Tue, 23 Jan 2024 18:23:09 +0700 Subject: [PATCH 04/28] update middleware --- database/migration/main.go | 8 +- domain/entity/user.go | 4 +- internal/consts/consts.go | 24 ++--- internal/middleware/middleware.go | 69 ++++--------- internal/middleware/middleware_test.go | 128 ++----------------------- 5 files changed, 43 insertions(+), 190 deletions(-) diff --git a/database/migration/main.go b/database/migration/main.go index b8de018..220e377 100644 --- a/database/migration/main.go +++ b/database/migration/main.go @@ -63,7 +63,7 @@ func main() { } } - // Seed master-RBAC data (roles and permissions) + // Seed Roles if !config.GetAppInProduction() { seeding() } @@ -80,9 +80,7 @@ func dropAll() { } } -// seeding func seed data like role, permission, -// and role_has_permissions tables. You can add -// more seed if you want. +// seeding roles func seeding() { // Create a new transaction for seeding tx := db.Begin() @@ -90,7 +88,7 @@ func seeding() { log.Panicf("Error starting transaction for seeding: %s", tx.Error) } - // Seeding permission and role + // Seeding role for _, data := range role.AllRoles() { data.SetCreateTime() if createErr := tx.Create(&data).Error; createErr != nil { diff --git a/domain/entity/user.go b/domain/entity/user.go index 31052a5..2c49b85 100644 --- a/domain/entity/user.go +++ b/domain/entity/user.go @@ -22,9 +22,9 @@ func (e *User) TableName() string { } type UserHasRoles struct { - UserID int `json:"role_id"` + UserID int `json:"user_id"` User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` - RoleID int `json:"permission_id"` + RoleID int `json:"role_id"` Role Role `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"role"` } diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 9ad102a..de5f6c8 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -10,22 +10,18 @@ const ( const ( InvalidJSONBody = "invalid JSON body" - InvalidUserID = "invalid user ID" InvalidID = "invalid ID" - - RedisNil = "redis nil value" - - ErrGetIDFromJWT = "error while getting user ID from JWT-Claims" - ErrHashing = "error while hashing password, please try again" + NilValue = "error nil value" + InvalidToken = "invalid token / JWT, please logout and try-login" ) const ( - ShouldErr = "should error" - ShouldNotErr = "should not error" - ShouldNil = "should nil" - ShouldNotNil = "should not nil" - ShouldEqual = "should equal" - ShouldNotEqual = "should not equal" - - LoginShouldSuccess = "login should success" + ShouldErr = "should error" + ShouldNotErr = "should not error" + ShouldNil = "should nil" + ShouldNotNil = "should not nil" + ShouldEqual = "should equal" + ShouldNotEqual = "should not equal" + ShouldSuccess = "should success" + ShouldNotSuccess = "should not success" ) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index c57812c..1f225f9 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -26,12 +26,12 @@ type JWTHandler struct { } // Claims struct will be generated as token,contains -// user data like ID, email, role and permissions. -// You can add new field if you want. +// user data like ID, email and roles. +// You can add new field/property if you want. type Claims struct { - ID int `json:"id"` - Email string `json:"email"` - Role string `json:"role"` + ID int `json:"id"` + Email string `json:"email"` + Roles map[string]uint8 `json:"roles"` jwt.RegisteredClaims } @@ -66,12 +66,12 @@ func NewJWTHandler() *JWTHandler { } // GenerateJWT func generate new token with expire time for user -func (j *JWTHandler) GenerateJWT(id int, email, role string, expired time.Time) (t string, err error) { +func (j *JWTHandler) GenerateJWT(id int, email string, roles map[string]uint8, expired time.Time) (t string, err error) { // Create Claims claims := Claims{ ID: id, Email: email, - Role: role, + Roles: roles, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: &jwt.NumericDate{Time: expired}, NotBefore: &jwt.NumericDate{Time: time.Now()}, @@ -173,52 +173,19 @@ func (j JWTHandler) GenerateClaims(cookieToken string) *Claims { return &claims } -// BuildBitGroups func builds bit-group that can contains -// so much permissions data inside with fast and effective -// with bit manipulations. See the example : -// permissions = {9:10,10:256} -// => read as : bit-group-9th, contains 2 permissions -// => read as : bit-group-10th, contains 8 permissions -// per group contain max 8 permissions sequentially, -// for more You can read in paper (for link, see in readme-md) -func BuildBitGroups(permIDs ...int) map[int]int { - groups := make(map[int]int) - for _, id := range permIDs { - group := (id - 1) / 8 - bitPosition := uint(id - 1 - (group * 8)) - groups[group+1] |= 1 << bitPosition - } - return groups -} - -// CheckHasPermission func checks if bitGroups (map[int]int) -// contains require permission ID or not -func CheckHasPermission(requirePermID int, userPermissions map[int]int) bool { - endpointBits := BuildBitGroups(requirePermID) - // it seems O(n), but it's actually O(1) - // because length of $endpointBits is 1 - for key, requiredBits := range endpointBits { - userBits, ok := userPermissions[key] - if !ok || requiredBits&userBits == 0 { - return false - } - } - return true -} - -// HasRole func check claims-role equal or not with require role -func (j JWTHandler) HasRole(c *fiber.Ctx, role string) error { - claims, ok := c.Locals("claims").(*Claims) - if !ok || role != claims.Role { - return response.Unauthorized(c) - } - return c.Next() -} - // CheckHasRole func is handler/middleware that // called before the controller for checks the fiber ctx -func (j JWTHandler) CheckHasRole(role string) func(c *fiber.Ctx) error { +func (j JWTHandler) HasRole(roles ...string) func(c *fiber.Ctx) error { + if len(roles) < 1 { + return response.Unauthorized + } return func(c *fiber.Ctx) error { - return j.HasRole(c, role) + claims, ok := c.Locals("claims").(*Claims) + for _, role := range roles { + if !ok || claims.Roles[role] != 1 { + return response.Unauthorized(c) + } + } + return c.Next() } } diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index e73e0aa..d92d0bc 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -1,9 +1,6 @@ package middleware import ( - "fmt" - "math" - "reflect" "testing" "time" @@ -16,8 +13,7 @@ import ( type GenTokenParams struct { ID int Email string - Role string - Per map[int]int + Roles map[string]uint8 Exp time.Time wantErr bool } @@ -32,19 +28,9 @@ func init() { timeNow := time.Now() params = GenTokenParams{ - ID: 1, - Email: helper.RandomEmail(), - Role: "test-role", - Per: map[int]int{ - 1: 1, - 2: 1, - 3: 1, - 4: 1, - 5: 1, - 6: 1, - 7: 1, - 8: 1, - }, + ID: 1, + Email: helper.RandomEmail(), + Roles: map[string]uint8{"test-role": 1}, Exp: timeNow.Add(5 * time.Minute), wantErr: false, } @@ -63,7 +49,7 @@ func TestNewJWTHandler(t *testing.T) { func TestGenerateClaims(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(1, params.Email, params.Role, params.Exp) + token, err := jwtHandler.GenerateJWT(1, params.Email, params.Roles, params.Exp) if err != nil || token == "" { t.Fatal("should not error") } @@ -95,7 +81,7 @@ func TestGenerateClaims(t *testing.T) { func TestJWTHandlerInvalidateToken(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("error while generating token") } @@ -118,7 +104,7 @@ func TestJWTHandlerInvalidateToken(t *testing.T) { func TestJWTHandlerIsBlacklisted(t *testing.T) { jwtHandler := NewJWTHandler() cookie, err := jwtHandler.GenerateJWT(1000, - helper.RandomEmail(), "example-role", + helper.RandomEmail(), params.Roles, time.Now().Add(1*time.Hour)) if err != nil { t.Error("generate cookie/token should not error") @@ -151,7 +137,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { func TestJWTHandlerIsAuthenticated(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("error while generating token") } @@ -187,37 +173,9 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { }() } -func TestJWTHandlerHasRole(t *testing.T) { - jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) - if err != nil { - t.Error("Error while generating token:", err) - } - if token == "" { - t.Error("Error: Token is empty") - } - c := helper.NewFiberCtx() - c.Request().Header.Add(fiber.HeaderAuthorization, "Bearer "+token) - jwtHandler.HasRole(c, "test-role") - if c.Response().Header.StatusCode() != fiber.StatusUnauthorized { - t.Error(consts.Unauthorized) - } -} - -func TestJWTHandlerCheckHasPermission(t *testing.T) { - jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) - if err != nil { - t.Error("Error while generating token:", err) - } - if token == "" { - t.Error("Error: Token is empty") - } -} - func TestJWTHandlerCheckHasRole(t *testing.T) { jwtHandler := NewJWTHandler() - token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Exp) + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("Error while generating token:", err) } @@ -225,74 +183,8 @@ func TestJWTHandlerCheckHasRole(t *testing.T) { t.Error("Error: Token is empty") } - checkErr := jwtHandler.CheckHasRole("permission-1") + checkErr := jwtHandler.HasRole("role-x-1") if checkErr == nil { t.Error(consts.Unauthorized) } } - -func TestPermissionBitGroup(t *testing.T) { - d := 8 - testCases := []struct { - input int - result map[int]int - }{ - { - input: d, - result: map[int]int{ - 1: int(math.Pow(2, 7)), - }, - }, - { - input: 10 * d, - result: map[int]int{ - 10: int(math.Pow(2, 7)), - }, - }, - { - input: d + 7, - result: map[int]int{ - 2: int(math.Pow(2, 6)), - }, - }, - { - input: d, - result: map[int]int{ - 1: int(math.Pow(2, 7)), - }, - }, - } - - for _, tc := range testCases { - result := BuildBitGroups(tc.input) - if !reflect.DeepEqual(result, tc.result) { - t.Error("should same, but got", result, "want", tc.result) - } - } - - permIDs := make([]int, 0) - for i := 1; i < 90; i++ { - if i%2 != 0 { - continue - } - permIDs = append(permIDs, i) - } - - result := BuildBitGroups(permIDs...) - for group, bits := range result { - fmt.Printf("%d : %08b\n", group, bits) - } -} - -func TestCheckHasPermission(t *testing.T) { - // user perms - permIDs := make([]int, 0) - for i := 1; i <= 19; i++ { - permIDs = append(permIDs, i) - } - - bitGroups := BuildBitGroups(permIDs...) - for i := 1; i <= 30; i++ { - fmt.Println(i, ":", CheckHasPermission(i, bitGroups)) - } -} From 313d2fc89496476a7c74049ee76d890fa7e844e6 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Wed, 24 Jan 2024 08:00:33 +0700 Subject: [PATCH 05/28] add repository --- repository/role/role_repository.go | 136 ++++++ repository/role/role_repository_test.go | 392 +++++++++++++++++ repository/user/user_repository.go | 182 ++++++++ repository/user/user_repository_test.go | 550 ++++++++++++++++++++++++ 4 files changed, 1260 insertions(+) create mode 100644 repository/role/role_repository.go create mode 100644 repository/role/role_repository_test.go create mode 100644 repository/user/user_repository.go create mode 100644 repository/user/user_repository_test.go diff --git a/repository/role/role_repository.go b/repository/role/role_repository.go new file mode 100644 index 0000000..485f505 --- /dev/null +++ b/repository/role/role_repository.go @@ -0,0 +1,136 @@ +package repository + +import ( + "context" + "sync" + + "github.com/Lukmanern/gost/database/connector" + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" + "gorm.io/gorm" +) + +type RoleRepository interface { + // Create adds a new role to the repository. + Create(ctx context.Context, role entity.Role) (id int, err error) + + // GetByID retrieves a role by its unique identifier. + GetByID(ctx context.Context, id int) (role *entity.Role, err error) + + // GetByName retrieves a role by its name. + GetByName(ctx context.Context, name string) (role *entity.Role, err error) + + // GetAll retrieves all roles based on a filter for pagination. + GetAll(ctx context.Context, filter model.RequestGetAll) (roles []entity.Role, total int, err error) + + // Update modifies role information in the repository. + Update(ctx context.Context, role entity.Role) (err error) + + // Delete removes a role from the repository by its ID. + Delete(ctx context.Context, id int) (err error) +} + +type RoleRepositoryImpl struct { + db *gorm.DB +} + +var ( + roleRepositoryImpl *RoleRepositoryImpl + roleRepositoryImplOnce sync.Once +) + +func NewRoleRepository() RoleRepository { + roleRepositoryImplOnce.Do(func() { + roleRepositoryImpl = &RoleRepositoryImpl{ + db: connector.LoadDatabase(), + } + }) + return roleRepositoryImpl +} + +func (repo *RoleRepositoryImpl) Create(ctx context.Context, role entity.Role) (id int, err error) { + err = repo.db.Transaction(func(tx *gorm.DB) error { + res := tx.Create(&role) + if res.Error != nil { + tx.Rollback() + return res.Error + } + id = role.ID + return nil + }) + if err != nil { + return 0, err + } + + return id, nil +} + +func (repo *RoleRepositoryImpl) GetByID(ctx context.Context, id int) (role *entity.Role, err error) { + role = &entity.Role{} + result := repo.db.Where("id = ?", id).First(&role) + if result.Error != nil { + return nil, result.Error + } + return role, nil +} + +func (repo *RoleRepositoryImpl) GetByName(ctx context.Context, name string) (role *entity.Role, err error) { + role = &entity.Role{} + result := repo.db.Where("name = ?", name).First(&role) + if result.Error != nil { + return nil, result.Error + } + return role, nil +} + +func (repo *RoleRepositoryImpl) GetAll(ctx context.Context, filter model.RequestGetAll) (roles []entity.Role, total int, err error) { + var count int64 + args := []interface{}{"%" + filter.Keyword + "%"} + cond := "name LIKE ?" + result := repo.db.Where(cond, args...).Find(&roles) + count = result.RowsAffected + if result.Error != nil { + return nil, 0, result.Error + } + roles = []entity.Role{} + skip := int64(filter.Limit * (filter.Page - 1)) + limit := int64(filter.Limit) + result = repo.db.Where(cond, args...).Limit(int(limit)).Offset(int(skip)).Find(&roles) + if result.Error != nil { + return nil, 0, result.Error + } + total = int(count) + return roles, total, nil +} + +func (repo *RoleRepositoryImpl) Update(ctx context.Context, role entity.Role) (err error) { + err = repo.db.Transaction(func(tx *gorm.DB) error { + var oldData entity.Role + result := tx.Where("id = ?", role.ID).First(&oldData) + if result.Error != nil { + tx.Rollback() + return result.Error + } + + oldData.Name = role.Name + oldData.Description = role.Description + oldData.UpdatedAt = role.UpdatedAt + result = tx.Save(&oldData) + if result.Error != nil { + tx.Rollback() + return result.Error + } + return nil + }) + + return err +} + +func (repo *RoleRepositoryImpl) Delete(ctx context.Context, id int) (err error) { + deleted := entity.Role{} + result := repo.db.Where("id = ?", id).Delete(&deleted) + if result.Error != nil { + return result.Error + } + return nil +} diff --git a/repository/role/role_repository_test.go b/repository/role/role_repository_test.go new file mode 100644 index 0000000..20cc90f --- /dev/null +++ b/repository/role/role_repository_test.go @@ -0,0 +1,392 @@ +package repository + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/Lukmanern/gost/database/connector" + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/env" +) + +var ( + roleRepoImpl RoleRepositoryImpl + timeNow time.Time + ctx context.Context +) + +func init() { + filePath := "./../../.env" + env.ReadConfig(filePath) + + timeNow = time.Now() + ctx = context.Background() + + roleRepoImpl = RoleRepositoryImpl{ + db: connector.LoadDatabase(), + } +} + +func createOneRole(t *testing.T, namePrefix string) *entity.Role { + role := entity.Role{ + Name: "valid-role-name-" + namePrefix, + Description: "valid-role-description-" + namePrefix, + TimeFields: entity.TimeFields{ + CreatedAt: &timeNow, + UpdatedAt: &timeNow, + }, + } + id, createErr := roleRepoImpl.Create(ctx, role) + if createErr != nil { + t.Error("error while creating role : ", createErr.Error()) + } + role.ID = id + return &role +} + +func TestNewRoleRepository(t *testing.T) { + roleRepo := NewRoleRepository() + if roleRepo == nil { + t.Error("should not nil") + } +} + +func TestCreate(t *testing.T) { + role := createOneRole(t, "create-same-name") + if role == nil { + t.Error("failed creating role : role is nil") + } + defer func() { + roleRepoImpl.Delete(ctx, role.ID) + }() + + type args struct { + ctx context.Context + role entity.Role + } + tests := []struct { + name string + repo RoleRepositoryImpl + args args + wantErr bool + wantPanic bool + }{ + { + name: "error while creating with the same name", + wantErr: true, + args: args{ + ctx: ctx, + role: entity.Role{ + Name: role.Name, + Description: "", + TimeFields: entity.TimeFields{ + CreatedAt: &timeNow, + UpdatedAt: &timeNow, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r == nil && tt.wantPanic { + t.Errorf("create() do not panic") + } + }() + gotID, err := tt.repo.Create(tt.args.ctx, tt.args.role) + if (err != nil) != tt.wantErr { + t.Errorf("RoleRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotID <= 0 { + t.Errorf("ID should be positive") + } + }) + } +} + +func TestGetByID(t *testing.T) { + role := createOneRole(t, "TestGetByID") + if role == nil { + t.Error("failed creating role : role is nil") + } + defer func() { + roleRepoImpl.Delete(ctx, role.ID) + }() + + type args struct { + ctx context.Context + id int + } + tests := []struct { + name string + repo RoleRepositoryImpl + args args + wantErr bool + }{ + { + name: "success get role", + repo: roleRepoImpl, + args: args{ + ctx: ctx, + id: role.ID, + }, + wantErr: false, + }, + { + name: "failed get role: invalid id", + repo: roleRepoImpl, + args: args{ + ctx: ctx, + id: -10, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRole, err := tt.repo.GetByID(tt.args.ctx, tt.args.id) + if (err != nil) != tt.wantErr { + t.Errorf("RoleRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && gotRole == nil { + t.Error("role should not nil") + } + }) + } +} + +func TestGetByName(t *testing.T) { + role := createOneRole(t, "TestGetByName") + if role == nil { + t.Error("failed creating role : role is nil") + } + defer func() { + roleRepoImpl.Delete(ctx, role.ID) + }() + + type args struct { + ctx context.Context + name string + } + tests := []struct { + name string + repo RoleRepositoryImpl + args args + wantErr bool + }{ + { + name: "success get role by valid id", + repo: roleRepoImpl, + args: args{ + ctx: ctx, + name: role.Name, + }, + wantErr: false, + }, + { + name: "failed get role by invalid id", + repo: roleRepoImpl, + args: args{ + ctx: ctx, + name: "unknown name", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRole, err := tt.repo.GetByName(tt.args.ctx, tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("RoleRepositoryImpl.GetByName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && gotRole == nil { + t.Error("role should not nil") + } + }) + } +} + +func TestGetAll(t *testing.T) { + roles := make([]entity.Role, 0) + for i := 0; i < 10; i++ { + role := createOneRole(t, "TestGetAll"+strconv.Itoa(i)) + if role == nil { + continue + } + defer func() { + roleRepoImpl.Delete(ctx, role.ID) + }() + + roles = append(roles, *role) + } + lenRoles := len(roles) + type args struct { + ctx context.Context + filter model.RequestGetAll + } + tests := []struct { + name string + repo RoleRepositoryImpl + args args + wantErr bool + }{ + { + name: "success get all", + repo: roleRepoImpl, + args: args{ + ctx: ctx, + filter: model.RequestGetAll{ + Limit: 1000, + Page: 1, + }, + }, + wantErr: false, + }, + { + name: "success get all", + repo: roleRepoImpl, + args: args{ + ctx: ctx, + filter: model.RequestGetAll{ + Limit: 1, + Page: 1, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRoles, gotTotal, err := tt.repo.GetAll(tt.args.ctx, tt.args.filter) + if (err != nil) != tt.wantErr { + t.Errorf("RoleRepositoryImpl.GetAll() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.args.filter.Limit > lenRoles && len(gotRoles) < lenRoles { + t.Error("role should be $lenRoles or more") + } + if tt.args.filter.Limit > lenRoles && gotTotal < lenRoles { + t.Error("total role should be $lenRoles or more") + } + if tt.args.filter.Limit < lenRoles && len(gotRoles) > lenRoles { + t.Error("role should be less than $lenRoles") + } + }) + } +} + +func TestUpdate(t *testing.T) { + role := createOneRole(t, "TestUpdateByID") + if role == nil { + t.Error("failed creating role : role is nil") + } + defer func() { + roleRepoImpl.Delete(ctx, role.ID) + }() + + type args struct { + ctx context.Context + role entity.Role + } + tests := []struct { + name string + repo RoleRepositoryImpl + args args + wantErr bool + }{ + { + name: "success update name and desc", + repo: roleRepoImpl, + wantErr: false, + args: args{ + ctx: ctx, + role: entity.Role{ + ID: role.ID, + Name: "updated name", + Description: "updated description", + }, + }, + }, + { + name: "failed update name and desc with invalid id", + repo: roleRepoImpl, + wantErr: true, + args: args{ + ctx: ctx, + role: entity.Role{ + ID: -10, + Name: "updated name", + Description: "updated description", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.repo.Update(tt.args.ctx, tt.args.role); (err != nil) != tt.wantErr { + t.Errorf("RoleRepositoryImpl.Update() error = %v, wantErr %v", err, tt.wantErr) + } + + p, err := tt.repo.GetByID(tt.args.ctx, role.ID) + if err != nil { + t.Error("error while getting role") + } + if p.Name != tt.args.role.Name || p.Description != tt.args.role.Description { + t.Error("name and description failed to update") + } + }) + } +} + +func TestDelete(t *testing.T) { + role := createOneRole(t, "TestDeleteByID") + if role == nil { + t.Error("failed creating role : role is nil") + } + defer func() { + roleRepoImpl.Delete(ctx, role.ID) + }() + + type args struct { + ctx context.Context + id int + } + tests := []struct { + name string + repo RoleRepositoryImpl + args args + wantErr bool + }{ + { + name: "success update role", + repo: roleRepoImpl, + wantErr: false, + args: args{ + ctx: ctx, + id: role.ID, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.repo.Delete(tt.args.ctx, tt.args.id); (err != nil) != tt.wantErr { + t.Errorf("RoleRepositoryImpl.Delete() error = %v, wantErr %v", err, tt.wantErr) + } + + role, err := tt.repo.GetByID(tt.args.ctx, tt.args.id) + if !tt.wantErr && err == nil { + t.Error("should error") + } + if !tt.wantErr && role != nil { + t.Error("role should nil") + } + }) + } +} diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go new file mode 100644 index 0000000..bdfa812 --- /dev/null +++ b/repository/user/user_repository.go @@ -0,0 +1,182 @@ +// used by user auth service + +package repository + +import ( + "context" + "sync" + + "gorm.io/gorm" + + "github.com/Lukmanern/gost/database/connector" + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" +) + +type UserRepository interface { + // Create adds a new user to the repository with a specified role. + Create(ctx context.Context, user entity.User, roleID int) (id int, err error) + + // GetByID retrieves a user by their unique identifier. + GetByID(ctx context.Context, id int) (user *entity.User, err error) + + // GetByEmail retrieves a user by their email address. + GetByEmail(ctx context.Context, email string) (user *entity.User, err error) + + // GetByConditions retrieves a user based on specified conditions. + GetByConditions(ctx context.Context, conds map[string]any) (user *entity.User, err error) + + // GetAll retrieves all users based on a filter for pagination. + GetAll(ctx context.Context, filter model.RequestGetAll) (users []entity.User, total int, err error) + + // Update modifies user information in the repository. + Update(ctx context.Context, user entity.User) (err error) + + // Delete removes a user from the repository by their ID. + Delete(ctx context.Context, id int) (err error) + + // UpdatePassword updates a user's password in the repository. + UpdatePassword(ctx context.Context, id int, passwordHashed string) (err error) +} + +type UserRepositoryImpl struct { + db *gorm.DB +} + +var ( + userRepositoryImpl *UserRepositoryImpl + userRepositoryImplOnce sync.Once +) + +func NewUserRepository() UserRepository { + userRepositoryImplOnce.Do(func() { + userRepositoryImpl = &UserRepositoryImpl{ + db: connector.LoadDatabase(), + } + }) + return userRepositoryImpl +} + +func (repo *UserRepositoryImpl) Create(ctx context.Context, user entity.User, roleID int) (id int, err error) { + err = repo.db.Transaction(func(tx *gorm.DB) error { + if res := tx.Create(&user); res.Error != nil { + tx.Rollback() + return res.Error + } + id = user.ID + + if res := tx.Create(&entity.UserHasRoles{ + UserID: id, + RoleID: roleID, + }); res.Error != nil { + tx.Rollback() + return res.Error + } + return nil + }) + if err != nil { + return 0, err + } + return id, nil +} + +func (repo *UserRepositoryImpl) GetByID(ctx context.Context, id int) (user *entity.User, err error) { + user = &entity.User{} + result := repo.db.Where("id = ?", id).Preload("Roles").First(&user) + if result.Error != nil { + return nil, result.Error + } + return user, nil +} + +func (repo *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (user *entity.User, err error) { + user = &entity.User{} + result := repo.db.Where("email = ?", email).Preload("Roles").First(&user) + if result.Error != nil { + return nil, result.Error + } + return user, nil +} + +func (repo *UserRepositoryImpl) GetByConditions(ctx context.Context, conds map[string]any) (user *entity.User, err error) { + // this func is easy-contain-vunarable by default + user = &entity.User{} + query := repo.db + for con, val := range conds { + query = query.Where(con+" ?", val) + } + result := query.Preload("Roles").First(&user) + if result.Error != nil { + return nil, result.Error + } + return user, nil +} + +func (repo *UserRepositoryImpl) GetAll(ctx context.Context, filter model.RequestGetAll) (users []entity.User, total int, err error) { + var count int64 + args := []interface{}{"%" + filter.Keyword + "%"} + cond := "name LIKE ?" + result := repo.db.Where(cond, args...).Find(&users) + count = result.RowsAffected + if result.Error != nil { + return nil, 0, result.Error + } + users = []entity.User{} + skip := int64(filter.Limit * (filter.Page - 1)) + limit := int64(filter.Limit) + result = repo.db.Where(cond, args...).Limit(int(limit)).Offset(int(skip)).Find(&users) + if result.Error != nil { + return nil, 0, result.Error + } + total = int(count) + return users, total, nil +} + +func (repo *UserRepositoryImpl) Update(ctx context.Context, user entity.User) (err error) { + err = repo.db.Transaction(func(tx *gorm.DB) error { + var oldData entity.User + result := tx.Where("id = ?", user.ID).First(&oldData) + if result.Error != nil { + return result.Error + } + + oldData.Name = user.Name + oldData.ActivatedAt = user.ActivatedAt + oldData.UpdatedAt = user.UpdatedAt + result = tx.Save(&oldData) + if result.Error != nil { + return result.Error + } + return nil + }) + + return err +} + +func (repo *UserRepositoryImpl) Delete(ctx context.Context, id int) (err error) { + deleted := entity.User{} + result := repo.db.Where("id = ?", id).Delete(&deleted) + if result.Error != nil { + return result.Error + } + return nil +} + +func (repo *UserRepositoryImpl) UpdatePassword(ctx context.Context, id int, passwordHashed string) (err error) { + err = repo.db.Transaction(func(tx *gorm.DB) error { + var user entity.User + result := tx.Where("id = ?", id).First(&user) + if result.Error != nil { + return result.Error + } + user.Password = passwordHashed + user.SetUpdateTime() + result = tx.Save(&user) + if result.Error != nil { + return result.Error + } + return nil + }) + + return err +} diff --git a/repository/user/user_repository_test.go b/repository/user/user_repository_test.go new file mode 100644 index 0000000..730e7b6 --- /dev/null +++ b/repository/user/user_repository_test.go @@ -0,0 +1,550 @@ +package repository + +import ( + "context" + "testing" + "time" + + "github.com/Lukmanern/gost/database/connector" + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/env" + "github.com/Lukmanern/gost/internal/helper" +) + +var ( + timeNow time.Time + ctx context.Context +) + +func init() { + filePath := "./../../.env" + env.ReadConfig(filePath) + timeNow = time.Now() + ctx = context.Background() +} + +func TestNewUserRepository(t *testing.T) { + userRepository := NewUserRepository() + if userRepository == nil { + t.Error("should not nil") + } +} + +func TestUserRepositoryImplCreate(t *testing.T) { + userRepositoryImpl := UserRepositoryImpl{ + db: connector.LoadDatabase(), + } + + type args struct { + ctx context.Context + user entity.User + } + tests := []struct { + name string + repo UserRepositoryImpl + wantErr bool + wantPanic bool + args args + }{ + { + name: "success create new user", + repo: userRepositoryImpl, + wantErr: false, + wantPanic: false, + args: args{ + ctx: context.Background(), + user: entity.User{ + Name: "validname", + Email: "valid1@email.com", + Password: "example-password", + TimeFields: entity.TimeFields{ + CreatedAt: &timeNow, + UpdatedAt: &timeNow, + }, + }, + }, + }, + { + name: "success create new user with void data", + repo: userRepositoryImpl, + wantErr: false, + wantPanic: false, + }, + { + name: "failed create new user with void data and nil repository", + repo: UserRepositoryImpl{ + db: nil, + }, + wantErr: true, + wantPanic: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if !tt.wantPanic { + gotID, err := tt.repo.Create(tt.args.ctx, tt.args.user, 1) + if (err != nil) != tt.wantErr { + t.Errorf("UserRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) + return + } + gotID2, err2 := tt.repo.Create(tt.args.ctx, tt.args.user, 1) + if err2 == nil || gotID2 != 0 { + t.Error("should be error, couse email is already used") + } + tt.repo.Delete(tt.args.ctx, gotID) + + return + } + // want panic + defer func() { + if r := recover(); r == nil { + t.Errorf("create() do not panic") + } + }() + userID, err := tt.repo.Create(tt.args.ctx, tt.args.user, 1) + if (err != nil) != tt.wantErr { + t.Errorf("UserRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) + return + } + userRepositoryImpl.Delete(ctx, userID) + }) + } +} + +func TestUserRepositoryImplGetByID(t *testing.T) { + // create user + user := entity.User{ + Name: "validname", + Email: "valid2@email.com", + Password: "example-password", + TimeFields: entity.TimeFields{ + CreatedAt: &timeNow, + UpdatedAt: &timeNow, + }, + } + userRepositoryImpl := UserRepositoryImpl{ + db: connector.LoadDatabase(), + } + id, createErr := userRepositoryImpl.Create(ctx, user, 1) + if createErr != nil { + t.Errorf("error while creating user") + } + defer func() { + userRepositoryImpl.Delete(ctx, id) + }() + + type args struct { + ctx context.Context + id int + } + tests := []struct { + name string + repo UserRepositoryImpl + wantErr bool + wantUser bool + args args + }{ + { + name: "Success get user by id", + repo: userRepositoryImpl, + wantErr: false, + wantUser: true, + args: args{ + ctx: ctx, + id: id, + }, + }, + { + name: "Failed get user by negative id", + repo: userRepositoryImpl, + wantErr: true, + wantUser: false, + args: args{ + ctx: ctx, + id: -10, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotUser, err := tt.repo.GetByID(tt.args.ctx, tt.args.id) + if (err != nil) != tt.wantErr { + t.Errorf("UserRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantUser { + if gotUser == nil { + t.Error("error user shouldn't nil") + } + } + }) + } +} + +func TestUserRepositoryImplGetByEmail(t *testing.T) { + // create user + user := entity.User{ + Name: "validname", + Email: "valid3@email.com", + Password: "example-password", + TimeFields: entity.TimeFields{ + CreatedAt: &timeNow, + UpdatedAt: &timeNow, + }, + } + userRepositoryImpl := UserRepositoryImpl{ + db: connector.LoadDatabase(), + } + id, createErr := userRepositoryImpl.Create(ctx, user, 1) + if createErr != nil { + t.Errorf("error while creating user") + } + defer func() { + userRepositoryImpl.Delete(ctx, id) + }() + + type args struct { + ctx context.Context + email string + } + tests := []struct { + name string + repo UserRepositoryImpl + wantUser bool + wantErr bool + args args + }{ + { + name: "Success get user by valid email", + repo: userRepositoryImpl, + wantErr: false, + wantUser: true, + args: args{ + ctx: ctx, + email: user.Email, + }, + }, + { + name: "Failed get user by invalid-email", + repo: userRepositoryImpl, + wantErr: true, + wantUser: false, + args: args{ + ctx: ctx, + email: "invalid-email", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotUser, err := tt.repo.GetByEmail(tt.args.ctx, tt.args.email) + if (err != nil) != tt.wantErr { + t.Errorf("UserRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantUser { + if gotUser == nil { + t.Error("error user shouldn't nil") + } + } + }) + } +} + +func TestUserRepositoryImplGetAll(t *testing.T) { + // create user + allUsersID := make([]int, 0) + userRepositoryImpl := UserRepositoryImpl{ + db: connector.LoadDatabase(), + } + for _, id := range []string{"4", "5", "6", "7", "8"} { + user := entity.User{ + Name: "validname", + Email: "valid" + id + "@email.com", // email is unique + Password: "example-password", + TimeFields: entity.TimeFields{ + CreatedAt: &timeNow, + UpdatedAt: &timeNow, + }, + } + newUserID, createErr := userRepositoryImpl.Create(ctx, user, 1) + if createErr != nil { + t.Errorf("error while creating user :" + id) + } + allUsersID = append(allUsersID, newUserID) + } + defer func() { + for _, userID := range allUsersID { + userRepositoryImpl.Delete(ctx, userID) + } + }() + + type args struct { + ctx context.Context + filter model.RequestGetAll + } + tests := []struct { + name string + repo UserRepositoryImpl + wantErr bool + args args + }{ + { + name: "success get 5 or more users", + repo: userRepositoryImpl, + wantErr: false, + args: args{ + ctx: ctx, + filter: model.RequestGetAll{ + Page: 1, + Limit: 1000, + Keyword: "", + }, + }, + }, + { + name: "success get less than 5", + repo: userRepositoryImpl, + wantErr: false, + args: args{ + ctx: ctx, + filter: model.RequestGetAll{ + Page: 1, + Limit: 1, + Keyword: "", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotUsers, gotTotal, err := tt.repo.GetAll(tt.args.ctx, tt.args.filter) + if (err != nil) != tt.wantErr { + t.Errorf("UserRepositoryImpl.GetAll() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.args.filter.Limit > 5 && len(gotUsers) < 5 { + t.Error("users should be 5 or more") + } + if tt.args.filter.Limit > 5 && gotTotal < 5 { + t.Error("total users should be 5 or more") + } + if tt.args.filter.Limit < 5 && len(gotUsers) > 5 { + t.Error("users should be less than 5") + } + }) + } +} + +func TestUserRepositoryImplUpdate(t *testing.T) { + // create user + user := entity.User{ + Name: "validname", + Email: "valid9@email.com", + Password: "example-password", + TimeFields: entity.TimeFields{ + CreatedAt: &timeNow, + UpdatedAt: &timeNow, + }, + } + userRepositoryImpl := UserRepositoryImpl{ + db: connector.LoadDatabase(), + } + id, createErr := userRepositoryImpl.Create(ctx, user, 1) + if createErr != nil { + t.Errorf("error while creating user") + } + // add id to user + user.ID = id + defer func() { + userRepositoryImpl.Delete(ctx, id) + }() + + type args struct { + ctx context.Context + user entity.User + } + tests := []struct { + name string + repo UserRepositoryImpl + wantErr bool + newUserName string + args args + }{ + { + name: "success update user's name", + repo: userRepositoryImpl, + wantErr: false, + newUserName: "test-update-001", + args: args{ + ctx: ctx, + user: user, + }, + }, + } + for _, tt := range tests { + tt.args.user.Name = tt.newUserName + t.Run(tt.name, func(t *testing.T) { + if err := tt.repo.Update(tt.args.ctx, tt.args.user); (err != nil) != tt.wantErr { + t.Errorf("UserRepositoryImpl.Update() error = %v, wantErr %v", err, tt.wantErr) + } + getUser, getErr := tt.repo.GetByID(tt.args.ctx, id) + if getErr != nil { + t.Error("error while getting user") + } + if getUser.Name != tt.newUserName { + t.Error("update name failed") + } + }) + } +} + +func TestUserRepositoryImplDelete(t *testing.T) { + userRepository := NewUserRepository() + if userRepository == nil { + t.Error("shouldn't nil") + } + + ctx := context.Background() + err := userRepository.Delete(ctx, -2) + if err != nil { + t.Error("delete shouldn't error") + } + if ctx.Err() != nil { + t.Error("delete shouldn't error") + } +} + +func TestUserRepositoryImplUpdatePassword(t *testing.T) { + // create user + user := entity.User{ + Name: "validname", + Email: helper.RandomEmail(), + Password: "example-password", + TimeFields: entity.TimeFields{ + CreatedAt: &timeNow, + UpdatedAt: &timeNow, + }, + } + userRepositoryImpl := UserRepositoryImpl{ + db: connector.LoadDatabase(), + } + id, createErr := userRepositoryImpl.Create(ctx, user, 1) + if createErr != nil { + t.Errorf("error while creating user") + } + // add id to user + user.ID = id + defer func() { + userRepositoryImpl.Delete(ctx, id) + }() + + type args struct { + ctx context.Context + id int + passwordHashed string + } + tests := []struct { + name string + repo UserRepositoryImpl + args args + wantErr bool + }{ + { + name: "success update user's password", + repo: userRepositoryImpl, + wantErr: false, + args: args{ + ctx: ctx, + id: id, + passwordHashed: "new-password-hashed", + }, + }, + { + name: "failed getting user with negative id", + repo: userRepositoryImpl, + wantErr: true, + args: args{ + ctx: ctx, + id: -100, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.repo.UpdatePassword(tt.args.ctx, tt.args.id, tt.args.passwordHashed); (err != nil) != tt.wantErr { + t.Errorf("UserRepositoryImpl.UpdatePassword() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + getUser, getErr := tt.repo.GetByID(tt.args.ctx, id) + if getErr != nil { + t.Error("error while getting user") + } + if getUser.Password != tt.args.passwordHashed { + t.Error("failed to update user's password") + } + } + }) + } +} + +func TestUserRepositoryImplGetByConditions(t *testing.T) { + user := entity.User{ + Name: "validname", + Email: helper.RandomEmail(), + Password: "example-password", + TimeFields: entity.TimeFields{ + CreatedAt: &timeNow, + UpdatedAt: &timeNow, + }, + } + userRepositoryImpl := UserRepositoryImpl{ + db: connector.LoadDatabase(), + } + id, createErr := userRepositoryImpl.Create(ctx, user, 1) + if createErr != nil { + t.Errorf("error while creating user") + } + // add id to user + user.ID = id + defer func() { + userRepositoryImpl.Delete(ctx, id) + }() + + type args struct { + ctx context.Context + conds map[string]any + } + tests := []struct { + name string + repo UserRepositoryImpl + args args + wantErr bool + }{ + { + name: "success get data", + repo: userRepositoryImpl, + args: args{ + ctx: ctx, + conds: map[string]any{ + "name =": user.Name, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotUser, err := tt.repo.GetByConditions(tt.args.ctx, tt.args.conds) + if (err != nil) != tt.wantErr { + t.Errorf("UserRepositoryImpl.GetByConditions() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotUser.ID != user.ID || gotUser.Email != user.Email || gotUser.Password != user.Password { + t.Error("should got same ID/ Email/ Password") + } + }) + } +} From dfca9b1cee57d2fd505b90db74847a14b1e31f2b Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Wed, 24 Jan 2024 11:49:49 +0700 Subject: [PATCH 06/28] add repositories --- go.mod | 1 + go.sum | 2 + internal/helper/helper.go | 17 + internal/helper/helper_test.go | 20 + internal/middleware/middleware_test.go | 2 +- repository/role/role_repository.go | 1 + repository/user/user_repository.go | 6 +- repository/user/user_repository_test.go | 798 +++++++++++------------- 8 files changed, 397 insertions(+), 450 deletions(-) diff --git a/go.mod b/go.mod index 236fe21..6a8e6f9 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( require ( github.com/BurntSushi/toml v1.2.1 // indirect + github.com/XANi/loremipsum v1.1.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.3.1 // indirect diff --git a/go.sum b/go.sum index 4581fec..02fdf22 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/XANi/loremipsum v1.1.0 h1:pNqL9b0ORlhmlhGPXggwOPe7NifWoQPZmqohLCx04z8= +github.com/XANi/loremipsum v1.1.0/go.mod h1:5W6tlNr1vBCP1dzk36OtF+6e3kWMk06fbgbjS7lspyM= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/internal/helper/helper.go b/internal/helper/helper.go index e6ac12b..f8ad03a 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -8,12 +8,22 @@ import ( "strings" "time" + "github.com/XANi/loremipsum" "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" "golang.org/x/text/cases" "golang.org/x/text/language" ) +func RandomWords(n int) string { + if n < 2 { + n = 2 + } + loremIpsumGenerator := loremipsum.New() + words := loremIpsumGenerator.Words(n) + return words +} + // RandomString func generate random string // used for testing and any needs. func RandomString(n uint) string { @@ -72,3 +82,10 @@ func NewFiberCtx() *fiber.Ctx { func ToTitle(s string) string { return cases.Title(language.Und).String(s) } + +func GenerateRandomID() int { + rand.New(rand.NewSource(time.Now().UnixNano())) + min := 9000000 + max := 10000000 + return rand.Intn(max-min) + min +} diff --git a/internal/helper/helper_test.go b/internal/helper/helper_test.go index ea3b019..8998794 100644 --- a/internal/helper/helper_test.go +++ b/internal/helper/helper_test.go @@ -1,13 +1,26 @@ package helper import ( + "log" "net" + "strings" "testing" "github.com/Lukmanern/gost/internal/consts" "github.com/stretchr/testify/assert" ) +func TestRandomWords(t *testing.T) { + for i := 2; i < 12; i++ { + words := RandomWords(i) + wordsSlice := strings.Split(words, " ") + log.Println(words) + if len(wordsSlice) < i { + t.Fatal("should equal") + } + } +} + func TestRandomString(t *testing.T) { for i := 0; i < 25; i++ { s := RandomString(uint(i)) @@ -78,3 +91,10 @@ func TestToTitle(t *testing.T) { } } } + +func TestGenerateRandomID(t *testing.T) { + for i := 0; i < 10; i++ { + v := GenerateRandomID() + assert.True(t, v > 0, "shoult true") + } +} diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index d92d0bc..026ebf4 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -28,7 +28,7 @@ func init() { timeNow := time.Now() params = GenTokenParams{ - ID: 1, + ID: helper.GenerateRandomID(), Email: helper.RandomEmail(), Roles: map[string]uint8{"test-role": 1}, Exp: timeNow.Add(5 * time.Minute), diff --git a/repository/role/role_repository.go b/repository/role/role_repository.go index 485f505..2a0c498 100644 --- a/repository/role/role_repository.go +++ b/repository/role/role_repository.go @@ -85,6 +85,7 @@ func (repo *RoleRepositoryImpl) GetByName(ctx context.Context, name string) (rol func (repo *RoleRepositoryImpl) GetAll(ctx context.Context, filter model.RequestGetAll) (roles []entity.Role, total int, err error) { var count int64 + filter.Sort = "" args := []interface{}{"%" + filter.Keyword + "%"} cond := "name LIKE ?" result := repo.db.Where(cond, args...).Find(&roles) diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go index bdfa812..694e946 100644 --- a/repository/user/user_repository.go +++ b/repository/user/user_repository.go @@ -124,7 +124,11 @@ func (repo *UserRepositoryImpl) GetAll(ctx context.Context, filter model.Request users = []entity.User{} skip := int64(filter.Limit * (filter.Page - 1)) limit := int64(filter.Limit) - result = repo.db.Where(cond, args...).Limit(int(limit)).Offset(int(skip)).Find(&users) + result = repo.db.Where(cond, args...).Limit(int(limit)).Offset(int(skip)) + if filter.Sort != "" { + result = result.Order(filter.Sort + " ASC") + } + result = result.Find(&users) if result.Error != nil { return nil, 0, result.Error } diff --git a/repository/user/user_repository_test.go b/repository/user/user_repository_test.go index 730e7b6..5fd9f9e 100644 --- a/repository/user/user_repository_test.go +++ b/repository/user/user_repository_test.go @@ -1,550 +1,452 @@ package repository import ( - "context" + "log" + "strconv" "testing" "time" - "github.com/Lukmanern/gost/database/connector" "github.com/Lukmanern/gost/domain/entity" "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/consts" "github.com/Lukmanern/gost/internal/env" + "github.com/Lukmanern/gost/internal/hash" "github.com/Lukmanern/gost/internal/helper" + "github.com/stretchr/testify/assert" +) + +const ( + headerTestName string = "at UserRepoTest" ) var ( timeNow time.Time - ctx context.Context ) func init() { - filePath := "./../../.env" - env.ReadConfig(filePath) + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) timeNow = time.Now() - ctx = context.Background() } -func TestNewUserRepository(t *testing.T) { - userRepository := NewUserRepository() - if userRepository == nil { - t.Error("should not nil") - } -} +func TestCreateDelete(t *testing.T) { + repository := NewUserRepository() + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) -func TestUserRepositoryImplCreate(t *testing.T) { - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), + type testCase struct { + Name string + Payload entity.User + WantErr bool } + pwHashed, hashErr := hash.Generate(helper.RandomString(8)) + assert.NoError(t, hashErr, consts.ShouldNotErr, headerTestName) - type args struct { - ctx context.Context - user entity.User - } - tests := []struct { - name string - repo UserRepositoryImpl - wantErr bool - wantPanic bool - args args - }{ + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + testCases := []testCase{ { - name: "success create new user", - repo: userRepositoryImpl, - wantErr: false, - wantPanic: false, - args: args{ - ctx: context.Background(), - user: entity.User{ - Name: "validname", - Email: "valid1@email.com", - Password: "example-password", - TimeFields: entity.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - }, + Name: "Failed Create User -1: email already used", + Payload: entity.User{ + Name: helper.RandomString(12), + Email: validUser.Email, + Password: pwHashed, + ActivatedAt: &timeNow, }, + WantErr: true, }, { - name: "success create new user with void data", - repo: userRepositoryImpl, - wantErr: false, - wantPanic: false, + Name: "Success Create User -1", + Payload: entity.User{ + Name: helper.RandomString(10), + Email: helper.RandomEmail(), + Password: pwHashed, + ActivatedAt: &timeNow, + }, + WantErr: false, }, { - name: "failed create new user with void data and nil repository", - repo: UserRepositoryImpl{ - db: nil, + Name: "Success Create User -2", + Payload: entity.User{ + Name: helper.RandomString(10), + Email: helper.RandomEmail(), + Password: pwHashed, + ActivatedAt: &timeNow, }, - wantErr: true, - wantPanic: true, + WantErr: false, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if !tt.wantPanic { - gotID, err := tt.repo.Create(tt.args.ctx, tt.args.user, 1) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - gotID2, err2 := tt.repo.Create(tt.args.ctx, tt.args.user, 1) - if err2 == nil || gotID2 != 0 { - t.Error("should be error, couse email is already used") - } - tt.repo.Delete(tt.args.ctx, gotID) - - return - } - // want panic - defer func() { - if r := recover(); r == nil { - t.Errorf("create() do not panic") - } - }() - userID, err := tt.repo.Create(tt.args.ctx, tt.args.user, 1) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - userRepositoryImpl.Delete(ctx, userID) - }) + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + tc.Payload.SetCreateTime() + id, createErr := repository.Create(ctx, tc.Payload, 1) + if tc.WantErr { + assert.Error(t, createErr, consts.ShouldErr, tc.Name, headerTestName) + continue + } + + assert.NoError(t, createErr, consts.ShouldNotErr, tc.Name, headerTestName) + + deleteErr := repository.Delete(ctx, id) + assert.NoError(t, deleteErr, consts.ShouldNotErr, headerTestName) } } -func TestUserRepositoryImplGetByID(t *testing.T) { - // create user - user := entity.User{ - Name: "validname", - Email: "valid2@email.com", - Password: "example-password", - TimeFields: entity.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") +func TestGetByID(t *testing.T) { + repository := NewUserRepository() + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + userIDs := make([]int, 2) + for i := range userIDs { + userIDs[i] = createUser().ID + defer repository.Delete(ctx, userIDs[i]) } - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - type args struct { - ctx context.Context - id int + type testCase struct { + Name string + ID int + WantErr bool } - tests := []struct { - name string - repo UserRepositoryImpl - wantErr bool - wantUser bool - args args - }{ + testCases := []testCase{ { - name: "Success get user by id", - repo: userRepositoryImpl, - wantErr: false, - wantUser: true, - args: args{ - ctx: ctx, - id: id, - }, + Name: "Failed Get User -1: invalid ID", + WantErr: true, + ID: -1, }, { - name: "Failed get user by negative id", - repo: userRepositoryImpl, - wantErr: true, - wantUser: false, - args: args{ - ctx: ctx, - id: -10, - }, + Name: "Failed Get User -2: Data not found", + WantErr: true, + ID: userIDs[0] * 99, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUser, err := tt.repo.GetByID(tt.args.ctx, tt.args.id) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantUser { - if gotUser == nil { - t.Error("error user shouldn't nil") - } - } + for i, id := range userIDs { + testCases = append(testCases, testCase{ + Name: "Success Get data-" + strconv.Itoa(i+1), + ID: id, + WantErr: false, }) } -} -func TestUserRepositoryImplGetByEmail(t *testing.T) { - // create user - user := entity.User{ - Name: "validname", - Email: "valid3@email.com", - Password: "example-password", - TimeFields: entity.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + user, err := repository.GetByID(ctx, tc.ID) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, headerTestName) + continue + } + assert.NoError(t, err, consts.ShouldNotErr, headerTestName) + assert.NotNil(t, user, consts.ShouldNotNil, headerTestName) } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") +} + +func TestGetByEmail(t *testing.T) { + repository := NewUserRepository() + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + userEmails := make([]string, 2) + for i := range userEmails { + user := createUser() + userEmails[i] = user.Email + defer repository.Delete(ctx, user.ID) } - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - type args struct { - ctx context.Context - email string + type testCase struct { + Name string + Email string + WantErr bool } - tests := []struct { - name string - repo UserRepositoryImpl - wantUser bool - wantErr bool - args args - }{ + testCases := []testCase{ { - name: "Success get user by valid email", - repo: userRepositoryImpl, - wantErr: false, - wantUser: true, - args: args{ - ctx: ctx, - email: user.Email, - }, + Name: "Failed Get User -1: invalid Email", + WantErr: true, + Email: "", }, { - name: "Failed get user by invalid-email", - repo: userRepositoryImpl, - wantErr: true, - wantUser: false, - args: args{ - ctx: ctx, - email: "invalid-email", - }, + Name: "Failed Get User -2: Data not found", + WantErr: true, + Email: "validemail@example.xyz", }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUser, err := tt.repo.GetByEmail(tt.args.ctx, tt.args.email) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.GetByID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantUser { - if gotUser == nil { - t.Error("error user shouldn't nil") - } - } + for i, email := range userEmails { + testCases = append(testCases, testCase{ + Name: "Success Get data-" + strconv.Itoa(i+1), + Email: email, + WantErr: false, }) } -} -func TestUserRepositoryImplGetAll(t *testing.T) { - // create user - allUsersID := make([]int, 0) - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - for _, id := range []string{"4", "5", "6", "7", "8"} { - user := entity.User{ - Name: "validname", - Email: "valid" + id + "@email.com", // email is unique - Password: "example-password", - TimeFields: entity.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - newUserID, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user :" + id) + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + user, err := repository.GetByEmail(ctx, tc.Email) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, headerTestName) + continue } - allUsersID = append(allUsersID, newUserID) + assert.NoError(t, err, consts.ShouldNotErr, headerTestName) + assert.NotNil(t, user, consts.ShouldNotNil, headerTestName) } - defer func() { - for _, userID := range allUsersID { - userRepositoryImpl.Delete(ctx, userID) - } - }() +} + +func TestGetByConditions(t *testing.T) { + repository := NewUserRepository() + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - type args struct { - ctx context.Context - filter model.RequestGetAll + userEmails := make([]string, 2) + for i := range userEmails { + user := createUser() + userEmails[i] = user.Email + defer repository.Delete(ctx, user.ID) } - tests := []struct { - name string - repo UserRepositoryImpl - wantErr bool - args args - }{ + + type testCase struct { + Name string + Conditions map[string]any + WantErr bool + } + testCases := []testCase{ { - name: "success get 5 or more users", - repo: userRepositoryImpl, - wantErr: false, - args: args{ - ctx: ctx, - filter: model.RequestGetAll{ - Page: 1, - Limit: 1000, - Keyword: "", - }, + Name: "Failed Get User -1: invalid Conditions", + WantErr: true, + Conditions: map[string]any{ + "invalid =": 90, }, }, { - name: "success get less than 5", - repo: userRepositoryImpl, - wantErr: false, - args: args{ - ctx: ctx, - filter: model.RequestGetAll{ - Page: 1, - Limit: 1, - Keyword: "", - }, + Name: "Failed Get User -2: Data not found", + WantErr: true, + Conditions: map[string]any{ + "email =": helper.RandomEmail(), }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUsers, gotTotal, err := tt.repo.GetAll(tt.args.ctx, tt.args.filter) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.GetAll() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.args.filter.Limit > 5 && len(gotUsers) < 5 { - t.Error("users should be 5 or more") - } - if tt.args.filter.Limit > 5 && gotTotal < 5 { - t.Error("total users should be 5 or more") - } - if tt.args.filter.Limit < 5 && len(gotUsers) > 5 { - t.Error("users should be less than 5") - } + for i, email := range userEmails { + testCases = append(testCases, testCase{ + Name: "Success Get data-" + strconv.Itoa(i+1), + WantErr: false, + Conditions: map[string]any{ + "email =": email, + }, }) } -} -func TestUserRepositoryImplUpdate(t *testing.T) { - // create user - user := entity.User{ - Name: "validname", - Email: "valid9@email.com", - Password: "example-password", - TimeFields: entity.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + user, err := repository.GetByConditions(ctx, tc.Conditions) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, headerTestName) + continue + } + assert.NoError(t, err, consts.ShouldNotErr, headerTestName) + assert.NotNil(t, user, consts.ShouldNotNil, headerTestName) } - // add id to user - user.ID = id - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - - type args struct { - ctx context.Context - user entity.User +} + +func TestGetAll(t *testing.T) { + repository := NewUserRepository() + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + type testCase struct { + Name string + Payload model.RequestGetAll + WantErr bool } - tests := []struct { - name string - repo UserRepositoryImpl - wantErr bool - newUserName string - args args - }{ + + testCases := []testCase{ + { + Name: "Success Get All -1", + Payload: model.RequestGetAll{ + Page: 1, + Limit: 100, + }, + WantErr: false, + }, + { + Name: "Success Get All -2", + Payload: model.RequestGetAll{ + Page: 1, + Limit: 10, + }, + WantErr: false, + }, { - name: "success update user's name", - repo: userRepositoryImpl, - wantErr: false, - newUserName: "test-update-001", - args: args{ - ctx: ctx, - user: user, + Name: "Failed Get All -1: invalid sort", + Payload: model.RequestGetAll{ + Page: 1, + Limit: 10, + Sort: "invalid-sort", }, + WantErr: true, }, } - for _, tt := range tests { - tt.args.user.Name = tt.newUserName - t.Run(tt.name, func(t *testing.T) { - if err := tt.repo.Update(tt.args.ctx, tt.args.user); (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.Update() error = %v, wantErr %v", err, tt.wantErr) - } - getUser, getErr := tt.repo.GetByID(tt.args.ctx, id) - if getErr != nil { - t.Error("error while getting user") - } - if getUser.Name != tt.newUserName { - t.Error("update name failed") - } - }) + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + users, total, getErr := repository.GetAll(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, getErr, consts.ShouldErr, headerTestName) + continue + } + assert.NoError(t, getErr, consts.ShouldNotErr, headerTestName) + assert.True(t, total >= 0, headerTestName) + assert.True(t, len(users) >= 0, headerTestName) } } -func TestUserRepositoryImplDelete(t *testing.T) { - userRepository := NewUserRepository() - if userRepository == nil { - t.Error("shouldn't nil") - } +func TestUpdate(t *testing.T) { + repository := NewUserRepository() + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - ctx := context.Background() - err := userRepository.Delete(ctx, -2) - if err != nil { - t.Error("delete shouldn't error") - } - if ctx.Err() != nil { - t.Error("delete shouldn't error") + // create user for testing {payload} + user := createUser() + defer repository.Delete(ctx, user.ID) + + type testCase struct { + Name string + Payload entity.User + WantErr bool } -} -func TestUserRepositoryImplUpdatePassword(t *testing.T) { - // create user - user := entity.User{ - Name: "validname", - Email: helper.RandomEmail(), - Password: "example-password", - TimeFields: entity.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, + testCases := []testCase{ + { + Name: "Success Update -1", + Payload: entity.User{ + ID: user.ID, + Name: helper.RandomString(12), + }, + WantErr: false, + }, + { + Name: "Success Update -2", + Payload: entity.User{ + ID: user.ID, + Name: helper.RandomString(12), + }, + WantErr: false, }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") - } - // add id to user - user.ID = id - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - - type args struct { - ctx context.Context - id int - passwordHashed string - } - tests := []struct { - name string - repo UserRepositoryImpl - args args - wantErr bool - }{ { - name: "success update user's password", - repo: userRepositoryImpl, - wantErr: false, - args: args{ - ctx: ctx, - id: id, - passwordHashed: "new-password-hashed", + Name: "Failed Update -1: invalid ID", + Payload: entity.User{ + ID: -10, + Name: helper.RandomString(12), }, + WantErr: true, }, { - name: "failed getting user with negative id", - repo: userRepositoryImpl, - wantErr: true, - args: args{ - ctx: ctx, - id: -100, + Name: "Failed Update -2: invalid id", + Payload: entity.User{ + ID: 0, + Name: helper.RandomString(12), }, + WantErr: true, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.repo.UpdatePassword(tt.args.ctx, tt.args.id, tt.args.passwordHashed); (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.UpdatePassword() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr { - getUser, getErr := tt.repo.GetByID(tt.args.ctx, id) - if getErr != nil { - t.Error("error while getting user") - } - if getUser.Password != tt.args.passwordHashed { - t.Error("failed to update user's password") - } - } - }) + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + tc.Payload.SetUpdateTime() + updateErr := repository.Update(ctx, tc.Payload) + if tc.WantErr { + // error by data not found not detected + // assert.Error(t, updateErr, consts.ShouldErr, headerTestName) + continue + } + assert.NoError(t, updateErr, consts.ShouldNotErr, headerTestName) + + user, getErr := repository.GetByID(ctx, tc.Payload.ID) + assert.NoError(t, getErr, consts.ShouldNotErr, headerTestName) + assert.Equal(t, user.Name, tc.Payload.Name) } } -func TestUserRepositoryImplGetByConditions(t *testing.T) { - user := entity.User{ - Name: "validname", - Email: helper.RandomEmail(), - Password: "example-password", - TimeFields: entity.TimeFields{ - CreatedAt: &timeNow, - UpdatedAt: &timeNow, - }, - } - userRepositoryImpl := UserRepositoryImpl{ - db: connector.LoadDatabase(), - } - id, createErr := userRepositoryImpl.Create(ctx, user, 1) - if createErr != nil { - t.Errorf("error while creating user") - } - // add id to user - user.ID = id - defer func() { - userRepositoryImpl.Delete(ctx, id) - }() - - type args struct { - ctx context.Context - conds map[string]any +func TestUpdatePassword(t *testing.T) { + repository := NewUserRepository() + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + user := createUser() + defer repository.Delete(ctx, user.ID) + + type testCase struct { + Name string + Payload entity.User + WantErr bool } - tests := []struct { - name string - repo UserRepositoryImpl - args args - wantErr bool - }{ + + testCases := []testCase{ + { + Name: "Success Update -1", + Payload: entity.User{ + ID: user.ID, + Password: helper.RandomString(12), + }, + WantErr: false, + }, { - name: "success get data", - repo: userRepositoryImpl, - args: args{ - ctx: ctx, - conds: map[string]any{ - "name =": user.Name, - }, + Name: "Failed Update: invalid id", + Payload: entity.User{ + ID: -10, + Password: helper.RandomString(12), }, - wantErr: false, + WantErr: true, + }, + { + Name: "Failed Update: invalid id", + Payload: entity.User{ + ID: 0, + Password: helper.RandomString(12), + }, + WantErr: true, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUser, err := tt.repo.GetByConditions(tt.args.ctx, tt.args.conds) - if (err != nil) != tt.wantErr { - t.Errorf("UserRepositoryImpl.GetByConditions() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotUser.ID != user.ID || gotUser.Email != user.Email || gotUser.Password != user.Password { - t.Error("should got same ID/ Email/ Password") - } - }) + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + tc.Payload.SetUpdateTime() + updateErr := repository.UpdatePassword(ctx, tc.Payload.ID, tc.Payload.Password) + if tc.WantErr { + assert.Error(t, updateErr, consts.ShouldErr, headerTestName) + continue + } + assert.NoError(t, updateErr, consts.ShouldNotErr, headerTestName) + } +} + +func createUser() entity.User { + repository := NewUserRepository() + ctx := helper.NewFiberCtx().Context() + pwHashed, _ := hash.Generate(helper.RandomString(10)) + newUser := entity.User{ + Name: helper.RandomString(10), + Email: helper.RandomEmail(), + Password: pwHashed, + ActivatedAt: &timeNow, + } + newUser.SetCreateTime() + userID, createErr := repository.Create(ctx, newUser, 1) + if createErr != nil { + log.Fatal("error while create new user", headerTestName) } + newUser.ID = userID + return newUser } From 191895b82ad016a15208b47fc1d58684d8c2bde0 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Wed, 24 Jan 2024 13:37:21 +0700 Subject: [PATCH 07/28] add email and user service --- domain/model/user.go | 27 +- go.mod | 2 +- internal/consts/consts.go | 1 + repository/user/user_repository.go | 18 +- repository/user/user_repository_test.go | 4 +- service/email_service/email_service.go | 66 +++ service/user/user_service.go | 217 ++++++++++ service/user/user_service_test.go | 528 ++++++++++++++++++++++++ 8 files changed, 851 insertions(+), 12 deletions(-) create mode 100644 service/email_service/email_service.go create mode 100644 service/user/user_service.go create mode 100644 service/user/user_service_test.go diff --git a/domain/model/user.go b/domain/model/user.go index 45d0a1f..ebda579 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -6,11 +6,19 @@ import ( "github.com/Lukmanern/gost/domain/entity" ) +type User struct { + ID int `gorm:"type:bigserial;primaryKey" json:"id"` + Name string `gorm:"type:varchar(100) not null" json:"name"` + Email string `gorm:"type:varchar(100) not null unique" json:"email"` + Password string `gorm:"type:varchar(255) not null" json:"password"` + ActivatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"activated_at"` +} + type UserRegister struct { Name string `validate:"required,min=2,max=60" json:"name"` Email string `validate:"required,email,min=5,max=60" json:"email"` Password string `validate:"required,min=8,max=30" json:"password"` - RoleID int `validate:"required,numeric,min=1" json:"role_id"` + RolesID []int `validate:"required" json:"role_id"` } type UserActivation struct { @@ -24,6 +32,23 @@ type UserLogin struct { IP string `validate:"required,min=4,max=20" json:"ip"` } +// ID int `gorm:"type:bigserial;primaryKey" json:"id"` +// Name string `gorm:"type:varchar(100) not null" json:"name"` +// Email string `gorm:"type:varchar(100) not null unique" json:"email"` +// Password string `gorm:"type:varchar(255) not null" json:"password"` +// ActivatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"activated_at"` +// Roles []Role `gorm:"many2many:user_has_roles" json:"roles"` + +type UserUpdate struct { + ID int `gorm:"type:bigserial;primaryKey" json:"id"` + Name string `gorm:"type:varchar(100) not null" json:"name"` +} + +type UserUpdateRoles struct { + ID int `gorm:"type:bigserial;primaryKey" json:"id"` + RolesID []int `validate:"required" json:"role_id"` +} + type UserForgetPassword struct { Email string `validate:"required,email,min=5,max=60" json:"email"` } diff --git a/go.mod b/go.mod index 6a8e6f9..278a207 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Lukmanern/gost go 1.20 require ( + github.com/XANi/loremipsum v1.1.0 github.com/go-redis/redis v6.15.9+incompatible github.com/gofiber/fiber/v2 v2.50.0 github.com/golang-jwt/jwt/v5 v5.0.0 @@ -17,7 +18,6 @@ require ( require ( github.com/BurntSushi/toml v1.2.1 // indirect - github.com/XANi/loremipsum v1.1.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.3.1 // indirect diff --git a/internal/consts/consts.go b/internal/consts/consts.go index de5f6c8..40c769a 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -13,6 +13,7 @@ const ( InvalidID = "invalid ID" NilValue = "error nil value" InvalidToken = "invalid token / JWT, please logout and try-login" + ErrHashing = "error while hashing password" ) const ( diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go index 694e946..4a8f98e 100644 --- a/repository/user/user_repository.go +++ b/repository/user/user_repository.go @@ -15,7 +15,7 @@ import ( type UserRepository interface { // Create adds a new user to the repository with a specified role. - Create(ctx context.Context, user entity.User, roleID int) (id int, err error) + Create(ctx context.Context, user entity.User, roleIDs []int) (id int, err error) // GetByID retrieves a user by their unique identifier. GetByID(ctx context.Context, id int) (user *entity.User, err error) @@ -57,7 +57,7 @@ func NewUserRepository() UserRepository { return userRepositoryImpl } -func (repo *UserRepositoryImpl) Create(ctx context.Context, user entity.User, roleID int) (id int, err error) { +func (repo *UserRepositoryImpl) Create(ctx context.Context, user entity.User, roleIDs []int) (id int, err error) { err = repo.db.Transaction(func(tx *gorm.DB) error { if res := tx.Create(&user); res.Error != nil { tx.Rollback() @@ -65,12 +65,14 @@ func (repo *UserRepositoryImpl) Create(ctx context.Context, user entity.User, ro } id = user.ID - if res := tx.Create(&entity.UserHasRoles{ - UserID: id, - RoleID: roleID, - }); res.Error != nil { - tx.Rollback() - return res.Error + for _, roleID := range roleIDs { + if res := tx.Create(&entity.UserHasRoles{ + UserID: id, + RoleID: roleID, + }); res.Error != nil { + tx.Rollback() + return res.Error + } } return nil }) diff --git a/repository/user/user_repository_test.go b/repository/user/user_repository_test.go index 5fd9f9e..33b5920 100644 --- a/repository/user/user_repository_test.go +++ b/repository/user/user_repository_test.go @@ -83,7 +83,7 @@ func TestCreateDelete(t *testing.T) { log.Println(tc.Name, headerTestName) tc.Payload.SetCreateTime() - id, createErr := repository.Create(ctx, tc.Payload, 1) + id, createErr := repository.Create(ctx, tc.Payload, []int{1}) if tc.WantErr { assert.Error(t, createErr, consts.ShouldErr, tc.Name, headerTestName) continue @@ -443,7 +443,7 @@ func createUser() entity.User { ActivatedAt: &timeNow, } newUser.SetCreateTime() - userID, createErr := repository.Create(ctx, newUser, 1) + userID, createErr := repository.Create(ctx, newUser, []int{1}) if createErr != nil { log.Fatal("error while create new user", headerTestName) } diff --git a/service/email_service/email_service.go b/service/email_service/email_service.go new file mode 100644 index 0000000..b87c772 --- /dev/null +++ b/service/email_service/email_service.go @@ -0,0 +1,66 @@ +package service + +import ( + "fmt" + "net/smtp" + "strings" + "sync" + + "github.com/Lukmanern/gost/internal/env" + "github.com/Lukmanern/gost/internal/helper" +) + +type EmailService interface { + // SendMail func sends message with subject to some emails address. + SendMail(subject, message string, emails ...string) error +} + +type EmailServiceImpl struct { + Server string + Port int + Email string + Password string + SmptAuth smtp.Auth + SmptMime string + SmptAddr string +} + +var ( + emailService *EmailServiceImpl + emailServiceOnce sync.Once +) + +func NewEmailService() EmailService { + emailServiceOnce.Do(func() { + config := env.Configuration() + emailService = &EmailServiceImpl{ + Server: config.SMTPServer, + Port: config.SMTPPort, + Email: config.SMTPEmail, + Password: config.SMTPPassword, + } + + emailService.SmptAuth = smtp.PlainAuth("", emailService.Email, emailService.Password, emailService.Server) + emailService.SmptAddr = fmt.Sprintf("%s:%d", emailService.Server, emailService.Port) + emailService.SmptMime = "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\r\n" + }) + + return emailService +} + +func (svc *EmailServiceImpl) SendMail(subject, message string, emails ...string) error { + validateErr := helper.ValidateEmails(emails...) + if validateErr != nil { + return validateErr + } + body := "From: " + "CONFIG_SENDER_NAME" + "\n" + + "To: " + strings.Join(emails, ",") + "\n" + + "Subject: " + subject + "\n" + svc.SmptMime + "\n\n" + + message + + err := smtp.SendMail(svc.SmptAddr, svc.SmptAuth, svc.Email, emails, []byte(body)) + if err != nil { + return err + } + return nil +} diff --git a/service/user/user_service.go b/service/user/user_service.go new file mode 100644 index 0000000..35b7452 --- /dev/null +++ b/service/user/user_service.go @@ -0,0 +1,217 @@ +package service + +import ( + "context" + "errors" + "strings" + "sync" + "time" + + "github.com/Lukmanern/gost/database/connector" + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/hash" + "github.com/Lukmanern/gost/internal/middleware" + repository "github.com/Lukmanern/gost/repository/user" + "github.com/go-redis/redis" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +type UserService interface { + GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) + Register(ctx context.Context, data model.UserRegister) (id int, err error) + Login(ctx context.Context, data model.UserLogin) (token string, err error) + Logout(c *fiber.Ctx) (err error) + MyProfile(ctx context.Context, id int) (profile model.User, err error) + UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) + UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) + Delete(ctx context.Context, id int) (err error) +} + +type UserServiceImpl struct { + jwtHandler *middleware.JWTHandler + repository repository.UserRepository + redis *redis.Client +} + +var ( + userSvcImpl *UserServiceImpl + userSvcImplOnce sync.Once +) + +func NewUserService() UserService { + userSvcImplOnce.Do(func() { + userSvcImpl = &UserServiceImpl{ + jwtHandler: middleware.NewJWTHandler(), + repository: repository.NewUserRepository(), + redis: connector.LoadRedisCache(), + } + }) + return userSvcImpl +} + +func (svc *UserServiceImpl) GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) { + entityUsers, total, getErr := svc.repository.GetAll(ctx, filter) + if getErr != nil { + return nil, 0, getErr + } + for _, entityUser := range entityUsers { + users = append(users, entityToResponse(&entityUser)) + } + + return users, total, err +} + +func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegister) (id int, err error) { + _, getErr := svc.repository.GetByEmail(ctx, data.Email) + if getErr == nil { + return 0, fiber.NewError(fiber.StatusBadRequest, "email already used") + } + + pwHashed, hashErr := hash.Generate(data.Password) + if hashErr != nil { + return 0, errors.New(consts.ErrHashing) + } + + data.Password = pwHashed + entityUser := modelRegisterToEntity(data) + entityUser.SetCreateTime() + id, err = svc.repository.Create(ctx, entityUser, []int{1}) + if err != nil { + return 0, err + } + return id, nil +} + +func (svc *UserServiceImpl) Login(ctx context.Context, data model.UserLogin) (token string, err error) { + user, getErr := svc.repository.GetByEmail(ctx, data.Email) + if getErr != nil { + if getErr == gorm.ErrRecordNotFound { + return "", fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + return "", getErr + } + + res, verifyErr := hash.Verify(user.Password, data.Password) + if verifyErr != nil || !res { + return "", fiber.NewError(fiber.StatusBadRequest, "wrong password") + } + + jwtHandler := middleware.NewJWTHandler() + expired := time.Now().Add(4 * 24 * time.Hour) + token, err = jwtHandler.GenerateJWT(user.ID, user.Email, map[string]uint8{}, expired) + if err != nil { + return "", fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return token, nil +} + +func (svc *UserServiceImpl) Logout(c *fiber.Ctx) (err error) { + err = svc.jwtHandler.InvalidateToken(c) + if err != nil { + return err + } + return nil +} + +func (svc *UserServiceImpl) MyProfile(ctx context.Context, id int) (profile model.User, err error) { + entityUser, getErr := svc.repository.GetByID(ctx, id) + if getErr != nil { + if getErr == gorm.ErrRecordNotFound { + return profile, fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + return profile, getErr + } + + profile = entityToResponse(entityUser) + return profile, nil +} + +func (svc *UserServiceImpl) UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) { + _, getErr := svc.repository.GetByID(ctx, data.ID) + if getErr != nil { + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + return getErr + } + + user := modelUpdateToEntity(data) + user.SetUpdateTime() + err = svc.repository.Update(ctx, user) + if err != nil { + return err + } + return nil +} + +func (svc *UserServiceImpl) UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) { + user, getErr := svc.repository.GetByID(ctx, data.ID) + if getErr != nil { + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + return getErr + } + + res, verifyErr := hash.Verify(user.Password, data.OldPassword) + if verifyErr != nil || !res { + return fiber.NewError(fiber.StatusBadRequest, "wrong password, failed to update") + } + + if user.ActivatedAt != nil || user.DeletedAt != nil { + return fiber.NewError(fiber.StatusBadRequest, "your account is inactive") + } + + pwHashed, hashErr := hash.Generate(data.NewPassword) + if hashErr != nil { + return errors.New(consts.ErrHashing) + } + + updateErr := svc.repository.UpdatePassword(ctx, data.ID, pwHashed) + if updateErr != nil { + return updateErr + } + return nil +} + +func (svc *UserServiceImpl) Delete(ctx context.Context, id int) (err error) { + _, getErr := svc.repository.GetByID(ctx, id) + if getErr != nil { + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + return getErr + } + err = svc.repository.Delete(ctx, id) + if err != nil { + return err + } + return nil +} + +func modelRegisterToEntity(data model.UserRegister) entity.User { + return entity.User{ + Name: data.Name, + Email: strings.ToLower(data.Email), + Password: data.Password, + } +} + +func entityToResponse(data *entity.User) model.User { + return model.User{ + ID: data.ID, + Name: data.Name, + Email: data.Email, + ActivatedAt: data.ActivatedAt, + } +} + +func modelUpdateToEntity(data model.UserUpdate) entity.User { + return entity.User{ + ID: data.ID, + Name: data.Name, + } +} diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go new file mode 100644 index 0000000..a1957e0 --- /dev/null +++ b/service/user/user_service_test.go @@ -0,0 +1,528 @@ +package service + +import ( + "log" + "strconv" + "testing" + "time" + + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/env" + "github.com/Lukmanern/gost/internal/hash" + "github.com/Lukmanern/gost/internal/helper" + repository "github.com/Lukmanern/gost/repository/user" + "github.com/stretchr/testify/assert" +) + +const ( + headerTestName string = "at UserServiceTest" +) + +var ( + timeNow time.Time + userRepository repository.UserRepository +) + +func init() { + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) + timeNow = time.Now() + userRepository = repository.NewUserRepository() +} + +func TestRegister(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + type testCase struct { + Name string + Payload model.UserRegister + WantErr bool + } + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + testCases := []testCase{ + { + Name: "Failed Create -1: email already used", + Payload: model.UserRegister{ + Name: helper.RandomString(15), + Email: validUser.Email, + Password: "password00", + }, + WantErr: true, + }, + { + Name: "Success Create -1", + Payload: model.UserRegister{ + Name: helper.RandomString(15), + Email: helper.RandomEmail(), + Password: "password00", + }, + WantErr: false, + }, + { + Name: "Success Create -2", + Payload: model.UserRegister{ + Name: helper.RandomString(15), + Email: helper.RandomEmail(), + Password: "password00", + }, + WantErr: false, + }, + } + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + id, createErr := service.Register(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, createErr, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, createErr, consts.ShouldNotErr, tc.Name, headerTestName) + + user, getErr := repository.GetByID(ctx, id) + assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) + assert.NotNil(t, user, consts.ShouldNotNil, tc.Name, headerTestName) + + deleteErr := service.Delete(ctx, id) + assert.NoError(t, deleteErr, consts.ShouldNotErr, tc.Name, headerTestName) + + // value reset + user = nil + getErr = nil + user, getErr = repository.GetByID(ctx, id) + assert.Error(t, getErr, consts.ShouldErr, tc.Name, headerTestName) + assert.Nil(t, user, consts.ShouldNil, tc.Name, headerTestName) + } +} + +func TestLogin(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + users := make([]entity.User, 2) + for i := range users { + users[i] = createUser() + defer repository.Delete(ctx, users[i].ID) + } + + type testCase struct { + Name string + Payload model.UserLogin + WantErr bool + } + testCases := []testCase{ + { + Name: "Failed Login -1: void payload", + WantErr: true, + }, + { + Name: "Failed Login -2: data not found", + WantErr: true, + Payload: model.UserLogin{ + Email: "wrong-email", + Password: "xx", + }, + }, + { + Name: "Failed Login -3: data not found", + WantErr: true, + Payload: model.UserLogin{ + Email: "", + Password: "xx", + }, + }, + { + Name: "Failed Login -3: wrong password", + WantErr: true, + Payload: model.UserLogin{ + Email: users[0].Email, + Password: "wrong-password", + }, + }, + } + for i, user := range users { + testCases = append(testCases, testCase{ + Name: "Success login -" + strconv.Itoa(i+1), + Payload: model.UserLogin{ + Email: user.Email, + Password: user.Password, + }, + WantErr: false, + }) + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + _, loginErr := service.Login(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, loginErr, consts.ShouldErr, headerTestName) + continue + } + assert.NoError(t, loginErr, consts.ShouldNotErr) + } +} + +// func TestLogout(t *testing.T) { +// service := NewUserService() +// assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) +// repository := userRepository +// assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) +// c := helper.NewFiberCtx() +// assert.NotNil(t, c, consts.ShouldNotNil, headerTestName) + +// logoutErr := service.Logout(c) +// assert.Error(t, logoutErr, consts.ShouldErr, headerTestName) +// } + +func TestGetAll(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + type testCase struct { + Name string + Payload model.RequestGetAll + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success Get All -1", + Payload: model.RequestGetAll{ + Limit: 100, + Page: 1, + }, + WantErr: false, + }, + { + Name: "Success Get All -2", + Payload: model.RequestGetAll{ + Limit: 12, + Page: 2, + Sort: "name", + }, + WantErr: false, + }, + { + Name: "Failed Get All -1: invalid sort", + Payload: model.RequestGetAll{ + Limit: 12, + Page: 2, + Sort: "invalid", + }, + WantErr: true, + }, + } + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + _, _, getErr := service.GetAll(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, getErr, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) + } +} + +func TestMyProfile(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + validUser2 := createUser() + defer repository.Delete(ctx, validUser2.ID) + + type testCase struct { + Name string + ID int + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success Get My Profile -1", + ID: validUser.ID, + WantErr: false, + }, + { + Name: "Success Get My Profile -2", + ID: validUser2.ID, + WantErr: false, + }, + { + Name: "Failed Get My Profile -1: data not found", + ID: validUser2.ID * 99, + WantErr: true, + }, + { + Name: "Failed Get My Profile -2: invalid ID", + ID: -1, + WantErr: true, + }, + { + Name: "Failed Get My Profile -3: invalid ID", + ID: 0, + WantErr: true, + }, + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + user, getErr := service.MyProfile(ctx, tc.ID) + if tc.WantErr { + assert.Error(t, getErr, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) + assert.NotNil(t, user, consts.ShouldNotErr, tc.Name, headerTestName) + } +} + +func TestUpdateProfile(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + type testCase struct { + Name string + Payload model.UserUpdate + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success Update -1", + Payload: model.UserUpdate{ + ID: validUser.ID, + Name: helper.RandomString(12), + }, + WantErr: false, + }, + { + Name: "Success Update -2", + Payload: model.UserUpdate{ + ID: validUser.ID, + Name: helper.RandomString(6), + }, + WantErr: false, + }, + { + Name: "Failed Update -1: invalid ID", + Payload: model.UserUpdate{ + ID: -1, + Name: helper.RandomString(12), + }, + WantErr: true, + }, + { + Name: "Failed Update -2: invalid ID", + Payload: model.UserUpdate{ + ID: 0, + Name: helper.RandomString(12), + }, + WantErr: true, + }, + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + updateErr := service.UpdateProfile(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, updateErr, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, updateErr, consts.ShouldNotErr, tc.Name, headerTestName) + + user, getErr := repository.GetByID(ctx, tc.Payload.ID) + assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) + assert.NotNil(t, user, consts.ShouldNotNil, tc.Name, headerTestName) + assert.Equal(t, user.Name, tc.Payload.Name, consts.ShouldNotNil, tc.Name, headerTestName) + } +} + +// func TestUpdatePassword(t *testing.T) { +// service := NewUserService() +// assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) +// repository := userRepository +// assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) +// ctx := helper.NewFiberCtx().Context() +// assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + +// validUser := createUser() +// defer repository.Delete(ctx, validUser.ID) + +// type testCase struct { +// Name string +// Payload model.UserPasswordUpdate +// WantErr bool +// } + +// testCases := []testCase{ +// { +// Name: "Success Update -1", +// Payload: model.UserPasswordUpdate{ +// ID: validUser.ID, +// OldPassword: validUser.Password, +// NewPassword: helper.RandomString(16), +// }, +// WantErr: false, +// }, +// { +// Name: "Failed Update -1: wrong password / password is already changed", +// Payload: model.UserPasswordUpdate{ +// ID: validUser.ID, +// OldPassword: validUser.Password, +// NewPassword: helper.RandomString(16), +// }, +// WantErr: true, +// }, +// { +// Name: "Failed Update -2: invalid ID", +// Payload: model.UserPasswordUpdate{ +// ID: -1, +// OldPassword: validUser.Password, +// }, +// WantErr: true, +// }, +// { +// Name: "Failed Update -3: invalid ID", +// Payload: model.UserPasswordUpdate{ +// ID: 0, +// OldPassword: validUser.Password, +// }, +// WantErr: true, +// }, +// } + +// for _, tc := range testCases { +// log.Println(tc.Name, headerTestName) + +// updateErr := service.UpdatePassword(ctx, tc.Payload) +// if tc.WantErr { +// assert.Error(t, updateErr, consts.ShouldErr, tc.Name, headerTestName) +// continue +// } +// assert.NoError(t, updateErr, consts.ShouldNotErr, tc.Name, headerTestName) + +// if tc.Payload.ID == validUser.ID { +// token, loginErr := service.Login(ctx, model.UserLogin{ +// Email: validUser.Email, +// Password: tc.Payload.NewPassword, +// }) +// assert.NoError(t, loginErr, consts.ShouldNotErr, tc.Name, headerTestName) +// assert.True(t, token != "", consts.ShouldNotNil, tc.Name, headerTestName) +// } +// } +// } + +func TestDelete(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + userIDs := make([]int, 2) + for i := range userIDs { + user := createUser() + userIDs[i] = user.ID + defer repository.Delete(ctx, user.ID) + } + + type testCase struct { + Name string + ID int + WantErr bool + } + + testCases := []testCase{ + { + Name: "Failed Delete User -1: invalid ID", + ID: -1, + WantErr: true, + }, + { + Name: "Failed Delete User -2: data not found", + ID: userIDs[0] * 99, + WantErr: true, + }, + } + for i, id := range userIDs { + testCases = append(testCases, testCase{ + Name: "Success Delete User -" + strconv.Itoa(i+1), + ID: id, + WantErr: false, + }) + testCases = append(testCases, testCase{ + Name: "Failed Delete User -" + strconv.Itoa(i+3) + ": already deleted", + ID: id, + WantErr: true, + }) + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + deleteErr := service.Delete(ctx, tc.ID) + if tc.WantErr { + assert.Error(t, deleteErr, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, deleteErr, consts.ShouldNotErr, tc.Name, headerTestName) + + _, getErr := service.MyProfile(ctx, tc.ID) + assert.Error(t, getErr, consts.ShouldErr, tc.Name, headerTestName) + } +} + +func createUser() entity.User { + pw := helper.RandomString(15) + pwHashed, _ := hash.Generate(pw) + repository := userRepository + ctx := helper.NewFiberCtx().Context() + data := entity.User{ + Name: helper.RandomString(15), + Email: helper.RandomEmail(), + Password: pwHashed, + ActivatedAt: &timeNow, + } + data.SetCreateTime() + id, err := repository.Create(ctx, data, []int{1}) + if err != nil { + log.Fatal("failed create a new user", headerTestName) + } + data.Password = pw + data.ID = id + return data +} From ae468ffca466f5d852f8a2d89d06d1005fb1b08e Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Wed, 24 Jan 2024 14:16:24 +0700 Subject: [PATCH 08/28] update user service --- domain/model/user.go | 4 +- repository/user/user_repository.go | 3 +- service/user/user_service.go | 124 ++++++++++++++++------------- service/user/user_service_test.go | 2 +- 4 files changed, 71 insertions(+), 62 deletions(-) diff --git a/domain/model/user.go b/domain/model/user.go index ebda579..9bf37df 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -18,7 +18,7 @@ type UserRegister struct { Name string `validate:"required,min=2,max=60" json:"name"` Email string `validate:"required,email,min=5,max=60" json:"email"` Password string `validate:"required,min=8,max=30" json:"password"` - RolesID []int `validate:"required" json:"role_id"` + RoleIDs []int `validate:"required" json:"role_id"` } type UserActivation struct { @@ -46,7 +46,7 @@ type UserUpdate struct { type UserUpdateRoles struct { ID int `gorm:"type:bigserial;primaryKey" json:"id"` - RolesID []int `validate:"required" json:"role_id"` + RoleIDs []int `validate:"required" json:"role_id"` } type UserForgetPassword struct { diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go index 4a8f98e..c270dd9 100644 --- a/repository/user/user_repository.go +++ b/repository/user/user_repository.go @@ -147,8 +147,7 @@ func (repo *UserRepositoryImpl) Update(ctx context.Context, user entity.User) (e } oldData.Name = user.Name - oldData.ActivatedAt = user.ActivatedAt - oldData.UpdatedAt = user.UpdatedAt + oldData.SetUpdateTime() result = tx.Save(&oldData) if result.Error != nil { return result.Error diff --git a/service/user/user_service.go b/service/user/user_service.go index 35b7452..c3f76c3 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -20,11 +20,13 @@ import ( ) type UserService interface { - GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) Register(ctx context.Context, data model.UserRegister) (id int, err error) Login(ctx context.Context, data model.UserLogin) (token string, err error) Logout(c *fiber.Ctx) (err error) + + GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) MyProfile(ctx context.Context, id int) (profile model.User, err error) + UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) Delete(ctx context.Context, id int) (err error) @@ -52,18 +54,6 @@ func NewUserService() UserService { return userSvcImpl } -func (svc *UserServiceImpl) GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) { - entityUsers, total, getErr := svc.repository.GetAll(ctx, filter) - if getErr != nil { - return nil, 0, getErr - } - for _, entityUser := range entityUsers { - users = append(users, entityToResponse(&entityUser)) - } - - return users, total, err -} - func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegister) (id int, err error) { _, getErr := svc.repository.GetByEmail(ctx, data.Email) if getErr == nil { @@ -78,29 +68,33 @@ func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegiste data.Password = pwHashed entityUser := modelRegisterToEntity(data) entityUser.SetCreateTime() - id, err = svc.repository.Create(ctx, entityUser, []int{1}) + entityUser.ActivatedAt = nil + id, err = svc.repository.Create(ctx, entityUser, data.RoleIDs) if err != nil { - return 0, err + return 0, errors.New("error while storing user data") } return id, nil } func (svc *UserServiceImpl) Login(ctx context.Context, data model.UserLogin) (token string, err error) { user, getErr := svc.repository.GetByEmail(ctx, data.Email) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return "", fiber.NewError(fiber.StatusNotFound, consts.NotFound) - } - return "", getErr + if getErr == gorm.ErrRecordNotFound { + return "", fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if getErr != nil || user == nil { + return "", errors.New("error while getting user data") } res, verifyErr := hash.Verify(user.Password, data.Password) if verifyErr != nil || !res { return "", fiber.NewError(fiber.StatusBadRequest, "wrong password") } + if user.ActivatedAt == nil || user.DeletedAt != nil { + return "", fiber.NewError(fiber.StatusBadRequest, "your account is inactive, please do activation") + } jwtHandler := middleware.NewJWTHandler() - expired := time.Now().Add(4 * 24 * time.Hour) + expired := time.Now().Add(4 * 24 * time.Hour) // 4 days active token, err = jwtHandler.GenerateJWT(user.ID, user.Email, map[string]uint8{}, expired) if err != nil { return "", fiber.NewError(fiber.StatusInternalServerError, err.Error()) @@ -111,60 +105,75 @@ func (svc *UserServiceImpl) Login(ctx context.Context, data model.UserLogin) (to func (svc *UserServiceImpl) Logout(c *fiber.Ctx) (err error) { err = svc.jwtHandler.InvalidateToken(c) if err != nil { - return err + return errors.New("error while logout") } return nil } +func (svc *UserServiceImpl) GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) { + entityUsers, total, err := svc.repository.GetAll(ctx, filter) + if err != nil { + return nil, 0, err + } + + for _, entityUser := range entityUsers { + users = append(users, entityToResponse(&entityUser)) + } + return users, total, nil +} + func (svc *UserServiceImpl) MyProfile(ctx context.Context, id int) (profile model.User, err error) { - entityUser, getErr := svc.repository.GetByID(ctx, id) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return profile, fiber.NewError(fiber.StatusNotFound, consts.NotFound) - } - return profile, getErr + user, getErr := svc.repository.GetByID(ctx, id) + if getErr == gorm.ErrRecordNotFound { + return model.User{}, fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if getErr != nil || user == nil { + return model.User{}, errors.New("error while getting user data") + } + if user.ActivatedAt == nil || user.DeletedAt != nil { + return model.User{}, fiber.NewError(fiber.StatusBadRequest, "your account is inactive, please do activation") } - profile = entityToResponse(entityUser) + profile = entityToResponse(user) return profile, nil } func (svc *UserServiceImpl) UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) { - _, getErr := svc.repository.GetByID(ctx, data.ID) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, consts.NotFound) - } - return getErr + user, getErr := svc.repository.GetByID(ctx, data.ID) + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if getErr != nil || user == nil { + return errors.New("error while getting user data") + } + if user.ActivatedAt == nil || user.DeletedAt != nil { + return fiber.NewError(fiber.StatusBadRequest, "your account is inactive, please do activation") } - user := modelUpdateToEntity(data) - user.SetUpdateTime() - err = svc.repository.Update(ctx, user) + enttUser := modelUpdateToEntity(data) + err = svc.repository.Update(ctx, enttUser) if err != nil { - return err + return errors.New("error while updating user data") } return nil } func (svc *UserServiceImpl) UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) { user, getErr := svc.repository.GetByID(ctx, data.ID) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, consts.NotFound) - } - return getErr + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if getErr != nil || user == nil { + return errors.New("error while getting user data") + } + if user.ActivatedAt == nil || user.DeletedAt != nil { + return fiber.NewError(fiber.StatusBadRequest, "your account is inactive, please do activation") } res, verifyErr := hash.Verify(user.Password, data.OldPassword) if verifyErr != nil || !res { return fiber.NewError(fiber.StatusBadRequest, "wrong password, failed to update") } - - if user.ActivatedAt != nil || user.DeletedAt != nil { - return fiber.NewError(fiber.StatusBadRequest, "your account is inactive") - } - pwHashed, hashErr := hash.Generate(data.NewPassword) if hashErr != nil { return errors.New(consts.ErrHashing) @@ -172,22 +181,23 @@ func (svc *UserServiceImpl) UpdatePassword(ctx context.Context, data model.UserP updateErr := svc.repository.UpdatePassword(ctx, data.ID, pwHashed) if updateErr != nil { - return updateErr + return errors.New("error while updating user password") } return nil } func (svc *UserServiceImpl) Delete(ctx context.Context, id int) (err error) { - _, getErr := svc.repository.GetByID(ctx, id) - if getErr != nil { - if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, consts.NotFound) - } - return getErr + user, getErr := svc.repository.GetByID(ctx, id) + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) } + if getErr != nil || user == nil { + return errors.New("error while getting user data") + } + err = svc.repository.Delete(ctx, id) if err != nil { - return err + return errors.New("error while deleting user password") } return nil } diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go index a1957e0..39e7e69 100644 --- a/service/user/user_service_test.go +++ b/service/user/user_service_test.go @@ -323,7 +323,7 @@ func TestUpdateProfile(t *testing.T) { Name: "Success Update -1", Payload: model.UserUpdate{ ID: validUser.ID, - Name: helper.RandomString(12), + Name: helper.RandomString(12) + "xxxx", }, WantErr: false, }, From caf0cc302b5604613606503434e876e008e8bda9 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Wed, 24 Jan 2024 17:18:33 +0700 Subject: [PATCH 09/28] add role service --- service/role/role_service.go | 147 +++++++++++++++++ service/role/role_service_test.go | 259 ++++++++++++++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 service/role/role_service.go create mode 100644 service/role/role_service_test.go diff --git a/service/role/role_service.go b/service/role/role_service.go new file mode 100644 index 0000000..f677209 --- /dev/null +++ b/service/role/role_service.go @@ -0,0 +1,147 @@ +package service + +import ( + "context" + "errors" + "strings" + "sync" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/consts" + repository "github.com/Lukmanern/gost/repository/role" +) + +type RoleService interface { + + // Create func create one role. + Create(ctx context.Context, data model.RoleCreate) (id int, err error) + + // GetByID func get one role. + GetByID(ctx context.Context, id int) (role *entity.Role, err error) + + // GetAll func get some roles. + GetAll(ctx context.Context, filter model.RequestGetAll) (roles []model.RoleResponse, total int, err error) + + // Update func update one role. + Update(ctx context.Context, data model.RoleUpdate) (err error) + + // Delete func delete one role. + Delete(ctx context.Context, id int) (err error) +} + +type RoleServiceImpl struct { + repository repository.RoleRepository +} + +var ( + roleServiceImpl *RoleServiceImpl + roleServiceImplOnce sync.Once +) + +func NewRoleService() RoleService { + roleServiceImplOnce.Do(func() { + roleServiceImpl = &RoleServiceImpl{ + repository: repository.NewRoleRepository(), + } + }) + return roleServiceImpl +} + +func (svc *RoleServiceImpl) Create(ctx context.Context, data model.RoleCreate) (id int, err error) { + data.Name = strings.ToLower(data.Name) + role, getErr := svc.repository.GetByName(ctx, data.Name) + if getErr == nil || role != nil { + return 0, fiber.NewError(fiber.StatusBadRequest, "role name has been used") + } + + entityRole := entity.Role{ + Name: data.Name, + Description: data.Description, + } + entityRole.SetCreateTime() + id, err = svc.repository.Create(ctx, entityRole) + if err != nil { + return 0, err + } + return id, nil +} + +func (svc *RoleServiceImpl) GetByID(ctx context.Context, id int) (role *entity.Role, err error) { + role, err = svc.repository.GetByID(ctx, id) + if err == gorm.ErrRecordNotFound { + return nil, fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if err != nil || role == nil { + return nil, errors.New("error while getting role data") + } + return role, nil +} + +func (svc *RoleServiceImpl) GetAll(ctx context.Context, filter model.RequestGetAll) (roles []model.RoleResponse, total int, err error) { + roleEntities, total, err := svc.repository.GetAll(ctx, filter) + if err != nil { + return nil, 0, err + } + + roles = []model.RoleResponse{} + for _, roleEntity := range roleEntities { + newRole := model.RoleResponse{ + ID: roleEntity.ID, + Name: roleEntity.Name, + Description: roleEntity.Description, + } + roles = append(roles, newRole) + } + return roles, total, nil +} + +func (svc *RoleServiceImpl) Update(ctx context.Context, data model.RoleUpdate) (err error) { + data.Name = strings.ToLower(data.Name) + roleByName, getErr := svc.repository.GetByName(ctx, data.Name) + if getErr != nil && getErr != gorm.ErrRecordNotFound { + return getErr + } + if roleByName != nil && roleByName.ID != data.ID { + return fiber.NewError(fiber.StatusBadRequest, "role name has been used") + } + + role, err := svc.repository.GetByID(ctx, data.ID) + if err == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound) + } + if err != nil || role == nil { + return errors.New("error while getting role data") + } + + entityRole := entity.Role{ + ID: data.ID, + Name: data.Name, + Description: data.Description, + } + entityRole.SetUpdateTime() + err = svc.repository.Update(ctx, entityRole) + if err != nil { + return err + } + return nil +} + +func (svc *RoleServiceImpl) Delete(ctx context.Context, id int) (err error) { + role, err := svc.repository.GetByID(ctx, id) + if err == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound) + } + if err != nil || role == nil { + return errors.New("error while getting role data") + } + + err = svc.repository.Delete(ctx, id) + if err != nil { + return err + } + return nil +} diff --git a/service/role/role_service_test.go b/service/role/role_service_test.go new file mode 100644 index 0000000..8b550fe --- /dev/null +++ b/service/role/role_service_test.go @@ -0,0 +1,259 @@ +package service + +import ( + "log" + "strings" + "testing" + "time" + + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/env" + "github.com/Lukmanern/gost/internal/helper" + repository "github.com/Lukmanern/gost/repository/role" + "github.com/stretchr/testify/assert" +) + +const ( + headerTestName string = "at RoleServiceTest" +) + +var ( + timeNow time.Time + roleRepository repository.RoleRepository +) + +func init() { + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) + timeNow = time.Now() + roleRepository = repository.NewRoleRepository() +} + +// type Role struct { +// ID int `gorm:"type:serial;primaryKey" json:"id"` +// Name string `gorm:"type:varchar(255) not null unique" json:"name"` +// Description string `gorm:"type:varchar(255) not null" json:"description"` +// TimeFields +// } + +func TestCreate(t *testing.T) { + service := NewRoleService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := roleRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + role := createRole() + defer repository.Delete(ctx, role.ID) + + type testCase struct { + Name string + Payload model.RoleCreate + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success Create Role -1", + Payload: model.RoleCreate{ + Name: strings.ToLower(helper.RandomString(6)), + Description: helper.RandomWords(8), + }, + WantErr: false, + }, + { + Name: "Success Create Role -2", + Payload: model.RoleCreate{ + Name: strings.ToLower(helper.RandomString(6)), + Description: helper.RandomWords(8), + }, + WantErr: false, + }, + { + Name: "Failed Create Role -2: name has been used", + Payload: model.RoleCreate{ + Name: role.Name, + Description: helper.RandomWords(8), + }, + WantErr: true, + }, + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + id, err := service.Create(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, err, consts.ShouldNotErr, tc.Name, headerTestName) + + // expect no error + role, getErr := service.GetByID(ctx, id) + assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) + assert.Equal(t, role.Name, tc.Payload.Name, tc.Name, headerTestName) + assert.Equal(t, role.Description, tc.Payload.Description, tc.Name, headerTestName) + + deleteErr := service.Delete(ctx, id) + assert.NoError(t, deleteErr, consts.ShouldNotErr, tc.Name, headerTestName) + + // expect error + _, getErr = service.GetByID(ctx, id) + assert.Error(t, getErr, consts.ShouldErr, tc.Name, headerTestName) + + deleteErr = service.Delete(ctx, id) + assert.Error(t, deleteErr, consts.ShouldErr, tc.Name, headerTestName) + + } +} + +func TestGetAll(t *testing.T) { + service := NewRoleService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := roleRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + totalRoleCreated := 5 + + for i := 0; i < totalRoleCreated; i++ { + role := createRole() + defer repository.Delete(ctx, role.ID) + } + + type testCase struct { + Name string + Payload model.RequestGetAll + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success Get All Role -1", + Payload: model.RequestGetAll{ + Page: 1, + Limit: 100, + }, + WantErr: false, + }, + { + Name: "Success Get All Role -2", + Payload: model.RequestGetAll{ + Page: 1, + Limit: 10 + totalRoleCreated, + }, + WantErr: false, + }, + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + roles, total, err := service.GetAll(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, err, consts.ShouldNotErr, tc.Name, headerTestName) + assert.True(t, len(roles) >= totalRoleCreated, consts.ShouldNotErr, tc.Name, headerTestName) + assert.True(t, total >= totalRoleCreated, consts.ShouldNotErr, tc.Name, headerTestName) + } +} + +func TestUpdate(t *testing.T) { + service := NewRoleService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := roleRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + role := createRole() + defer repository.Delete(ctx, role.ID) + + type testCase struct { + Name string + Payload model.RoleUpdate + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success Update Role -1", + Payload: model.RoleUpdate{ + ID: role.ID, + Name: strings.ToLower(helper.RandomString(5)), + Description: helper.RandomWords(8), + }, + WantErr: false, + }, + { + Name: "Success Update Role -2", + Payload: model.RoleUpdate{ + ID: role.ID, + Name: strings.ToLower(helper.RandomString(5)), + Description: helper.RandomWords(8), + }, + WantErr: false, + }, + { + Name: "Failed Update Role -1: invalid ID", + Payload: model.RoleUpdate{ + ID: -10, + }, + WantErr: true, + }, + { + Name: "Failed Update Role -2: role not found", + Payload: model.RoleUpdate{ + ID: role.ID + 999, + }, + WantErr: true, + }, + { + Name: "Failed Update Role -3: name has been used", + Payload: model.RoleUpdate{ + ID: role.ID, + Name: "admin", + }, + WantErr: true, + }, + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + err := service.Update(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, err, consts.ShouldNotErr, tc.Name, headerTestName) + + role, err := service.GetByID(ctx, tc.Payload.ID) + assert.NoError(t, err, consts.ShouldNotErr, tc.Name, headerTestName) + assert.Equal(t, role.Name, tc.Payload.Name, consts.ShouldNotErr, tc.Name, headerTestName) + assert.Equal(t, role.Description, tc.Payload.Description, consts.ShouldNotErr, tc.Name, headerTestName) + } +} + +func createRole() entity.Role { + repository := roleRepository + ctx := helper.NewFiberCtx().Context() + role := entity.Role{ + Name: strings.ToLower(helper.RandomString(15)), + Description: helper.RandomWords(8), + } + role.SetCreateTime() + id, err := repository.Create(ctx, role) + if err != nil { + log.Fatal("failed create a new user", headerTestName) + } + role.ID = id + return role +} From a62151d49741b8008367c0ef344c6ca877f87ea3 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Thu, 25 Jan 2024 06:57:50 +0700 Subject: [PATCH 10/28] update service and test --- service/role/role_service.go | 59 +++++++++++++++++++------------ service/user/user_service.go | 17 +++++++-- service/user/user_service_test.go | 10 ++++++ 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/service/role/role_service.go b/service/role/role_service.go index f677209..0abd454 100644 --- a/service/role/role_service.go +++ b/service/role/role_service.go @@ -21,7 +21,7 @@ type RoleService interface { Create(ctx context.Context, data model.RoleCreate) (id int, err error) // GetByID func get one role. - GetByID(ctx context.Context, id int) (role *entity.Role, err error) + GetByID(ctx context.Context, id int) (role model.RoleResponse, err error) // GetAll func get some roles. GetAll(ctx context.Context, filter model.RequestGetAll) (roles []model.RoleResponse, total int, err error) @@ -58,10 +58,7 @@ func (svc *RoleServiceImpl) Create(ctx context.Context, data model.RoleCreate) ( return 0, fiber.NewError(fiber.StatusBadRequest, "role name has been used") } - entityRole := entity.Role{ - Name: data.Name, - Description: data.Description, - } + entityRole := modelCreateToEntity(data) entityRole.SetCreateTime() id, err = svc.repository.Create(ctx, entityRole) if err != nil { @@ -70,31 +67,27 @@ func (svc *RoleServiceImpl) Create(ctx context.Context, data model.RoleCreate) ( return id, nil } -func (svc *RoleServiceImpl) GetByID(ctx context.Context, id int) (role *entity.Role, err error) { - role, err = svc.repository.GetByID(ctx, id) +func (svc *RoleServiceImpl) GetByID(ctx context.Context, id int) (role model.RoleResponse, err error) { + enttRole, err := svc.repository.GetByID(ctx, id) if err == gorm.ErrRecordNotFound { - return nil, fiber.NewError(fiber.StatusNotFound, consts.NotFound) + return role, fiber.NewError(fiber.StatusNotFound, consts.NotFound) } - if err != nil || role == nil { - return nil, errors.New("error while getting role data") + if err != nil || enttRole == nil { + return role, errors.New("error while getting role data") } + role = entityToResponse(enttRole) return role, nil } func (svc *RoleServiceImpl) GetAll(ctx context.Context, filter model.RequestGetAll) (roles []model.RoleResponse, total int, err error) { - roleEntities, total, err := svc.repository.GetAll(ctx, filter) + enttRoles, total, err := svc.repository.GetAll(ctx, filter) if err != nil { return nil, 0, err } roles = []model.RoleResponse{} - for _, roleEntity := range roleEntities { - newRole := model.RoleResponse{ - ID: roleEntity.ID, - Name: roleEntity.Name, - Description: roleEntity.Description, - } - roles = append(roles, newRole) + for _, enttRole := range enttRoles { + roles = append(roles, entityToResponse(&enttRole)) } return roles, total, nil } @@ -117,11 +110,7 @@ func (svc *RoleServiceImpl) Update(ctx context.Context, data model.RoleUpdate) ( return errors.New("error while getting role data") } - entityRole := entity.Role{ - ID: data.ID, - Name: data.Name, - Description: data.Description, - } + entityRole := modelUpdateToEntity(data) entityRole.SetUpdateTime() err = svc.repository.Update(ctx, entityRole) if err != nil { @@ -145,3 +134,27 @@ func (svc *RoleServiceImpl) Delete(ctx context.Context, id int) (err error) { } return nil } + +func modelCreateToEntity(data model.RoleCreate) entity.Role { + return entity.Role{ + Name: data.Name, + Description: data.Description, + } +} + +func modelUpdateToEntity(data model.RoleUpdate) entity.Role { + return entity.Role{ + ID: data.ID, + Name: data.Name, + Description: data.Description, + } +} + +func entityToResponse(data *entity.Role) model.RoleResponse { + return model.RoleResponse{ + ID: data.ID, + Name: data.Name, + Description: data.Description, + TimeFields: data.TimeFields, + } +} diff --git a/service/user/user_service.go b/service/user/user_service.go index c3f76c3..fefdb42 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -13,6 +13,7 @@ import ( "github.com/Lukmanern/gost/internal/consts" "github.com/Lukmanern/gost/internal/hash" "github.com/Lukmanern/gost/internal/middleware" + roleRepository "github.com/Lukmanern/gost/repository/role" repository "github.com/Lukmanern/gost/repository/user" "github.com/go-redis/redis" "github.com/gofiber/fiber/v2" @@ -33,9 +34,10 @@ type UserService interface { } type UserServiceImpl struct { + redis *redis.Client jwtHandler *middleware.JWTHandler repository repository.UserRepository - redis *redis.Client + roleRepo roleRepository.RoleRepository } var ( @@ -46,9 +48,10 @@ var ( func NewUserService() UserService { userSvcImplOnce.Do(func() { userSvcImpl = &UserServiceImpl{ + redis: connector.LoadRedisCache(), jwtHandler: middleware.NewJWTHandler(), repository: repository.NewUserRepository(), - redis: connector.LoadRedisCache(), + roleRepo: roleRepository.NewRoleRepository(), } }) return userSvcImpl @@ -60,6 +63,16 @@ func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegiste return 0, fiber.NewError(fiber.StatusBadRequest, "email already used") } + for _, roleID := range data.RoleIDs { + enttRole, err := svc.repository.GetByID(ctx, roleID) + if err == gorm.ErrRecordNotFound { + return 0, fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if err != nil || enttRole == nil { + return 0, errors.New("error while getting role data") + } + } + pwHashed, hashErr := hash.Generate(data.Password) if hashErr != nil { return 0, errors.New(consts.ErrHashing) diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go index 39e7e69..3fc6b6e 100644 --- a/service/user/user_service_test.go +++ b/service/user/user_service_test.go @@ -59,6 +59,16 @@ func TestRegister(t *testing.T) { }, WantErr: true, }, + { + Name: "Failed Create -2: invalid role ID", + Payload: model.UserRegister{ + Name: helper.RandomString(15), + Email: helper.RandomEmail(), + Password: "password00", + RoleIDs: []int{-1, 0}, + }, + WantErr: true, + }, { Name: "Success Create -1", Payload: model.UserRegister{ From 489e0b58b1f85874dc88f61f5f15f2c6c7a65788 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Fri, 26 Jan 2024 14:48:16 +0700 Subject: [PATCH 11/28] add role controller --- controller/role/role_controller.go | 178 +++++++++++++++ controller/user/user_controller.txt | 339 ++++++++++++++++++++++++++++ go.mod | 8 + go.sum | 15 ++ internal/consts/consts.go | 1 + internal/response/response.go | 4 +- internal/response/response_test.go | 2 +- service/user/user_service.go | 18 ++ 8 files changed, 562 insertions(+), 3 deletions(-) create mode 100644 controller/role/role_controller.go create mode 100644 controller/user/user_controller.txt diff --git a/controller/role/role_controller.go b/controller/role/role_controller.go new file mode 100644 index 0000000..ea83695 --- /dev/null +++ b/controller/role/role_controller.go @@ -0,0 +1,178 @@ +package controller + +import ( + "math" + "sync" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/response" + service "github.com/Lukmanern/gost/service/role" +) + +type RoleController interface { + + // Create func creates a new role + Create(c *fiber.Ctx) error + + // Get func gets a role + Get(c *fiber.Ctx) error + + // GetAll func gets some roles + GetAll(c *fiber.Ctx) error + + // Update func updates a role + Update(c *fiber.Ctx) error + + // Delete func deletes a role + Delete(c *fiber.Ctx) error +} + +type RoleControllerImpl struct { + service service.RoleService +} + +var ( + roleControllerImpl *RoleControllerImpl + roleControllerImplOnce sync.Once +) + +func NewRoleController(service service.RoleService) RoleController { + roleControllerImplOnce.Do(func() { + roleControllerImpl = &RoleControllerImpl{ + service: service, + } + }) + return roleControllerImpl +} + +func (ctr *RoleControllerImpl) Create(c *fiber.Ctx) error { + var role model.RoleCreate + if err := c.BodyParser(&role); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + validate := validator.New() + if err := validate.Struct(&role); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + + ctx := c.Context() + id, createErr := ctr.service.Create(ctx, role) + if createErr != nil { + fiberErr, ok := createErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) + } + return response.Error(c, consts.ErrServer+createErr.Error()) + } + data := map[string]any{ + "id": id, + } + return response.SuccessCreated(c, data) +} + +func (ctr *RoleControllerImpl) Get(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + return response.BadRequest(c, consts.InvalidID) + } + + ctx := c.Context() + role, getErr := ctr.service.GetByID(ctx, id) + if getErr != nil { + fiberErr, ok := getErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) + } + return response.Error(c, consts.ErrServer+getErr.Error()) + } + return response.SuccessLoaded(c, role) +} + +func (ctr *RoleControllerImpl) GetAll(c *fiber.Ctx) error { + request := model.RequestGetAll{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 20), + Keyword: c.Query("search"), + Sort: c.Query("sort"), + } + if request.Page <= 0 || request.Limit <= 0 { + return response.BadRequest(c, "invalid page or limit value") + } + + ctx := c.Context() + roles, total, getErr := ctr.service.GetAll(ctx, request) + if getErr != nil { + return response.Error(c, consts.ErrServer+getErr.Error()) + } + + data := make([]interface{}, len(roles)) + for i := range roles { + data[i] = roles[i] + } + responseData := model.GetAllResponse{ + Meta: model.PageMeta{ + TotalData: total, + TotalPages: int(math.Ceil(float64(total) / float64(request.Limit))), + AtPage: request.Page, + }, + Data: data, + } + return response.SuccessLoaded(c, responseData) +} + +func (ctr *RoleControllerImpl) Update(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + return response.BadRequest(c, consts.InvalidID) + } + var role model.RoleUpdate + role.ID = id + if err := c.BodyParser(&role); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + validate := validator.New() + if err := validate.Struct(&role); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + + ctx := c.Context() + updateErr := ctr.service.Update(ctx, role) + if updateErr != nil { + fiberErr, ok := updateErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) + } + return response.Error(c, consts.ErrServer+updateErr.Error()) + } + return response.SuccessNoContent(c) +} + +func (ctr *RoleControllerImpl) Delete(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + return response.BadRequest(c, consts.InvalidID) + } + + ctx := c.Context() + deleteErr := ctr.service.Delete(ctx, id) + if deleteErr != nil { + fiberErr, ok := deleteErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) + } + return response.Error(c, consts.ErrServer+deleteErr.Error()) + } + return response.SuccessNoContent(c) +} diff --git a/controller/user/user_controller.txt b/controller/user/user_controller.txt new file mode 100644 index 0000000..cc80233 --- /dev/null +++ b/controller/user/user_controller.txt @@ -0,0 +1,339 @@ +package controller + +import ( + "net" + "strings" + "sync" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/middleware" + "github.com/Lukmanern/gost/internal/response" + service "github.com/Lukmanern/gost/service/user" +) + +type UserController interface { + // Register function register user account, + // than send verification-code to email + Register(c *fiber.Ctx) error + + // AccountActivation function activates user account with + // verification code that has been sended to the user's email + AccountActivation(c *fiber.Ctx) error + + // DeleteUserByVerification function deletes user data if the + // user account is not yet verified. This implies that the email + // owner hasn't actually registered the email, indicating that + // the user who registered may be making typing errors or may + // be a hacker attempting to get the verification code. + DeleteAccountActivation(c *fiber.Ctx) error + + // ForgetPassword function send + // verification code into user's email + ForgetPassword(c *fiber.Ctx) error + + // ResetPassword func resets password by creating + // new password by email and verification code + ResetPassword(c *fiber.Ctx) error + + // Login func gives token and access to user + Login(c *fiber.Ctx) error + + // Logout func stores user's token into Redis + Logout(c *fiber.Ctx) error + + // UpdatePassword func updates user's password + UpdatePassword(c *fiber.Ctx) error + + // UpdateProfile func updates user's profile data + UpdateProfile(c *fiber.Ctx) error + + // MyProfile func shows user's profile data + MyProfile(c *fiber.Ctx) error +} + +type UserControllerImpl struct { + service service.UserService +} + +var ( + userController *UserControllerImpl + userControllerOnce sync.Once +) + +func NewUserController(service service.UserService) UserController { + userControllerOnce.Do(func() { + userController = &UserControllerImpl{ + service: service, + } + }) + + return userController +} + +func (ctr *UserControllerImpl) Register(c *fiber.Ctx) error { + var user model.UserRegister + if err := c.BodyParser(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + user.Email = strings.ToLower(user.Email) + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + + ctx := c.Context() + id, regisErr := ctr.service.Register(ctx, user) + if regisErr != nil { + fiberErr, ok := regisErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + } + return response.Error(c, consts.ErrServer+regisErr.Error()) + } + + message := "Account success created. please check " + user.Email + " " + message += "inbox, our system has sended verification code or link." + data := map[string]any{ + "id": id, + } + return response.CreateResponse(c, fiber.StatusCreated, true, message, data) +} + +func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { + var user model.UserVerificationCode + if err := c.BodyParser(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + ctx := c.Context() + err := ctr.service.Verification(ctx, user) + if err != nil { + fiberErr, ok := err.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + } + return response.Error(c, consts.ErrServer+err.Error()) + } + + message := "Thank you for your confirmation. Your account is active now, you can login." + return response.CreateResponse(c, fiber.StatusOK, true, message, nil) +} + +func (ctr *UserControllerImpl) DeleteAccountActivation(c *fiber.Ctx) error { + var verifyData model.UserVerificationCode + if err := c.BodyParser(&verifyData); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + validate := validator.New() + if err := validate.Struct(&verifyData); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + ctx := c.Context() + err := ctr.service.DeleteUserByVerification(ctx, verifyData) + if err != nil { + fiberErr, ok := err.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + } + return response.Error(c, consts.ErrServer+err.Error()) + } + + message := "Your data is already deleted, thank you for your confirmation." + return response.CreateResponse(c, fiber.StatusOK, true, message, nil) +} + +func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { + var user model.UserLogin + // user.IP = c.IP() // Note : uncomment this line in production + if err := c.BodyParser(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + + userIP := net.ParseIP(user.IP) + if userIP == nil { + return response.BadRequest(c, consts.InvalidJSONBody+"invalid user ip address") + } + counter, _ := ctr.service.FailedLoginCounter(userIP.String(), false) + ipBlockMsg := "Your IP has been blocked by system. Please try again in 1 or 2 Hour" + if counter >= 5 { + return response.CreateResponse(c, fiber.StatusBadRequest, false, ipBlockMsg, nil) + } + + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + + ctx := c.Context() + token, loginErr := ctr.service.Login(ctx, user) + if loginErr != nil { + counter, _ := ctr.service.FailedLoginCounter(userIP.String(), true) + if counter >= 5 { + return response.CreateResponse(c, fiber.StatusBadRequest, false, ipBlockMsg, nil) + } + fiberErr, ok := loginErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + } + return response.Error(c, consts.ErrServer+loginErr.Error()) + } + + data := map[string]any{ + "token": token, + "token-length": len(token), + } + return response.CreateResponse(c, fiber.StatusOK, true, "success login", data) +} + +func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + logoutErr := ctr.service.Logout(c) + if logoutErr != nil { + return response.Error(c, consts.ErrServer+logoutErr.Error()) + } + return response.CreateResponse(c, fiber.StatusOK, true, "success logout", nil) +} + +func (ctr *UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { + var user model.UserForgetPassword + if err := c.BodyParser(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + + ctx := c.Context() + forgetErr := ctr.service.ForgetPassword(ctx, user) + if forgetErr != nil { + fiberErr, ok := forgetErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + } + return response.Error(c, consts.ErrServer+forgetErr.Error()) + } + + message := "success sending link for reset password to email, check your email inbox" + return response.CreateResponse(c, fiber.StatusAccepted, true, message, nil) +} + +func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { + var user model.UserResetPassword + if err := c.BodyParser(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + if user.NewPassword != user.NewPasswordConfirm { + return response.BadRequest(c, "password confirmation not match") + } + + ctx := c.Context() + resetErr := ctr.service.ResetPassword(ctx, user) + if resetErr != nil { + fiberErr, ok := resetErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + } + return response.Error(c, consts.ErrServer+resetErr.Error()) + } + + message := "your password already updated, you can login with your new password, thank you" + return response.CreateResponse(c, fiber.StatusAccepted, true, message, nil) +} + +func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + + var user model.UserPasswordUpdate + if err := c.BodyParser(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + user.ID = userClaims.ID + + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + if user.NewPassword != user.NewPasswordConfirm { + return response.BadRequest(c, "new password confirmation is wrong") + } + if user.NewPassword == user.OldPassword { + return response.BadRequest(c, "no new password, try another new password") + } + + ctx := c.Context() + updateErr := ctr.service.UpdatePassword(ctx, user) + if updateErr != nil { + fiberErr, ok := updateErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + } + return response.Error(c, consts.ErrServer+updateErr.Error()) + } + + return response.SuccessNoContent(c) +} + +func (ctr *UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + + var user model.UserProfileUpdate + if err := c.BodyParser(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + user.ID = userClaims.ID + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + + ctx := c.Context() + updateErr := ctr.service.UpdateProfile(ctx, user) + if updateErr != nil { + fiberErr, ok := updateErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + } + return response.Error(c, consts.ErrServer+updateErr.Error()) + } + + return response.SuccessNoContent(c) +} + +func (ctr *UserControllerImpl) MyProfile(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + + ctx := c.Context() + userProfile, getErr := ctr.service.MyProfile(ctx, userClaims.ID) + if getErr != nil { + fiberErr, ok := getErr.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + } + return response.Error(c, consts.ErrServer+getErr.Error()) + } + return response.SuccessLoaded(c, userProfile) +} diff --git a/go.mod b/go.mod index 278a207..c59dcfa 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,18 @@ require ( gorm.io/gorm v1.25.4 ) +require ( + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect +) + require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-playground/validator/v10 v10.17.0 github.com/google/uuid v1.3.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect diff --git a/go.sum b/go.sum index 02fdf22..bee1bd1 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,14 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= +github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -51,6 +59,8 @@ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -76,9 +86,14 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 40c769a..cfb8059 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -14,6 +14,7 @@ const ( NilValue = "error nil value" InvalidToken = "invalid token / JWT, please logout and try-login" ErrHashing = "error while hashing password" + ErrServer = "internal server error" ) const ( diff --git a/internal/response/response.go b/internal/response/response.go index 826525a..24acf2d 100644 --- a/internal/response/response.go +++ b/internal/response/response.go @@ -52,9 +52,9 @@ func SuccessCreated(c *fiber.Ctx, data interface{}) error { } // BadRequest formats a response with HTTP status 400. -func BadRequest(c *fiber.Ctx) error { +func BadRequest(c *fiber.Ctx, message string) error { return CreateResponse(c, fiber.StatusBadRequest, Response{ - Message: consts.BadRequest, + Message: message, Success: false, Data: nil, }) diff --git a/internal/response/response_test.go b/internal/response/response_test.go index 4679d99..973335d 100644 --- a/internal/response/response_test.go +++ b/internal/response/response_test.go @@ -185,7 +185,7 @@ func TestBadRequest(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := BadRequest(tt.args.c); (err != nil) != tt.wantErr { + if err := BadRequest(tt.args.c, tt.args.message); (err != nil) != tt.wantErr { t.Errorf("BadRequest() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/service/user/user_service.go b/service/user/user_service.go index fefdb42..b785974 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -28,6 +28,13 @@ type UserService interface { GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) MyProfile(ctx context.Context, id int) (profile model.User, err error) + // ForgetPassword func send verification code into user's email + ForgetPassword(ctx context.Context, user model.UserForgetPassword) (err error) + + // ResetPassword func resets password by creating + // new password by email and verification code + ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) + UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) Delete(ctx context.Context, id int) (err error) @@ -57,6 +64,17 @@ func NewUserService() UserService { return userSvcImpl } +// ForgetPassword func send verification code into user's email +func (svg *UserServiceImpl) ForgetPassword(ctx context.Context, user model.UserForgetPassword) (err error) { + return nil +} + +// ResetPassword func resets password by creating +// new password by email and verification code +func (svg *UserServiceImpl) ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) { + return nil +} + func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegister) (id int, err error) { _, getErr := svc.repository.GetByEmail(ctx, data.Email) if getErr == nil { From ba8d7eb9b8a0020238f5e0bbee427814f39585f6 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Fri, 26 Jan 2024 21:11:11 +0700 Subject: [PATCH 12/28] add user controller --- ...user_controller.txt => user_controller.go} | 111 ++++++++---------- service/user/user_service.go | 5 + 2 files changed, 54 insertions(+), 62 deletions(-) rename controller/user/{user_controller.txt => user_controller.go} (69%) diff --git a/controller/user/user_controller.txt b/controller/user/user_controller.go similarity index 69% rename from controller/user/user_controller.txt rename to controller/user/user_controller.go index cc80233..1fc48f8 100644 --- a/controller/user/user_controller.txt +++ b/controller/user/user_controller.go @@ -1,7 +1,6 @@ package controller import ( - "net" "strings" "sync" @@ -24,13 +23,6 @@ type UserController interface { // verification code that has been sended to the user's email AccountActivation(c *fiber.Ctx) error - // DeleteUserByVerification function deletes user data if the - // user account is not yet verified. This implies that the email - // owner hasn't actually registered the email, indicating that - // the user who registered may be making typing errors or may - // be a hacker attempting to get the verification code. - DeleteAccountActivation(c *fiber.Ctx) error - // ForgetPassword function send // verification code into user's email ForgetPassword(c *fiber.Ctx) error @@ -90,7 +82,9 @@ func (ctr *UserControllerImpl) Register(c *fiber.Ctx) error { if regisErr != nil { fiberErr, ok := regisErr.(*fiber.Error) if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } return response.Error(c, consts.ErrServer+regisErr.Error()) } @@ -100,11 +94,11 @@ func (ctr *UserControllerImpl) Register(c *fiber.Ctx) error { data := map[string]any{ "id": id, } - return response.CreateResponse(c, fiber.StatusCreated, true, message, data) + return response.SuccessCreated(c, data) } func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { - var user model.UserVerificationCode + var user model.UserActivation if err := c.BodyParser(&user); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } @@ -113,40 +107,23 @@ func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } ctx := c.Context() - err := ctr.service.Verification(ctx, user) + err := ctr.service.AccountActivation(ctx, user) if err != nil { fiberErr, ok := err.(*fiber.Error) if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } return response.Error(c, consts.ErrServer+err.Error()) } message := "Thank you for your confirmation. Your account is active now, you can login." - return response.CreateResponse(c, fiber.StatusOK, true, message, nil) -} - -func (ctr *UserControllerImpl) DeleteAccountActivation(c *fiber.Ctx) error { - var verifyData model.UserVerificationCode - if err := c.BodyParser(&verifyData); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) - } - validate := validator.New() - if err := validate.Struct(&verifyData); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) - } - ctx := c.Context() - err := ctr.service.DeleteUserByVerification(ctx, verifyData) - if err != nil { - fiberErr, ok := err.(*fiber.Error) - if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) - } - return response.Error(c, consts.ErrServer+err.Error()) - } - - message := "Your data is already deleted, thank you for your confirmation." - return response.CreateResponse(c, fiber.StatusOK, true, message, nil) + return response.CreateResponse(c, fiber.StatusOK, response.Response{ + Message: message, + Success: true, + Data: nil, + }) } func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { @@ -156,16 +133,6 @@ func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } - userIP := net.ParseIP(user.IP) - if userIP == nil { - return response.BadRequest(c, consts.InvalidJSONBody+"invalid user ip address") - } - counter, _ := ctr.service.FailedLoginCounter(userIP.String(), false) - ipBlockMsg := "Your IP has been blocked by system. Please try again in 1 or 2 Hour" - if counter >= 5 { - return response.CreateResponse(c, fiber.StatusBadRequest, false, ipBlockMsg, nil) - } - validate := validator.New() if err := validate.Struct(&user); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) @@ -174,13 +141,11 @@ func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { ctx := c.Context() token, loginErr := ctr.service.Login(ctx, user) if loginErr != nil { - counter, _ := ctr.service.FailedLoginCounter(userIP.String(), true) - if counter >= 5 { - return response.CreateResponse(c, fiber.StatusBadRequest, false, ipBlockMsg, nil) - } fiberErr, ok := loginErr.(*fiber.Error) if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } return response.Error(c, consts.ErrServer+loginErr.Error()) } @@ -189,7 +154,11 @@ func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { "token": token, "token-length": len(token), } - return response.CreateResponse(c, fiber.StatusOK, true, "success login", data) + return response.CreateResponse(c, fiber.StatusOK, response.Response{ + Message: "Success Login", + Success: true, + Data: data, + }) } func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { @@ -201,7 +170,7 @@ func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { if logoutErr != nil { return response.Error(c, consts.ErrServer+logoutErr.Error()) } - return response.CreateResponse(c, fiber.StatusOK, true, "success logout", nil) + return response.SuccessNoContent(c) } func (ctr *UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { @@ -219,13 +188,19 @@ func (ctr *UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { if forgetErr != nil { fiberErr, ok := forgetErr.(*fiber.Error) if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } return response.Error(c, consts.ErrServer+forgetErr.Error()) } message := "success sending link for reset password to email, check your email inbox" - return response.CreateResponse(c, fiber.StatusAccepted, true, message, nil) + return response.CreateResponse(c, fiber.StatusAccepted, response.Response{ + Message: message, + Success: true, + Data: nil, + }) } func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { @@ -246,13 +221,19 @@ func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { if resetErr != nil { fiberErr, ok := resetErr.(*fiber.Error) if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } return response.Error(c, consts.ErrServer+resetErr.Error()) } message := "your password already updated, you can login with your new password, thank you" - return response.CreateResponse(c, fiber.StatusAccepted, true, message, nil) + return response.CreateResponse(c, fiber.StatusAccepted, response.Response{ + Message: message, + Success: true, + Data: nil, + }) } func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { @@ -283,7 +264,9 @@ func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { if updateErr != nil { fiberErr, ok := updateErr.(*fiber.Error) if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } return response.Error(c, consts.ErrServer+updateErr.Error()) } @@ -297,7 +280,7 @@ func (ctr *UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { return response.Unauthorized(c) } - var user model.UserProfileUpdate + var user model.UserUpdate if err := c.BodyParser(&user); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } @@ -312,7 +295,9 @@ func (ctr *UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { if updateErr != nil { fiberErr, ok := updateErr.(*fiber.Error) if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } return response.Error(c, consts.ErrServer+updateErr.Error()) } @@ -331,7 +316,9 @@ func (ctr *UserControllerImpl) MyProfile(c *fiber.Ctx) error { if getErr != nil { fiberErr, ok := getErr.(*fiber.Error) if ok { - return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } return response.Error(c, consts.ErrServer+getErr.Error()) } diff --git a/service/user/user_service.go b/service/user/user_service.go index b785974..1586bf5 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -22,6 +22,7 @@ import ( type UserService interface { Register(ctx context.Context, data model.UserRegister) (id int, err error) + AccountActivation(ctx context.Context, data model.UserActivation) (err error) Login(ctx context.Context, data model.UserLogin) (token string, err error) Logout(c *fiber.Ctx) (err error) @@ -75,6 +76,10 @@ func (svg *UserServiceImpl) ResetPassword(ctx context.Context, user model.UserRe return nil } +func (svg *UserServiceImpl) AccountActivation(ctx context.Context, data model.UserActivation) (err error) { + return nil +} + func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegister) (id int, err error) { _, getErr := svc.repository.GetByEmail(ctx, data.Email) if getErr == nil { From a9998c16c13e81caab8a7a830cfe814b4db27aa2 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Sat, 27 Jan 2024 09:18:19 +0700 Subject: [PATCH 13/28] update user controller --- controller/role/role_controller.go | 1 - controller/user/user_controller.go | 90 +++++++++++++++--------------- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/controller/role/role_controller.go b/controller/role/role_controller.go index ea83695..b6dff9e 100644 --- a/controller/role/role_controller.go +++ b/controller/role/role_controller.go @@ -14,7 +14,6 @@ import ( ) type RoleController interface { - // Create func creates a new role Create(c *fiber.Ctx) error diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go index 1fc48f8..b3c54dd 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -78,23 +78,27 @@ func (ctr *UserControllerImpl) Register(c *fiber.Ctx) error { } ctx := c.Context() - id, regisErr := ctr.service.Register(ctx, user) - if regisErr != nil { - fiberErr, ok := regisErr.(*fiber.Error) + id, err := ctr.service.Register(ctx, user) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+regisErr.Error()) + return response.Error(c, consts.ErrServer) } - message := "Account success created. please check " + user.Email + " " - message += "inbox, our system has sended verification code or link." + message := "account successfully created. please check " + user.Email + message += " inbox; our system has sent a verification code or link." data := map[string]any{ "id": id, } - return response.SuccessCreated(c, data) + return response.CreateResponse(c, fiber.StatusCreated, response.Response{ + Message: message, + Success: true, + Data: data, + }) } func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { @@ -115,12 +119,11 @@ func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+err.Error()) + return response.Error(c, consts.ErrServer) } - message := "Thank you for your confirmation. Your account is active now, you can login." return response.CreateResponse(c, fiber.StatusOK, response.Response{ - Message: message, + Message: "thank you for your confirmation. your account is active now, you can login.", Success: true, Data: nil, }) @@ -139,25 +142,24 @@ func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { } ctx := c.Context() - token, loginErr := ctr.service.Login(ctx, user) - if loginErr != nil { - fiberErr, ok := loginErr.(*fiber.Error) + token, err := ctr.service.Login(ctx, user) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+loginErr.Error()) + return response.Error(c, consts.ErrServer) } - data := map[string]any{ - "token": token, - "token-length": len(token), - } return response.CreateResponse(c, fiber.StatusOK, response.Response{ - Message: "Success Login", + Message: "success login", Success: true, - Data: data, + Data: map[string]any{ + "token": token, + "token-length": len(token), + }, }) } @@ -166,9 +168,9 @@ func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { if !ok || userClaims == nil { return response.Unauthorized(c) } - logoutErr := ctr.service.Logout(c) - if logoutErr != nil { - return response.Error(c, consts.ErrServer+logoutErr.Error()) + err := ctr.service.Logout(c) + if err != nil { + return response.Error(c, consts.ErrServer) } return response.SuccessNoContent(c) } @@ -184,20 +186,19 @@ func (ctr *UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { } ctx := c.Context() - forgetErr := ctr.service.ForgetPassword(ctx, user) - if forgetErr != nil { - fiberErr, ok := forgetErr.(*fiber.Error) + err := ctr.service.ForgetPassword(ctx, user) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+forgetErr.Error()) + return response.Error(c, consts.ErrServer) } - message := "success sending link for reset password to email, check your email inbox" return response.CreateResponse(c, fiber.StatusAccepted, response.Response{ - Message: message, + Message: "success sending link for reset password to email, check your email inbox", Success: true, Data: nil, }) @@ -213,24 +214,23 @@ func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } if user.NewPassword != user.NewPasswordConfirm { - return response.BadRequest(c, "password confirmation not match") + return response.BadRequest(c, "password confirmation isn't match") } ctx := c.Context() - resetErr := ctr.service.ResetPassword(ctx, user) - if resetErr != nil { - fiberErr, ok := resetErr.(*fiber.Error) + err := ctr.service.ResetPassword(ctx, user) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+resetErr.Error()) + return response.Error(c, consts.ErrServer) } - message := "your password already updated, you can login with your new password, thank you" return response.CreateResponse(c, fiber.StatusAccepted, response.Response{ - Message: message, + Message: "your password already updated, you can login with the new password", Success: true, Data: nil, }) @@ -260,17 +260,16 @@ func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { } ctx := c.Context() - updateErr := ctr.service.UpdatePassword(ctx, user) - if updateErr != nil { - fiberErr, ok := updateErr.(*fiber.Error) + err := ctr.service.UpdatePassword(ctx, user) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+updateErr.Error()) + return response.Error(c, consts.ErrServer) } - return response.SuccessNoContent(c) } @@ -291,17 +290,16 @@ func (ctr *UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { } ctx := c.Context() - updateErr := ctr.service.UpdateProfile(ctx, user) - if updateErr != nil { - fiberErr, ok := updateErr.(*fiber.Error) + err := ctr.service.UpdateProfile(ctx, user) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+updateErr.Error()) + return response.Error(c, consts.ErrServer) } - return response.SuccessNoContent(c) } From e29ecfc1d1697b307a7da16375ca90e256bff930 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Sat, 27 Jan 2024 09:21:16 +0700 Subject: [PATCH 14/28] update role controller --- controller/role/role_controller.go | 58 +++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/controller/role/role_controller.go b/controller/role/role_controller.go index b6dff9e..efcbe43 100644 --- a/controller/role/role_controller.go +++ b/controller/role/role_controller.go @@ -9,6 +9,7 @@ import ( "github.com/Lukmanern/gost/domain/model" "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/middleware" "github.com/Lukmanern/gost/internal/response" service "github.com/Lukmanern/gost/service/role" ) @@ -49,6 +50,11 @@ func NewRoleController(service service.RoleService) RoleController { } func (ctr *RoleControllerImpl) Create(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + var role model.RoleCreate if err := c.BodyParser(&role); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) @@ -59,15 +65,15 @@ func (ctr *RoleControllerImpl) Create(c *fiber.Ctx) error { } ctx := c.Context() - id, createErr := ctr.service.Create(ctx, role) - if createErr != nil { - fiberErr, ok := createErr.(*fiber.Error) + id, err := ctr.service.Create(ctx, role) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+createErr.Error()) + return response.Error(c, consts.ErrServer) } data := map[string]any{ "id": id, @@ -76,26 +82,36 @@ func (ctr *RoleControllerImpl) Create(c *fiber.Ctx) error { } func (ctr *RoleControllerImpl) Get(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + id, err := c.ParamsInt("id") if err != nil || id <= 0 { return response.BadRequest(c, consts.InvalidID) } ctx := c.Context() - role, getErr := ctr.service.GetByID(ctx, id) - if getErr != nil { - fiberErr, ok := getErr.(*fiber.Error) + role, err := ctr.service.GetByID(ctx, id) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+getErr.Error()) + return response.Error(c, consts.ErrServer) } return response.SuccessLoaded(c, role) } func (ctr *RoleControllerImpl) GetAll(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + request := model.RequestGetAll{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 20), @@ -128,6 +144,11 @@ func (ctr *RoleControllerImpl) GetAll(c *fiber.Ctx) error { } func (ctr *RoleControllerImpl) Update(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + id, err := c.ParamsInt("id") if err != nil || id <= 0 { return response.BadRequest(c, consts.InvalidID) @@ -143,35 +164,40 @@ func (ctr *RoleControllerImpl) Update(c *fiber.Ctx) error { } ctx := c.Context() - updateErr := ctr.service.Update(ctx, role) - if updateErr != nil { - fiberErr, ok := updateErr.(*fiber.Error) + err = ctr.service.Update(ctx, role) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+updateErr.Error()) + return response.Error(c, consts.ErrServer) } return response.SuccessNoContent(c) } func (ctr *RoleControllerImpl) Delete(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + id, err := c.ParamsInt("id") if err != nil || id <= 0 { return response.BadRequest(c, consts.InvalidID) } ctx := c.Context() - deleteErr := ctr.service.Delete(ctx, id) - if deleteErr != nil { - fiberErr, ok := deleteErr.(*fiber.Error) + err = ctr.service.Delete(ctx, id) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+deleteErr.Error()) + return response.Error(c, consts.ErrServer) } return response.SuccessNoContent(c) } From c1949aba4f9104d5110fb0a486a48e1614a60b7f Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Sat, 27 Jan 2024 11:46:16 +0700 Subject: [PATCH 15/28] update user controller --- controller/user/user_controller.go | 140 +++++++++++++++++------------ service/role/role_service.go | 1 - service/user/user_service.go | 19 ++-- 3 files changed, 89 insertions(+), 71 deletions(-) diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go index b3c54dd..86130fd 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -1,6 +1,7 @@ package controller import ( + "math" "strings" "sync" @@ -15,36 +16,20 @@ import ( ) type UserController interface { - // Register function register user account, - // than send verification-code to email + // no-auth Register(c *fiber.Ctx) error - - // AccountActivation function activates user account with - // verification code that has been sended to the user's email AccountActivation(c *fiber.Ctx) error - - // ForgetPassword function send - // verification code into user's email + Login(c *fiber.Ctx) error ForgetPassword(c *fiber.Ctx) error - - // ResetPassword func resets password by creating - // new password by email and verification code ResetPassword(c *fiber.Ctx) error - - // Login func gives token and access to user - Login(c *fiber.Ctx) error - - // Logout func stores user's token into Redis + // auth+admin + GetAll(c *fiber.Ctx) error + // auth + MyProfile(c *fiber.Ctx) error Logout(c *fiber.Ctx) error - - // UpdatePassword func updates user's password - UpdatePassword(c *fiber.Ctx) error - - // UpdateProfile func updates user's profile data UpdateProfile(c *fiber.Ctx) error - - // MyProfile func shows user's profile data - MyProfile(c *fiber.Ctx) error + UpdatePassword(c *fiber.Ctx) error + Delete(c *fiber.Ctx) error } type UserControllerImpl struct { @@ -163,18 +148,6 @@ func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { }) } -func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { - userClaims, ok := c.Locals("claims").(*middleware.Claims) - if !ok || userClaims == nil { - return response.Unauthorized(c) - } - err := ctr.service.Logout(c) - if err != nil { - return response.Error(c, consts.ErrServer) - } - return response.SuccessNoContent(c) -} - func (ctr *UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { var user model.UserForgetPassword if err := c.BodyParser(&user); err != nil { @@ -236,38 +209,70 @@ func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { }) } -func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) GetAll(c *fiber.Ctx) error { userClaims, ok := c.Locals("claims").(*middleware.Claims) if !ok || userClaims == nil { return response.Unauthorized(c) } - var user model.UserPasswordUpdate - if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + request := model.RequestGetAll{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 20), + Keyword: c.Query("search"), + Sort: c.Query("sort"), + } + if request.Page <= 0 || request.Limit <= 0 { + return response.BadRequest(c, "invalid page or limit value") } - user.ID = userClaims.ID - validate := validator.New() - if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + ctx := c.Context() + roles, total, getErr := ctr.service.GetAll(ctx, request) + if getErr != nil { + return response.Error(c, consts.ErrServer+getErr.Error()) } - if user.NewPassword != user.NewPasswordConfirm { - return response.BadRequest(c, "new password confirmation is wrong") + + data := make([]interface{}, len(roles)) + for i := range roles { + data[i] = roles[i] } - if user.NewPassword == user.OldPassword { - return response.BadRequest(c, "no new password, try another new password") + responseData := model.GetAllResponse{ + Meta: model.PageMeta{ + TotalData: total, + TotalPages: int(math.Ceil(float64(total) / float64(request.Limit))), + AtPage: request.Page, + }, + Data: data, + } + return response.SuccessLoaded(c, responseData) +} + +func (ctr *UserControllerImpl) MyProfile(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) } ctx := c.Context() - err := ctr.service.UpdatePassword(ctx, user) - if err != nil { - fiberErr, ok := err.(*fiber.Error) + userProfile, getErr := ctr.service.MyProfile(ctx, userClaims.ID) + if getErr != nil { + fiberErr, ok := getErr.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } + return response.Error(c, consts.ErrServer+getErr.Error()) + } + return response.SuccessLoaded(c, userProfile) +} + +func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + err := ctr.service.Logout(c) + if err != nil { return response.Error(c, consts.ErrServer) } return response.SuccessNoContent(c) @@ -303,22 +308,43 @@ func (ctr *UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { return response.SuccessNoContent(c) } -func (ctr *UserControllerImpl) MyProfile(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { userClaims, ok := c.Locals("claims").(*middleware.Claims) if !ok || userClaims == nil { return response.Unauthorized(c) } + var user model.UserPasswordUpdate + if err := c.BodyParser(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + user.ID = userClaims.ID + + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + if user.NewPassword != user.NewPasswordConfirm { + return response.BadRequest(c, "new password confirmation is wrong") + } + if user.NewPassword == user.OldPassword { + return response.BadRequest(c, "no new password, try another new password") + } + ctx := c.Context() - userProfile, getErr := ctr.service.MyProfile(ctx, userClaims.ID) - if getErr != nil { - fiberErr, ok := getErr.(*fiber.Error) + err := ctr.service.UpdatePassword(ctx, user) + if err != nil { + fiberErr, ok := err.(*fiber.Error) if ok { return response.CreateResponse(c, fiberErr.Code, response.Response{ Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer+getErr.Error()) + return response.Error(c, consts.ErrServer) } - return response.SuccessLoaded(c, userProfile) + return response.SuccessNoContent(c) +} + +func (ctr *UserControllerImpl) Delete(c *fiber.Ctx) error { + return response.SuccessNoContent(c) } diff --git a/service/role/role_service.go b/service/role/role_service.go index 0abd454..6bb8fea 100644 --- a/service/role/role_service.go +++ b/service/role/role_service.go @@ -16,7 +16,6 @@ import ( ) type RoleService interface { - // Create func create one role. Create(ctx context.Context, data model.RoleCreate) (id int, err error) diff --git a/service/user/user_service.go b/service/user/user_service.go index 1586bf5..24d5dbb 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -21,21 +21,17 @@ import ( ) type UserService interface { + // no-auth Register(ctx context.Context, data model.UserRegister) (id int, err error) AccountActivation(ctx context.Context, data model.UserActivation) (err error) Login(ctx context.Context, data model.UserLogin) (token string, err error) - Logout(c *fiber.Ctx) (err error) - - GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) - MyProfile(ctx context.Context, id int) (profile model.User, err error) - - // ForgetPassword func send verification code into user's email ForgetPassword(ctx context.Context, user model.UserForgetPassword) (err error) - - // ResetPassword func resets password by creating - // new password by email and verification code ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) - + // auth+admin + GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) + // auth + MyProfile(ctx context.Context, id int) (profile model.User, err error) + Logout(c *fiber.Ctx) (err error) UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) Delete(ctx context.Context, id int) (err error) @@ -65,13 +61,10 @@ func NewUserService() UserService { return userSvcImpl } -// ForgetPassword func send verification code into user's email func (svg *UserServiceImpl) ForgetPassword(ctx context.Context, user model.UserForgetPassword) (err error) { return nil } -// ResetPassword func resets password by creating -// new password by email and verification code func (svg *UserServiceImpl) ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) { return nil } From 385a0a23ea8d1e7d787941b2e8a09623fb1f1d40 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Sat, 27 Jan 2024 14:46:17 +0700 Subject: [PATCH 16/28] add user controller test --- controller/role/role_controller.go | 10 +- controller/role/role_controller_test.go | 1 + controller/user/user_controller.go | 29 +- controller/user/user_controller_test.go | 864 ++++++++++++++++++++++++ domain/model/user.go | 48 +- internal/helper/helper.go | 12 + internal/middleware/middleware_test.go | 37 +- service/role/role_service.go | 10 +- service/user/user_service.go | 6 +- service/user/user_service_test.go | 4 +- 10 files changed, 936 insertions(+), 85 deletions(-) create mode 100644 controller/role/role_controller_test.go create mode 100644 controller/user/user_controller_test.go diff --git a/controller/role/role_controller.go b/controller/role/role_controller.go index efcbe43..6c46ebc 100644 --- a/controller/role/role_controller.go +++ b/controller/role/role_controller.go @@ -15,19 +15,11 @@ import ( ) type RoleController interface { - // Create func creates a new role + // auth + admin Create(c *fiber.Ctx) error - - // Get func gets a role Get(c *fiber.Ctx) error - - // GetAll func gets some roles GetAll(c *fiber.Ctx) error - - // Update func updates a role Update(c *fiber.Ctx) error - - // Delete func deletes a role Delete(c *fiber.Ctx) error } diff --git a/controller/role/role_controller_test.go b/controller/role/role_controller_test.go new file mode 100644 index 0000000..4ea4e28 --- /dev/null +++ b/controller/role/role_controller_test.go @@ -0,0 +1 @@ +package controller diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go index 86130fd..fe0755e 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -10,6 +10,7 @@ import ( "github.com/Lukmanern/gost/domain/model" "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/helper" "github.com/Lukmanern/gost/internal/middleware" "github.com/Lukmanern/gost/internal/response" service "github.com/Lukmanern/gost/service/user" @@ -22,14 +23,14 @@ type UserController interface { Login(c *fiber.Ctx) error ForgetPassword(c *fiber.Ctx) error ResetPassword(c *fiber.Ctx) error - // auth+admin + // auth + admin GetAll(c *fiber.Ctx) error // auth MyProfile(c *fiber.Ctx) error Logout(c *fiber.Ctx) error UpdateProfile(c *fiber.Ctx) error UpdatePassword(c *fiber.Ctx) error - Delete(c *fiber.Ctx) error + DeleteAccount(c *fiber.Ctx) error } type UserControllerImpl struct { @@ -58,6 +59,9 @@ func (ctr *UserControllerImpl) Register(c *fiber.Ctx) error { } user.Email = strings.ToLower(user.Email) validate := validator.New() + if len(user.RoleIDs) < 1 { + return response.BadRequest(c, "please choose one or more role") + } if err := validate.Struct(&user); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } @@ -116,11 +120,10 @@ func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { var user model.UserLogin - // user.IP = c.IP() // Note : uncomment this line in production if err := c.BodyParser(&user); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } - + user.IP = helper.RandomIPAddress() // Todo : update to c.IP() validate := validator.New() if err := validate.Struct(&user); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) @@ -345,6 +348,22 @@ func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { return response.SuccessNoContent(c) } -func (ctr *UserControllerImpl) Delete(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) DeleteAccount(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + + ctx := c.Context() + err := ctr.service.DeleteAccount(ctx, userClaims.ID) + if err != nil { + fiberErr, ok := err.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) + } + return response.Error(c, consts.ErrServer) + } return response.SuccessNoContent(c) } diff --git a/controller/user/user_controller_test.go b/controller/user/user_controller_test.go new file mode 100644 index 0000000..0213903 --- /dev/null +++ b/controller/user/user_controller_test.go @@ -0,0 +1,864 @@ +package controller + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + + "github.com/Lukmanern/gost/database/connector" + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/env" + "github.com/Lukmanern/gost/internal/hash" + "github.com/Lukmanern/gost/internal/helper" + "github.com/Lukmanern/gost/internal/middleware" + "github.com/Lukmanern/gost/internal/response" + repository "github.com/Lukmanern/gost/repository/user" + service "github.com/Lukmanern/gost/service/user" +) + +const ( + headerTestName string = "at UserController Test" +) + +var ( + baseURL string + timeNow time.Time + adminRepo repository.UserRepository +) + +func init() { + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) + config := env.Configuration() + baseURL = config.AppURL + timeNow = time.Now() + adminRepo = repository.NewUserRepository() + + connector.LoadDatabase() + r := connector.LoadRedisCache() + r.FlushAll() // clear all key:value in redis +} + +type testCase struct { + Name string + ResCode int + Payload any +} + +func TestUnauthorized(t *testing.T) { + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + + handlers := []func(c *fiber.Ctx) error{ + controller.GetAll, + controller.MyProfile, + controller.Logout, + controller.UpdateProfile, + controller.UpdatePassword, + controller.DeleteAccount, + } + for _, handler := range handlers { + c := helper.NewFiberCtx() + c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + handler(c) + res := c.Response() + assert.Equalf(t, res.StatusCode(), fiber.StatusUnauthorized, "Expected response code %d, but got %d", fiber.StatusUnauthorized, res.StatusCode()) + } +} + +func TestJSONParser(t *testing.T) { + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + fakeClaims := middleware.Claims{ + Email: helper.RandomEmail(), + Roles: map[string]uint8{"Full Access": 1}, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "999", + ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(5 * time.Minute)}, + NotBefore: &jwt.NumericDate{Time: time.Now()}, + IssuedAt: &jwt.NumericDate{Time: time.Now()}, + }, + } + + handlers := []func(c *fiber.Ctx) error{ + controller.Register, + controller.AccountActivation, + controller.Login, + controller.ForgetPassword, + controller.ResetPassword, + } + for _, handler := range handlers { + c := helper.NewFiberCtx() + c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + c.Locals("claims", &fakeClaims) + handler(c) + res := c.Response() + expectCode := fiber.StatusBadRequest + assert.Equalf(t, res.StatusCode(), expectCode, "Expected response code %d, but got %d", expectCode, res.StatusCode()) + } +} + +func TestRegister(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + testCases := []testCase{ + { + Name: "Success Register -1", + ResCode: fiber.StatusCreated, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: helper.RandomString(11), + Email: helper.RandomEmail(), + Password: helper.RandomString(12), + }, + }, + { + Name: "Success Register -2", + ResCode: fiber.StatusCreated, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: helper.RandomString(10), + Email: helper.RandomEmail(), + Password: helper.RandomString(12), + }, + }, + { + Name: "Failed Register -1: email is already used", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: validUser.Name, + Email: validUser.Email, + Password: helper.RandomString(12), + }, + }, + { + Name: "Failed Register -2: invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: helper.RandomString(10), + Email: "invalid email", + Password: helper.RandomString(12), + }, + }, + { + Name: "Failed Register -3: password too short", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: helper.RandomString(10), + Email: helper.RandomEmail(), + Password: "--", + }, + }, + { + Name: "Failed Register -4: no role id", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: nil, + Name: helper.RandomString(10), + Email: helper.RandomEmail(), + Password: helper.RandomString(10), + }, + }, + } + + pathURL := "user/register" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, controller.Register) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode == fiber.StatusCreated { + payload, ok := tc.Payload.(model.UserRegister) + assert.True(t, ok, "should true", headerTestName) + log.Println(payload) + entityUser, getErr := repository.GetByEmail(ctx, payload.Email) + assert.NoError(t, getErr, consts.ShouldNotErr, headerTestName) + deleteErr := repository.Delete(ctx, entityUser.ID) + assert.NoError(t, deleteErr, consts.ShouldNotErr, headerTestName) + } + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestLogin(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + entityUser := createUser() + defer repository.Delete(ctx, entityUser.ID) + + testCases := []testCase{ + { + Name: "Success Login -1", + ResCode: fiber.StatusOK, + Payload: model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }, + }, + { + Name: "Success Login -2", + ResCode: fiber.StatusOK, + Payload: model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }, + }, + { + Name: "Failed Login -1 : invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserLogin{ + Email: "invalid-email-", + Password: entityUser.Password, + }, + }, + { + Name: "Failed Login -2 : data not found", + ResCode: fiber.StatusNotFound, + Payload: model.UserLogin{ + Email: "validemail@gost.project", + Password: entityUser.Password, + }, + }, + { + Name: "Failed Login -3 : password too short", + ResCode: fiber.StatusBadRequest, + Payload: model.UserLogin{ + Email: entityUser.Email, + Password: "--", + }, + }, + } + + pathURL := "user/login" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, controller.Login) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestLogout(t *testing.T) { + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + + tokens := make([]string, 1) + for i := range tokens { + tokens[i] = helper.GenerateToken() + } + + type testCase struct { + Name string + ResCode int + Token string + } + + testCases := []testCase{ + { + Name: "Failed Login -1: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "--", + }, + { + Name: "Failed Login -2: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "INVALID-TOKEN", + }, + } + + for i, token := range tokens { + testCases = append(testCases, testCase{ + Name: "Success Logout -" + strconv.Itoa(i+2), + ResCode: fiber.StatusNoContent, + Token: token, + }) + testCases = append(testCases, testCase{ + Name: "Failed Logout -" + strconv.Itoa(i+3), + ResCode: fiber.StatusUnauthorized, + Token: token, + }) + } + + pathURL := "user/logout" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, jwtHandler.IsAuthenticated, controller.Logout) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestMyProfile(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + tokens := make([]string, 2) + for i := range tokens { + tokens[i] = helper.GenerateToken() + } + + entityUser := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }) + defer repository.Delete(ctx, entityUser.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) + + type testCase struct { + Name string + ResCode int + Token string + } + + testCases := []testCase{ + { + Name: "Success Get My Profile -1", + ResCode: fiber.StatusOK, + Token: validToken, + }, + { + Name: "Failed Get My Profile -1: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "--", + }, + { + Name: "Failed Get My Profile -2: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "INVALID-TOKEN", + }, + } + + for i, token := range tokens { + testCases = append(testCases, testCase{ + Name: "Failed Get My Profile -" + strconv.Itoa(i+3), + ResCode: fiber.StatusNotFound, + Token: token, + }) + } + + pathURL := "user/my-profile" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodGet, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Get(pathURL, jwtHandler.IsAuthenticated, controller.MyProfile) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestGetAll(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + token := helper.GenerateToken() + assert.True(t, token != "", consts.ShouldNotNil, headerTestName) + + type testCase struct { + Name string + Params string + ResCode int + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success get all -1", + Params: "?limit=100&page=1", + ResCode: fiber.StatusOK, + WantErr: false, + }, + { + Name: "Success get all -2", + Params: "?limit=12&page=1", + ResCode: fiber.StatusOK, + WantErr: false, + }, + { + Name: "Failed get all: invalid limit", + Params: "?limit=-1&page=1", + ResCode: fiber.StatusBadRequest, + WantErr: true, + }, + { + Name: "Failed get all: invalid page", + Params: "?limit=1&page=-1", + ResCode: fiber.StatusBadRequest, + WantErr: true, + }, + { + Name: "Failed get all: invalid sort", + Params: "?limit=1&page=1&sort=invalid", // sort should name + ResCode: fiber.StatusInternalServerError, + WantErr: true, + }, + } + + pathURL := "user/" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodGet, URL+tc.Params, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Get(pathURL, jwtHandler.IsAuthenticated, controller.GetAll) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestUpdateProfile(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer service.DeleteAccount(ctx, validUser.ID) + + validToken, err := service.Login(ctx, model.UserLogin{ + Email: validUser.Email, + Password: validUser.Password, + }) + assert.NoError(t, err, consts.ShouldNil, err, headerTestName) + + type testCase struct { + Name string + ResCode int + Payload model.UserUpdate + Token string + } + + testCases := []testCase{ + { + Name: "Success Update Profile -1", + ResCode: fiber.StatusNoContent, + Payload: model.UserUpdate{ + Name: "test update name", + }, + Token: validToken, + }, + { + Name: "Failed Update Profile -1: Invalid Token", + ResCode: fiber.StatusUnauthorized, + Payload: model.UserUpdate{ + Name: "test update", + }, + Token: "invalid-token", + }, + { + Name: "Failed Update Profile -2: Name too short", + ResCode: fiber.StatusBadRequest, + Payload: model.UserUpdate{ + Name: "", + }, + Token: helper.GenerateToken(), // valid token + }, + } + + pathURL := "user/profile" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPut, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Put(pathURL, jwtHandler.IsAuthenticated, controller.UpdateProfile) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr, headerTestName) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestUpdatePassword(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + fakeToken := helper.GenerateToken() + + entityUser := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }) + defer repository.Delete(ctx, entityUser.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) + + type testCase struct { + Name string + ResCode int + Payload model.UserPasswordUpdate + Token string + } + + testCases := []testCase{ + { + Name: "Success Update Password", + ResCode: fiber.StatusNoContent, + Token: validToken, + Payload: model.UserPasswordUpdate{ + OldPassword: entityUser.Password, + NewPassword: entityUser.Password + "00", + NewPasswordConfirm: entityUser.Password + "00", + }, + }, + { + Name: "Failed Update Password -1: user not found (invalid token)", + ResCode: fiber.StatusNotFound, + Token: fakeToken, + Payload: model.UserPasswordUpdate{ + OldPassword: entityUser.Password, + NewPassword: entityUser.Password + "00", + NewPasswordConfirm: entityUser.Password + "00", + }, + }, + { + Name: "Failed Update Password -2: old and new password is equal", + ResCode: fiber.StatusBadRequest, + Token: validToken, + Payload: model.UserPasswordUpdate{ + OldPassword: entityUser.Password, + NewPassword: entityUser.Password, + NewPasswordConfirm: entityUser.Password, + }, + }, + { + Name: "Failed Update Password -3: new and new password confirm is not equal", + ResCode: fiber.StatusBadRequest, + Token: fakeToken, + Payload: model.UserPasswordUpdate{ + OldPassword: entityUser.Password, + NewPassword: entityUser.Password + "000", + NewPasswordConfirm: entityUser.Password + "00", + }, + }, + { + Name: "Failed Update Password -4: password too short", + ResCode: fiber.StatusBadRequest, + Token: fakeToken, + Payload: model.UserPasswordUpdate{ + OldPassword: "", + NewPassword: "" + "000", + NewPasswordConfirm: "" + "00", + }, + }, + } + + pathURL := "user/update-password" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPut, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Put(pathURL, jwtHandler.IsAuthenticated, controller.UpdatePassword) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestDeleteAccount(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + fakeToken := helper.GenerateToken() + + entityUser := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }) + defer repository.Delete(ctx, entityUser.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) + + type testCase struct { + Name string + ResCode int + Token string + } + + testCases := []testCase{ + { + Name: "Success Delete Account -1", + ResCode: fiber.StatusNoContent, + Token: validToken, + }, + { + Name: "Failed Delete Account -1: user not found (invalid token)", + ResCode: fiber.StatusNotFound, + Token: fakeToken, // fake but valid + }, + { + Name: "Failed Delete Account -2: user already deleted", + ResCode: fiber.StatusNotFound, + Token: validToken, // is deleted before + }, + } + + pathURL := "user" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodDelete, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Delete(pathURL, jwtHandler.IsAuthenticated, controller.DeleteAccount) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} +func createUser() entity.User { + pw := helper.RandomString(15) + pwHashed, _ := hash.Generate(pw) + repo := adminRepo + ctx := helper.NewFiberCtx().Context() + data := entity.User{ + Name: helper.RandomString(15), + Email: helper.RandomEmail(), + Password: pwHashed, + ActivatedAt: &timeNow, + } + data.SetCreateTime() + id, err := repo.Create(ctx, data, []int{1, 2}) + if err != nil { + log.Fatal("Failed create user", headerTestName) + } + data.Password = pw + data.ID = id + return data +} diff --git a/domain/model/user.go b/domain/model/user.go index 9bf37df..43b6be1 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -2,16 +2,14 @@ package model import ( "time" - - "github.com/Lukmanern/gost/domain/entity" ) type User struct { - ID int `gorm:"type:bigserial;primaryKey" json:"id"` - Name string `gorm:"type:varchar(100) not null" json:"name"` - Email string `gorm:"type:varchar(100) not null unique" json:"email"` - Password string `gorm:"type:varchar(255) not null" json:"password"` - ActivatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"activated_at"` + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Password string `json:"password"` + ActivatedAt *time.Time `json:"activated_at"` } type UserRegister struct { @@ -32,20 +30,13 @@ type UserLogin struct { IP string `validate:"required,min=4,max=20" json:"ip"` } -// ID int `gorm:"type:bigserial;primaryKey" json:"id"` -// Name string `gorm:"type:varchar(100) not null" json:"name"` -// Email string `gorm:"type:varchar(100) not null unique" json:"email"` -// Password string `gorm:"type:varchar(255) not null" json:"password"` -// ActivatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"activated_at"` -// Roles []Role `gorm:"many2many:user_has_roles" json:"roles"` - type UserUpdate struct { - ID int `gorm:"type:bigserial;primaryKey" json:"id"` - Name string `gorm:"type:varchar(100) not null" json:"name"` + ID int `validate:"required,numeric,min=1" json:"id"` + Name string `validate:"required,min=2,max=60" json:"name"` } type UserUpdateRoles struct { - ID int `gorm:"type:bigserial;primaryKey" json:"id"` + ID int `validate:"required,numeric,min=1" json:"id"` RoleIDs []int `validate:"required" json:"role_id"` } @@ -61,29 +52,8 @@ type UserResetPassword struct { } type UserPasswordUpdate struct { - ID int `validate:"required,numeric,min=1"` + ID int `validate:"required,numeric,min=1" json:"id"` OldPassword string `validate:"required,min=8,max=30" json:"old_password"` NewPassword string `validate:"required,min=8,max=30" json:"new_password"` NewPasswordConfirm string `validate:"required,min=8,max=30" json:"new_password_confirm"` } - -type UserProfile struct { - Email string - Name string - ActivatedAt *time.Time - Roles []string -} - -type UserResponse struct { - ID int - Name string - ActivatedAt *time.Time -} - -type UserResponseDetail struct { - ID int - Email string - Name string - ActivatedAt *time.Time - Roles []entity.Role -} diff --git a/internal/helper/helper.go b/internal/helper/helper.go index f8ad03a..9c750d6 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/Lukmanern/gost/internal/middleware" "github.com/XANi/loremipsum" "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" @@ -83,6 +84,17 @@ func ToTitle(s string) string { return cases.Title(language.Und).String(s) } +// Generate token for admin role : Full Access +func GenerateToken() string { + jwtHandler := middleware.NewJWTHandler() + expire := time.Now().Add(15 * time.Hour) + token, err := jwtHandler.GenerateJWT(GenerateRandomID(), RandomEmail(), map[string]uint8{"admin": 1}, expire) + if err != nil { + return "" + } + return token +} + func GenerateRandomID() int { rand.New(rand.NewSource(time.Now().UnixNano())) min := 9000000 diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index 026ebf4..513c2d6 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -1,4 +1,4 @@ -package middleware +package middleware_test import ( "testing" @@ -7,6 +7,7 @@ import ( "github.com/Lukmanern/gost/internal/consts" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" + "github.com/Lukmanern/gost/internal/middleware" "github.com/gofiber/fiber/v2" ) @@ -36,19 +37,19 @@ func init() { } } -func TestNewJWTHandler(t *testing.T) { - jwtHandler := NewJWTHandler() - if jwtHandler.publicKey == nil { - t.Errorf("Public key parsing should have failed") - } +// func TestNewJWTHandler(t *testing.T) { +// jwtHandler := middleware.NewJWTHandler() +// if jwtHandler.publicKey == nil { +// t.Errorf("Public key parsing should have failed") +// } - if jwtHandler.privateKey == nil { - t.Errorf("Private key parsing should have failed") - } -} +// if jwtHandler.privateKey == nil { +// t.Errorf("Private key parsing should have failed") +// } +// } func TestGenerateClaims(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() token, err := jwtHandler.GenerateJWT(1, params.Email, params.Roles, params.Exp) if err != nil || token == "" { t.Fatal("should not error") @@ -80,7 +81,7 @@ func TestGenerateClaims(t *testing.T) { } func TestJWTHandlerInvalidateToken(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("error while generating token") @@ -102,7 +103,7 @@ func TestJWTHandlerInvalidateToken(t *testing.T) { } func TestJWTHandlerIsBlacklisted(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() cookie, err := jwtHandler.GenerateJWT(1000, helper.RandomEmail(), params.Roles, time.Now().Add(1*time.Hour)) @@ -115,7 +116,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { } tests := []struct { name string - j JWTHandler + j middleware.JWTHandler args args want bool }{ @@ -136,7 +137,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { } func TestJWTHandlerIsAuthenticated(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("error while generating token") @@ -146,7 +147,7 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { } func() { - jwtHandler1 := NewJWTHandler() + jwtHandler1 := middleware.NewJWTHandler() c := helper.NewFiberCtx() jwtHandler1.IsAuthenticated(c) c.Status(fiber.StatusUnauthorized) @@ -162,7 +163,7 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { t.Error("should not panic", r) } }() - jwtHandler3 := NewJWTHandler() + jwtHandler3 := middleware.NewJWTHandler() c := helper.NewFiberCtx() c.Request().Header.Add(fiber.HeaderAuthorization, " "+token) c.Status(fiber.StatusUnauthorized) @@ -174,7 +175,7 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { } func TestJWTHandlerCheckHasRole(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("Error while generating token:", err) diff --git a/service/role/role_service.go b/service/role/role_service.go index 6bb8fea..84e4ca5 100644 --- a/service/role/role_service.go +++ b/service/role/role_service.go @@ -16,19 +16,11 @@ import ( ) type RoleService interface { - // Create func create one role. + // auth + admin Create(ctx context.Context, data model.RoleCreate) (id int, err error) - - // GetByID func get one role. GetByID(ctx context.Context, id int) (role model.RoleResponse, err error) - - // GetAll func get some roles. GetAll(ctx context.Context, filter model.RequestGetAll) (roles []model.RoleResponse, total int, err error) - - // Update func update one role. Update(ctx context.Context, data model.RoleUpdate) (err error) - - // Delete func delete one role. Delete(ctx context.Context, id int) (err error) } diff --git a/service/user/user_service.go b/service/user/user_service.go index 24d5dbb..d193347 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -34,7 +34,7 @@ type UserService interface { Logout(c *fiber.Ctx) (err error) UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) - Delete(ctx context.Context, id int) (err error) + DeleteAccount(ctx context.Context, id int) (err error) } type UserServiceImpl struct { @@ -80,7 +80,7 @@ func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegiste } for _, roleID := range data.RoleIDs { - enttRole, err := svc.repository.GetByID(ctx, roleID) + enttRole, err := svc.roleRepo.GetByID(ctx, roleID) if err == gorm.ErrRecordNotFound { return 0, fiber.NewError(fiber.StatusNotFound, consts.NotFound) } @@ -215,7 +215,7 @@ func (svc *UserServiceImpl) UpdatePassword(ctx context.Context, data model.UserP return nil } -func (svc *UserServiceImpl) Delete(ctx context.Context, id int) (err error) { +func (svc *UserServiceImpl) DeleteAccount(ctx context.Context, id int) (err error) { user, getErr := svc.repository.GetByID(ctx, id) if getErr == gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusNotFound, consts.NotFound) diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go index 3fc6b6e..1a47760 100644 --- a/service/user/user_service_test.go +++ b/service/user/user_service_test.go @@ -102,7 +102,7 @@ func TestRegister(t *testing.T) { assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) assert.NotNil(t, user, consts.ShouldNotNil, tc.Name, headerTestName) - deleteErr := service.Delete(ctx, id) + deleteErr := service.DeleteAccount(ctx, id) assert.NoError(t, deleteErr, consts.ShouldNotErr, tc.Name, headerTestName) // value reset @@ -504,7 +504,7 @@ func TestDelete(t *testing.T) { for _, tc := range testCases { log.Println(tc.Name, headerTestName) - deleteErr := service.Delete(ctx, tc.ID) + deleteErr := service.DeleteAccount(ctx, tc.ID) if tc.WantErr { assert.Error(t, deleteErr, consts.ShouldErr, tc.Name, headerTestName) continue From 7956509cabca067e9a460a2302734a0f29881099 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Sun, 28 Jan 2024 12:52:53 +0700 Subject: [PATCH 17/28] add app dir --- application/app.go | 129 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 6 ++- 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 application/app.go diff --git a/application/app.go b/application/app.go new file mode 100644 index 0000000..ec361e0 --- /dev/null +++ b/application/app.go @@ -0,0 +1,129 @@ +// 📌 Origin Github Repository: https://github.com/Lukmanerngost + +// 🔍 README +// Application package configures middleware, error management, and +// handles OS signals for gracefully stopping the server when receiving +// an interrupt signal. This package provides routes related to user +// management and role-based access control (RBAC). And so on. + +package application + +import ( + "errors" + "fmt" + "log" + "net" + "os" + "os/signal" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/logger" + + "github.com/Lukmanern/gost/database/connector" + "github.com/Lukmanern/gost/internal/env" +) + +var ( + port int + + // Create a new fiber instance with custom config + router = fiber.New(fiber.Config{ + AppName: "Gost Project", + // Override default error handler + ErrorHandler: func(ctx *fiber.Ctx, err error) error { + // Status code defaults to 500 + code := fiber.StatusInternalServerError + + // Retrieve the custom status code + // if it's a *fiber.Error + var e *fiber.Error + if errors.As(err, &e) { + code = e.Code + } + + // Send custom error page + err = ctx.Status(code).JSON(fiber.Map{ + "message": e.Message, + }) + if err != nil { + return ctx.Status(fiber.StatusInternalServerError). + SendString("Internal Server Error") + } + return nil + }, + // memory management + // ReduceMemoryUsage: true, + // ReadBufferSize: 5120, + }) +) + +func setup() { + // Check env and database + env.ReadConfig("./.env") + config := env.Configuration() + privKey := config.GetPrivateKey() + pubKey := config.GetPublicKey() + if privKey == nil || pubKey == nil { + log.Fatal("private and public keys are not valid or not found") + } + port = config.AppPort + + connector.LoadDatabase() + connector.LoadRedisCache() +} + +func checkLocalPort(port int) { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + log.Fatal("port is being used by other process") + } + defer listener.Close() +} + +func RunApp() { + setup() + checkLocalPort(port) + router.Use(cors.New(cors.Config{ + AllowCredentials: true, + })) + router.Use(logger.New()) + // Custom File Writer + _ = os.MkdirAll("./log", os.ModePerm) + fileName := fmt.Sprintf("./log/%s.log", time.Now().Format("20060102")) + file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Fatalf("error opening file: %v", err) + } + defer file.Close() + router.Use(logger.New(logger.Config{ + Output: file, + })) + + // Create channel for idle connections. + idleConnsClosed := make(chan struct{}) + + go func() { + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt) // Catch OS signals. + <-sigint + + // Received an interrupt signal, shutdown. + // ctrl+c + if err := router.Shutdown(); err != nil { + // Error from closing listeners, or context timeout: + log.Printf("Oops... Server is not shutting down! Reason: %v", err) + } + + close(idleConnsClosed) + }() + + // set routes here + + if err := router.Listen(fmt.Sprintf(":%d", port)); err != nil { + log.Printf("Oops... Server is not running! Reason: %v", err) + } + + <-idleConnsClosed +} diff --git a/main.go b/main.go index 47125b9..a2558ed 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,9 @@ package main -import "fmt" +import ( + "github.com/Lukmanern/gost/application" +) func main() { - fmt.Println("hello world") + application.RunApp() } From d4400a9ccf0fc34aa8b8c02b92451f00e976317d Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Mon, 29 Jan 2024 13:41:24 +0700 Subject: [PATCH 18/28] add base role test case --- controller/role/role_controller_test.go | 162 ++++++++++++++++++++++++ controller/user/user_controller_test.go | 13 +- 2 files changed, 169 insertions(+), 6 deletions(-) diff --git a/controller/role/role_controller_test.go b/controller/role/role_controller_test.go index 4ea4e28..26bbad8 100644 --- a/controller/role/role_controller_test.go +++ b/controller/role/role_controller_test.go @@ -1 +1,163 @@ package controller + +import ( + "log" + "testing" + "time" + + "github.com/Lukmanern/gost/database/connector" + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/env" + "github.com/Lukmanern/gost/internal/helper" + repository "github.com/Lukmanern/gost/repository/role" + service "github.com/Lukmanern/gost/service/role" + "github.com/stretchr/testify/assert" +) + +const ( + headerTestName string = "at Role Controller Test" +) + +var ( + baseURL string + token string + timeNow time.Time + roleRepo repository.RoleRepository +) + +func init() { + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) + config := env.Configuration() + baseURL = config.AppURL + token = helper.GenerateToken() + timeNow = time.Now() + roleRepo = repository.NewRoleRepository() + + connector.LoadDatabase() + r := connector.LoadRedisCache() + r.FlushAll() // clear all key:value in redis +} + +func TestCreate(t *testing.T) { + repository := repository.NewRoleRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewRoleService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewRoleController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + _ = createRole() + + // pathURL := "role" + // URL := baseURL + pathURL + // for _, tc := range testCases { + // log.Println(tc.Name, headerTestName) + + // // Marshal payload to JSON + // jsonData, marshalErr := json.Marshal(&tc.Payload) + // assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // // Create HTTP request + // req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + // req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // // Set up Fiber app and handle the request with the controller + // app := fiber.New() + // app.Post(pathURL, controller.Create) + // req.Close = true + + // // run test + // res, testErr := app.Test(req, -1) + // assert.Nil(t, testErr, consts.ShouldNil, testErr) + // defer res.Body.Close() + // assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + // if res.StatusCode == fiber.StatusCreated { + // payload, ok := tc.Payload.(model.RoleCreate) + // assert.True(t, ok, "should true", headerTestName) + // log.Println(payload) + // entityRole, getErr := repository.GetByID(ctx, payload.Email) + // assert.NoError(t, getErr, consts.ShouldNotErr, headerTestName) + // deleteErr := repository.Delete(ctx, entityRole.ID) + // assert.NoError(t, deleteErr, consts.ShouldNotErr, headerTestName) + // } + + // if res.StatusCode != fiber.StatusNoContent { + // responseStruct := response.Response{} + // err := json.NewDecoder(res.Body).Decode(&responseStruct) + // assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + // } + // } +} + +func TestGet(t *testing.T) { + repository := repository.NewRoleRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewRoleService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewRoleController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + _ = createRole() +} + +func TestGetAll(t *testing.T) { + repository := repository.NewRoleRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewRoleService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewRoleController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + _ = createRole() +} + +func TestUpdate(t *testing.T) { + repository := repository.NewRoleRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewRoleService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewRoleController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + _ = createRole() +} + +func TestDelete(t *testing.T) { + repository := repository.NewRoleRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewRoleService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewRoleController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + _ = createRole() +} + +func createRole() entity.Role { + repo := roleRepo + ctx := helper.NewFiberCtx().Context() + data := entity.Role{ + Name: helper.RandomString(15), + Description: helper.RandomWords(10), + } + data.SetCreateTime() + id, err := repo.Create(ctx, data) + if err != nil { + log.Fatal("Failed create user", headerTestName) + } + data.ID = id + return data +} diff --git a/controller/user/user_controller_test.go b/controller/user/user_controller_test.go index 0213903..83d5c94 100644 --- a/controller/user/user_controller_test.go +++ b/controller/user/user_controller_test.go @@ -28,13 +28,13 @@ import ( ) const ( - headerTestName string = "at UserController Test" + headerTestName string = "at User Controller Test" ) var ( - baseURL string - timeNow time.Time - adminRepo repository.UserRepository + baseURL string + timeNow time.Time + userRepo repository.UserRepository ) func init() { @@ -43,7 +43,7 @@ func init() { config := env.Configuration() baseURL = config.AppURL timeNow = time.Now() - adminRepo = repository.NewUserRepository() + userRepo = repository.NewUserRepository() connector.LoadDatabase() r := connector.LoadRedisCache() @@ -842,10 +842,11 @@ func TestDeleteAccount(t *testing.T) { } } } + func createUser() entity.User { pw := helper.RandomString(15) pwHashed, _ := hash.Generate(pw) - repo := adminRepo + repo := userRepo ctx := helper.NewFiberCtx().Context() data := entity.User{ Name: helper.RandomString(15), From f972d75544229f05c62937989ea977607aef8947 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Mon, 29 Jan 2024 22:57:01 +0700 Subject: [PATCH 19/28] add test case for role controller --- controller/role/role_controller.go | 12 +- controller/role/role_controller_test.go | 479 +++++++++++++++++++++--- domain/model/role.go | 4 +- 3 files changed, 440 insertions(+), 55 deletions(-) diff --git a/controller/role/role_controller.go b/controller/role/role_controller.go index 6c46ebc..c00e498 100644 --- a/controller/role/role_controller.go +++ b/controller/role/role_controller.go @@ -49,11 +49,11 @@ func (ctr *RoleControllerImpl) Create(c *fiber.Ctx) error { var role model.RoleCreate if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + return response.BadRequest(c, consts.InvalidJSONBody) } validate := validator.New() if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + return response.BadRequest(c, consts.InvalidJSONBody) } ctx := c.Context() @@ -140,19 +140,19 @@ func (ctr *RoleControllerImpl) Update(c *fiber.Ctx) error { if !ok || userClaims == nil { return response.Unauthorized(c) } - id, err := c.ParamsInt("id") if err != nil || id <= 0 { return response.BadRequest(c, consts.InvalidID) } + var role model.RoleUpdate - role.ID = id if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + return response.BadRequest(c, consts.InvalidJSONBody) } + role.ID = id validate := validator.New() if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + return response.BadRequest(c, consts.InvalidJSONBody) } ctx := c.Context() diff --git a/controller/role/role_controller_test.go b/controller/role/role_controller_test.go index 26bbad8..93c4fae 100644 --- a/controller/role/role_controller_test.go +++ b/controller/role/role_controller_test.go @@ -1,17 +1,27 @@ package controller import ( + "bytes" + "encoding/json" + "fmt" "log" + "net/http/httptest" + "strconv" + "strings" "testing" "time" "github.com/Lukmanern/gost/database/connector" "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" "github.com/Lukmanern/gost/internal/consts" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" + "github.com/Lukmanern/gost/internal/middleware" + "github.com/Lukmanern/gost/internal/response" repository "github.com/Lukmanern/gost/repository/role" service "github.com/Lukmanern/gost/service/role" + "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" ) @@ -40,6 +50,28 @@ func init() { r.FlushAll() // clear all key:value in redis } +func TestUnauthorized(t *testing.T) { + service := service.NewRoleService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewRoleController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + + handlers := []func(c *fiber.Ctx) error{ + controller.Get, + controller.GetAll, + controller.Create, + controller.Update, + controller.Delete, + } + for _, handler := range handlers { + c := helper.NewFiberCtx() + c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + handler(c) + res := c.Response() + assert.Equalf(t, res.StatusCode(), fiber.StatusUnauthorized, "Expected response code %d, but got %d", fiber.StatusUnauthorized, res.StatusCode()) + } +} + func TestCreate(t *testing.T) { repository := repository.NewRoleRepository() assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) @@ -47,51 +79,114 @@ func TestCreate(t *testing.T) { assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) controller := NewRoleController(service) assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - _ = createRole() - - // pathURL := "role" - // URL := baseURL + pathURL - // for _, tc := range testCases { - // log.Println(tc.Name, headerTestName) - - // // Marshal payload to JSON - // jsonData, marshalErr := json.Marshal(&tc.Payload) - // assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) - - // // Create HTTP request - // req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) - // req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - // // Set up Fiber app and handle the request with the controller - // app := fiber.New() - // app.Post(pathURL, controller.Create) - // req.Close = true - - // // run test - // res, testErr := app.Test(req, -1) - // assert.Nil(t, testErr, consts.ShouldNil, testErr) - // defer res.Body.Close() - // assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) - - // if res.StatusCode == fiber.StatusCreated { - // payload, ok := tc.Payload.(model.RoleCreate) - // assert.True(t, ok, "should true", headerTestName) - // log.Println(payload) - // entityRole, getErr := repository.GetByID(ctx, payload.Email) - // assert.NoError(t, getErr, consts.ShouldNotErr, headerTestName) - // deleteErr := repository.Delete(ctx, entityRole.ID) - // assert.NoError(t, deleteErr, consts.ShouldNotErr, headerTestName) - // } - - // if res.StatusCode != fiber.StatusNoContent { - // responseStruct := response.Response{} - // err := json.NewDecoder(res.Body).Decode(&responseStruct) - // assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) - // } - // } + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) + + type testCase struct { + Name string + ResCode int + Payload model.RoleCreate + } + + testCases := []testCase{ + { + Name: "Success Create Role -1", + ResCode: fiber.StatusCreated, + Payload: model.RoleCreate{ + Name: strings.ToLower(helper.RandomString(14)), + Description: helper.RandomWords(10), + }, + }, + { + Name: "Success Create Role -2", + ResCode: fiber.StatusCreated, + Payload: model.RoleCreate{ + Name: strings.ToLower(helper.RandomString(14)), + Description: helper.RandomWords(10), + }, + }, + { + Name: "Failed Create Role -1: invalid name / name is already used", + ResCode: fiber.StatusBadRequest, + Payload: model.RoleCreate{ + Name: validRole.Name, + Description: helper.RandomWords(10), + }, + }, + { + Name: "Failed Create Role -2: invalid name / name too short", + ResCode: fiber.StatusBadRequest, + Payload: model.RoleCreate{ + Name: "", + Description: helper.RandomWords(10), + }, + }, + { + Name: "Failed Create Role -3: invalid name / name too long", + ResCode: fiber.StatusBadRequest, + Payload: model.RoleCreate{ + Name: helper.RandomWords(100), + Description: helper.RandomWords(10), + }, + }, + } + + pathURL := "role" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, jwtHandler.IsAuthenticated, controller.Create) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + var data response.Response + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + t.Fatal("failed to decode JSON:", err) + } + + if res.StatusCode == fiber.StatusCreated { + data, ok1 := data.Data.(map[string]interface{}) + if !ok1 { + t.Error("should ok1") + } + anyID, ok2 := data["id"] + if !ok2 { + t.Error("should ok2") + } + intID, ok3 := anyID.(float64) + if !ok3 { + t.Error("should ok3") + } + deleteErr := repository.Delete(ctx, int(intID)) + if deleteErr != nil { + t.Error(consts.ShouldNotErr) + } + } + } + } } func TestGet(t *testing.T) { @@ -101,10 +196,71 @@ func TestGet(t *testing.T) { assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) controller := NewRoleController(service) assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - _ = createRole() + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) + + type testCase struct { + Name string + ResCode int + ID string + } + + testCases := []testCase{ + { + Name: "Success Create Role -1", + ResCode: fiber.StatusOK, + ID: strconv.Itoa(validRole.ID), + }, + { + Name: "Failed Create Role -1: invalid ID", + ResCode: fiber.StatusBadRequest, + ID: strconv.Itoa(-1), + }, + { + Name: "Failed Create Role -2: invalid ID", + ResCode: fiber.StatusBadRequest, + ID: "invalid-id", + }, + { + Name: "Failed Create Role -3: not found", + ResCode: fiber.StatusNotFound, + ID: strconv.Itoa(validRole.ID + 99), + }, + } + + for _, tc := range testCases { + pathURL := "role/" // "/role/:id" + URL := baseURL + pathURL + tc.ID + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodGet, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Get(pathURL+":id", jwtHandler.IsAuthenticated, controller.Get) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + var data response.Response + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + t.Fatal("failed to decode JSON:", err) + } + } + } } func TestGetAll(t *testing.T) { @@ -114,10 +270,66 @@ func TestGetAll(t *testing.T) { assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) controller := NewRoleController(service) assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - _ = createRole() + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) + + type testCase struct { + Name string + ResCode int + Params string + } + + testCases := []testCase{ + { + Name: "Success Get All Role -1", + ResCode: fiber.StatusOK, + Params: "?limit=100&page=1", + }, + { + Name: "Failed Get All Role -1: invalid parameter", + ResCode: fiber.StatusBadRequest, + Params: "?limit=-1&page=1", + }, + { + Name: "Failed Get All Role -2: invalid parameter", + ResCode: fiber.StatusBadRequest, + Params: "?limit=100&page=-10", + }, + } + + for _, tc := range testCases { + pathURL := "role/" + URL := baseURL + pathURL + tc.Params + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodGet, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Get(pathURL, jwtHandler.IsAuthenticated, controller.GetAll) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + var data response.Response + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + t.Fatal("failed to decode JSON:", err) + } + } + } } func TestUpdate(t *testing.T) { @@ -127,10 +339,117 @@ func TestUpdate(t *testing.T) { assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) controller := NewRoleController(service) assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - _ = createRole() + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) + + type testCase struct { + Name string + ResCode int + ID string + Payload model.RoleUpdate + } + + testCases := []testCase{ + { + Name: "Success Update Role -1", + ResCode: fiber.StatusNoContent, + ID: strconv.Itoa(validRole.ID), + Payload: model.RoleUpdate{ + Name: strings.ToLower(helper.RandomString(10)), + Description: helper.RandomWords(10), + }, + }, + { + Name: "Success Update Role -2", + ResCode: fiber.StatusNoContent, + ID: strconv.Itoa(validRole.ID), + Payload: model.RoleUpdate{ + Name: strings.ToLower(helper.RandomString(20)), + Description: helper.RandomWords(5), + }, + }, + { + Name: "Failed Update Role -1: name too long", + ResCode: fiber.StatusBadRequest, + ID: strconv.Itoa(validRole.ID), + Payload: model.RoleUpdate{ + Name: strings.ToLower(helper.RandomWords(50)), + Description: helper.RandomWords(5), + }, + }, + { + Name: "Failed Update Role -2: name too short", + ResCode: fiber.StatusBadRequest, + ID: strconv.Itoa(validRole.ID), + Payload: model.RoleUpdate{ + Name: "", + Description: helper.RandomWords(5), + }, + }, + { + Name: "Failed Update Role -2: data not found", + ResCode: fiber.StatusNotFound, + ID: strconv.Itoa(validRole.ID + 999), + Payload: model.RoleUpdate{ + Name: helper.RandomString(10), + Description: helper.RandomWords(5), + }, + }, + { + Name: "Failed Update Role -2: invalid ID", + ResCode: fiber.StatusBadRequest, + ID: strconv.Itoa(-10), + Payload: model.RoleUpdate{ + Name: helper.RandomString(10), + Description: helper.RandomWords(5), + }, + }, + } + + for _, tc := range testCases { + pathURL := "role/" + URL := baseURL + pathURL + tc.ID + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPut, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Put(pathURL+":id", jwtHandler.IsAuthenticated, controller.Update) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + var data response.Response + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + t.Fatal("failed to decode JSON:", err) + } + } + if res.StatusCode == fiber.StatusNoContent { + id, _ := strconv.Atoi(tc.ID) + enttRole, getErr := repository.GetByID(ctx, id) + assert.Nil(t, getErr, consts.ShouldNil, testErr, tc.Name, headerTestName) + assert.Equal(t, enttRole.Name, tc.Payload.Name, consts.ShouldEqual, tc.Name, headerTestName) + assert.Equal(t, enttRole.Description, tc.Payload.Description, consts.ShouldEqual, tc.Name, headerTestName) + } + } } func TestDelete(t *testing.T) { @@ -140,17 +459,83 @@ func TestDelete(t *testing.T) { assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) controller := NewRoleController(service) assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - _ = createRole() + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) + + type testCase struct { + Name string + ResCode int + ID string + } + + testCases := []testCase{ + { + Name: "Success Delete Role -1", + ResCode: fiber.StatusNoContent, + ID: strconv.Itoa(validRole.ID), + }, + { + Name: "Failed Delete Role -1: data not found / already deleted", + ResCode: fiber.StatusNotFound, + ID: strconv.Itoa(validRole.ID), + }, + { + Name: "Failed Delete Role -2: data not found", + ResCode: fiber.StatusNotFound, + ID: strconv.Itoa(validRole.ID + 999), + }, + { + Name: "Failed Delete Role -3: invalid ID", + ResCode: fiber.StatusBadRequest, + ID: "invalid-id", + }, + { + Name: "Failed Delete Role -4: invalid ID", + ResCode: fiber.StatusBadRequest, + ID: "-10", + }, + } + + for _, tc := range testCases { + pathURL := "role/" + URL := baseURL + pathURL + tc.ID + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodDelete, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Delete(pathURL+":id", jwtHandler.IsAuthenticated, controller.Delete) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + var data response.Response + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + t.Fatal("failed to decode JSON:", err) + } + } + } } func createRole() entity.Role { repo := roleRepo ctx := helper.NewFiberCtx().Context() data := entity.Role{ - Name: helper.RandomString(15), + Name: strings.ToLower(helper.RandomString(15)), Description: helper.RandomWords(10), } data.SetCreateTime() diff --git a/domain/model/role.go b/domain/model/role.go index efd7171..6d39829 100644 --- a/domain/model/role.go +++ b/domain/model/role.go @@ -10,12 +10,12 @@ type RoleResponse struct { } type RoleCreate struct { - Name string `validate:"required,max=60" json:"name"` + Name string `validate:"required,min=5,max=60" json:"name"` Description string `validate:"required,max=100" json:"description"` } type RoleUpdate struct { ID int `validate:"required,numeric,min=1"` - Name string `validate:"required,max=60" json:"name"` + Name string `validate:"required,min=5,max=60" json:"name"` Description string `validate:"required,max=100" json:"description"` } From 429edb01abdb8ae4da91b40db9e3cbe35ebe50f3 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Mon, 29 Jan 2024 23:11:12 +0700 Subject: [PATCH 20/28] add app --- application/app.go | 11 +++----- application/role_permission_router.go | 32 ++++++++++++++++++++++ application/user_router.go | 38 +++++++++++++++++++++++++++ main.go | 2 ++ 4 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 application/role_permission_router.go create mode 100644 application/user_router.go diff --git a/application/app.go b/application/app.go index ec361e0..5a86d8a 100644 --- a/application/app.go +++ b/application/app.go @@ -1,10 +1,4 @@ -// 📌 Origin Github Repository: https://github.com/Lukmanerngost - -// 🔍 README -// Application package configures middleware, error management, and -// handles OS signals for gracefully stopping the server when receiving -// an interrupt signal. This package provides routes related to user -// management and role-based access control (RBAC). And so on. +// 📌 Origin Github Repository: https://github.com/Lukmanern package application @@ -119,7 +113,8 @@ func RunApp() { close(idleConnsClosed) }() - // set routes here + getUserRoutes(router) + getRolePermissionRoutes(router) if err := router.Listen(fmt.Sprintf(":%d", port)); err != nil { log.Printf("Oops... Server is not running! Reason: %v", err) diff --git a/application/role_permission_router.go b/application/role_permission_router.go new file mode 100644 index 0000000..c139b55 --- /dev/null +++ b/application/role_permission_router.go @@ -0,0 +1,32 @@ +// 📌 Origin Github Repository: https://github.com/Lukmanern + +package application + +import ( + "github.com/gofiber/fiber/v2" + + "github.com/Lukmanern/gost/internal/middleware" + "github.com/Lukmanern/gost/internal/role" + + roleCtr "github.com/Lukmanern/gost/controller/role" + roleSvc "github.com/Lukmanern/gost/service/role" +) + +var ( + roleService roleSvc.RoleService + roleController roleCtr.RoleController +) + +func getRolePermissionRoutes(router fiber.Router) { + jwtHandler := middleware.NewJWTHandler() + + roleService = roleSvc.NewRoleService() + roleController = roleCtr.NewRoleController(roleService) + + roleRouter := router.Group("role").Use(jwtHandler.IsAuthenticated) + roleRouter.Post("", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.Create) + roleRouter.Get("", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.GetAll) + roleRouter.Get(":id", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.Get) + roleRouter.Put(":id", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.Update) + roleRouter.Delete(":id", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.Delete) +} diff --git a/application/user_router.go b/application/user_router.go new file mode 100644 index 0000000..7f4138d --- /dev/null +++ b/application/user_router.go @@ -0,0 +1,38 @@ +// 📌 Origin Github Repository: https://github.com/Lukmanern + +package application + +import ( + "github.com/gofiber/fiber/v2" + + "github.com/Lukmanern/gost/internal/middleware" + + controller "github.com/Lukmanern/gost/controller/user" + service "github.com/Lukmanern/gost/service/user" +) + +var ( + userService service.UserService + userController controller.UserController +) + +func getUserRoutes(router fiber.Router) { + jwtHandler := middleware.NewJWTHandler() + + userService = service.NewUserService() + userController = controller.NewUserController(userService) + + userRoute := router.Group("user") + userRoute.Post("login", userController.Login) + userRoute.Post("register", userController.Register) + userRoute.Post("verification", userController.AccountActivation) + userRoute.Post("forget-password", userController.ForgetPassword) + userRoute.Post("reset-password", userController.ResetPassword) + + // get all + userRouteAuth := userRoute.Use(jwtHandler.IsAuthenticated) + userRouteAuth.Post("logout", userController.Logout) + userRouteAuth.Get("my-profile", userController.MyProfile) + userRouteAuth.Put("profile-update", userController.UpdateProfile) + userRouteAuth.Post("update-password", userController.UpdatePassword) +} diff --git a/main.go b/main.go index a2558ed..8bd0541 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,5 @@ +// 📌 Origin Github Repository: https://github.com/Lukmanern + package main import ( From 299caa43b2aa5be73eff4477b73e1ee721ec61a1 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Mon, 29 Jan 2024 23:15:36 +0700 Subject: [PATCH 21/28] add sleep to test --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b05e77..4def61b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,6 +60,7 @@ jobs: - name: Run migration run: | go run database/migration/main.go + sleep 3s - name: Run Test run: go test -race ./... From 7a7552b56058afa99a6282f5d0f092b6508dc685 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Tue, 30 Jan 2024 07:05:09 +0700 Subject: [PATCH 22/28] update controller and service -1 --- application/user_router.go | 14 ++-- controller/user/user_controller.go | 104 ++++++++++++++++++----------- domain/model/user.go | 3 +- internal/helper/helper.go | 2 +- repository/user/user_repository.go | 25 +++---- service/user/user_service.go | 38 +++++++++-- 6 files changed, 121 insertions(+), 65 deletions(-) diff --git a/application/user_router.go b/application/user_router.go index 7f4138d..fb93e13 100644 --- a/application/user_router.go +++ b/application/user_router.go @@ -6,6 +6,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/Lukmanern/gost/internal/middleware" + "github.com/Lukmanern/gost/internal/role" controller "github.com/Lukmanern/gost/controller/user" service "github.com/Lukmanern/gost/service/user" @@ -23,16 +24,19 @@ func getUserRoutes(router fiber.Router) { userController = controller.NewUserController(userService) userRoute := router.Group("user") + userRoute.Post("register", userController.Register) // send email + userRoute.Post("account-activation", userController.AccountActivation) userRoute.Post("login", userController.Login) - userRoute.Post("register", userController.Register) - userRoute.Post("verification", userController.AccountActivation) - userRoute.Post("forget-password", userController.ForgetPassword) + userRoute.Post("forget-password", userController.ForgetPassword) // send email userRoute.Post("reset-password", userController.ResetPassword) - // get all userRouteAuth := userRoute.Use(jwtHandler.IsAuthenticated) - userRouteAuth.Post("logout", userController.Logout) userRouteAuth.Get("my-profile", userController.MyProfile) + userRouteAuth.Post("logout", userController.Logout) userRouteAuth.Put("profile-update", userController.UpdateProfile) userRouteAuth.Post("update-password", userController.UpdatePassword) + userRouteAuth.Delete("delete-account", userController.DeleteAccount) + + userRouteAuth.Get("", jwtHandler.HasRole(role.RoleAdmin), userController.GetAll) + userRouteAuth.Delete("delete/:id", jwtHandler.HasRole(role.RoleAdmin), userController.BanAccount) } diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go index fe0755e..c30e991 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -23,14 +23,15 @@ type UserController interface { Login(c *fiber.Ctx) error ForgetPassword(c *fiber.Ctx) error ResetPassword(c *fiber.Ctx) error - // auth + admin - GetAll(c *fiber.Ctx) error // auth MyProfile(c *fiber.Ctx) error Logout(c *fiber.Ctx) error UpdateProfile(c *fiber.Ctx) error UpdatePassword(c *fiber.Ctx) error DeleteAccount(c *fiber.Ctx) error + // auth + admin + GetAll(c *fiber.Ctx) error + BanAccount(c *fiber.Ctx) error } type UserControllerImpl struct { @@ -212,43 +213,6 @@ func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { }) } -func (ctr *UserControllerImpl) GetAll(c *fiber.Ctx) error { - userClaims, ok := c.Locals("claims").(*middleware.Claims) - if !ok || userClaims == nil { - return response.Unauthorized(c) - } - - request := model.RequestGetAll{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 20), - Keyword: c.Query("search"), - Sort: c.Query("sort"), - } - if request.Page <= 0 || request.Limit <= 0 { - return response.BadRequest(c, "invalid page or limit value") - } - - ctx := c.Context() - roles, total, getErr := ctr.service.GetAll(ctx, request) - if getErr != nil { - return response.Error(c, consts.ErrServer+getErr.Error()) - } - - data := make([]interface{}, len(roles)) - for i := range roles { - data[i] = roles[i] - } - responseData := model.GetAllResponse{ - Meta: model.PageMeta{ - TotalData: total, - TotalPages: int(math.Ceil(float64(total) / float64(request.Limit))), - AtPage: request.Page, - }, - Data: data, - } - return response.SuccessLoaded(c, responseData) -} - func (ctr *UserControllerImpl) MyProfile(c *fiber.Ctx) error { userClaims, ok := c.Locals("claims").(*middleware.Claims) if !ok || userClaims == nil { @@ -367,3 +331,65 @@ func (ctr *UserControllerImpl) DeleteAccount(c *fiber.Ctx) error { } return response.SuccessNoContent(c) } + +func (ctr *UserControllerImpl) GetAll(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + + request := model.RequestGetAll{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 20), + Keyword: c.Query("search"), + Sort: c.Query("sort"), + } + if request.Page <= 0 || request.Limit <= 0 { + return response.BadRequest(c, "invalid page or limit value") + } + + ctx := c.Context() + roles, total, getErr := ctr.service.GetAll(ctx, request) + if getErr != nil { + return response.Error(c, consts.ErrServer+getErr.Error()) + } + + data := make([]interface{}, len(roles)) + for i := range roles { + data[i] = roles[i] + } + responseData := model.GetAllResponse{ + Meta: model.PageMeta{ + TotalData: total, + TotalPages: int(math.Ceil(float64(total) / float64(request.Limit))), + AtPage: request.Page, + }, + Data: data, + } + return response.SuccessLoaded(c, responseData) +} + +func (ctr *UserControllerImpl) BanAccount(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + return response.BadRequest(c, consts.InvalidID) + } + + ctx := c.Context() + err = ctr.service.DeleteAccount(ctx, id) + if err != nil { + fiberErr, ok := err.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) + } + return response.Error(c, consts.ErrServer) + } + return response.SuccessNoContent(c) +} diff --git a/domain/model/user.go b/domain/model/user.go index 43b6be1..0c8d7f9 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -5,11 +5,10 @@ import ( ) type User struct { - ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` - Password string `json:"password"` ActivatedAt *time.Time `json:"activated_at"` + Roles []string `json:"roles"` } type UserRegister struct { diff --git a/internal/helper/helper.go b/internal/helper/helper.go index 9c750d6..35aaded 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -65,7 +65,7 @@ func ValidateEmails(emails ...string) error { for _, email := range emails { _, err := mail.ParseAddress(email) if err != nil { - return errors.New("one or more email/s is invalid " + email) + return errors.New("one or more email/s is invalid: " + email) } } return nil diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go index c270dd9..60d1a0f 100644 --- a/repository/user/user_repository.go +++ b/repository/user/user_repository.go @@ -32,11 +32,11 @@ type UserRepository interface { // Update modifies user information in the repository. Update(ctx context.Context, user entity.User) (err error) - // Delete removes a user from the repository by their ID. - Delete(ctx context.Context, id int) (err error) - // UpdatePassword updates a user's password in the repository. UpdatePassword(ctx context.Context, id int, passwordHashed string) (err error) + + // Delete removes a user from the repository by their ID. + Delete(ctx context.Context, id int) (err error) } type UserRepositoryImpl struct { @@ -147,6 +147,7 @@ func (repo *UserRepositoryImpl) Update(ctx context.Context, user entity.User) (e } oldData.Name = user.Name + oldData.DeletedAt = user.DeletedAt oldData.SetUpdateTime() result = tx.Save(&oldData) if result.Error != nil { @@ -158,15 +159,6 @@ func (repo *UserRepositoryImpl) Update(ctx context.Context, user entity.User) (e return err } -func (repo *UserRepositoryImpl) Delete(ctx context.Context, id int) (err error) { - deleted := entity.User{} - result := repo.db.Where("id = ?", id).Delete(&deleted) - if result.Error != nil { - return result.Error - } - return nil -} - func (repo *UserRepositoryImpl) UpdatePassword(ctx context.Context, id int, passwordHashed string) (err error) { err = repo.db.Transaction(func(tx *gorm.DB) error { var user entity.User @@ -185,3 +177,12 @@ func (repo *UserRepositoryImpl) UpdatePassword(ctx context.Context, id int, pass return err } + +func (repo *UserRepositoryImpl) Delete(ctx context.Context, id int) (err error) { + deleted := entity.User{} + result := repo.db.Where("id = ?", id).Delete(&deleted) + if result.Error != nil { + return result.Error + } + return nil +} diff --git a/service/user/user_service.go b/service/user/user_service.go index d193347..8d2cb0e 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -29,6 +29,7 @@ type UserService interface { ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) // auth+admin GetAll(ctx context.Context, filter model.RequestGetAll) (users []model.User, total int, err error) + SoftDelete(ctx context.Context, id int) (err error) // auth MyProfile(ctx context.Context, id int) (profile model.User, err error) Logout(c *fiber.Ctx) (err error) @@ -119,12 +120,16 @@ func (svc *UserServiceImpl) Login(ctx context.Context, data model.UserLogin) (to return "", fiber.NewError(fiber.StatusBadRequest, "wrong password") } if user.ActivatedAt == nil || user.DeletedAt != nil { - return "", fiber.NewError(fiber.StatusBadRequest, "your account is inactive, please do activation") + return "", fiber.NewError(fiber.StatusBadRequest, "account is inactive, please do activation") } jwtHandler := middleware.NewJWTHandler() expired := time.Now().Add(4 * 24 * time.Hour) // 4 days active - token, err = jwtHandler.GenerateJWT(user.ID, user.Email, map[string]uint8{}, expired) + roles := make(map[string]uint8) + for _, role := range user.Roles { + roles[role.Name] = 1 + } + token, err = jwtHandler.GenerateJWT(user.ID, user.Email, roles, expired) if err != nil { return "", fiber.NewError(fiber.StatusInternalServerError, err.Error()) } @@ -151,6 +156,23 @@ func (svc *UserServiceImpl) GetAll(ctx context.Context, filter model.RequestGetA return users, total, nil } +func (svc *UserServiceImpl) SoftDelete(ctx context.Context, id int) (err error) { + user, getErr := svc.repository.GetByID(ctx, id) + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if getErr != nil || user == nil { + return errors.New("error while getting user data") + } + + user.SetDeleteTime() + err = svc.repository.Update(ctx, *user) + if err != nil { + return errors.New("error while updating user data") + } + return nil +} + func (svc *UserServiceImpl) MyProfile(ctx context.Context, id int) (profile model.User, err error) { user, getErr := svc.repository.GetByID(ctx, id) if getErr == gorm.ErrRecordNotFound { @@ -160,7 +182,7 @@ func (svc *UserServiceImpl) MyProfile(ctx context.Context, id int) (profile mode return model.User{}, errors.New("error while getting user data") } if user.ActivatedAt == nil || user.DeletedAt != nil { - return model.User{}, fiber.NewError(fiber.StatusBadRequest, "your account is inactive, please do activation") + return model.User{}, fiber.NewError(fiber.StatusBadRequest, "account is inactive, please do activation") } profile = entityToResponse(user) @@ -176,7 +198,7 @@ func (svc *UserServiceImpl) UpdateProfile(ctx context.Context, data model.UserUp return errors.New("error while getting user data") } if user.ActivatedAt == nil || user.DeletedAt != nil { - return fiber.NewError(fiber.StatusBadRequest, "your account is inactive, please do activation") + return fiber.NewError(fiber.StatusBadRequest, "account is inactive, please do activation") } enttUser := modelUpdateToEntity(data) @@ -196,7 +218,7 @@ func (svc *UserServiceImpl) UpdatePassword(ctx context.Context, data model.UserP return errors.New("error while getting user data") } if user.ActivatedAt == nil || user.DeletedAt != nil { - return fiber.NewError(fiber.StatusBadRequest, "your account is inactive, please do activation") + return fiber.NewError(fiber.StatusBadRequest, "account is inactive, please do activation") } res, verifyErr := hash.Verify(user.Password, data.OldPassword) @@ -240,11 +262,15 @@ func modelRegisterToEntity(data model.UserRegister) entity.User { } func entityToResponse(data *entity.User) model.User { + roles := make([]string, 0) + for _, role := range data.Roles { + roles = append(roles, role.Name) + } return model.User{ - ID: data.ID, Name: data.Name, Email: data.Email, ActivatedAt: data.ActivatedAt, + Roles: roles, } } From d725b9fa72e33595ea9268779053e73e7c700728 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Tue, 30 Jan 2024 14:19:57 +0700 Subject: [PATCH 23/28] update controller and service -2 --- README.md | 4 + ...le_permission_router.go => role_router.go} | 12 +- controller/user/user_controller.go | 20 +-- internal/consts/consts.go | 4 +- repository/user/user_repository.go | 8 +- service/user/user_service.go | 134 +++++++++++++++--- 6 files changed, 143 insertions(+), 39 deletions(-) rename application/{role_permission_router.go => role_router.go} (72%) diff --git a/README.md b/README.md index e443402..e4e8611 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,7 @@ Techs and tools were used in this project: This project is under license from MIT. For more details, see the [LICENSE](LICENSE) file.   + +## Todo + +- Add password and password confirm at DeleteProfile (permanent) by User diff --git a/application/role_permission_router.go b/application/role_router.go similarity index 72% rename from application/role_permission_router.go rename to application/role_router.go index c139b55..e95b99e 100644 --- a/application/role_permission_router.go +++ b/application/role_router.go @@ -8,20 +8,20 @@ import ( "github.com/Lukmanern/gost/internal/middleware" "github.com/Lukmanern/gost/internal/role" - roleCtr "github.com/Lukmanern/gost/controller/role" - roleSvc "github.com/Lukmanern/gost/service/role" + controller "github.com/Lukmanern/gost/controller/role" + service "github.com/Lukmanern/gost/service/role" ) var ( - roleService roleSvc.RoleService - roleController roleCtr.RoleController + roleService service.RoleService + roleController controller.RoleController ) func getRolePermissionRoutes(router fiber.Router) { jwtHandler := middleware.NewJWTHandler() - roleService = roleSvc.NewRoleService() - roleController = roleCtr.NewRoleController(roleService) + roleService = service.NewRoleService() + roleController = controller.NewRoleController(roleService) roleRouter := router.Group("role").Use(jwtHandler.IsAuthenticated) roleRouter.Post("", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.Create) diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go index c30e991..6e1c4b6 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -76,7 +76,7 @@ func (ctr *UserControllerImpl) Register(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } message := "account successfully created. please check " + user.Email @@ -109,7 +109,7 @@ func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } return response.CreateResponse(c, fiber.StatusOK, response.Response{ @@ -139,7 +139,7 @@ func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } return response.CreateResponse(c, fiber.StatusOK, response.Response{ @@ -171,7 +171,7 @@ func (ctr *UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } return response.CreateResponse(c, fiber.StatusAccepted, response.Response{ @@ -203,7 +203,7 @@ func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } return response.CreateResponse(c, fiber.StatusAccepted, response.Response{ @@ -240,7 +240,7 @@ func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { } err := ctr.service.Logout(c) if err != nil { - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } return response.SuccessNoContent(c) } @@ -270,7 +270,7 @@ func (ctr *UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } return response.SuccessNoContent(c) } @@ -307,7 +307,7 @@ func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } return response.SuccessNoContent(c) } @@ -327,7 +327,7 @@ func (ctr *UserControllerImpl) DeleteAccount(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } return response.SuccessNoContent(c) } @@ -389,7 +389,7 @@ func (ctr *UserControllerImpl) BanAccount(c *fiber.Ctx) error { Message: fiberErr.Message, Success: false, Data: nil, }) } - return response.Error(c, consts.ErrServer) + return response.Error(c, consts.ErrServer+err.Error()) } return response.SuccessNoContent(c) } diff --git a/internal/consts/consts.go b/internal/consts/consts.go index cfb8059..58bf34a 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -9,12 +9,12 @@ const ( ) const ( - InvalidJSONBody = "invalid JSON body" + InvalidJSONBody = "invalid JSON body: " InvalidID = "invalid ID" NilValue = "error nil value" InvalidToken = "invalid token / JWT, please logout and try-login" ErrHashing = "error while hashing password" - ErrServer = "internal server error" + ErrServer = "internal server error: " ) const ( diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go index 60d1a0f..b37546a 100644 --- a/repository/user/user_repository.go +++ b/repository/user/user_repository.go @@ -146,8 +146,14 @@ func (repo *UserRepositoryImpl) Update(ctx context.Context, user entity.User) (e return result.Error } + if user.DeletedAt != nil { + oldData.DeletedAt = user.DeletedAt + } + if user.ActivatedAt != nil { + oldData.ActivatedAt = user.ActivatedAt + } + oldData.Name = user.Name - oldData.DeletedAt = user.DeletedAt oldData.SetUpdateTime() result = tx.Save(&oldData) if result.Error != nil { diff --git a/service/user/user_service.go b/service/user/user_service.go index 8d2cb0e..672c8ac 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -12,9 +12,11 @@ import ( "github.com/Lukmanern/gost/domain/model" "github.com/Lukmanern/gost/internal/consts" "github.com/Lukmanern/gost/internal/hash" + "github.com/Lukmanern/gost/internal/helper" "github.com/Lukmanern/gost/internal/middleware" roleRepository "github.com/Lukmanern/gost/repository/role" repository "github.com/Lukmanern/gost/repository/user" + service "github.com/Lukmanern/gost/service/email_service" "github.com/go-redis/redis" "github.com/gofiber/fiber/v2" "gorm.io/gorm" @@ -39,12 +41,18 @@ type UserService interface { } type UserServiceImpl struct { - redis *redis.Client - jwtHandler *middleware.JWTHandler - repository repository.UserRepository - roleRepo roleRepository.RoleRepository + redis *redis.Client + jwtHandler *middleware.JWTHandler + repository repository.UserRepository + roleRepo roleRepository.RoleRepository + emailService service.EmailService } +const ( + KEY_FORGET_PASSWORD = "-forget-password" + KEY_ACCOUNT_ACTIVATION = "-account-activation" +) + var ( userSvcImpl *UserServiceImpl userSvcImplOnce sync.Once @@ -53,27 +61,16 @@ var ( func NewUserService() UserService { userSvcImplOnce.Do(func() { userSvcImpl = &UserServiceImpl{ - redis: connector.LoadRedisCache(), - jwtHandler: middleware.NewJWTHandler(), - repository: repository.NewUserRepository(), - roleRepo: roleRepository.NewRoleRepository(), + redis: connector.LoadRedisCache(), + jwtHandler: middleware.NewJWTHandler(), + repository: repository.NewUserRepository(), + roleRepo: roleRepository.NewRoleRepository(), + emailService: service.NewEmailService(), } }) return userSvcImpl } -func (svg *UserServiceImpl) ForgetPassword(ctx context.Context, user model.UserForgetPassword) (err error) { - return nil -} - -func (svg *UserServiceImpl) ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) { - return nil -} - -func (svg *UserServiceImpl) AccountActivation(ctx context.Context, data model.UserActivation) (err error) { - return nil -} - func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegister) (id int, err error) { _, getErr := svc.repository.GetByEmail(ctx, data.Email) if getErr == nil { @@ -103,9 +100,59 @@ func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegiste if err != nil { return 0, errors.New("error while storing user data") } + + code := helper.RandomString(32) + key := data.Email + KEY_ACCOUNT_ACTIVATION + exp := time.Hour * 3 + redisStatus := svc.redis.Set(key, code, exp) + if redisStatus.Err() != nil { + return id, errors.New("error while storing data to redis") + } + + subject := "From Gost Project : Successfully User Register" + message := "This is your verification / activation code." + message += "This code will expire in 3 hours.

Code : " + code + sendErr := svc.emailService.SendMail(subject, message, strings.ToLower(data.Email)) + if sendErr != nil { + return id, errors.New("error while sending email confirmation") + } + return id, nil } +func (svc *UserServiceImpl) AccountActivation(ctx context.Context, data model.UserActivation) (err error) { + user, getErr := svc.repository.GetByEmail(ctx, data.Email) + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if getErr != nil || user == nil { + return errors.New("error while getting user data") + } + if user.ActivatedAt != nil || user.DeletedAt != nil { + return fiber.NewError(fiber.StatusBadRequest, "activation failed, account is active or already deleted") + } + + key := data.Email + KEY_ACCOUNT_ACTIVATION + redisStatus := svc.redis.Get(key) + if redisStatus.Err() != nil { + return errors.New("error while getting data from redis") + } + if redisStatus.Val() != data.Code { + return fiber.NewError(fiber.StatusBadRequest, "verification code isn't match") + } + + // delete verification code from redis + svc.redis.Del(key) + + timeNow := time.Now() + user.ActivatedAt = &timeNow + err = svc.repository.Update(ctx, *user) + if err != nil { + return errors.New("error while updating user data") + } + return nil +} + func (svc *UserServiceImpl) Login(ctx context.Context, data model.UserLogin) (token string, err error) { user, getErr := svc.repository.GetByEmail(ctx, data.Email) if getErr == gorm.ErrRecordNotFound { @@ -136,6 +183,53 @@ func (svc *UserServiceImpl) Login(ctx context.Context, data model.UserLogin) (to return token, nil } +func (svc *UserServiceImpl) ForgetPassword(ctx context.Context, data model.UserForgetPassword) (err error) { + user, getErr := svc.repository.GetByEmail(ctx, data.Email) + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if getErr != nil || user == nil { + return errors.New("error while getting user data") + } + + key := data.Email + KEY_FORGET_PASSWORD + value := helper.RandomString(30) + exp := time.Hour * 1 + redisStatus := svc.redis.Set(key, value, exp) + if redisStatus.Err() != nil { + return errors.New("error while storing data to redis") + } + + return nil +} + +func (svc *UserServiceImpl) ResetPassword(ctx context.Context, data model.UserResetPassword) (err error) { + user, getErr := svc.repository.GetByEmail(ctx, data.Email) + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) + } + if getErr != nil || user == nil { + return errors.New("error while getting user data") + } + key := data.Email + KEY_FORGET_PASSWORD + code := svc.redis.Get(key).Val() + if code == "" || code != data.Code { + return fiber.NewError(fiber.StatusNotFound, "verfication code isn't found") + } + + pwHashed, err := hash.Generate(data.NewPassword) + if err != nil { + return errors.New("error while hashing password, please try again") + } + err = svc.repository.UpdatePassword(ctx, user.ID, pwHashed) + if err != nil { + return errors.New("error while updating password, please try again") + } + svc.redis.Del(key) + + return nil +} + func (svc *UserServiceImpl) Logout(c *fiber.Ctx) (err error) { err = svc.jwtHandler.InvalidateToken(c) if err != nil { From af53a71b615c0195944e63bd9b938e21f2228daa Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Tue, 30 Jan 2024 22:46:34 +0700 Subject: [PATCH 24/28] update controller and service -3 --- application/user_router.go | 1 + controller/role/role_controller.go | 4 ++-- go.mod | 2 +- go.sum | 5 +++-- repository/user/user_repository.go | 8 +++++--- service/user/user_service.go | 16 ++++++++++++---- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/application/user_router.go b/application/user_router.go index fb93e13..eeb1606 100644 --- a/application/user_router.go +++ b/application/user_router.go @@ -39,4 +39,5 @@ func getUserRoutes(router fiber.Router) { userRouteAuth.Get("", jwtHandler.HasRole(role.RoleAdmin), userController.GetAll) userRouteAuth.Delete("delete/:id", jwtHandler.HasRole(role.RoleAdmin), userController.BanAccount) + // Todo : get all users by role } diff --git a/controller/role/role_controller.go b/controller/role/role_controller.go index c00e498..70e0078 100644 --- a/controller/role/role_controller.go +++ b/controller/role/role_controller.go @@ -49,11 +49,11 @@ func (ctr *RoleControllerImpl) Create(c *fiber.Ctx) error { var role model.RoleCreate if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody) + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } validate := validator.New() if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, consts.InvalidJSONBody) + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } ctx := c.Context() diff --git a/go.mod b/go.mod index c59dcfa..57b37f1 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/klauspost/compress v1.16.7 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.28.0 // indirect diff --git a/go.sum b/go.sum index bee1bd1..6ca5877 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,7 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -64,8 +65,8 @@ github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNa github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go index b37546a..317a917 100644 --- a/repository/user/user_repository.go +++ b/repository/user/user_repository.go @@ -126,7 +126,8 @@ func (repo *UserRepositoryImpl) GetAll(ctx context.Context, filter model.Request users = []entity.User{} skip := int64(filter.Limit * (filter.Page - 1)) limit := int64(filter.Limit) - result = repo.db.Where(cond, args...).Limit(int(limit)).Offset(int(skip)) + result = repo.db.Where(cond, args...).Preload("Roles") + result = result.Limit(int(limit)).Offset(int(skip)) if filter.Sort != "" { result = result.Order(filter.Sort + " ASC") } @@ -152,8 +153,9 @@ func (repo *UserRepositoryImpl) Update(ctx context.Context, user entity.User) (e if user.ActivatedAt != nil { oldData.ActivatedAt = user.ActivatedAt } - - oldData.Name = user.Name + if user.Name != "" { + oldData.Name = user.Name + } oldData.SetUpdateTime() result = tx.Save(&oldData) if result.Error != nil { diff --git a/service/user/user_service.go b/service/user/user_service.go index 672c8ac..bb6b89a 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -101,7 +101,7 @@ func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegiste return 0, errors.New("error while storing user data") } - code := helper.RandomString(32) + code := helper.RandomString(32) // verification code key := data.Email + KEY_ACCOUNT_ACTIVATION exp := time.Hour * 3 redisStatus := svc.redis.Set(key, code, exp) @@ -193,13 +193,20 @@ func (svc *UserServiceImpl) ForgetPassword(ctx context.Context, data model.UserF } key := data.Email + KEY_FORGET_PASSWORD - value := helper.RandomString(30) + code := helper.RandomString(32) exp := time.Hour * 1 - redisStatus := svc.redis.Set(key, value, exp) + redisStatus := svc.redis.Set(key, code, exp) if redisStatus.Err() != nil { return errors.New("error while storing data to redis") } + subject := "From Gost Project : Code for Reset Password" + message := "This code will expire in 1 hours.

Code : " + code + sendErr := svc.emailService.SendMail(subject, message, strings.ToLower(data.Email)) + if sendErr != nil { + return errors.New("error while sending email confirmation") + } + return nil } @@ -225,8 +232,9 @@ func (svc *UserServiceImpl) ResetPassword(ctx context.Context, data model.UserRe if err != nil { return errors.New("error while updating password, please try again") } - svc.redis.Del(key) + // delete verification code from redis + svc.redis.Del(key) return nil } From 3073b9fca6b4071a30a78d66f3c9a459e5ae41d5 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Wed, 31 Jan 2024 10:32:38 +0700 Subject: [PATCH 25/28] finishing features -1 --- application/role_router.go | 10 ++-- application/user_router.go | 6 +-- controller/role/role_controller_test.go | 6 ++- controller/user/user_controller.go | 32 +++++++++--- controller/user/user_controller_test.go | 66 +++++++++++++++++++++---- domain/model/user.go | 13 ++++- internal/middleware/middleware.go | 36 ++++++++++++-- internal/middleware/middleware_test.go | 6 ++- service/user/user_service.go | 15 ++++-- service/user/user_service_test.go | 51 +++++++++++++------ 10 files changed, 187 insertions(+), 54 deletions(-) diff --git a/application/role_router.go b/application/role_router.go index e95b99e..29985a7 100644 --- a/application/role_router.go +++ b/application/role_router.go @@ -24,9 +24,9 @@ func getRolePermissionRoutes(router fiber.Router) { roleController = controller.NewRoleController(roleService) roleRouter := router.Group("role").Use(jwtHandler.IsAuthenticated) - roleRouter.Post("", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.Create) - roleRouter.Get("", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.GetAll) - roleRouter.Get(":id", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.Get) - roleRouter.Put(":id", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.Update) - roleRouter.Delete(":id", jwtHandler.HasRole(role.RoleSuperAdmin), roleController.Delete) + roleRouter.Post("", jwtHandler.HasOneRole(role.RoleSuperAdmin, role.RoleAdmin), roleController.Create) + roleRouter.Get("", jwtHandler.HasOneRole(role.RoleSuperAdmin, role.RoleAdmin), roleController.GetAll) + roleRouter.Get(":id", jwtHandler.HasOneRole(role.RoleSuperAdmin, role.RoleAdmin), roleController.Get) + roleRouter.Put(":id", jwtHandler.HasOneRole(role.RoleSuperAdmin, role.RoleAdmin), roleController.Update) + roleRouter.Delete(":id", jwtHandler.HasOneRole(role.RoleSuperAdmin, role.RoleAdmin), roleController.Delete) } diff --git a/application/user_router.go b/application/user_router.go index eeb1606..21e4a4e 100644 --- a/application/user_router.go +++ b/application/user_router.go @@ -37,7 +37,7 @@ func getUserRoutes(router fiber.Router) { userRouteAuth.Post("update-password", userController.UpdatePassword) userRouteAuth.Delete("delete-account", userController.DeleteAccount) - userRouteAuth.Get("", jwtHandler.HasRole(role.RoleAdmin), userController.GetAll) - userRouteAuth.Delete("delete/:id", jwtHandler.HasRole(role.RoleAdmin), userController.BanAccount) - // Todo : get all users by role + // for admin + userRouteAuth.Get("", jwtHandler.HasOneRole(role.RoleSuperAdmin, role.RoleAdmin), userController.GetAll) + userRouteAuth.Put("ban-user/:id", jwtHandler.HasOneRole(role.RoleSuperAdmin, role.RoleAdmin), userController.BanAccount) } diff --git a/controller/role/role_controller_test.go b/controller/role/role_controller_test.go index 93c4fae..ae7e4ad 100644 --- a/controller/role/role_controller_test.go +++ b/controller/role/role_controller_test.go @@ -275,8 +275,10 @@ func TestGetAll(t *testing.T) { ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - validRole := createRole() - defer repository.Delete(ctx, validRole.ID) + for i := 0; i < 3; i++ { + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) + } type testCase struct { Name string diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go index 6e1c4b6..5910c87 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -318,8 +318,21 @@ func (ctr *UserControllerImpl) DeleteAccount(c *fiber.Ctx) error { return response.Unauthorized(c) } + var user model.UserDeleteAccount + if err := c.BodyParser(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + user.ID = userClaims.ID + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + if user.Password != user.PasswordConfirm { + return response.BadRequest(c, "password confirmation isn't match") + } + ctx := c.Context() - err := ctr.service.DeleteAccount(ctx, userClaims.ID) + err := ctr.service.DeleteAccount(ctx, user) if err != nil { fiberErr, ok := err.(*fiber.Error) if ok { @@ -329,6 +342,11 @@ func (ctr *UserControllerImpl) DeleteAccount(c *fiber.Ctx) error { } return response.Error(c, consts.ErrServer+err.Error()) } + // invalidate / blacklist the token + logoutErr := ctr.service.Logout(c) + if logoutErr != nil { + return response.Error(c, consts.ErrServer+logoutErr.Error()) + } return response.SuccessNoContent(c) } @@ -349,14 +367,14 @@ func (ctr *UserControllerImpl) GetAll(c *fiber.Ctx) error { } ctx := c.Context() - roles, total, getErr := ctr.service.GetAll(ctx, request) + users, total, getErr := ctr.service.GetAll(ctx, request) if getErr != nil { return response.Error(c, consts.ErrServer+getErr.Error()) } - data := make([]interface{}, len(roles)) - for i := range roles { - data[i] = roles[i] + data := make([]interface{}, len(users)) + for i := range users { + data[i] = users[i] } responseData := model.GetAllResponse{ Meta: model.PageMeta{ @@ -376,12 +394,12 @@ func (ctr *UserControllerImpl) BanAccount(c *fiber.Ctx) error { } id, err := c.ParamsInt("id") - if err != nil || id <= 0 { + if err != nil || id <= 0 || userClaims.ID == id { return response.BadRequest(c, consts.InvalidID) } ctx := c.Context() - err = ctr.service.DeleteAccount(ctx, id) + err = ctr.service.SoftDelete(ctx, id) if err != nil { fiberErr, ok := err.(*fiber.Error) if ok { diff --git a/controller/user/user_controller_test.go b/controller/user/user_controller_test.go index 83d5c94..c4d8367 100644 --- a/controller/user/user_controller_test.go +++ b/controller/user/user_controller_test.go @@ -493,6 +493,11 @@ func TestGetAll(t *testing.T) { ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + for i := 0; i < 3; i++ { + entityUser := createUser() + defer repository.Delete(ctx, entityUser.ID) + } + token := helper.GenerateToken() assert.True(t, token != "", consts.ShouldNotNil, headerTestName) @@ -578,7 +583,7 @@ func TestUpdateProfile(t *testing.T) { assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) validUser := createUser() - defer service.DeleteAccount(ctx, validUser.ID) + defer repository.Delete(ctx, validUser.ID) validToken, err := service.Login(ctx, model.UserLogin{ Email: validUser.Email, @@ -781,7 +786,6 @@ func TestDeleteAccount(t *testing.T) { assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) fakeToken := helper.GenerateToken() - entityUser := createUser() validToken, loginErr := service.Login(ctx, model.UserLogin{ Email: entityUser.Email, @@ -794,23 +798,63 @@ func TestDeleteAccount(t *testing.T) { Name string ResCode int Token string + Payload model.UserDeleteAccount } testCases := []testCase{ + { + Name: "Failed Delete Account -1: wrong password", + ResCode: fiber.StatusBadRequest, + Token: validToken, + Payload: model.UserDeleteAccount{ + Password: "valid-password-xyz", + PasswordConfirm: "valid-password-xyz", + }, + }, + { + Name: "Failed Delete Account -2: password isn't match", + ResCode: fiber.StatusBadRequest, + Token: validToken, // fake but valid + Payload: model.UserDeleteAccount{ + Password: "valid-password", + PasswordConfirm: "invalid-password", + }, + }, + { + Name: "Failed Delete Account -3: password too short", + ResCode: fiber.StatusBadRequest, + Token: validToken, // fake but valid + Payload: model.UserDeleteAccount{ + Password: "", + PasswordConfirm: "invalid-password", + }, + }, { Name: "Success Delete Account -1", ResCode: fiber.StatusNoContent, Token: validToken, + Payload: model.UserDeleteAccount{ + Password: entityUser.Password, + PasswordConfirm: entityUser.Password, + }, }, { - Name: "Failed Delete Account -1: user not found (invalid token)", - ResCode: fiber.StatusNotFound, - Token: fakeToken, // fake but valid + Name: "Failed Delete Account -4: user already deleted", + ResCode: fiber.StatusUnauthorized, + Token: validToken, // token is invalid + Payload: model.UserDeleteAccount{ + Password: entityUser.Password, + PasswordConfirm: entityUser.Password, + }, }, { - Name: "Failed Delete Account -2: user already deleted", + Name: "Failed Delete Account -4: user not found", ResCode: fiber.StatusNotFound, - Token: validToken, // is deleted before + Token: fakeToken, // user not found + Payload: model.UserDeleteAccount{ + Password: "valid-password", + PasswordConfirm: "valid-password", + }, }, } @@ -819,8 +863,12 @@ func TestDeleteAccount(t *testing.T) { for _, tc := range testCases { log.Println(tc.Name, headerTestName) + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + // Create HTTP request - req := httptest.NewRequest(fiber.MethodDelete, URL, nil) + req := httptest.NewRequest(fiber.MethodDelete, URL, bytes.NewReader(jsonData)) req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -833,7 +881,7 @@ func TestDeleteAccount(t *testing.T) { res, testErr := app.Test(req, -1) assert.Nil(t, testErr, consts.ShouldNil, testErr) defer res.Body.Close() - assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode, tc.Name) if res.StatusCode != fiber.StatusNoContent { responseStruct := response.Response{} diff --git a/domain/model/user.go b/domain/model/user.go index 0c8d7f9..05d9854 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -5,9 +5,11 @@ import ( ) type User struct { + ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` ActivatedAt *time.Time `json:"activated_at"` + DeletedAt *time.Time `json:"deleted_at"` Roles []string `json:"roles"` } @@ -30,8 +32,9 @@ type UserLogin struct { } type UserUpdate struct { - ID int `validate:"required,numeric,min=1" json:"id"` - Name string `validate:"required,min=2,max=60" json:"name"` + ID int `validate:"required,numeric,min=1" json:"id"` + Name string `validate:"required,min=2,max=60" json:"name"` + DeletedAt *time.Time } type UserUpdateRoles struct { @@ -56,3 +59,9 @@ type UserPasswordUpdate struct { NewPassword string `validate:"required,min=8,max=30" json:"new_password"` NewPasswordConfirm string `validate:"required,min=8,max=30" json:"new_password_confirm"` } + +type UserDeleteAccount struct { + ID int `validate:"required,numeric,min=1" json:"id"` + Password string `validate:"required,min=8,max=30" json:"password"` + PasswordConfirm string `validate:"required,min=8,max=30" json:"password_confirm"` +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 1f225f9..8fb2841 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -173,19 +173,47 @@ func (j JWTHandler) GenerateClaims(cookieToken string) *Claims { return &claims } -// CheckHasRole func is handler/middleware that -// called before the controller for checks the fiber ctx -func (j JWTHandler) HasRole(roles ...string) func(c *fiber.Ctx) error { +// HasRoles is a middleware function that checks if the user associated with the incoming request +// possesses all the specified roles in the JWT claims. If the user lacks any of the roles, +// it returns an "Unauthorized" response; otherwise, it allows the request to proceed. +func (j JWTHandler) HasRoles(roles ...string) func(c *fiber.Ctx) error { if len(roles) < 1 { return response.Unauthorized } return func(c *fiber.Ctx) error { claims, ok := c.Locals("claims").(*Claims) + if !ok || claims == nil { + return response.Unauthorized(c) + } + // Check if user has all specified roles for _, role := range roles { - if !ok || claims.Roles[role] != 1 { + if claims.Roles[role] != 1 { return response.Unauthorized(c) } } return c.Next() } } + +// HasOneRole is a middleware function that checks if the user associated with the incoming request +// possesses at least one of the specified roles in the JWT claims. If the user has at least one role, +// it allows the request to proceed; otherwise, it returns an "Unauthorized" response. +func (j JWTHandler) HasOneRole(roles ...string) func(c *fiber.Ctx) error { + if len(roles) < 1 { + return response.Unauthorized + } + return func(c *fiber.Ctx) error { + claims, ok := c.Locals("claims").(*Claims) + if !ok || claims == nil { + return response.Unauthorized(c) + } + // Check if user has at least one + // of the specified roles + for _, role := range roles { + if claims.Roles[role] == 1 { + return c.Next() + } + } + return response.Unauthorized(c) + } +} diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index 513c2d6..56faa76 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -184,8 +184,10 @@ func TestJWTHandlerCheckHasRole(t *testing.T) { t.Error("Error: Token is empty") } - checkErr := jwtHandler.HasRole("role-x-1") - if checkErr == nil { + if checkErr := jwtHandler.HasOneRole("role-x-1"); checkErr == nil { + t.Error(consts.Unauthorized) + } + if checkErr := jwtHandler.HasRoles("role-x-1"); checkErr == nil { t.Error(consts.Unauthorized) } } diff --git a/service/user/user_service.go b/service/user/user_service.go index bb6b89a..ec1baae 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -37,7 +37,7 @@ type UserService interface { Logout(c *fiber.Ctx) (err error) UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) - DeleteAccount(ctx context.Context, id int) (err error) + DeleteAccount(ctx context.Context, data model.UserDeleteAccount) (err error) } type UserServiceImpl struct { @@ -339,8 +339,8 @@ func (svc *UserServiceImpl) UpdatePassword(ctx context.Context, data model.UserP return nil } -func (svc *UserServiceImpl) DeleteAccount(ctx context.Context, id int) (err error) { - user, getErr := svc.repository.GetByID(ctx, id) +func (svc *UserServiceImpl) DeleteAccount(ctx context.Context, data model.UserDeleteAccount) (err error) { + user, getErr := svc.repository.GetByID(ctx, data.ID) if getErr == gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusNotFound, consts.NotFound) } @@ -348,7 +348,12 @@ func (svc *UserServiceImpl) DeleteAccount(ctx context.Context, id int) (err erro return errors.New("error while getting user data") } - err = svc.repository.Delete(ctx, id) + res, err := hash.Verify(user.Password, data.Password) + if err != nil || !res { + return fiber.NewError(fiber.StatusBadRequest, "wrong password, please try again") + } + + err = svc.repository.Delete(ctx, data.ID) if err != nil { return errors.New("error while deleting user password") } @@ -369,9 +374,11 @@ func entityToResponse(data *entity.User) model.User { roles = append(roles, role.Name) } return model.User{ + ID: data.ID, Name: data.Name, Email: data.Email, ActivatedAt: data.ActivatedAt, + DeletedAt: data.DeletedAt, Roles: roles, } } diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go index 1a47760..27a8666 100644 --- a/service/user/user_service_test.go +++ b/service/user/user_service_test.go @@ -102,7 +102,10 @@ func TestRegister(t *testing.T) { assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) assert.NotNil(t, user, consts.ShouldNotNil, tc.Name, headerTestName) - deleteErr := service.DeleteAccount(ctx, id) + deleteErr := service.DeleteAccount(ctx, model.UserDeleteAccount{ + ID: id, + Password: tc.Payload.Password, + }) assert.NoError(t, deleteErr, consts.ShouldNotErr, tc.Name, headerTestName) // value reset @@ -463,40 +466,53 @@ func TestDelete(t *testing.T) { ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - userIDs := make([]int, 2) - for i := range userIDs { + users := make([]model.UserDeleteAccount, 2) + for i := range users { user := createUser() - userIDs[i] = user.ID + users[i] = model.UserDeleteAccount{ + ID: user.ID, + Password: user.Password, + } defer repository.Delete(ctx, user.ID) } type testCase struct { Name string - ID int + Payload model.UserDeleteAccount WantErr bool } testCases := []testCase{ { - Name: "Failed Delete User -1: invalid ID", - ID: -1, + Name: "Failed Delete User -1: invalid ID", + Payload: model.UserDeleteAccount{ + ID: -1, + }, WantErr: true, }, { - Name: "Failed Delete User -2: data not found", - ID: userIDs[0] * 99, + Name: "Failed Delete User -2: data not found", + Payload: model.UserDeleteAccount{ + ID: users[0].ID * 99, + }, WantErr: true, }, } - for i, id := range userIDs { + for i, user := range users { testCases = append(testCases, testCase{ - Name: "Success Delete User -" + strconv.Itoa(i+1), - ID: id, + Name: "Success Delete User -" + strconv.Itoa(i+1), + Payload: model.UserDeleteAccount{ + ID: user.ID, + Password: user.Password, + }, WantErr: false, }) testCases = append(testCases, testCase{ - Name: "Failed Delete User -" + strconv.Itoa(i+3) + ": already deleted", - ID: id, + Name: "Failed Delete User -" + strconv.Itoa(i+3) + ": already deleted", + Payload: model.UserDeleteAccount{ + ID: user.ID, + Password: user.Password, + }, WantErr: true, }) } @@ -504,14 +520,17 @@ func TestDelete(t *testing.T) { for _, tc := range testCases { log.Println(tc.Name, headerTestName) - deleteErr := service.DeleteAccount(ctx, tc.ID) + deleteErr := service.DeleteAccount(ctx, model.UserDeleteAccount{ + ID: tc.Payload.ID, + Password: tc.Payload.Password, + }) if tc.WantErr { assert.Error(t, deleteErr, consts.ShouldErr, tc.Name, headerTestName) continue } assert.NoError(t, deleteErr, consts.ShouldNotErr, tc.Name, headerTestName) - _, getErr := service.MyProfile(ctx, tc.ID) + _, getErr := service.MyProfile(ctx, tc.Payload.ID) assert.Error(t, getErr, consts.ShouldErr, tc.Name, headerTestName) } } From 5c3c0c3c84afc79b6e665dd9a6a6dbe27c23fbe0 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Wed, 31 Jan 2024 19:36:30 +0700 Subject: [PATCH 26/28] finishing features -2: testing --- service/email_service/email_service_test.go | 30 ++ service/role/role_service_test.go | 2 +- service/user/user_service_test.go | 429 ++++++++++++++++---- 3 files changed, 375 insertions(+), 86 deletions(-) create mode 100644 service/email_service/email_service_test.go diff --git a/service/email_service/email_service_test.go b/service/email_service/email_service_test.go new file mode 100644 index 0000000..6fca902 --- /dev/null +++ b/service/email_service/email_service_test.go @@ -0,0 +1,30 @@ +package service + +import ( + "testing" + + "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/env" + "github.com/stretchr/testify/assert" +) + +const ( + headerTestName string = "at Email Service Test" +) + +func init() { + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) +} + +func TestEmailService(t *testing.T) { + service := NewEmailService() + + // success + err := service.SendMail("subject", "message", "your.valid.email@email.test") + assert.Nil(t, err, consts.ShouldNil, headerTestName) + + // failed: invalid email + err = service.SendMail("subject", "message", "_invalid .email@email.test") + assert.NotNil(t, err, consts.ShouldNotNil, headerTestName) +} diff --git a/service/role/role_service_test.go b/service/role/role_service_test.go index 8b550fe..661d767 100644 --- a/service/role/role_service_test.go +++ b/service/role/role_service_test.go @@ -16,7 +16,7 @@ import ( ) const ( - headerTestName string = "at RoleServiceTest" + headerTestName string = "at Role Service Test" ) var ( diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go index 27a8666..8b5e814 100644 --- a/service/user/user_service_test.go +++ b/service/user/user_service_test.go @@ -3,9 +3,11 @@ package service import ( "log" "strconv" + "strings" "testing" "time" + "github.com/Lukmanern/gost/database/connector" "github.com/Lukmanern/gost/domain/entity" "github.com/Lukmanern/gost/domain/model" "github.com/Lukmanern/gost/internal/consts" @@ -13,16 +15,18 @@ import ( "github.com/Lukmanern/gost/internal/hash" "github.com/Lukmanern/gost/internal/helper" repository "github.com/Lukmanern/gost/repository/user" + "github.com/go-redis/redis" "github.com/stretchr/testify/assert" ) const ( - headerTestName string = "at UserServiceTest" + headerTestName string = "at User Service Test" ) var ( timeNow time.Time userRepository repository.UserRepository + redisConTest *redis.Client ) func init() { @@ -30,6 +34,7 @@ func init() { env.ReadConfig(envFilePath) timeNow = time.Now() userRepository = repository.NewUserRepository() + redisConTest = connector.LoadRedisCache() } func TestRegister(t *testing.T) { @@ -117,6 +122,81 @@ func TestRegister(t *testing.T) { } } +func TestAccountActivation(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := model.UserRegister{ + Name: strings.ToLower(helper.RandomString(12)), + Email: helper.RandomEmail(), + Password: helper.RandomString(12), + RoleIDs: []int{1, 2, 3}, + } + id, err := service.Register(ctx, validUser) + assert.Nil(t, err, consts.ShouldNotNil, headerTestName) + defer repository.Delete(ctx, id) + + key := validUser.Email + KEY_ACCOUNT_ACTIVATION + validCode := redisConTest.Get(key).Val() + assert.True(t, len(validCode) > 0, consts.ShouldNotNil, headerTestName) + + type testCase struct { + Name string + Payload model.UserActivation + WantErr bool + } + + testCases := []testCase{ + { + Name: "Failed Activation -1: wrong code", + Payload: model.UserActivation{ + Code: "wrongcode", + Email: validUser.Email, + }, + WantErr: true, + }, + { + Name: "Success Activation -1", + Payload: model.UserActivation{ + Code: validCode, + Email: validUser.Email, + }, + WantErr: false, + }, + { + Name: "Failed Activation -2: code is already used", + Payload: model.UserActivation{ + Code: validCode, + Email: validUser.Email, + }, + WantErr: true, + }, + { + Name: "Failed Activation -3: account not found", + Payload: model.UserActivation{ + Code: validCode, + Email: helper.RandomEmail(), + }, + WantErr: true, + }, + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + err := service.AccountActivation(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, err, consts.ShouldNotErr, tc.Name, headerTestName) + } +} + func TestLogin(t *testing.T) { service := NewUserService() assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) @@ -188,17 +268,131 @@ func TestLogin(t *testing.T) { } } -// func TestLogout(t *testing.T) { -// service := NewUserService() -// assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) -// repository := userRepository -// assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) -// c := helper.NewFiberCtx() -// assert.NotNil(t, c, consts.ShouldNotNil, headerTestName) +func TestForgetPassword(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + type testCase struct { + Name string + WantErr bool + Payload model.UserForgetPassword + } + + testCases := []testCase{ + { + Name: "Success Forget Password -1", + Payload: model.UserForgetPassword{Email: validUser.Email}, + WantErr: false, + }, + { + Name: "Success Forget Password -2", + Payload: model.UserForgetPassword{Email: validUser.Email}, + WantErr: false, + }, + { + Name: "Failed Forget Password -1: user not found", + Payload: model.UserForgetPassword{Email: helper.RandomEmail()}, + WantErr: true, + }, + { + Name: "Failed Forget Password -2: user not found", + Payload: model.UserForgetPassword{Email: helper.RandomEmail()}, + WantErr: true, + }, + } + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + err := service.ForgetPassword(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, err, consts.ShouldNotErr, tc.Name, headerTestName) + } +} + +func TestResetPassword(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + err := service.ForgetPassword(ctx, model.UserForgetPassword{Email: validUser.Email}) + assert.Nil(t, err, consts.ShouldNil, headerTestName) + + key := validUser.Email + KEY_FORGET_PASSWORD + validCode := redisConTest.Get(key).Val() + assert.True(t, len(validCode) > 0, consts.ShouldNotNil, headerTestName) + + type testCase struct { + Name string + Payload model.UserResetPassword + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success Reset Password -1", + Payload: model.UserResetPassword{ + Email: validUser.Email, + Code: validCode, + NewPassword: "new-password", + }, + WantErr: false, + }, + { + Name: "Failed Reset Password -1: code not found", + Payload: model.UserResetPassword{ + Email: validUser.Email, + Code: validCode, + NewPassword: "new-password", + }, + WantErr: true, + }, + { + Name: "Failed Reset Password -2: user not found", + Payload: model.UserResetPassword{ + Email: helper.RandomEmail(), + Code: validCode, + NewPassword: "new-password", + }, + WantErr: true, + }, + } + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + err := service.ResetPassword(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, err, consts.ShouldNotErr, tc.Name, headerTestName) + } +} + +func TestLogout(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + c := helper.NewFiberCtx() + assert.NotNil(t, c, consts.ShouldNotNil, headerTestName) -// logoutErr := service.Logout(c) -// assert.Error(t, logoutErr, consts.ShouldErr, headerTestName) -// } + logoutErr := service.Logout(c) + assert.NoError(t, logoutErr, consts.ShouldErr, headerTestName) +} func TestGetAll(t *testing.T) { service := NewUserService() @@ -208,6 +402,11 @@ func TestGetAll(t *testing.T) { ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + for i := 0; i < 3; i++ { + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + } + type testCase struct { Name string Payload model.RequestGetAll @@ -254,6 +453,58 @@ func TestGetAll(t *testing.T) { } } +func TestSoftDelete(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + type testCase struct { + Name string + WantErr bool + ID int + } + + testCases := []testCase{ + { + Name: "Success Soft Delete -1", + WantErr: false, + ID: validUser.ID, + }, + { + Name: "Failed Soft Delete -1: user not found", + WantErr: true, + ID: validUser.ID + 99, + }, + { + Name: "Failed Soft Delete -1: invalid ID / user not found", + WantErr: true, + ID: -10, + }, + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + err := service.SoftDelete(ctx, tc.ID) + if tc.WantErr { + assert.Error(t, err, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, err, consts.ShouldNotErr, tc.Name, headerTestName) + + user, getErr := repository.GetByID(ctx, tc.ID) + assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) + assert.NotNil(t, user, consts.ShouldNotNil, tc.Name, headerTestName) + assert.NotNil(t, user.DeletedAt, consts.ShouldNotNil, tc.Name, headerTestName) + } +} + func TestMyProfile(t *testing.T) { service := NewUserService() assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) @@ -383,80 +634,80 @@ func TestUpdateProfile(t *testing.T) { } } -// func TestUpdatePassword(t *testing.T) { -// service := NewUserService() -// assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) -// repository := userRepository -// assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) -// ctx := helper.NewFiberCtx().Context() -// assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - -// validUser := createUser() -// defer repository.Delete(ctx, validUser.ID) - -// type testCase struct { -// Name string -// Payload model.UserPasswordUpdate -// WantErr bool -// } - -// testCases := []testCase{ -// { -// Name: "Success Update -1", -// Payload: model.UserPasswordUpdate{ -// ID: validUser.ID, -// OldPassword: validUser.Password, -// NewPassword: helper.RandomString(16), -// }, -// WantErr: false, -// }, -// { -// Name: "Failed Update -1: wrong password / password is already changed", -// Payload: model.UserPasswordUpdate{ -// ID: validUser.ID, -// OldPassword: validUser.Password, -// NewPassword: helper.RandomString(16), -// }, -// WantErr: true, -// }, -// { -// Name: "Failed Update -2: invalid ID", -// Payload: model.UserPasswordUpdate{ -// ID: -1, -// OldPassword: validUser.Password, -// }, -// WantErr: true, -// }, -// { -// Name: "Failed Update -3: invalid ID", -// Payload: model.UserPasswordUpdate{ -// ID: 0, -// OldPassword: validUser.Password, -// }, -// WantErr: true, -// }, -// } - -// for _, tc := range testCases { -// log.Println(tc.Name, headerTestName) - -// updateErr := service.UpdatePassword(ctx, tc.Payload) -// if tc.WantErr { -// assert.Error(t, updateErr, consts.ShouldErr, tc.Name, headerTestName) -// continue -// } -// assert.NoError(t, updateErr, consts.ShouldNotErr, tc.Name, headerTestName) - -// if tc.Payload.ID == validUser.ID { -// token, loginErr := service.Login(ctx, model.UserLogin{ -// Email: validUser.Email, -// Password: tc.Payload.NewPassword, -// }) -// assert.NoError(t, loginErr, consts.ShouldNotErr, tc.Name, headerTestName) -// assert.True(t, token != "", consts.ShouldNotNil, tc.Name, headerTestName) -// } -// } -// } +func TestUpdatePassword(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + repository := userRepository + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + type testCase struct { + Name string + Payload model.UserPasswordUpdate + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success Update Password -1", + Payload: model.UserPasswordUpdate{ + ID: validUser.ID, + OldPassword: validUser.Password, + NewPassword: helper.RandomString(16), + }, + WantErr: false, + }, + { + Name: "Failed Update Password -1: wrong password / password is already changed", + Payload: model.UserPasswordUpdate{ + ID: validUser.ID, + OldPassword: validUser.Password, + NewPassword: helper.RandomString(16), + }, + WantErr: true, + }, + { + Name: "Failed Update Password -2: invalid ID", + Payload: model.UserPasswordUpdate{ + ID: -1, + OldPassword: validUser.Password, + }, + WantErr: true, + }, + { + Name: "Failed Update Password -3: invalid ID", + Payload: model.UserPasswordUpdate{ + ID: 0, + OldPassword: validUser.Password, + }, + WantErr: true, + }, + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + updateErr := service.UpdatePassword(ctx, tc.Payload) + if tc.WantErr { + assert.Error(t, updateErr, consts.ShouldErr, tc.Name, headerTestName) + continue + } + assert.NoError(t, updateErr, consts.ShouldNotErr, tc.Name, headerTestName) + + if tc.Payload.ID == validUser.ID { + token, loginErr := service.Login(ctx, model.UserLogin{ + Email: validUser.Email, + Password: tc.Payload.NewPassword, + }) + assert.NoError(t, loginErr, consts.ShouldNotErr, tc.Name, headerTestName) + assert.True(t, token != "", consts.ShouldNotNil, tc.Name, headerTestName) + } + } +} func TestDelete(t *testing.T) { service := NewUserService() @@ -497,6 +748,14 @@ func TestDelete(t *testing.T) { }, WantErr: true, }, + { + Name: "Failed Delete User -3: wrong password", + Payload: model.UserDeleteAccount{ + ID: users[0].ID, + Password: "wrong-password", + }, + WantErr: true, + }, } for i, user := range users { testCases = append(testCases, testCase{ From ae9a55a9e190929792baeb9b5fc654f1f12209d2 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Wed, 31 Jan 2024 22:21:30 +0700 Subject: [PATCH 27/28] finishing features -3: testing --- controller/user/user_controller.go | 4 +- controller/user/user_controller_test.go | 382 +++++++++++++++++++++++- 2 files changed, 376 insertions(+), 10 deletions(-) diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go index 5910c87..020db72 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -174,7 +174,7 @@ func (ctr *UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { return response.Error(c, consts.ErrServer+err.Error()) } - return response.CreateResponse(c, fiber.StatusAccepted, response.Response{ + return response.CreateResponse(c, fiber.StatusOK, response.Response{ Message: "success sending link for reset password to email, check your email inbox", Success: true, Data: nil, @@ -206,7 +206,7 @@ func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { return response.Error(c, consts.ErrServer+err.Error()) } - return response.CreateResponse(c, fiber.StatusAccepted, response.Response{ + return response.CreateResponse(c, fiber.StatusOK, response.Response{ Message: "your password already updated, you can login with the new password", Success: true, Data: nil, diff --git a/controller/user/user_controller_test.go b/controller/user/user_controller_test.go index c4d8367..7a0c5cf 100644 --- a/controller/user/user_controller_test.go +++ b/controller/user/user_controller_test.go @@ -232,6 +232,107 @@ func TestRegister(t *testing.T) { } } +func TestAccountActivation(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + _service := service.NewUserService() + assert.NotNil(t, _service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(_service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + // validUser2 := createUser() + // defer repository.Delete(ctx, validUser2.ID) + + validUser := model.UserRegister{ + Name: helper.RandomString(15), + Email: helper.RandomEmail(), + Password: helper.RandomString(14), + RoleIDs: []int{1, 2, 3}, + } + id, err := _service.Register(ctx, validUser) + defer repository.Delete(ctx, id) + assert.Nil(t, err, consts.ShouldNil, headerTestName) + + redisConTest := connector.LoadRedisCache() + key := validUser.Email + service.KEY_ACCOUNT_ACTIVATION + validCode := redisConTest.Get(key).Val() + + type testCase struct { + Name string + ResCode int + Payload model.UserActivation + } + + testCases := []testCase{ + { + Name: "Success Account Activation -1", + ResCode: fiber.StatusOK, + Payload: model.UserActivation{ + Email: validUser.Email, + Code: validCode, + }, + }, + { + Name: "Failed Account Activation -1 : account already active", + ResCode: fiber.StatusBadRequest, + Payload: model.UserActivation{ + Email: validUser.Email, + Code: validCode, + }, + }, + { + Name: "Failed Account Activation -2 : data not found", + ResCode: fiber.StatusNotFound, + Payload: model.UserActivation{ + Email: helper.RandomEmail(), + Code: validCode, + }, + }, + { + Name: "Failed Account Activation -3 : invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserActivation{ + Email: "invalid-email", + Code: helper.RandomString(15), + }, + }, + } + + pathURL := "user/account-activation" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, controller.AccountActivation) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + func TestLogin(t *testing.T) { // Initialize repository, service and controller repository := repository.NewUserRepository() @@ -623,6 +724,14 @@ func TestUpdateProfile(t *testing.T) { }, Token: helper.GenerateToken(), // valid token }, + { + Name: "Failed Update Profile -3: User not found", + ResCode: fiber.StatusNotFound, + Payload: model.UserUpdate{ + Name: "valid-name", + }, + Token: helper.GenerateToken(), // valid token + }, } pathURL := "user/profile" @@ -786,12 +895,12 @@ func TestDeleteAccount(t *testing.T) { assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) fakeToken := helper.GenerateToken() - entityUser := createUser() + validUser := createUser() validToken, loginErr := service.Login(ctx, model.UserLogin{ - Email: entityUser.Email, - Password: entityUser.Password, + Email: validUser.Email, + Password: validUser.Password, }) - defer repository.Delete(ctx, entityUser.ID) + defer repository.Delete(ctx, validUser.ID) assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) type testCase struct { @@ -834,8 +943,8 @@ func TestDeleteAccount(t *testing.T) { ResCode: fiber.StatusNoContent, Token: validToken, Payload: model.UserDeleteAccount{ - Password: entityUser.Password, - PasswordConfirm: entityUser.Password, + Password: validUser.Password, + PasswordConfirm: validUser.Password, }, }, { @@ -843,8 +952,8 @@ func TestDeleteAccount(t *testing.T) { ResCode: fiber.StatusUnauthorized, Token: validToken, // token is invalid Payload: model.UserDeleteAccount{ - Password: entityUser.Password, - PasswordConfirm: entityUser.Password, + Password: validUser.Password, + PasswordConfirm: validUser.Password, }, }, { @@ -911,3 +1020,260 @@ func createUser() entity.User { data.ID = id return data } + +func TestForgetPassword(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + testCases := []testCase{ + { + Name: "Success Forgot Password -1", + ResCode: fiber.StatusOK, + Payload: model.UserForgetPassword{ + Email: validUser.Email, + }, + }, + { + Name: "Failed Forgot Password -1: user isn't found", + ResCode: fiber.StatusNotFound, + Payload: model.UserForgetPassword{ + Email: helper.RandomEmail(), + }, + }, + { + Name: "Failed Forgot Password -2: invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserForgetPassword{ + Email: "invalid-email", + }, + }, + } + + pathURL := "user/forget-password" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, controller.ForgetPassword) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestResetPassword(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + _service := service.NewUserService() + assert.NotNil(t, _service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(_service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := model.UserRegister{ + Name: helper.RandomString(13), + Email: helper.RandomEmail(), + Password: helper.RandomString(13), + RoleIDs: []int{1, 2, 3}, + } + id, err := _service.Register(ctx, validUser) + defer repository.Delete(ctx, id) + assert.Nil(t, err, consts.ShouldNotNil, headerTestName) + err = _service.ForgetPassword(ctx, model.UserForgetPassword{Email: validUser.Email}) + assert.Nil(t, err, consts.ShouldNil, headerTestName) + + redisConTest := connector.LoadRedisCache() + key := validUser.Email + service.KEY_FORGET_PASSWORD + validCode := redisConTest.Get(key).Val() + assert.True(t, len(validCode) >= 21, "should true", headerTestName) + + testCases := []testCase{ + { + Name: "Success Reset Password -1", + ResCode: fiber.StatusOK, + Payload: model.UserResetPassword{ + Email: validUser.Email, + Code: validCode, + NewPassword: "new-password-00", + NewPasswordConfirm: "new-password-00", + }, + }, + { + Name: "Failed Reset Password -1: user isn't found", + ResCode: fiber.StatusNotFound, + Payload: model.UserResetPassword{ + Email: helper.RandomEmail(), + Code: helper.RandomString(28), + NewPassword: "valid-passwd-00", + NewPasswordConfirm: "valid-passwd-00", + }, + }, + { + Name: "Failed Reset Password -2: invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserResetPassword{ + Email: "invalid-email", + Code: helper.RandomString(28), + NewPassword: "valid-passwd-00", + NewPasswordConfirm: "valid-passwd-00", + }, + }, + { + Name: "Failed Reset Password -2: password isn't match", + ResCode: fiber.StatusBadRequest, + Payload: model.UserResetPassword{ + Email: helper.RandomEmail(), + Code: helper.RandomString(28), + NewPassword: "valid-passwd-11", + NewPasswordConfirm: "valid-passwd-00", + }, + }, + } + + pathURL := "user/reset-password" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, controller.ResetPassword) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr, tc.Name) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode, tc.Name) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err, tc.Name) + } + } + +} + +func TestBanAccount(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser1 := createUser() + defer repository.Delete(ctx, validUser1.ID) + + fakeToken := helper.GenerateToken() + + validUser2 := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: validUser2.Email, + Password: validUser2.Password, + }) + defer repository.Delete(ctx, validUser2.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) + + type testCase struct { + Name string + ResCode int + Token string + ID string + } + + testCases := []testCase{ + { + Name: "Success Ban Account -1", + ResCode: fiber.StatusNoContent, + Token: validToken, + ID: strconv.Itoa(validUser1.ID), + }, + { + Name: "Failed Ban Account -1: user not found", + ResCode: fiber.StatusNotFound, + Token: validToken, + ID: strconv.Itoa(validUser1.ID + 100), + }, + { + Name: "Failed Ban Account -2: invalid ID", + ResCode: fiber.StatusBadRequest, + Token: fakeToken, + ID: "-10", + }, + } + + pathURL := "user/ban-user/" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPut, URL+tc.ID, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Put(pathURL+":id", jwtHandler.IsAuthenticated, controller.BanAccount) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode, tc.Name) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} From 503629bf81b148a79ffc04a54803ec3feb2a76a7 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Wed, 31 Jan 2024 22:36:03 +0700 Subject: [PATCH 28/28] finishing features -4: sorting --- controller/user/user_controller_test.go | 450 ++++++++++++------------ 1 file changed, 225 insertions(+), 225 deletions(-) diff --git a/controller/user/user_controller_test.go b/controller/user/user_controller_test.go index 7a0c5cf..1af7876 100644 --- a/controller/user/user_controller_test.go +++ b/controller/user/user_controller_test.go @@ -422,78 +422,178 @@ func TestLogin(t *testing.T) { } } -func TestLogout(t *testing.T) { +func TestForgetPassword(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) service := service.NewUserService() assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) controller := NewUserController(service) assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) - jwtHandler := middleware.NewJWTHandler() - assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - tokens := make([]string, 1) - for i := range tokens { - tokens[i] = helper.GenerateToken() + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + testCases := []testCase{ + { + Name: "Success Forgot Password -1", + ResCode: fiber.StatusOK, + Payload: model.UserForgetPassword{ + Email: validUser.Email, + }, + }, + { + Name: "Failed Forgot Password -1: user isn't found", + ResCode: fiber.StatusNotFound, + Payload: model.UserForgetPassword{ + Email: helper.RandomEmail(), + }, + }, + { + Name: "Failed Forgot Password -2: invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserForgetPassword{ + Email: "invalid-email", + }, + }, } - type testCase struct { - Name string - ResCode int - Token string + pathURL := "user/forget-password" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, controller.ForgetPassword) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestResetPassword(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + _service := service.NewUserService() + assert.NotNil(t, _service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(_service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := model.UserRegister{ + Name: helper.RandomString(13), + Email: helper.RandomEmail(), + Password: helper.RandomString(13), + RoleIDs: []int{1, 2, 3}, } + id, err := _service.Register(ctx, validUser) + defer repository.Delete(ctx, id) + assert.Nil(t, err, consts.ShouldNotNil, headerTestName) + err = _service.ForgetPassword(ctx, model.UserForgetPassword{Email: validUser.Email}) + assert.Nil(t, err, consts.ShouldNil, headerTestName) + + redisConTest := connector.LoadRedisCache() + key := validUser.Email + service.KEY_FORGET_PASSWORD + validCode := redisConTest.Get(key).Val() + assert.True(t, len(validCode) >= 21, "should true", headerTestName) testCases := []testCase{ { - Name: "Failed Login -1: invalid token", - ResCode: fiber.StatusUnauthorized, - Token: "--", + Name: "Success Reset Password -1", + ResCode: fiber.StatusOK, + Payload: model.UserResetPassword{ + Email: validUser.Email, + Code: validCode, + NewPassword: "new-password-00", + NewPasswordConfirm: "new-password-00", + }, }, { - Name: "Failed Login -2: invalid token", - ResCode: fiber.StatusUnauthorized, - Token: "INVALID-TOKEN", + Name: "Failed Reset Password -1: user isn't found", + ResCode: fiber.StatusNotFound, + Payload: model.UserResetPassword{ + Email: helper.RandomEmail(), + Code: helper.RandomString(28), + NewPassword: "valid-passwd-00", + NewPasswordConfirm: "valid-passwd-00", + }, + }, + { + Name: "Failed Reset Password -2: invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserResetPassword{ + Email: "invalid-email", + Code: helper.RandomString(28), + NewPassword: "valid-passwd-00", + NewPasswordConfirm: "valid-passwd-00", + }, + }, + { + Name: "Failed Reset Password -2: password isn't match", + ResCode: fiber.StatusBadRequest, + Payload: model.UserResetPassword{ + Email: helper.RandomEmail(), + Code: helper.RandomString(28), + NewPassword: "valid-passwd-11", + NewPasswordConfirm: "valid-passwd-00", + }, }, } - for i, token := range tokens { - testCases = append(testCases, testCase{ - Name: "Success Logout -" + strconv.Itoa(i+2), - ResCode: fiber.StatusNoContent, - Token: token, - }) - testCases = append(testCases, testCase{ - Name: "Failed Logout -" + strconv.Itoa(i+3), - ResCode: fiber.StatusUnauthorized, - Token: token, - }) - } - - pathURL := "user/logout" + pathURL := "user/reset-password" URL := baseURL + pathURL for _, tc := range testCases { log.Println(tc.Name, headerTestName) + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + // Create HTTP request - req := httptest.NewRequest(fiber.MethodPost, URL, nil) - req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) // Set up Fiber app and handle the request with the controller app := fiber.New() - app.Post(pathURL, jwtHandler.IsAuthenticated, controller.Logout) + app.Post(pathURL, controller.ResetPassword) req.Close = true // run test res, testErr := app.Test(req, -1) - assert.Nil(t, testErr, consts.ShouldNil, testErr) + assert.Nil(t, testErr, consts.ShouldNil, testErr, tc.Name) defer res.Body.Close() - assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode) + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode, tc.Name) if res.StatusCode != fiber.StatusNoContent { responseStruct := response.Response{} err := json.NewDecoder(res.Body).Decode(&responseStruct) - assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err, tc.Name) } } + } func TestMyProfile(t *testing.T) { @@ -582,86 +682,71 @@ func TestMyProfile(t *testing.T) { } } -func TestGetAll(t *testing.T) { - repository := repository.NewUserRepository() - assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) +func TestLogout(t *testing.T) { service := service.NewUserService() assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) controller := NewUserController(service) assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) jwtHandler := middleware.NewJWTHandler() assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) - ctx := helper.NewFiberCtx().Context() - assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - for i := 0; i < 3; i++ { - entityUser := createUser() - defer repository.Delete(ctx, entityUser.ID) + tokens := make([]string, 1) + for i := range tokens { + tokens[i] = helper.GenerateToken() } - token := helper.GenerateToken() - assert.True(t, token != "", consts.ShouldNotNil, headerTestName) - type testCase struct { Name string - Params string ResCode int - WantErr bool + Token string } testCases := []testCase{ { - Name: "Success get all -1", - Params: "?limit=100&page=1", - ResCode: fiber.StatusOK, - WantErr: false, - }, - { - Name: "Success get all -2", - Params: "?limit=12&page=1", - ResCode: fiber.StatusOK, - WantErr: false, - }, - { - Name: "Failed get all: invalid limit", - Params: "?limit=-1&page=1", - ResCode: fiber.StatusBadRequest, - WantErr: true, - }, - { - Name: "Failed get all: invalid page", - Params: "?limit=1&page=-1", - ResCode: fiber.StatusBadRequest, - WantErr: true, + Name: "Failed Login -1: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "--", }, { - Name: "Failed get all: invalid sort", - Params: "?limit=1&page=1&sort=invalid", // sort should name - ResCode: fiber.StatusInternalServerError, - WantErr: true, + Name: "Failed Login -2: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "INVALID-TOKEN", }, } - pathURL := "user/" + for i, token := range tokens { + testCases = append(testCases, testCase{ + Name: "Success Logout -" + strconv.Itoa(i+2), + ResCode: fiber.StatusNoContent, + Token: token, + }) + testCases = append(testCases, testCase{ + Name: "Failed Logout -" + strconv.Itoa(i+3), + ResCode: fiber.StatusUnauthorized, + Token: token, + }) + } + + pathURL := "user/logout" URL := baseURL + pathURL for _, tc := range testCases { log.Println(tc.Name, headerTestName) // Create HTTP request - req := httptest.NewRequest(fiber.MethodGet, URL+tc.Params, nil) - req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + req := httptest.NewRequest(fiber.MethodPost, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) // Set up Fiber app and handle the request with the controller app := fiber.New() - app.Get(pathURL, jwtHandler.IsAuthenticated, controller.GetAll) + app.Post(pathURL, jwtHandler.IsAuthenticated, controller.Logout) req.Close = true // run test res, testErr := app.Test(req, -1) assert.Nil(t, testErr, consts.ShouldNil, testErr) defer res.Body.Close() - assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode, res.StatusCode) + assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode) if res.StatusCode != fiber.StatusNoContent { responseStruct := response.Response{} @@ -1000,199 +1085,93 @@ func TestDeleteAccount(t *testing.T) { } } -func createUser() entity.User { - pw := helper.RandomString(15) - pwHashed, _ := hash.Generate(pw) - repo := userRepo - ctx := helper.NewFiberCtx().Context() - data := entity.User{ - Name: helper.RandomString(15), - Email: helper.RandomEmail(), - Password: pwHashed, - ActivatedAt: &timeNow, - } - data.SetCreateTime() - id, err := repo.Create(ctx, data, []int{1, 2}) - if err != nil { - log.Fatal("Failed create user", headerTestName) - } - data.Password = pw - data.ID = id - return data -} - -func TestForgetPassword(t *testing.T) { - // Initialize repository, service and controller +func TestGetAll(t *testing.T) { repository := repository.NewUserRepository() assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) service := service.NewUserService() assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) controller := NewUserController(service) assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) ctx := helper.NewFiberCtx().Context() assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - validUser := createUser() - defer repository.Delete(ctx, validUser.ID) - - testCases := []testCase{ - { - Name: "Success Forgot Password -1", - ResCode: fiber.StatusOK, - Payload: model.UserForgetPassword{ - Email: validUser.Email, - }, - }, - { - Name: "Failed Forgot Password -1: user isn't found", - ResCode: fiber.StatusNotFound, - Payload: model.UserForgetPassword{ - Email: helper.RandomEmail(), - }, - }, - { - Name: "Failed Forgot Password -2: invalid email", - ResCode: fiber.StatusBadRequest, - Payload: model.UserForgetPassword{ - Email: "invalid-email", - }, - }, - } - - pathURL := "user/forget-password" - URL := baseURL + pathURL - for _, tc := range testCases { - log.Println(tc.Name, headerTestName) - - // Marshal payload to JSON - jsonData, marshalErr := json.Marshal(&tc.Payload) - assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) - - // Create HTTP request - req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - - // Set up Fiber app and handle the request with the controller - app := fiber.New() - app.Post(pathURL, controller.ForgetPassword) - req.Close = true - - // run test - res, testErr := app.Test(req, -1) - assert.Nil(t, testErr, consts.ShouldNil, testErr) - defer res.Body.Close() - assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) - - if res.StatusCode != fiber.StatusNoContent { - responseStruct := response.Response{} - err := json.NewDecoder(res.Body).Decode(&responseStruct) - assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) - } + for i := 0; i < 3; i++ { + entityUser := createUser() + defer repository.Delete(ctx, entityUser.ID) } -} -func TestResetPassword(t *testing.T) { - // Initialize repository, service and controller - repository := repository.NewUserRepository() - assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) - _service := service.NewUserService() - assert.NotNil(t, _service, consts.ShouldNotNil, headerTestName) - controller := NewUserController(_service) - assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) - ctx := helper.NewFiberCtx().Context() - assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + token := helper.GenerateToken() + assert.True(t, token != "", consts.ShouldNotNil, headerTestName) - validUser := model.UserRegister{ - Name: helper.RandomString(13), - Email: helper.RandomEmail(), - Password: helper.RandomString(13), - RoleIDs: []int{1, 2, 3}, + type testCase struct { + Name string + Params string + ResCode int + WantErr bool } - id, err := _service.Register(ctx, validUser) - defer repository.Delete(ctx, id) - assert.Nil(t, err, consts.ShouldNotNil, headerTestName) - err = _service.ForgetPassword(ctx, model.UserForgetPassword{Email: validUser.Email}) - assert.Nil(t, err, consts.ShouldNil, headerTestName) - - redisConTest := connector.LoadRedisCache() - key := validUser.Email + service.KEY_FORGET_PASSWORD - validCode := redisConTest.Get(key).Val() - assert.True(t, len(validCode) >= 21, "should true", headerTestName) testCases := []testCase{ { - Name: "Success Reset Password -1", + Name: "Success get all -1", + Params: "?limit=100&page=1", ResCode: fiber.StatusOK, - Payload: model.UserResetPassword{ - Email: validUser.Email, - Code: validCode, - NewPassword: "new-password-00", - NewPasswordConfirm: "new-password-00", - }, + WantErr: false, }, { - Name: "Failed Reset Password -1: user isn't found", - ResCode: fiber.StatusNotFound, - Payload: model.UserResetPassword{ - Email: helper.RandomEmail(), - Code: helper.RandomString(28), - NewPassword: "valid-passwd-00", - NewPasswordConfirm: "valid-passwd-00", - }, + Name: "Success get all -2", + Params: "?limit=12&page=1", + ResCode: fiber.StatusOK, + WantErr: false, }, { - Name: "Failed Reset Password -2: invalid email", + Name: "Failed get all: invalid limit", + Params: "?limit=-1&page=1", ResCode: fiber.StatusBadRequest, - Payload: model.UserResetPassword{ - Email: "invalid-email", - Code: helper.RandomString(28), - NewPassword: "valid-passwd-00", - NewPasswordConfirm: "valid-passwd-00", - }, + WantErr: true, }, { - Name: "Failed Reset Password -2: password isn't match", + Name: "Failed get all: invalid page", + Params: "?limit=1&page=-1", ResCode: fiber.StatusBadRequest, - Payload: model.UserResetPassword{ - Email: helper.RandomEmail(), - Code: helper.RandomString(28), - NewPassword: "valid-passwd-11", - NewPasswordConfirm: "valid-passwd-00", - }, + WantErr: true, + }, + { + Name: "Failed get all: invalid sort", + Params: "?limit=1&page=1&sort=invalid", // sort should name + ResCode: fiber.StatusInternalServerError, + WantErr: true, }, } - pathURL := "user/reset-password" + pathURL := "user/" URL := baseURL + pathURL for _, tc := range testCases { log.Println(tc.Name, headerTestName) - // Marshal payload to JSON - jsonData, marshalErr := json.Marshal(&tc.Payload) - assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) - // Create HTTP request - req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req := httptest.NewRequest(fiber.MethodGet, URL+tc.Params, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) // Set up Fiber app and handle the request with the controller app := fiber.New() - app.Post(pathURL, controller.ResetPassword) + app.Get(pathURL, jwtHandler.IsAuthenticated, controller.GetAll) req.Close = true // run test res, testErr := app.Test(req, -1) - assert.Nil(t, testErr, consts.ShouldNil, testErr, tc.Name) + assert.Nil(t, testErr, consts.ShouldNil, testErr) defer res.Body.Close() - assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode, tc.Name) + assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode, res.StatusCode) if res.StatusCode != fiber.StatusNoContent { responseStruct := response.Response{} err := json.NewDecoder(res.Body).Decode(&responseStruct) - assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err, tc.Name) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) } } - } func TestBanAccount(t *testing.T) { @@ -1277,3 +1256,24 @@ func TestBanAccount(t *testing.T) { } } } + +func createUser() entity.User { + pw := helper.RandomString(15) + pwHashed, _ := hash.Generate(pw) + repo := userRepo + ctx := helper.NewFiberCtx().Context() + data := entity.User{ + Name: helper.RandomString(15), + Email: helper.RandomEmail(), + Password: pwHashed, + ActivatedAt: &timeNow, + } + data.SetCreateTime() + id, err := repo.Create(ctx, data, []int{1, 2}) + if err != nil { + log.Fatal("Failed create user", headerTestName) + } + data.Password = pw + data.ID = id + return data +}