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/.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 ./... 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/.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 index 191558e..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 @@ -12,6 +6,7 @@ import ( "errors" "fmt" "log" + "net" "os" "os/signal" "time" @@ -73,8 +68,17 @@ func setup() { 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, })) @@ -109,10 +113,8 @@ func RunApp() { close(idleConnsClosed) }() - getUserManagementRoutes(router) // user CRUD without auth ⚠️ - getDevopmentRouter(router) // experimental without auth ⚠️ - getUserRoutes(router) // user with auth - getRolePermissionRoutes(router) // RBAC CRUD with auth + 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/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/role_router.go b/application/role_router.go new file mode 100644 index 0000000..29985a7 --- /dev/null +++ b/application/role_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" + + controller "github.com/Lukmanern/gost/controller/role" + service "github.com/Lukmanern/gost/service/role" +) + +var ( + roleService service.RoleService + roleController controller.RoleController +) + +func getRolePermissionRoutes(router fiber.Router) { + jwtHandler := middleware.NewJWTHandler() + + roleService = service.NewRoleService() + roleController = controller.NewRoleController(roleService) + + roleRouter := router.Group("role").Use(jwtHandler.IsAuthenticated) + 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_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 index 66bc972..21e4a4e 100644 --- a/application/user_router.go +++ b/application/user_router.go @@ -1,10 +1,4 @@ -// 📌 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. +// 📌 Origin Github Repository: https://github.com/Lukmanern package application @@ -12,39 +6,38 @@ 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" - - 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 + 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() + userService = service.NewUserService() + 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("request-delete", userController.DeleteAccountActivation) - userRoute.Post("forget-password", userController.ForgetPassword) + userRoute.Post("forget-password", userController.ForgetPassword) // send email userRoute.Post("reset-password", userController.ResetPassword) 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) + + // 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/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 index f794326..70e0078 100644 --- a/controller/role/role_controller.go +++ b/controller/role/role_controller.go @@ -7,32 +7,19 @@ import ( "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/consts" + "github.com/Lukmanern/gost/internal/middleware" "github.com/Lukmanern/gost/internal/response" service "github.com/Lukmanern/gost/service/role" ) type RoleController interface { - - // Create func creates a new role + // auth + admin 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 } @@ -55,34 +42,30 @@ 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, 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 + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } validate := validator.New() if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) + 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) + id, err := ctr.service.Create(ctx, role) + 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, constants.ServerErr+createErr.Error()) + return response.Error(c, consts.ErrServer) } data := map[string]any{ "id": id, @@ -90,59 +73,38 @@ func (ctr *RoleControllerImpl) Create(c *fiber.Ctx) error { 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()) +func (ctr *RoleControllerImpl) Get(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) } - 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) + 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, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } - return response.Error(c, constants.ServerErr+getErr.Error()) + return response.Error(c, consts.ErrServer) } return response.SuccessLoaded(c, role) } func (ctr *RoleControllerImpl) GetAll(c *fiber.Ctx) error { - request := base.RequestGetAll{ + 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"), @@ -155,18 +117,18 @@ func (ctr *RoleControllerImpl) GetAll(c *fiber.Ctx) error { ctx := c.Context() roles, total, getErr := ctr.service.GetAll(ctx, request) if getErr != nil { - return response.Error(c, constants.ServerErr+getErr.Error()) + return response.Error(c, consts.ErrServer+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, + responseData := model.GetAllResponse{ + Meta: model.PageMeta{ + TotalData: total, + TotalPages: int(math.Ceil(float64(total) / float64(request.Limit))), + AtPage: request.Page, }, Data: data, } @@ -174,46 +136,60 @@ 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, constants.InvalidID) + return response.BadRequest(c, consts.InvalidID) } + var role model.RoleUpdate - role.ID = id if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, constants.InvalidBody+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, constants.InvalidBody+err.Error()) + return response.BadRequest(c, consts.InvalidJSONBody) } 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, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } - return response.Error(c, constants.ServerErr+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, constants.InvalidID) + 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, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } - return response.Error(c, constants.ServerErr+deleteErr.Error()) + return response.Error(c, consts.ErrServer) } return response.SuccessNoContent(c) } diff --git a/controller/role/role_controller_test.go b/controller/role/role_controller_test.go index 1df10d4..ae7e4ad 100644 --- a/controller/role/role_controller_test.go +++ b/controller/role/role_controller_test.go @@ -1,7 +1,3 @@ -// 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 ( @@ -9,752 +5,546 @@ import ( "encoding/json" "fmt" "log" - "net/http" + "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/constants" + "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" +) - permissionController "github.com/Lukmanern/gost/controller/permission" - permSvc "github.com/Lukmanern/gost/service/permission" - service "github.com/Lukmanern/gost/service/role" +const ( + headerTestName string = "at Role Controller Test" ) var ( - permService permSvc.PermissionService - roleService service.RoleService - roleController RoleController - permController permissionController.PermissionController - appURL string + baseURL string + token string + timeNow time.Time + roleRepo repository.RoleRepository ) func init() { - env.ReadConfig("./../../.env") + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) config := env.Configuration() - appURL = config.AppURL + baseURL = config.AppURL + token = helper.GenerateToken() + timeNow = time.Now() + roleRepo = repository.NewRoleRepository() connector.LoadDatabase() - connector.LoadRedisCache() - - permService = permSvc.NewPermissionService() - permController = permissionController.NewPermissionController(permService) - - roleService = service.NewRoleService(permService) - roleController = NewRoleController(roleService) + r := connector.LoadRedisCache() + r.FlushAll() // clear all key:value in redis } -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) +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()) } +} - 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) - }() +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) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - permIDs = append(permIDs, permID) - } + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) - 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") + type testCase struct { + Name string + ResCode int + Payload model.RoleCreate } - 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 - }{ + + testCases := []testCase{ { - caseName: "success create -1", - respCode: http.StatusCreated, - payload: model.RoleCreate{ - Name: helper.RandomString(10), - Description: helper.RandomString(30), + Name: "Success Create Role -1", + ResCode: fiber.StatusCreated, + Payload: model.RoleCreate{ + Name: strings.ToLower(helper.RandomString(14)), + Description: helper.RandomWords(10), }, }, { - caseName: "success create -2", - respCode: http.StatusCreated, - payload: model.RoleCreate{ - Name: helper.RandomString(10), - Description: helper.RandomString(30), + Name: "Success Create Role -2", + ResCode: fiber.StatusCreated, + Payload: model.RoleCreate{ + Name: strings.ToLower(helper.RandomString(14)), + Description: helper.RandomWords(10), }, }, { - 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}, + Name: "Failed Create Role -1: invalid name / name is already used", + ResCode: fiber.StatusBadRequest, + Payload: model.RoleCreate{ + Name: validRole.Name, + Description: helper.RandomWords(10), }, }, { - caseName: "failed create: invalid name, too short", - respCode: http.StatusBadRequest, - payload: model.RoleCreate{ - Name: "", - Description: helper.RandomString(30), - PermissionsID: []int{permIDs[0] - 90}, + Name: "Failed Create Role -2: invalid name / name too short", + ResCode: fiber.StatusBadRequest, + Payload: model.RoleCreate{ + Name: "", + Description: helper.RandomWords(10), }, }, { - caseName: "failed create: invalid description, too short", - respCode: http.StatusBadRequest, - payload: model.RoleCreate{ - Name: helper.RandomString(10), - Description: "", - PermissionsID: []int{permIDs[0]}, + 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.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()) - } + 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("/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) - } + app.Post(pathURL, jwtHandler.IsAuthenticated, controller.Create) + req.Close = true - 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") + // 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) } - deleteErr := roleService.Delete(ctx, int(intID)) - if deleteErr != nil { - t.Error(constants.ShouldNotErr) + + 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 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) - } +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) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - 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) - }() + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) - permIDs = append(permIDs, permID) + type testCase struct { + Name string + ResCode int + ID string } - 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 - }{ + testCases := []testCase{ { - caseName: "success connect -1", - respCode: http.StatusCreated, - payload: model.RoleConnectToPermissions{ - RoleID: roleID, - PermissionsID: permIDs, - }, + Name: "Success Create Role -1", + ResCode: fiber.StatusOK, + ID: strconv.Itoa(validRole.ID), }, { - caseName: "success connect -2", - respCode: http.StatusCreated, - payload: model.RoleConnectToPermissions{ - RoleID: roleID, - PermissionsID: permIDs, - }, + Name: "Failed Create Role -1: invalid ID", + ResCode: fiber.StatusBadRequest, + ID: strconv.Itoa(-1), }, { - caseName: "failed connect: status not found", - respCode: http.StatusNotFound, - payload: model.RoleConnectToPermissions{ - RoleID: roleID + 99, - PermissionsID: permIDs, - }, + Name: "Failed Create Role -2: invalid ID", + ResCode: fiber.StatusBadRequest, + ID: "invalid-id", }, { - 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}, - }, + Name: "Failed Create Role -3: not found", + ResCode: fiber.StatusNotFound, + ID: strconv.Itoa(validRole.ID + 99), }, } 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()) - } + 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.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") - } + app.Get(pathURL+":id", jwtHandler.IsAuthenticated, controller.Get) + req.Close = true - if len(role.Permissions) != len(tc.payload.PermissionsID) { - t.Error("should equal") + // 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 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) - } +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) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - 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) + for i := 0; i < 3; i++ { + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) } - 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") + type testCase struct { + Name string + ResCode int + Params string } - defer func() { - roleService.Delete(ctx, roleID) - }() - - testCases := []struct { - caseName string - respCode int - roleID int - }{ - { - caseName: "success get -1", - respCode: http.StatusOK, - roleID: roleID, - }, + + testCases := []testCase{ { - caseName: "success get -2", - respCode: http.StatusOK, - roleID: roleID, + Name: "Success Get All Role -1", + ResCode: fiber.StatusOK, + Params: "?limit=100&page=1", }, { - caseName: "failed get: status not found", - respCode: http.StatusNotFound, - roleID: roleID + 99, + Name: "Failed Get All Role -1: invalid parameter", + ResCode: fiber.StatusBadRequest, + Params: "?limit=-1&page=1", }, { - caseName: "failed get: invalid id", - respCode: http.StatusBadRequest, - roleID: -10, + Name: "Failed Get All Role -2: invalid parameter", + ResCode: fiber.StatusBadRequest, + Params: "?limit=100&page=-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()) - } + 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("/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) - } - } -} + app.Get(pathURL, jwtHandler.IsAuthenticated, controller.GetAll) + req.Close = true -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) - } + // 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) - 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") + 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) + } } - 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 - }{ +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) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) + + type testCase struct { + Name string + ResCode int + ID string + Payload model.RoleUpdate + } + + testCases := []testCase{ { - 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", + 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), + }, }, - } - - 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), + 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), }, }, { - caseName: "success update -2", - respCode: http.StatusNoContent, - roleID: roleID, - payload: model.RoleUpdate{ - ID: roleID, - Name: helper.RandomString(12), - Description: helper.RandomString(20), + 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), }, }, { - caseName: "failed update: invalid name/description", - respCode: http.StatusBadRequest, - roleID: roleID, - payload: model.RoleUpdate{ - ID: roleID, + Name: "Failed Update Role -2: name too short", + ResCode: fiber.StatusBadRequest, + ID: strconv.Itoa(validRole.ID), + Payload: model.RoleUpdate{ Name: "", - Description: "", + Description: helper.RandomWords(5), }, }, { - caseName: "failed update: invalid id", - respCode: http.StatusBadRequest, - roleID: -10, + 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), + }, }, { - 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), + 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 { - 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()) - } + 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("/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 { + 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(resp.Body).Decode(&data); err != nil { + if err := json.NewDecoder(res.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") - } + 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 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) - } +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) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) - 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) - }() + validRole := createRole() + defer repository.Delete(ctx, validRole.ID) - permIDs = append(permIDs, permID) + type testCase struct { + Name string + ResCode int + ID string } - 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 - }{ + testCases := []testCase{ { - caseName: "success update -1", - respCode: http.StatusNoContent, - roleID: roleID, + Name: "Success Delete Role -1", + ResCode: fiber.StatusNoContent, + ID: strconv.Itoa(validRole.ID), }, { - caseName: "success update -2", - respCode: http.StatusNotFound, - roleID: roleID, + Name: "Failed Delete Role -1: data not found / already deleted", + ResCode: fiber.StatusNotFound, + ID: strconv.Itoa(validRole.ID), }, { - caseName: "failed update: invalid id", - respCode: http.StatusBadRequest, - roleID: -10, + Name: "Failed Delete Role -2: data not found", + ResCode: fiber.StatusNotFound, + ID: strconv.Itoa(validRole.ID + 999), }, { - caseName: "failed update: data not found", - respCode: http.StatusNotFound, - roleID: roleID + 99, + 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 { - 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()) - } + 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("/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 { + 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(resp.Body).Decode(&data); err != nil { + if err := json.NewDecoder(res.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") - } - } } } + +func createRole() entity.Role { + repo := roleRepo + ctx := helper.NewFiberCtx().Context() + data := entity.Role{ + Name: strings.ToLower(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.go b/controller/user/user_controller.go index 915d8b3..020db72 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -1,7 +1,7 @@ package controller import ( - "net" + "math" "strings" "sync" @@ -9,50 +9,29 @@ import ( "github.com/gofiber/fiber/v2" "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/constants" + "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" ) 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 - - // 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 + 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 + 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 + DeleteAccount(c *fiber.Ctx) error + // auth + admin + GetAll(c *fiber.Ctx) error + BanAccount(c *fiber.Ctx) error } type UserControllerImpl struct { @@ -77,182 +56,223 @@ func NewUserController(service service.UserService) 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()) + return response.BadRequest(c, consts.InvalidJSONBody+err.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, constants.InvalidBody+err.Error()) + 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) + id, err := ctr.service.Register(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, constants.ServerErr+regisErr.Error()) + return response.Error(c, consts.ErrServer+err.Error()) } - 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.CreateResponse(c, fiber.StatusCreated, true, message, data) + return response.CreateResponse(c, fiber.StatusCreated, response.Response{ + Message: message, + Success: true, + Data: 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, constants.InvalidBody+err.Error()) + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } validate := validator.New() if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.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, constants.ServerErr+err.Error()) + 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) + return response.CreateResponse(c, fiber.StatusOK, response.Response{ + Message: "thank you for your confirmation. your account is active now, you can login.", + Success: true, + Data: 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()) +func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { + var user model.UserLogin + 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(&verifyData); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } + ctx := c.Context() - err := ctr.service.DeleteUserByVerification(ctx, verifyData) + token, err := ctr.service.Login(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, constants.ServerErr+err.Error()) + 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: "success login", + Success: true, + Data: map[string]any{ + "token": token, + "token-length": len(token), + }, + }) } -func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { - var user model.UserLogin - // user.IP = c.IP() // Note : uncomment this line in production +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()) + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } - - userIP := net.ParseIP(user.IP) - if userIP == nil { - return response.BadRequest(c, constants.InvalidBody+"invalid user ip address") + validate := validator.New() + if err := validate.Struct(&user); err != nil { + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } - 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) + + ctx := c.Context() + 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+err.Error()) } + 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, + }) +} + +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, constants.InvalidBody+err.Error()) + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) + } + if user.NewPassword != user.NewPasswordConfirm { + return response.BadRequest(c, "password confirmation isn't match") } 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) + err := ctr.service.ResetPassword(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, constants.ServerErr+loginErr.Error()) + return response.Error(c, consts.ErrServer+err.Error()) } - data := map[string]any{ - "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: "your password already updated, you can login with the new password", + Success: true, + Data: nil, + }) } -func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) MyProfile(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) + 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.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } - return response.Error(c, constants.ServerErr+forgetErr.Error()) + return response.Error(c, consts.ErrServer+getErr.Error()) } + return response.SuccessLoaded(c, userProfile) +} - 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) 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+err.Error()) + } + return response.SuccessNoContent(c) } -func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { - var user model.UserResetPassword +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.UserUpdate if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) + 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, constants.InvalidBody+err.Error()) - } - if user.NewPassword != user.NewPasswordConfirm { - return response.BadRequest(c, "password confirmation not match") + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } ctx := c.Context() - resetErr := ctr.service.ResetPassword(ctx, user) - if resetErr != nil { - fiberErr, ok := resetErr.(*fiber.Error) + err := ctr.service.UpdateProfile(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, constants.ServerErr+resetErr.Error()) + return response.Error(c, consts.ErrServer+err.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.SuccessNoContent(c) } func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { @@ -263,13 +283,13 @@ func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { var user model.UserPasswordUpdate if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) + 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, constants.InvalidBody+err.Error()) + return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } if user.NewPassword != user.NewPasswordConfirm { return response.BadRequest(c, "new password confirmation is wrong") @@ -279,61 +299,115 @@ 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, false, fiberErr.Message, nil) + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) } - return response.Error(c, constants.ServerErr+updateErr.Error()) + return response.Error(c, consts.ErrServer+err.Error()) } - return response.SuccessNoContent(c) } -func (ctr *UserControllerImpl) UpdateProfile(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) } - var user model.UserProfileUpdate + var user model.UserDeleteAccount if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, constants.InvalidBody+err.Error()) + 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, constants.InvalidBody+err.Error()) + 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() - updateErr := ctr.service.UpdateProfile(ctx, user) - if updateErr != nil { - fiberErr, ok := updateErr.(*fiber.Error) + err := ctr.service.DeleteAccount(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, constants.ServerErr+updateErr.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) } -func (ctr *UserControllerImpl) MyProfile(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() - userProfile, getErr := ctr.service.MyProfile(ctx, userClaims.ID) + users, total, getErr := ctr.service.GetAll(ctx, request) if getErr != nil { - fiberErr, ok := getErr.(*fiber.Error) + return response.Error(c, consts.ErrServer+getErr.Error()) + } + + data := make([]interface{}, len(users)) + for i := range users { + data[i] = users[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 || userClaims.ID == id { + return response.BadRequest(c, consts.InvalidID) + } + + ctx := c.Context() + err = ctr.service.SoftDelete(ctx, id) + 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, constants.ServerErr+getErr.Error()) + return response.Error(c, consts.ErrServer+err.Error()) } - return response.SuccessLoaded(c, userProfile) + return response.SuccessNoContent(c) } diff --git a/controller/user/user_controller_test.go b/controller/user/user_controller_test.go index 97685a9..1af7876 100644 --- a/controller/user/user_controller_test.go +++ b/controller/user/user_controller_test.go @@ -5,1504 +5,1275 @@ import ( "encoding/json" "fmt" "log" - "net/http" - "strings" + "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/constants" + "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" - permService "github.com/Lukmanern/gost/service/permission" - roleService "github.com/Lukmanern/gost/service/role" service "github.com/Lukmanern/gost/service/user" ) +const ( + headerTestName string = "at User Controller Test" +) + var ( - userSvc service.UserService - userCtr UserController + baseURL string + timeNow time.Time userRepo repository.UserRepository - appURL string ) func init() { - env.ReadConfig("./../../.env") + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) config := env.Configuration() - appURL = config.AppURL + baseURL = config.AppURL + timeNow = time.Now() + userRepo = repository.NewUserRepository() 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) +type testCase struct { + Name string + ResCode int + Payload any +} - if userController == nil || userService == nil || roleService == nil || permService == nil { - t.Error(constants.ShouldNotNil) +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 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) +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()}, + }, } - 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 + handlers := []func(c *fiber.Ctx) error{ + controller.Register, + controller.AccountActivation, + controller.Login, + controller.ForgetPassword, + controller.ResetPassword, } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") + 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()) } - 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 - }{ +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{ { - caseName: "success register -1", - respCode: http.StatusCreated, - response: response.Response{ - Message: response.MessageSuccessCreated, - Success: true, - Data: nil, - }, - payload: &model.UserRegister{ - Name: helper.RandomString(10), + Name: "Success Register -1", + ResCode: fiber.StatusCreated, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: helper.RandomString(11), Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin + Password: helper.RandomString(12), }, }, { - caseName: "success register -2", - respCode: http.StatusCreated, - response: response.Response{ - Message: response.MessageSuccessCreated, - Success: true, - Data: nil, - }, - payload: &model.UserRegister{ + Name: "Success Register -2", + ResCode: fiber.StatusCreated, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, Name: helper.RandomString(10), Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin + Password: helper.RandomString(12), }, }, { - 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 + 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), }, }, { - caseName: "failed register: email already used", - respCode: http.StatusBadRequest, - response: response.Response{ - Message: "", - Success: false, - Data: nil, - }, - payload: &model.UserRegister{ + Name: "Failed Register -2: invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, Name: helper.RandomString(10), - Email: createdUser.Email, - Password: helper.RandomString(10), - RoleID: 1, // admin + Email: "invalid email", + Password: helper.RandomString(12), }, }, { - caseName: "failed register: name too short", - respCode: http.StatusBadRequest, - response: response.Response{ - Message: "", - Success: false, - Data: nil, - }, - payload: &model.UserRegister{ - Name: "", + 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: helper.RandomString(10), - RoleID: 1, // admin + Password: "--", }, }, { - caseName: "failed register: password too short", - respCode: http.StatusBadRequest, - response: response.Response{ - Message: "", - Success: false, - Data: nil, - }, - payload: &model.UserRegister{ + Name: "Failed Register -4: no role id", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: nil, Name: helper.RandomString(10), Email: helper.RandomEmail(), - Password: "", - RoleID: 1, // admin + Password: helper.RandomString(10), }, }, } - endp := "user/register" + pathURL := "user/register" + URL := baseURL + pathURL 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) - } + 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(endp, ctr.Register) + app.Post(pathURL, controller.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) - } + // 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 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") - } + 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 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), + // 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(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") + Password: helper.RandomString(14), + RoleIDs: []int{1, 2, 3}, } - defer func() { - userRepo.Delete(ctx, userID) + id, err := _service.Register(ctx, validUser) + defer repository.Delete(ctx, id) + assert.Nil(t, err, consts.ShouldNil, headerTestName) - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() + 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 := []struct { - caseName string - respCode int - payload *model.UserVerificationCode - }{ + testCases := []testCase{ { - caseName: "success verify", - respCode: http.StatusOK, - payload: &model.UserVerificationCode{ - Code: *vCode, - Email: createdUser.Email, + Name: "Success Account Activation -1", + ResCode: fiber.StatusOK, + Payload: model.UserActivation{ + Email: validUser.Email, + Code: validCode, }, }, { - caseName: "failed verify: code not found", - respCode: http.StatusNotFound, - payload: &model.UserVerificationCode{ - Code: *vCode, - Email: createdUser.Email, + Name: "Failed Account Activation -1 : account already active", + ResCode: fiber.StatusBadRequest, + Payload: model.UserActivation{ + Email: validUser.Email, + Code: validCode, }, }, { - caseName: "failed verify: code/email too short", - respCode: http.StatusBadRequest, - payload: &model.UserVerificationCode{ - Code: "", - Email: "", + 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), }, }, } - endp := "user/verification" + pathURL := "user/account-activation" + URL := baseURL + pathURL 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) - } + 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(endp, ctr.AccountActivation) + app.Post(pathURL, controller.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") - } + // 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 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 - }{ +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{ { - caseName: "success delete account", - respCode: http.StatusOK, - payload: &model.UserVerificationCode{ - Code: *vCode, - Email: createdUser.Email, + Name: "Success Login -1", + ResCode: fiber.StatusOK, + Payload: model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, }, }, { - caseName: "failed delete account: code not found", - respCode: http.StatusNotFound, - payload: &model.UserVerificationCode{ - Code: *vCode, - Email: createdUser.Email, + Name: "Success Login -2", + ResCode: fiber.StatusOK, + Payload: model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, }, }, { - caseName: "failed delete account: code/email too short", - respCode: http.StatusBadRequest, - payload: &model.UserVerificationCode{ - Code: "", - Email: "", + 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: "--", }, }, } - endp := "user/request-delete" + pathURL := "user/login" + URL := baseURL + pathURL 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) - } + 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(endp, ctr.DeleteAccountActivation) + app.Post(pathURL, controller.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("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") - } + // 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 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 - }{ + // 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{ { - caseName: "success forget password", - respCode: http.StatusAccepted, - payload: &model.UserForgetPassword{ - Email: createdUser.Email, + Name: "Success Forgot Password -1", + ResCode: fiber.StatusOK, + Payload: model.UserForgetPassword{ + Email: validUser.Email, }, }, { - caseName: "faield forget password: email not found", - respCode: http.StatusNotFound, - payload: &model.UserForgetPassword{ + Name: "Failed Forgot Password -1: user isn't found", + ResCode: fiber.StatusNotFound, + Payload: model.UserForgetPassword{ Email: helper.RandomEmail(), }, }, { - caseName: "faield forget password: invalid email", - respCode: http.StatusBadRequest, - payload: &model.UserForgetPassword{ + Name: "Failed Forgot Password -2: invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserForgetPassword{ Email: "invalid-email", }, }, } - endp := "user/forget-password" + pathURL := "user/forget-password" + URL := baseURL + pathURL 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) - } + 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(endp, ctr.ForgetPassword) + app.Post(pathURL, controller.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) + + // 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) { - 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), + // 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(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) + 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) - // 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) + redisConTest := connector.LoadRedisCache() + key := validUser.Email + service.KEY_FORGET_PASSWORD + validCode := redisConTest.Get(key).Val() + assert.True(t, len(validCode) >= 21, "should true", headerTestName) - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - payload *model.UserResetPassword - }{ + testCases := []testCase{ { - caseName: "success reset password", - respCode: http.StatusAccepted, - payload: &model.UserResetPassword{ - Email: userByID.Email, - Code: *userByID.VerificationCode, - NewPassword: "newPassword", - NewPasswordConfirm: "newPassword", + Name: "Success Reset Password -1", + ResCode: fiber.StatusOK, + Payload: model.UserResetPassword{ + Email: validUser.Email, + Code: validCode, + NewPassword: "new-password-00", + NewPasswordConfirm: "new-password-00", }, }, { - caseName: "failed reset password: password not match", - respCode: http.StatusBadRequest, - payload: &model.UserResetPassword{ - Email: userByID.Email, - Code: *userByID.VerificationCode, - NewPassword: "newPassword", - NewPasswordConfirm: "newPasswordNotMatch", + 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", }, }, { - caseName: "failed reset password: verification code too short", - respCode: http.StatusBadRequest, - payload: &model.UserResetPassword{ + 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: "short", - NewPassword: "newPassword", - NewPasswordConfirm: "newPasswordNotMatch", + Code: helper.RandomString(28), + NewPassword: "valid-passwd-11", + NewPasswordConfirm: "valid-passwd-00", }, }, } - endp := "user/reset-password" + pathURL := "user/reset-password" + URL := baseURL + pathURL 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) - } + 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(endp, ctr.ResetPassword) + app.Post(pathURL, controller.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") - } + // 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 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) +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) - // create inactive user - createdUser := model.UserRegister{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(10), - RoleID: 1, // admin + tokens := make([]string, 2) + for i := range tokens { + tokens[i] = helper.GenerateToken() } - userID, createErr := userSvc.Register(ctx, createdUser) - if createErr != nil || userID <= 0 { - t.Fatal("should success create user, user failed to create") + + 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 } - userByID, getErr := userRepo.GetByID(ctx, userID) - if getErr != nil || userByID == nil { - t.Fatal("should success get user by id") + + 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", + }, } - vCode := userByID.VerificationCode - if vCode == nil || userByID.ActivatedAt != nil { - t.Fatal("user should inactivate for now, but its get activated/ nulling vCode") + + for i, token := range tokens { + testCases = append(testCases, testCase{ + Name: "Failed Get My Profile -" + strconv.Itoa(i+3), + ResCode: fiber.StatusNotFound, + Token: token, + }) } - 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") - } + pathURL := "user/my-profile" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) - 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") - } + // 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) - 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") + // 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) } + } +} - createdActiveUser = *userByID - createdActiveUser.Password = createdUser2.Password - }() +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) - defer userRepo.Delete(ctx, createdActiveUser.ID) + tokens := make([]string, 1) + for i := range tokens { + tokens[i] = helper.GenerateToken() + } - 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(), - }, - }, + type testCase struct { + Name string + ResCode int + Token string + } + + testCases := []testCase{ { - caseName: "failed login -1: account is inactive", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Email: strings.ToLower(createdUser.Email), - Password: createdUser.Password, - IP: helper.RandomIPAddress(), - }, + Name: "Failed Login -1: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "--", }, { - 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(), - }, + 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 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 repository.Delete(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{ { - caseName: "failed login: invalid ip", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Password: "wrongPass11", - Email: createdUser.Email, - IP: "invalid-ip", + Name: "Success Update Profile -1", + ResCode: fiber.StatusNoContent, + Payload: model.UserUpdate{ + Name: "test update name", }, + Token: validToken, }, { - caseName: "faield login: email not found", - respCode: http.StatusNotFound, - payload: &model.UserLogin{ - Password: "secret123", - Email: helper.RandomEmail(), - IP: helper.RandomIPAddress(), + Name: "Failed Update Profile -1: Invalid Token", + ResCode: fiber.StatusUnauthorized, + Payload: model.UserUpdate{ + Name: "test update", }, + Token: "invalid-token", }, { - caseName: "faield login: invalid email", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Password: "secret", - Email: "invalid-email", - IP: helper.RandomIPAddress(), + Name: "Failed Update Profile -2: Name too short", + ResCode: fiber.StatusBadRequest, + Payload: model.UserUpdate{ + Name: "", }, + Token: helper.GenerateToken(), // valid token }, { - caseName: "faield login: payload too short", - respCode: http.StatusBadRequest, - payload: &model.UserLogin{ - Password: "", - Email: "", - IP: helper.RandomIPAddress(), + Name: "Failed Update Profile -3: User not found", + ResCode: fiber.StatusNotFound, + Payload: model.UserUpdate{ + Name: "valid-name", }, + Token: helper.GenerateToken(), // valid token }, } - endp := "user/login" + pathURL := "user/profile" + URL := baseURL + pathURL 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) + log.Println(tc.Name, headerTestName) - 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) - } - } + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) - // 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) - } + // 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.Post(endp, ctr.Login) + app.Put(pathURL, jwtHandler.IsAuthenticated, controller.UpdateProfile) 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) + // 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 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) - } +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) - // 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") - } + fakeToken := helper.GenerateToken() - verifyErr := userSvc.Verification(ctx, model.UserVerificationCode{ - Code: *vCode, - Email: userByID.Email, + entityUser := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, }) - 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") - } + defer repository.Delete(ctx, entityUser.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) - 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") + type testCase struct { + Name string + ResCode int + Payload model.UserPasswordUpdate + Token string } - defer func() { - userRepo.Delete(ctx, userID) - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() - - testCases := []struct { - caseName string - respCode int - token string - }{ + testCases := []testCase{ { - caseName: "success", - respCode: http.StatusOK, - token: userToken, + Name: "Success Update Password", + ResCode: fiber.StatusNoContent, + Token: validToken, + Payload: model.UserPasswordUpdate{ + OldPassword: entityUser.Password, + NewPassword: entityUser.Password + "00", + NewPasswordConfirm: entityUser.Password + "00", + }, }, { - caseName: "failed: fake claims", - respCode: http.StatusUnauthorized, - token: "fake-token", + 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", + }, }, { - caseName: "failed: payload nil, token nil", - respCode: http.StatusUnauthorized, - token: "", + 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", + }, }, } - jwtHandler := middleware.NewJWTHandler() + pathURL := "user/update-password" + URL := baseURL + pathURL 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()) - } + log.Println(tc.Name, headerTestName) - 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") - } - } - } -} + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) -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 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) - // 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") - } + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Put(pathURL, jwtHandler.IsAuthenticated, controller.UpdatePassword) + req.Close = true - 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") + // 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) + } } +} - userToken, loginErr := userSvc.Login(ctx, model.UserLogin{ - Email: createdUser.Email, - Password: createdUser.Password, - IP: helper.RandomIPAddress(), +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() + validUser := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: validUser.Email, + Password: validUser.Password, }) - if userToken == "" || loginErr != nil { - t.Error("login should success") + defer repository.Delete(ctx, validUser.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) + + type testCase struct { + Name string + ResCode int + Token string + Payload model.UserDeleteAccount } - 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 - }{ + testCases := []testCase{ { - caseName: "success", - respCode: http.StatusNoContent, - token: userToken, - payload: &model.UserPasswordUpdate{ - OldPassword: createdUser.Password, - NewPassword: "passwordNew123", - NewPasswordConfirm: "passwordNew123", + Name: "Failed Delete Account -1: wrong password", + ResCode: fiber.StatusBadRequest, + Token: validToken, + Payload: model.UserDeleteAccount{ + Password: "valid-password-xyz", + PasswordConfirm: "valid-password-xyz", }, }, { - caseName: "success", - respCode: http.StatusNoContent, - token: userToken, - payload: &model.UserPasswordUpdate{ - OldPassword: "passwordNew123", - NewPassword: "passwordNew12345", - NewPasswordConfirm: "passwordNew12345", + 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", }, }, { - caseName: "failed: no new password", - respCode: http.StatusBadRequest, - token: userToken, - payload: &model.UserPasswordUpdate{ - OldPassword: "noNewPassword", - NewPassword: "noNewPassword", - NewPasswordConfirm: "noNewPassword", + Name: "Failed Delete Account -3: password too short", + ResCode: fiber.StatusBadRequest, + Token: validToken, // fake but valid + Payload: model.UserDeleteAccount{ + Password: "", + PasswordConfirm: "invalid-password", }, }, { - caseName: "failed: payload nil", - respCode: http.StatusBadRequest, - token: userToken, + Name: "Success Delete Account -1", + ResCode: fiber.StatusNoContent, + Token: validToken, + Payload: model.UserDeleteAccount{ + Password: validUser.Password, + PasswordConfirm: validUser.Password, + }, }, { - caseName: "failed: fake claims", - respCode: http.StatusUnauthorized, - token: "fake-token", + Name: "Failed Delete Account -4: user already deleted", + ResCode: fiber.StatusUnauthorized, + Token: validToken, // token is invalid + Payload: model.UserDeleteAccount{ + Password: validUser.Password, + PasswordConfirm: validUser.Password, + }, }, { - caseName: "failed: payload nil, token nil", - respCode: http.StatusUnauthorized, - token: "", + Name: "Failed Delete Account -4: user not found", + ResCode: fiber.StatusNotFound, + Token: fakeToken, // user not found + Payload: model.UserDeleteAccount{ + Password: "valid-password", + PasswordConfirm: "valid-password", + }, }, } - jwtHandler := middleware.NewJWTHandler() + pathURL := "user" + URL := baseURL + pathURL 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) - } + 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, 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.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, tc.Name) - 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") - } + 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) { - t.Parallel() - // unaudit - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } +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) - // 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") + for i := 0; i < 3; i++ { + entityUser := createUser() + defer repository.Delete(ctx, entityUser.ID) } - 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") - } + token := helper.GenerateToken() + assert.True(t, token != "", consts.ShouldNotNil, headerTestName) - 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") + type testCase struct { + Name string + Params string + ResCode int + WantErr bool } - 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 - }{ + testCases := []testCase{ { - caseName: "success", - respCode: http.StatusNoContent, - token: userToken, - payload: &model.UserProfileUpdate{ - Name: helper.RandomString(11), - }, + Name: "Success get all -1", + Params: "?limit=100&page=1", + ResCode: fiber.StatusOK, + WantErr: false, }, { - caseName: "success", - respCode: http.StatusNoContent, - token: userToken, - payload: &model.UserProfileUpdate{ - Name: helper.RandomString(11), - }, + Name: "Success get all -2", + Params: "?limit=12&page=1", + ResCode: fiber.StatusOK, + WantErr: false, }, { - caseName: "failed: payload nil", - respCode: http.StatusBadRequest, - token: userToken, + Name: "Failed get all: invalid limit", + Params: "?limit=-1&page=1", + ResCode: fiber.StatusBadRequest, + WantErr: true, }, { - caseName: "failed: fake claims", - respCode: http.StatusUnauthorized, - token: "fake-token", + Name: "Failed get all: invalid page", + Params: "?limit=1&page=-1", + ResCode: fiber.StatusBadRequest, + WantErr: true, }, { - caseName: "failed: payload nil, token nil", - respCode: http.StatusUnauthorized, - token: "", + Name: "Failed get all: invalid sort", + Params: "?limit=1&page=1&sort=invalid", // sort should name + ResCode: fiber.StatusInternalServerError, + WantErr: true, }, } - jwtHandler := middleware.NewJWTHandler() + pathURL := "user/" + URL := baseURL + pathURL 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) - } + 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 - if resp.StatusCode() == http.StatusNoContent { - userByID, err := userRepo.GetByID(ctx, userID) - if err != nil || userByID == nil { - t.Error(constants.ShouldNotErr) - } + // 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 userByID.Name != helper.ToTitle(tc.payload.Name) { - t.Error("shoudl equal") - } + 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) { - t.Parallel() - c := helper.NewFiberCtx() - ctx := c.Context() - ctr := userCtr - if ctr == nil || c == nil || ctx == nil { - t.Error(constants.ShouldNotNil) - } +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) - // 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") - } + validUser1 := createUser() + defer repository.Delete(ctx, validUser1.ID) - 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") - } + fakeToken := helper.GenerateToken() - userToken, loginErr := userSvc.Login(ctx, model.UserLogin{ - Email: createdUser.Email, - Password: createdUser.Password, - IP: helper.RandomIPAddress(), + validUser2 := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: validUser2.Email, + Password: validUser2.Password, }) - if userToken == "" || loginErr != nil { - t.Error("login should success") - } - defer func() { - userRepo.Delete(ctx, userID) + defer repository.Delete(ctx, validUser2.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) - r := recover() - if r != nil { - t.Fatal("panic ::", r) - } - }() + type testCase struct { + Name string + ResCode int + Token string + ID string + } - testCases := []struct { - caseName string - respCode int - token string - }{ + testCases := []testCase{ { - caseName: "success", - respCode: http.StatusOK, - token: userToken, + Name: "Success Ban Account -1", + ResCode: fiber.StatusNoContent, + Token: validToken, + ID: strconv.Itoa(validUser1.ID), }, { - caseName: "failed: fake claims", - respCode: http.StatusUnauthorized, - token: "fake-token", + Name: "Failed Ban Account -1: user not found", + ResCode: fiber.StatusNotFound, + Token: validToken, + ID: strconv.Itoa(validUser1.ID + 100), }, { - caseName: "failed: payload nil, token nil", - respCode: http.StatusUnauthorized, - token: "", + Name: "Failed Ban Account -2: invalid ID", + ResCode: fiber.StatusBadRequest, + Token: fakeToken, + ID: "-10", }, } - jwtHandler := middleware.NewJWTHandler() + pathURL := "user/ban-user/" + URL := baseURL + pathURL 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()) - } + 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 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") - } + 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 := 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 +} 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..220e377 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" ) @@ -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,40 +88,14 @@ func seeding() { log.Panicf("Error starting transaction for seeding: %s", tx.Error) } - // Seeding permission and role - for _, data := range rbac.AllRoles() { + // Seeding role + 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..9ff3910 100644 --- a/domain/entity/all_entities.go +++ b/domain/entity/all_entities.go @@ -15,17 +15,16 @@ 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..2c49b85 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 { @@ -31,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/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..6d39829 --- /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,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,min=5,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..05d9854 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -1,15 +1,26 @@ package model -import "github.com/Lukmanern/gost/domain/entity" +import ( + "time" +) + +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"` +} 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"` + RoleIDs []int `validate:"required" 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"` } @@ -20,6 +31,17 @@ type UserLogin struct { IP string `validate:"required,min=4,max=20" json:"ip"` } +type UserUpdate struct { + 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 { + ID int `validate:"required,numeric,min=1" json:"id"` + RoleIDs []int `validate:"required" json:"role_id"` +} + type UserForgetPassword struct { Email string `validate:"required,email,min=5,max=60" json:"email"` } @@ -32,14 +54,14 @@ 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 - Role entity.Role +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/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..57b37f1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Lukmanern/gost go 1.20 require ( - github.com/go-playground/validator/v10 v10.15.5 + 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,12 +17,17 @@ require ( ) 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/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 @@ -32,9 +37,8 @@ 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-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 2a19284..6ca5877 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= @@ -16,8 +18,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o 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-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= @@ -63,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/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..58bf34a --- /dev/null +++ b/internal/consts/consts.go @@ -0,0 +1,29 @@ +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: " + 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: " +) + +const ( + 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/helper/helper.go b/internal/helper/helper.go index ac7fa44..35aaded 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -8,12 +8,23 @@ import ( "strings" "time" + "github.com/Lukmanern/gost/internal/middleware" + "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 { @@ -27,23 +38,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 { @@ -71,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 @@ -89,3 +83,21 @@ func NewFiberCtx() *fiber.Ctx { 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 + max := 10000000 + return rand.Intn(max-min) + min +} diff --git a/internal/helper/helper_test.go b/internal/helper/helper_test.go index 2ec1c68..8998794 100644 --- a/internal/helper/helper_test.go +++ b/internal/helper/helper_test.go @@ -1,14 +1,26 @@ package helper import ( + "log" "net" "strings" "testing" - "github.com/Lukmanern/gost/internal/constants" + "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)) @@ -16,17 +28,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() @@ -39,27 +40,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) { @@ -90,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.go b/internal/middleware/middleware.go index 323d52a..8fb2841 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -2,7 +2,6 @@ package middleware import ( "crypto/rsa" - "errors" "fmt" "log" "strings" @@ -27,12 +26,12 @@ type JWTHandler struct { } // Claims struct will be generated as token,contains -// user data like ID, email, role and permissions. +// 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"` - Permissions map[int]int `json:"permissions"` + ID int `json:"id"` + Email string `json:"email"` + Roles map[string]uint8 `json:"roles"` 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 string, roles map[string]uint8, expired time.Time) (t string, err error) { // Create Claims claims := Claims{ - ID: id, - Email: email, - Role: role, - Permissions: permissions, + ID: id, + Email: email, + Roles: roles, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: &jwt.NumericDate{Time: expired}, NotBefore: &jwt.NumericDate{Time: time.Now()}, @@ -178,79 +173,47 @@ 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 +// 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 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 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 claims.Roles[role] != 1 { + return response.Unauthorized(c) + } } + return c.Next() } - 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) +// 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 } - 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 func(c *fiber.Ctx) error { + claims, ok := c.Locals("claims").(*Claims) + if !ok || claims == nil { 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) - if !ok || role != claims.Role { + // 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) } - 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 { - return func(c *fiber.Ctx) error { - return j.HasRole(c, role) - } } diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index 0e51b7d..56faa76 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -1,23 +1,20 @@ -package middleware +package middleware_test import ( - "fmt" - "math" - "reflect" "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/Lukmanern/gost/internal/middleware" "github.com/gofiber/fiber/v2" ) type GenTokenParams struct { ID int Email string - Role string - Per map[int]int + Roles map[string]uint8 Exp time.Time wantErr bool } @@ -32,38 +29,28 @@ 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: helper.GenerateRandomID(), + Email: helper.RandomEmail(), + Roles: map[string]uint8{"test-role": 1}, Exp: timeNow.Add(5 * time.Minute), wantErr: false, } } -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() - token, err := jwtHandler.GenerateJWT(1, params.Email, params.Role, params.Per, params.Exp) + jwtHandler := middleware.NewJWTHandler() + token, err := jwtHandler.GenerateJWT(1, params.Email, params.Roles, params.Exp) if err != nil || token == "" { t.Fatal("should not error") } @@ -94,8 +81,8 @@ 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) + jwtHandler := middleware.NewJWTHandler() + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("error while generating token") } @@ -116,10 +103,10 @@ func TestJWTHandlerInvalidateToken(t *testing.T) { } func TestJWTHandlerIsBlacklisted(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() cookie, err := jwtHandler.GenerateJWT(1000, - helper.RandomEmail(), "example-role", - params.Per, time.Now().Add(1*time.Hour)) + helper.RandomEmail(), params.Roles, + time.Now().Add(1*time.Hour)) if err != nil { t.Error("generate cookie/token should not error") } @@ -129,7 +116,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { } tests := []struct { name string - j JWTHandler + j middleware.JWTHandler args args want bool }{ @@ -150,8 +137,8 @@ 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) + jwtHandler := middleware.NewJWTHandler() + token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("error while generating token") } @@ -160,7 +147,7 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { } func() { - jwtHandler1 := NewJWTHandler() + jwtHandler1 := middleware.NewJWTHandler() c := helper.NewFiberCtx() jwtHandler1.IsAuthenticated(c) c.Status(fiber.StatusUnauthorized) @@ -176,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) @@ -187,59 +174,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) - 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(constants.Unauthorized) - } -} - -func TestJWTHandlerCheckHasPermission(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") - } - - 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) + 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) } @@ -247,74 +184,10 @@ func TestJWTHandlerCheckHasRole(t *testing.T) { t.Error("Error: Token is empty") } - err2 := jwtHandler.CheckHasRole("permission-1") - if err2 == nil { - t.Error(constants.Unauthorized) + if checkErr := jwtHandler.HasOneRole("role-x-1"); 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)) + if checkErr := jwtHandler.HasRoles("role-x-1"); checkErr == nil { + t.Error(consts.Unauthorized) } } 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/response/response.go b/internal/response/response.go index eb7447d..24acf2d 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. +// BadRequest formats a response with HTTP status 400. func BadRequest(c *fiber.Ctx, message string) error { - return CreateResponse(c, fiber.StatusBadRequest, false, message, nil) + return CreateResponse(c, fiber.StatusBadRequest, Response{ + Message: message, + 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..973335d 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) } }) @@ -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) } }) 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 index a2558ed..8bd0541 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,5 @@ +// 📌 Origin Github Repository: https://github.com/Lukmanern + package main import ( 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 index c1f4d96..2a0c498 100644 --- a/repository/role/role_repository.go +++ b/repository/role/role_repository.go @@ -5,17 +5,14 @@ import ( "sync" "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" "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 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) + // 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) @@ -24,7 +21,7 @@ type RoleRepository interface { 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) + 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) @@ -51,7 +48,7 @@ func NewRoleRepository() RoleRepository { return roleRepositoryImpl } -func (repo *RoleRepositoryImpl) Create(ctx context.Context, role entity.Role, permissionsID []int) (id int, err error) { +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 { @@ -59,17 +56,6 @@ func (repo *RoleRepositoryImpl) Create(ctx context.Context, role entity.Role, pe 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 { @@ -79,36 +65,9 @@ func (repo *RoleRepositoryImpl) Create(ctx context.Context, role entity.Role, pe 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) + result := repo.db.Where("id = ?", id).First(&role) if result.Error != nil { return nil, result.Error } @@ -117,15 +76,16 @@ func (repo *RoleRepositoryImpl) GetByID(ctx context.Context, id int) (role *enti 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) + 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 base.RequestGetAll) (roles []entity.Role, total int, err error) { +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/role/role_repository_test.go b/repository/role/role_repository_test.go index 8835806..20cc90f 100644 --- a/repository/role/role_repository_test.go +++ b/repository/role/role_repository_test.go @@ -2,22 +2,20 @@ 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/domain/model" "github.com/Lukmanern/gost/internal/env" ) var ( - roleRepoImpl RoleRepositoryImpl - permissionsID []int - timeNow time.Time - ctx context.Context + roleRepoImpl RoleRepositoryImpl + timeNow time.Time + ctx context.Context ) func init() { @@ -30,20 +28,18 @@ func init() { 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{ + TimeFields: entity.TimeFields{ CreatedAt: &timeNow, UpdatedAt: &timeNow, }, } - id, createErr := roleRepoImpl.Create(ctx, role, permissionsID) + id, createErr := roleRepoImpl.Create(ctx, role) if createErr != nil { t.Error("error while creating role : ", createErr.Error()) } @@ -68,9 +64,8 @@ func TestCreate(t *testing.T) { }() type args struct { - ctx context.Context - role entity.Role - permissionsID []int + ctx context.Context + role entity.Role } tests := []struct { name string @@ -87,7 +82,7 @@ func TestCreate(t *testing.T) { role: entity.Role{ Name: role.Name, Description: "", - TimeFields: base.TimeFields{ + TimeFields: entity.TimeFields{ CreatedAt: &timeNow, UpdatedAt: &timeNow, }, @@ -102,7 +97,7 @@ func TestCreate(t *testing.T) { t.Errorf("create() do not panic") } }() - gotID, err := tt.repo.Create(tt.args.ctx, tt.args.role, tt.args.permissionsID) + 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 @@ -114,63 +109,6 @@ func TestCreate(t *testing.T) { } } -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 { @@ -191,7 +129,7 @@ func TestGetByID(t *testing.T) { wantErr bool }{ { - name: "success get permission by valid id", + name: "success get role", repo: roleRepoImpl, args: args{ ctx: ctx, @@ -200,7 +138,7 @@ func TestGetByID(t *testing.T) { wantErr: false, }, { - name: "failed get permission by invalid id", + name: "failed get role: invalid id", repo: roleRepoImpl, args: args{ ctx: ctx, @@ -243,7 +181,7 @@ func TestGetByName(t *testing.T) { wantErr bool }{ { - name: "success get permission by valid id", + name: "success get role by valid id", repo: roleRepoImpl, args: args{ ctx: ctx, @@ -252,7 +190,7 @@ func TestGetByName(t *testing.T) { wantErr: false, }, { - name: "failed get permission by invalid id", + name: "failed get role by invalid id", repo: roleRepoImpl, args: args{ ctx: ctx, @@ -291,7 +229,7 @@ func TestGetAll(t *testing.T) { lenRoles := len(roles) type args struct { ctx context.Context - filter base.RequestGetAll + filter model.RequestGetAll } tests := []struct { name string @@ -304,7 +242,7 @@ func TestGetAll(t *testing.T) { repo: roleRepoImpl, args: args{ ctx: ctx, - filter: base.RequestGetAll{ + filter: model.RequestGetAll{ Limit: 1000, Page: 1, }, @@ -316,7 +254,7 @@ func TestGetAll(t *testing.T) { repo: roleRepoImpl, args: args{ ctx: ctx, - filter: base.RequestGetAll{ + filter: model.RequestGetAll{ Limit: 1, Page: 1, }, @@ -332,13 +270,13 @@ func TestGetAll(t *testing.T) { return } if tt.args.filter.Limit > lenRoles && len(gotRoles) < lenRoles { - t.Error("permissions should be $lenRoles or more") + t.Error("role should be $lenRoles or more") } if tt.args.filter.Limit > lenRoles && gotTotal < lenRoles { - t.Error("total permissions should be $lenRoles or more") + t.Error("total role should be $lenRoles or more") } if tt.args.filter.Limit < lenRoles && len(gotRoles) > lenRoles { - t.Error("permissions should be less than $lenPermission") + t.Error("role should be less than $lenRoles") } }) } @@ -427,7 +365,7 @@ func TestDelete(t *testing.T) { wantErr bool }{ { - name: "success update permission", + name: "success update role", repo: roleRepoImpl, wantErr: false, args: args{ diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go index 51e36f8..317a917 100644 --- a/repository/user/user_repository.go +++ b/repository/user/user_repository.go @@ -9,13 +9,13 @@ import ( "gorm.io/gorm" "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/base" "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) + 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) @@ -27,16 +27,16 @@ type UserRepository interface { 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) + 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) + + // Delete removes a user from the repository by their ID. + Delete(ctx context.Context, id int) (err error) } type UserRepositoryImpl struct { @@ -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 }) @@ -82,7 +84,7 @@ func (repo *UserRepositoryImpl) Create(ctx context.Context, user entity.User, ro 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) + result := repo.db.Where("id = ?", id).Preload("Roles").First(&user) if result.Error != nil { return nil, result.Error } @@ -91,7 +93,7 @@ func (repo *UserRepositoryImpl) GetByID(ctx context.Context, id int) (user *enti 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) + result := repo.db.Where("email = ?", email).Preload("Roles").First(&user) if result.Error != nil { return nil, result.Error } @@ -105,14 +107,14 @@ func (repo *UserRepositoryImpl) GetByConditions(ctx context.Context, conds map[s for con, val := range conds { query = query.Where(con+" ?", val) } - result := query.Preload("Roles.Permissions").First(&user) + 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 base.RequestGetAll) (users []entity.User, total int, err error) { +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 ?" @@ -124,7 +126,12 @@ func (repo *UserRepositoryImpl) GetAll(ctx context.Context, filter base.RequestG 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...).Preload("Roles") + result = result.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 } @@ -140,10 +147,16 @@ func (repo *UserRepositoryImpl) Update(ctx context.Context, user entity.User) (e return result.Error } - oldData.Name = user.Name - oldData.ActivatedAt = user.ActivatedAt - oldData.VerificationCode = user.VerificationCode - oldData.UpdatedAt = user.UpdatedAt + if user.DeletedAt != nil { + oldData.DeletedAt = user.DeletedAt + } + if user.ActivatedAt != nil { + oldData.ActivatedAt = user.ActivatedAt + } + if user.Name != "" { + oldData.Name = user.Name + } + oldData.SetUpdateTime() result = tx.Save(&oldData) if result.Error != nil { return result.Error @@ -154,15 +167,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 @@ -181,3 +185,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/repository/user/user_repository_test.go b/repository/user/user_repository_test.go index 7d2a7f3..33b5920 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/base" "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: base.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, []int{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: 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") +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: base.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: base.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 base.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: base.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: base.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: 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") + 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: base.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: 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 +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, []int{1}) + if createErr != nil { + log.Fatal("error while create new user", headerTestName) } + newUser.ID = userID + return newUser } 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_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/email/email_service.go b/service/email_service/email_service.go similarity index 81% rename from service/email/email_service.go rename to service/email_service/email_service.go index e503d93..b87c772 100644 --- a/service/email/email_service.go +++ b/service/email_service/email_service.go @@ -12,12 +12,9 @@ import ( type EmailService interface { // SendMail func sends message with subject to some emails address. - SendMail(emails []string, subject, message string) error + SendMail(subject, message string, emails ...string) error } -// EmailServiceImpl struct contains all the -// SMTP needs for sending emails. -// SMTP => Simple Mail Transfer Protocol type EmailServiceImpl struct { Server string Port int @@ -51,7 +48,7 @@ func NewEmailService() EmailService { return emailService } -func (svc *EmailServiceImpl) SendMail(emails []string, subject, message string) error { +func (svc *EmailServiceImpl) SendMail(subject, message string, emails ...string) error { validateErr := helper.ValidateEmails(emails...) if validateErr != nil { return validateErr 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/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 index 5969f5b..84e4ca5 100644 --- a/service/role/role_service.go +++ b/service/role/role_service.go @@ -2,43 +2,30 @@ 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/consts" repository "github.com/Lukmanern/gost/repository/role" - permService "github.com/Lukmanern/gost/service/permission" ) type RoleService interface { - - // Create func create one role. + // auth + admin 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. + GetByID(ctx context.Context, id int) (role model.RoleResponse, err error) + GetAll(ctx context.Context, filter model.RequestGetAll) (roles []model.RoleResponse, total int, err error) 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 + repository repository.RoleRepository } var ( @@ -46,13 +33,10 @@ var ( roleServiceImplOnce sync.Once ) -const roleNotFound = "role/s not found" - -func NewRoleService(servicePermission permService.PermissionService) RoleService { +func NewRoleService() RoleService { roleServiceImplOnce.Do(func() { roleServiceImpl = &RoleServiceImpl{ - repository: repository.NewRoleRepository(), - servicePermission: servicePermission, + repository: repository.NewRoleRepository(), } }) return roleServiceImpl @@ -60,82 +44,41 @@ func NewRoleService(servicePermission permService.PermissionService) RoleService 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 := modelCreateToEntity(data) entityRole.SetCreateTime() - id, err = svc.repository.Create(ctx, entityRole, data.PermissionsID) + id, err = svc.repository.Create(ctx, entityRole) 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 +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 role, fiber.NewError(fiber.StatusNotFound, consts.NotFound) } - if role == nil { - return nil, fiber.NewError(fiber.StatusNotFound, roleNotFound) + 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 base.RequestGetAll) (roles []model.RoleResponse, total int, err error) { - roleEntities, total, err := svc.repository.GetAll(ctx, filter) +func (svc *RoleServiceImpl) GetAll(ctx context.Context, filter model.RequestGetAll) (roles []model.RoleResponse, total int, err error) { + 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 } @@ -150,22 +93,15 @@ func (svc *RoleServiceImpl) Update(ctx context.Context, data model.RoleUpdate) ( 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 + role, err := svc.repository.GetByID(ctx, data.ID) + if err == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound) } - if roleByID == nil { - return fiber.NewError(fiber.StatusNotFound, roleNotFound) + 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 := modelUpdateToEntity(data) entityRole.SetUpdateTime() err = svc.repository.Update(ctx, entityRole) if err != nil { @@ -175,19 +111,41 @@ func (svc *RoleServiceImpl) Update(ctx context.Context, data model.RoleUpdate) ( } 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 + role, err := svc.repository.GetByID(ctx, id) + if err == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound) } - if role == nil { - return fiber.NewError(fiber.StatusNotFound, roleNotFound) + 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 } + +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/role/role_service_test.go b/service/role/role_service_test.go index 1b54a19..661d767 100644 --- a/service/role/role_service_test.go +++ b/service/role/role_service_test.go @@ -1,214 +1,259 @@ -// 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 ( + "log" "strings" "testing" + "time" - "github.com/Lukmanern/gost/database/connector" - "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/consts" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" - permService "github.com/Lukmanern/gost/service/permission" + repository "github.com/Lukmanern/gost/repository/role" + "github.com/stretchr/testify/assert" ) -func init() { - // Check env and database - env.ReadConfig("./../../.env") +const ( + headerTestName string = "at Role Service Test" +) - connector.LoadDatabase() - connector.LoadRedisCache() -} +var ( + timeNow time.Time + roleRepository repository.RoleRepository +) -func TestNewRoleService(t *testing.T) { - permSvc := permService.NewPermissionService() - svc := NewRoleService(permSvc) - if svc == nil { - t.Error(constants.ShouldNotNil) - } +func init() { + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) + timeNow = time.Now() + roleRepository = repository.NewRoleRepository() } -// 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) - } +// 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) - modelRole := model.RoleCreate{ - Name: strings.ToLower(helper.RandomString(10)), - Description: helper.RandomString(30), + type testCase struct { + Name string + Payload model.RoleCreate + WantErr bool } - roleID, createErr := svc.Create(ctx, modelRole) - if createErr != nil || roleID < 1 { - t.Error("should not error and id should more than zero") + + 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, + }, } - // 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") + 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) - permsID = append(permsID, permID) - } + // 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) - defer func() { - svc.Delete(ctx, roleID) - for _, id := range permsID { - permSvc.Delete(ctx, id) - } - }() + deleteErr := service.Delete(ctx, id) + assert.NoError(t, deleteErr, consts.ShouldNotErr, tc.Name, headerTestName) - // Success connect - modelConnect := model.RoleConnectToPermissions{ - RoleID: roleID, - PermissionsID: permsID, - } - connectErr := svc.ConnectPermissions(ctx, modelConnect) - if connectErr != nil { - t.Error(constants.ShouldNotErr) - } + // expect error + _, getErr = service.GetByID(ctx, id) + assert.Error(t, getErr, consts.ShouldErr, tc.Name, headerTestName) - 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") - } + deleteErr = service.Delete(ctx, id) + assert.Error(t, deleteErr, consts.ShouldErr, tc.Name, headerTestName) - 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) - } +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) - // 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") + totalRoleCreated := 5 + + for i := 0; i < totalRoleCreated; i++ { + role := createRole() + defer repository.Delete(ctx, role.ID) } - if roleByID.Name != updateRoleModel.Name || roleByID.Description != updateRoleModel.Description { - t.Error("name and description should be the same") + + type testCase struct { + Name string + Payload model.RequestGetAll + WantErr bool } - deleteErr := svc.Delete(ctx, roleID) - if deleteErr != nil { - t.Error(constants.ShouldNotErr) + 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, + }, } - // 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") + 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 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) - } +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) - // 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") - } - }() + role := createRole() + defer repository.Delete(ctx, role.ID) - // success create - modelRole := model.RoleCreate{ - Name: strings.ToLower(helper.RandomString(10)), - Description: helper.RandomString(30), + type testCase struct { + Name string + Payload model.RoleUpdate + WantErr bool } - roleID, createErr := svc.Create(ctx, modelRole) - if createErr != nil || roleID < 1 { - t.Error("should not error and id should more than zero") + + 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, + }, } - defer func() { - svc.Delete(ctx, roleID) - }() + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) - // failed connect - modelConnectFailed := model.RoleConnectToPermissions{ - RoleID: roleID, - PermissionsID: []int{-3, -2, -1}, - } - connectErr := svc.ConnectPermissions(ctx, modelConnectFailed) - if connectErr == nil { - t.Error(constants.ShouldErr) - } + 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) - modelConnectFailed = model.RoleConnectToPermissions{ - RoleID: -1, - PermissionsID: []int{}, - } - connectErr = nil - connectErr = svc.ConnectPermissions(ctx, modelConnectFailed) - if connectErr == nil { - t.Error(constants.ShouldErr) + 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) } +} - // failed update - updateRoleModel := model.RoleUpdate{ - ID: -1, - Name: strings.ToLower(helper.RandomString(11)), - Description: helper.RandomString(31), +func createRole() entity.Role { + repository := roleRepository + ctx := helper.NewFiberCtx().Context() + role := entity.Role{ + Name: strings.ToLower(helper.RandomString(15)), + Description: helper.RandomWords(8), } - 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) + role.SetCreateTime() + id, err := repository.Create(ctx, role) + if err != nil { + log.Fatal("failed create a new user", headerTestName) } + role.ID = id + return role } diff --git a/service/user/user_service.go b/service/user/user_service.go index 3010183..ec1baae 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -3,500 +3,389 @@ package service import ( "context" "errors" - "fmt" - "strconv" + "strings" "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/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" - emailService "github.com/Lukmanern/gost/service/email" - roleService "github.com/Lukmanern/gost/service/role" + service "github.com/Lukmanern/gost/service/email_service" + "github.com/go-redis/redis" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" ) 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 + // 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) 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) + // 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) + UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) + UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) + DeleteAccount(ctx context.Context, data model.UserDeleteAccount) (err error) } type UserServiceImpl struct { - repository repository.UserRepository - roleService roleService.RoleService - emailService emailService.EmailService - jwtHandler *middleware.JWTHandler 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 ( - userService *UserServiceImpl - userServiceOnce sync.Once + userSvcImpl *UserServiceImpl + userSvcImplOnce sync.Once ) -func NewUserService(roleService roleService.RoleService) UserService { - userServiceOnce.Do(func() { - userService = &UserServiceImpl{ - roleService: roleService, - repository: repository.NewUserRepository(), - emailService: emailService.NewEmailService(), - jwtHandler: middleware.NewJWTHandler(), +func NewUserService() UserService { + userSvcImplOnce.Do(func() { + userSvcImpl = &UserServiceImpl{ redis: connector.LoadRedisCache(), + jwtHandler: middleware.NewJWTHandler(), + repository: repository.NewUserRepository(), + roleRepo: roleRepository.NewRoleRepository(), + emailService: service.NewEmailService(), } }) - - return userService + return userSvcImpl } -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") - } +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") } - // generate password hashed - counter = 0 - for { - passwordHashed, hashErr = hash.Generate(user.Password) - if hashErr == nil { - break + + for _, roleID := range data.RoleIDs { + enttRole, err := svc.roleRepo.GetByID(ctx, roleID) + if err == gorm.ErrRecordNotFound { + return 0, fiber.NewError(fiber.StatusNotFound, consts.NotFound) } - counter++ - if counter >= 150 { - return 0, errors.New("failed hashing user password") + if err != nil || enttRole == nil { + return 0, errors.New("error while getting role data") } } - userEntity := entity.User{ - Name: helper.ToTitle(user.Name), - Email: user.Email, - Password: passwordHashed, - VerificationCode: &verifCode, - ActivatedAt: nil, + pwHashed, hashErr := hash.Generate(data.Password) + if hashErr != nil { + return 0, errors.New(consts.ErrHashing) } - // set created_at and updated_at equal to now - userEntity.SetCreateTime() - id, err = svc.repository.Create(ctx, userEntity, user.RoleID) + + data.Password = pwHashed + entityUser := modelRegisterToEntity(data) + entityUser.SetCreateTime() + 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") } - 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 + code := helper.RandomString(32) // verification code + 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") + } - 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) + 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) 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") +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 userEntity.ActivatedAt != nil { - return fiber.NewError(fiber.StatusBadRequest, "your account already activated") + if getErr != nil || user == nil { + return errors.New("error while getting user data") } - // set updated_at, activated_at and - // nulling verification code - userEntity.SetActivateAccount() - userEntity.SetUpdateTime() - updateErr := svc.repository.Update(ctx, *userEntity) - if updateErr != nil { - return updateErr + if user.ActivatedAt != nil || user.DeletedAt != nil { + return fiber.NewError(fiber.StatusBadRequest, "activation failed, account is active or already deleted") } - 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") + key := data.Email + KEY_ACCOUNT_ACTIVATION + redisStatus := svc.redis.Get(key) + if redisStatus.Err() != nil { + return errors.New("error while getting data from redis") } - // check if account active or inactive - if userEntity.ActivatedAt != nil { - return fiber.NewError(fiber.StatusBadRequest, "can not delete your account, your account is active") + if redisStatus.Val() != data.Code { + return fiber.NewError(fiber.StatusBadRequest, "verification code isn't match") } - 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") - } + // 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 counter, nil + return 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 +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 { + return "", fiber.NewError(fiber.StatusNotFound, consts.NotFound) } - if userEntity == nil { - return "", fiber.NewError(fiber.StatusNotFound, constants.NotFound) + if getErr != nil || user == nil { + return "", errors.New("error while getting user data") } - res, verfiryErr := hash.Verify(userEntity.Password, user.Password) - if verfiryErr != nil { - return "", verfiryErr - } - if !res { + res, verifyErr := hash.Verify(user.Password, data.Password) + if verifyErr != nil || !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) + if user.ActivatedAt == nil || user.DeletedAt != nil { + return "", fiber.NewError(fiber.StatusBadRequest, "account is inactive, please do activation") } - userRole := userEntity.Roles[0] - permIDs := make([]int, 0) - for _, perm := range userRole.Permissions { - permIDs = append(permIDs, perm.ID) + jwtHandler := middleware.NewJWTHandler() + expired := time.Now().Add(4 * 24 * time.Hour) // 4 days active + roles := make(map[string]uint8) + for _, role := range user.Roles { + roles[role.Name] = 1 } - 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) + token, err = jwtHandler.GenerateJWT(user.ID, user.Email, roles, 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 errors.New("problem invalidating token") +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 + code := helper.RandomString(32) + exp := time.Hour * 1 + 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 } -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") - } +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") } - userEntity.VerificationCode = &verifCode - userEntity.SetUpdateTime() - err = svc.repository.Update(ctx, *userEntity) + 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 err + return errors.New("error while updating password, please try again") } - // 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 + // delete verification code from redis + svc.redis.Del(key) + return nil +} - 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) +func (svc *UserServiceImpl) Logout(c *fiber.Ctx) (err error) { + err = svc.jwtHandler.InvalidateToken(c) + if err != nil { + return errors.New("error while logout") } 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, - }) +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 { - 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") - } + return nil, 0, err } - userByCode.VerificationCode = nil - userByCode.SetUpdateTime() - updateErr := svc.repository.Update(ctx, *userByCode) - if updateErr != nil { - return updateErr + for _, entityUser := range entityUsers { + users = append(users, entityToResponse(&entityUser)) } + return users, total, nil +} - updatePasswdErr := svc.repository.UpdatePassword(ctx, userByCode.ID, passwdHashed) - if updatePasswdErr != nil { - return updatePasswdErr +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) 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 +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 { + 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 userByID == nil { - return fiber.NewError(fiber.StatusNotFound, "user not found") + if user.ActivatedAt == nil || user.DeletedAt != nil { + return model.User{}, fiber.NewError(fiber.StatusBadRequest, "account is inactive, please do activation") } - res, verfiryErr := hash.Verify(userByID.Password, user.OldPassword) - if verfiryErr != nil { - return verfiryErr + profile = entityToResponse(user) + return profile, nil +} + +func (svc *UserServiceImpl) UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) { + user, getErr := svc.repository.GetByID(ctx, data.ID) + if getErr == gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusNotFound, consts.NotFound) } - if !res { - return fiber.NewError(fiber.StatusBadRequest, "wrong password") + if getErr != nil || user == nil { + return errors.New("error while getting user data") } - - 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") - } + if user.ActivatedAt == nil || user.DeletedAt != nil { + return fiber.NewError(fiber.StatusBadRequest, "account is inactive, please do activation") } - updatePwErr := svc.repository.UpdatePassword(ctx, userByID.ID, passwdHashed) - if updatePwErr != nil { - return updatePwErr + enttUser := modelUpdateToEntity(data) + err = svc.repository.Update(ctx, enttUser) + if err != nil { + return errors.New("error while updating user data") } 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 +func (svc *UserServiceImpl) UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) { + 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 == nil { - return profile, fiber.NewError(fiber.StatusInternalServerError, "error while checking user") + if user.ActivatedAt == nil || user.DeletedAt != nil { + return fiber.NewError(fiber.StatusBadRequest, "account is inactive, please do activation") } - // set response - profile = model.UserProfile{ - Name: user.Name, - Email: user.Email, + res, verifyErr := hash.Verify(user.Password, data.OldPassword) + if verifyErr != nil || !res { + return fiber.NewError(fiber.StatusBadRequest, "wrong password, failed to update") } - if len(user.Roles) > 0 { - profile.Role = user.Roles[0] + pwHashed, hashErr := hash.Generate(data.NewPassword) + if hashErr != nil { + return errors.New(consts.ErrHashing) } - return profile, nil + + updateErr := svc.repository.UpdatePassword(ctx, data.ID, pwHashed) + if updateErr != nil { + return errors.New("error while updating user password") + } + return 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 +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) } - if userByID == nil { - return fiber.NewError(fiber.StatusNotFound, constants.NotFound) + if getErr != nil || user == nil { + return errors.New("error while getting user data") } - userEntity := entity.User{ - ID: user.ID, - Name: helper.ToTitle(user.Name), - VerificationCode: userByID.VerificationCode, - ActivatedAt: userByID.ActivatedAt, + res, err := hash.Verify(user.Password, data.Password) + if err != nil || !res { + return fiber.NewError(fiber.StatusBadRequest, "wrong password, please try again") } - userEntity.SetUpdateTime() - err = svc.repository.Update(ctx, userEntity) + err = svc.repository.Delete(ctx, data.ID) if err != nil { - return err + return errors.New("error while deleting user password") } 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 { + 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, + DeletedAt: data.DeletedAt, + Roles: roles, + } +} + +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 index 00aa5c0..8b5e814 100644 --- a/service/user/user_service_test.go +++ b/service/user/user_service_test.go @@ -1,393 +1,816 @@ -// 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 ( + "log" + "strconv" + "strings" "testing" - - "github.com/gofiber/fiber/v2" + "time" "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/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" repository "github.com/Lukmanern/gost/repository/user" - permService "github.com/Lukmanern/gost/service/permission" - roleService "github.com/Lukmanern/gost/service/role" + "github.com/go-redis/redis" + "github.com/stretchr/testify/assert" ) -func init() { - // Check env and database - env.ReadConfig("./../../.env") +const ( + headerTestName string = "at User Service Test" +) - connector.LoadDatabase() - connector.LoadRedisCache() -} +var ( + timeNow time.Time + userRepository repository.UserRepository + redisConTest *redis.Client +) -func TestNewUserService(t *testing.T) { - permSvc := permService.NewPermissionService() - roleSvc := roleService.NewRoleService(permSvc) - svc := NewUserService(roleSvc) - if svc == nil { - t.Error(constants.ShouldNotNil) - } +func init() { + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) + timeNow = time.Now() + userRepository = repository.NewUserRepository() + redisConTest = connector.LoadRedisCache() } -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") +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: "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{ + 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 } - } - - // 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) - } + assert.NoError(t, createErr, consts.ShouldNotErr, tc.Name, headerTestName) - 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) - } + user, getErr := repository.GetByID(ctx, id) + assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) + assert.NotNil(t, user, consts.ShouldNotNil, tc.Name, headerTestName) - // 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") - } + deleteErr := service.DeleteAccount(ctx, model.UserDeleteAccount{ + ID: id, + Password: tc.Payload.Password, + }) + assert.NoError(t, deleteErr, consts.ShouldNotErr, tc.Name, headerTestName) - // 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") + // 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) } +} - 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) - } +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) - // 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") + 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) } +} - modelUserUpdate := model.UserProfileUpdate{ - ID: userID, - Name: helper.RandomString(10), - } - updateProfileErr := svc.UpdateProfile(ctx, modelUserUpdate) - if updateProfileErr != nil { - t.Error(constants.ShouldNotErr) +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) } +} - profile, getErr := svc.MyProfile(ctx, userID) - if getErr != nil { - t.Error(constants.ShouldNotErr) - } - if profile.Name != helper.ToTitle(modelUserUpdate.Name) { - t.Error("should equal") +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) } +} - // success logout - cForLogout := helper.NewFiberCtx() - logoutErr := svc.Logout(cForLogout) - if logoutErr != nil { - t.Error("should no error") +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 TestFailedRegister(t *testing.T) { - defer func() { - connector.LoadRedisCache().FlushAll() - }() - permSvc := permService.NewPermissionService() - roleSvc := roleService.NewRoleService(permSvc) - svc := NewUserService(roleSvc) +func TestLogout(t *testing.T) { + service := NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) 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) - } + assert.NotNil(t, c, consts.ShouldNotNil, headerTestName) - 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) - }() + logoutErr := service.Logout(c) + assert.NoError(t, logoutErr, consts.ShouldErr, headerTestName) +} - 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") +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) + + for i := 0; i < 3; i++ { + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + } + + 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) } +} - 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") +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 } - } - - // failed login - _, loginErr := svc.Login(ctx, model.UserLogin{ - IP: helper.RandomIPAddress(), - }) - if loginErr == nil { - t.Error(constants.ShouldErr) - } + assert.NoError(t, err, consts.ShouldNotErr, tc.Name, headerTestName) - forgetErr := svc.ForgetPassword(ctx, model.UserForgetPassword{Email: "wrong_email@gost.project"}) - if forgetErr == nil { - t.Error(constants.ShouldErr) + 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) } +} - verifyErr := svc.ResetPassword(ctx, model.UserResetPassword{Code: "wrong-code"}) - if verifyErr == nil { - t.Error(constants.ShouldErr) +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) } +} - updatePasswdErr := svc.UpdatePassword(ctx, model.UserPasswordUpdate{ID: -1}) - if updatePasswdErr == nil { - t.Error(constants.ShouldErr) - } +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) + "xxxx", + }, + 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) - _, getErr := svc.MyProfile(ctx, -10) - if getErr == nil { - t.Error(constants.ShouldErr) + 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 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) +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) + } } +} - for i := 1; i <= 15; i++ { - counter, err := svc.FailedLoginCounter(helper.RandomIPAddress(), true) - if err != nil { - t.Error(constants.ShouldNotErr) +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) + + users := make([]model.UserDeleteAccount, 2) + for i := range users { + user := createUser() + users[i] = model.UserDeleteAccount{ + ID: user.ID, + Password: user.Password, } - if i >= 4 { - if counter == i { - t.Error("counter should error") - } + defer repository.Delete(ctx, user.ID) + } + + type testCase struct { + Name string + Payload model.UserDeleteAccount + WantErr bool + } + + testCases := []testCase{ + { + Name: "Failed Delete User -1: invalid ID", + Payload: model.UserDeleteAccount{ + ID: -1, + }, + WantErr: true, + }, + { + Name: "Failed Delete User -2: data not found", + Payload: model.UserDeleteAccount{ + ID: users[0].ID * 99, + }, + 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{ + 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", + Payload: model.UserDeleteAccount{ + ID: user.ID, + Password: user.Password, + }, + WantErr: true, + }) + } + + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + 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.Payload.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 +} 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") - } - } -}