diff --git a/.env.example b/.env.example index 4045b22..0d75e08 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,13 @@ APP_NAME=Gost - Go Fiber Project Starter APP_IN_PRODUCTION=false -APP_SECRET_KEY=RandomStringHere1234 APP_ACCESS_TOKEN_TTL=14320m APP_PORT=9009 APP_TIME_ZONE=Asia/Jakarta -DB_HOST=Host +DB_HOST=db.secret.supabase.co DB_PORT=5432 DB_USERNAME=postgres -DB_PASSWORD=Password +DB_PASSWORD=secret DB_DATABASE=postgres REDIS_URI=redis://user:@127.0.0.1:6379/1 @@ -18,6 +17,11 @@ PRIVATE_KEY=./keys/private.key SMTP_SERVER=smtp.gmail.com SMTP_PORT=587 -SMTP_EMAIL=youremail@gmail.com -SMTP_PASSWORD=passwd -CLIENT_URL=https://important.client \ No newline at end of file +SMTP_EMAIL=secret@gmail.com +SMTP_PASSWORD=secret +CLIENT_URL=https://important.client + +# supabase storage +SUPABASE_BUCKET_NAME=user_upload_public +SUPABASE_URL=https://secret.supabase.co +SUPABASE_TOKEN=SECRET \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d9ee07b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,51 @@ +name: Go Build + +on: + push: + branches: + - "*" + - "*/*" + pull_request: + branches: + - "*" + - "*/*" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ["1.19.x", "1.20.x", "1.21.x"] + + steps: + - uses: actions/checkout@v3 + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + + - name: Display Go version + run: go version + + - name: Verify dependencies + run: go mod verify + + - name: Create Keys Directory + run: mkdir -p keys + working-directory: ${{ github.workspace }} + + - name: Generate Self-Signed TLS Certificate + run: | + openssl req -x509 -newkey rsa:4096 -keyout keys/private.key -out keys/publickey.crt -days 365 -nodes -subj "/CN=localhost" + working-directory: ${{ github.workspace }} + + - name: Show Certificate + run: | + cat keys/publickey.crt + cat keys/private.key + + - name: Create .env file + run: echo "${{ secrets.ENV }}" > .env + + - name: Build + run: go build -o main main.go diff --git a/.github/workflows/snyk.yml b/.github/workflows/security.yml similarity index 65% rename from .github/workflows/snyk.yml rename to .github/workflows/security.yml index 59230bb..150a0f4 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/security.yml @@ -1,21 +1,25 @@ -name: Go Snyk +name: Go Security on: push: branches: - "*" - "*/*" - - "*/*/*" pull_request: branches: - "*" - "*/*" - - "*/*/*" jobs: security: runs-on: ubuntu-latest steps: + - name: Fetch Repository + uses: actions/checkout@v2 + + - name: Run GoSec Security Scanner + uses: securego/gosec@master + - uses: actions/checkout@master - name: Run Snyk to check for vulnerabilities uses: snyk/actions/golang@master diff --git a/.github/workflows/builder.yml b/.github/workflows/test.yml similarity index 67% rename from .github/workflows/builder.yml rename to .github/workflows/test.yml index 25c7161..534237d 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Go +name: Go Test on: push: @@ -11,7 +11,7 @@ on: - "*/*" jobs: - build: + test: runs-on: ubuntu-latest services: @@ -27,7 +27,7 @@ jobs: strategy: matrix: - go-version: ["1.18.x", "1.19.x", "1.20.x", "1.21.x"] + go-version: ["1.20.x"] steps: - uses: actions/checkout@v3 @@ -39,12 +39,6 @@ jobs: - name: Display Go version run: go version - - name: Install golint - run: go install golang.org/x/lint/golint@latest - - - name: Run golint - run: golint ./... - - name: Verify dependencies run: go mod verify @@ -62,11 +56,18 @@ jobs: cat keys/publickey.crt cat keys/private.key - - name: Copy env - run: cp .env.example .env + - name: Create .env file + run: echo "${{ secrets.ENV }}" > .env + + - name: Go Get + run: | + go get ./... - - name: Test - run: go test -race -timeout 20s ./internal/... + - name: Run migration + run: | + go run database/migration/main.go - - name: Build - run: go build -o main main.go + - name: Run Test + run: | + go test -p 1 -timeout 600s ./application/... + go test -race -timeout 600s ./controller/... ./database/... ./domain/.. ./internal/... ./repository/... ./service/... diff --git a/.gitignore b/.gitignore index 35c39c5..e4f7754 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ .env +.env.test /log /keys cover.out cover.html -/.cover \ No newline at end of file +/.cover +.dockerignore +docker-compose.yml +Dockerfile \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6631fd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Lukman Ernandi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 942b6b9..0f0c337 100644 --- a/Makefile +++ b/Makefile @@ -1,45 +1,31 @@ -test-clear: - go clean -testcache - -test: - go test -p 1 -timeout 240s -coverprofile=cover.out ./... - -test-per-dir: - go test -p 1 -timeout 120s ./application/... - go test -p 1 -timeout 120s ./controller/... - go test -p 1 -timeout 120s ./database/... - go test -p 1 -timeout 120s ./domain/... - go test -p 1 -timeout 120s ./internal/... - go test -p 1 -timeout 120s ./repository/... - go test -p 1 -timeout 120s ./service/... - go test -p 1 -timeout 120s ./tests/... - -# without ./application and ./tests -# go test -race -timeout 200s ./controller/... ./database/... ./internal/... ./repository/... ./service/... -test-race: - go clean -testcache - go test -race -timeout 120s ./controller/... - go test -race -timeout 120s ./database/... - go test -race -timeout 120s ./domain/... - go test -race -timeout 120s ./internal/... - go test -race -timeout 120s ./repository/... - go test -race -timeout 120s ./service/... - migrate: go run database/migration/main.go run: go run . -test-report: +test-clear: + go clean -testcache + +test: + go clean -testcache + go test -p 1 -timeout 330s -coverprofile=cover.out ./... go tool cover -html cover.out -o cover.html + del cover.out + +test-race: + go clean -testcache + go test -race -timeout 200s ./controller/... ./database/... ./internal/... ./repository/... ./service/... migrate-test-report: go run database/migration/main.go timeout 5 - go test -p 1 -timeout 240s -coverprofile=cover.out ./... + go clean -testcache + go test -race -timeout 200s ./controller/... ./database/... ./internal/... ./repository/... ./service/... + go test -p 1 -timeout 330s -coverprofile=cover.out ./... go tool cover -html cover.out -o cover.html + del cover.out -# !!windows only!! run redis in background +# windowsOS only st-redis: redis-server.exe --service-start \ No newline at end of file diff --git a/README.md b/README.md index df341b7..e443402 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,42 @@ -## Gost Project +

Gost: Go Starter

-Go-Fiber project starter, with jwt-auth and highly effective RBAC implementation. +
+ +Golang project starter with Fiber Framework, jwt-auth and highly effective bit-manipulation RBAC implementation to build a robust RestAPI Application. + +  + +## :rocket: Technologies And :wrench: Tools + +Techs and tools were used in this project: + +- [Go](https://go.dev) +- [Fiber Framework](https://docs.gofiber.io/) → Framework for routing & HTTP handler. +- [GORM](https://gorm.io/) → Database logics & queries. +- [Supabase Services: PostgreSQL & Bucket](https://www.supabase.com) → Free database & storage (bucket). +- [Github CLI](https://cli.github.com/) → Github management for repository's secrets & etc. +- [Github Action](https://github.com/features/actions) → Automated testing and building across multiple versions of Go. +- [Snyk](https://app.snyk.io/) → Dependency scanning. +- [SonarLint, VSCode ext.](https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode) → Detects & highlights issues that can lead to bugs & vulnerabilities. +- [GoLint](https://github.com/golang/lint) → CLI static code analytic for code-styling & many more. + +  + +## :closed_book: Read List + +- [Database Connection Configuration](https://www.alexedwards.net/blog/configuring-sqldb) +- [Go-Fiber Testing](https://dev.to/koddr/go-fiber-by-examples-testing-the-application-1ldf) +- [Production Checklist 1](https://aleksei-kornev.medium.com/production-readiness-checklist-for-backend-applications-8d2b0c57ccec/) +- [Production Checklist 2](https://github.com/gorrion-io/production-readiness-checklist/) +- [Production Checklist 3](https://www.cockroachlabs.com/docs/cockroachcloud/production-checklist/) +- [Deployment Checklist](https://last9.io/blog/deployment-readiness-checklists/) +- [CI with Github Actions](https://www.alexedwards.net/blog/ci-with-go-and-github-actions) +- [RestAPI Security Checklist](https://roadmap.sh/best-practices/api-security/) + +  + +## :memo: License + +This project is under license from MIT. For more details, see the [LICENSE](LICENSE) file. + +  diff --git a/application/app.go b/application/app.go index ac25798..7ca4f30 100644 --- a/application/app.go +++ b/application/app.go @@ -1,3 +1,11 @@ +// 📌 Origin Github Repository: https://github.com/Lukmanerngost + +// 🔍 README +// Application package configures middleware, error management, and +// handles OS signals for gracefully stopping the server when receiving +// an interrupt signal. This package provides routes related to user +// management and role-based access control (RBAC). And so on. + package application import ( @@ -17,6 +25,8 @@ import ( ) var ( + port int + // Create a new fiber instance with custom config router = fiber.New(fiber.Config{ // Override default error handler @@ -24,7 +34,8 @@ var ( // Status code defaults to 500 code := fiber.StatusInternalServerError - // Retrieve the custom status code if it's a *fiber.Error + // Retrieve the custom status code + // if it's a *fiber.Error var e *fiber.Error if errors.As(err, &e) { code = e.Code @@ -35,14 +46,14 @@ var ( "message": e.Message, }) if err != nil { - // In case the SendFile fails - return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error") + return ctx.Status(fiber.StatusInternalServerError). + SendString("Internal Server Error") } - - // Return from handler return nil }, - ReadBufferSize: 12000, + // memory management + // ReduceMemoryUsage: true, + // ReadBufferSize: 5120, }) ) @@ -50,15 +61,15 @@ func setup() { // Check env and database env.ReadConfig("./.env") config := env.Configuration() - dbURI := config.GetDatabaseURI() privKey := config.GetPrivateKey() pubKey := config.GetPublicKey() - if dbURI == "" || privKey == nil || pubKey == nil { - log.Fatal("Database URI or keys aren't valid") + if privKey == nil || pubKey == nil { + log.Fatal("private and public keys are not valid or not found") } + port = config.AppPort connector.LoadDatabase() - connector.LoadRedisDatabase() + connector.LoadRedisCache() } func RunApp() { @@ -69,7 +80,8 @@ func RunApp() { router.Use(logger.New()) // Custom File Writer _ = os.MkdirAll("./log", os.ModePerm) - file, err := os.OpenFile(fmt.Sprintf("./log/%s.log", time.Now().Format("20060102")), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + fileName := fmt.Sprintf("./log/%s.log", time.Now().Format("20060102")) + file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatalf("error opening file: %v", err) } @@ -96,13 +108,12 @@ func RunApp() { close(idleConnsClosed) }() - getDevopmentRouter(router) // don't use for production - getUserManagementRoutes(router) // don't use for production - - getUserRoutes(router) - getRbacRoutes(router) + getUserManagementRoutes(router) // user CRUD without auth ⚠️ + getDevopmentRouter(router) // experimental without auth ⚠️ + getUserRoutes(router) // user with auth + getRolePermissionRoutes(router) // RBAC CRUD with auth - if err := router.Listen(fmt.Sprintf(":%d", 9009)); err != nil { + if err := router.Listen(fmt.Sprintf(":%d", port)); err != nil { log.Printf("Oops... Server is not running! Reason: %v", err) } diff --git a/application/app_test.go b/application/app_test.go index 6b41f1a..a90bdca 100644 --- a/application/app_test.go +++ b/application/app_test.go @@ -21,13 +21,13 @@ var ( timeNow time.Time userRepo repository.UserRepository ctx context.Context - appUrl string + appURL string ) func init() { env.ReadConfig("./../.env") c := env.Configuration() - appUrl = c.AppUrl + appURL = c.AppURL jwtHandler = middleware.NewJWTHandler() timeNow = time.Now() @@ -35,7 +35,7 @@ func init() { ctx = context.Background() } -func Test_RunApp(t *testing.T) { +func TestRunApp(t *testing.T) { defer func() { r := recover() if r != nil { @@ -47,7 +47,7 @@ func Test_RunApp(t *testing.T) { time.Sleep(3 * time.Second) } -func Test_app_router(t *testing.T) { +func TestAppRouter(t *testing.T) { defer func() { r := recover() if r != nil { @@ -90,7 +90,7 @@ func TestRoutes(t *testing.T) { }) t.Run("getRBACAuthRoutes", func(t *testing.T) { - getRbacRoutes(router) + getRolePermissionRoutes(router) }) t.Run("getUserAuthRoutes", func(t *testing.T) { diff --git a/application/development_router.go b/application/development_router.go index 338c0d0..cc6b89e 100644 --- a/application/development_router.go +++ b/application/development_router.go @@ -1,6 +1,11 @@ -// don't use this for production -// use this file just for testing -// and testing management. +// 📌 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 @@ -8,7 +13,6 @@ import ( "github.com/gofiber/fiber/v2" controller "github.com/Lukmanern/gost/controller/development" - "github.com/Lukmanern/gost/internal/middleware" ) var ( @@ -16,7 +20,6 @@ var ( ) func getDevopmentRouter(router fiber.Router) { - jwtHandler := middleware.NewJWTHandler() devController = controller.NewDevControllerImpl() // Developement 'helper' Process devRouter := router.Group("development") @@ -31,9 +34,10 @@ func getDevopmentRouter(router fiber.Router) { // you should create new role named new-role-001 and new permission // named new-permission-001 from RBAC-endpoints to test these endpoints - 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) + // 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/rbac_router.go b/application/role_permission_router.go similarity index 71% rename from application/rbac_router.go rename to application/role_permission_router.go index 21b412f..168df60 100644 --- a/application/rbac_router.go +++ b/application/role_permission_router.go @@ -1,3 +1,10 @@ +// 📌 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 ( @@ -8,21 +15,22 @@ import ( permCtr "github.com/Lukmanern/gost/controller/permission" roleCtr "github.com/Lukmanern/gost/controller/role" - service "github.com/Lukmanern/gost/service/rbac" + permSvc "github.com/Lukmanern/gost/service/permission" + roleSvc "github.com/Lukmanern/gost/service/role" ) var ( - roleService service.RoleService + roleService roleSvc.RoleService roleController roleCtr.RoleController - permissionService service.PermissionService + permissionService permSvc.PermissionService permissionController permCtr.PermissionController ) -func getRbacRoutes(router fiber.Router) { +func getRolePermissionRoutes(router fiber.Router) { jwtHandler := middleware.NewJWTHandler() - permissionService = service.NewPermissionService() + permissionService = permSvc.NewPermissionService() permissionController = permCtr.NewPermissionController(permissionService) permissionRouter := router.Group("permission").Use(jwtHandler.IsAuthenticated) @@ -33,7 +41,7 @@ func getRbacRoutes(router fiber.Router) { permissionRouter.Put(":id", jwtHandler.CheckHasPermission(rbac.PermUpdatePermission.ID), permissionController.Update) permissionRouter.Delete(":id", jwtHandler.CheckHasPermission(rbac.PermDeletePermission.ID), permissionController.Delete) - roleService = service.NewRoleService(permissionService) + roleService = roleSvc.NewRoleService(permissionService) roleController = roleCtr.NewRoleController(roleService) roleRouter := router.Group("role").Use(jwtHandler.IsAuthenticated) diff --git a/application/user_management_router.go b/application/user_management_router.go index ae2d965..9110d57 100644 --- a/application/user_management_router.go +++ b/application/user_management_router.go @@ -1,6 +1,10 @@ -// don't use this for production -// use this file just for testing -// and testing management. +// 📌 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 diff --git a/application/user_router.go b/application/user_router.go index 787c886..66bc972 100644 --- a/application/user_router.go +++ b/application/user_router.go @@ -1,3 +1,11 @@ +// 📌 Origin Github Repository: https://github.com/Lukmanerngost + +// 🔍 README +// User Routes provides some features and action that user can use. +// User Routes provide the typical web application authentication flow, +// such as registration, sending verification codes, and verifying accounts +// with a verification code. + package application import ( @@ -8,19 +16,20 @@ import ( controller "github.com/Lukmanern/gost/controller/user" service "github.com/Lukmanern/gost/service/user" - rbacService "github.com/Lukmanern/gost/service/rbac" + permSvc "github.com/Lukmanern/gost/service/permission" + roleSvc "github.com/Lukmanern/gost/service/role" ) var ( - userPermService rbacService.PermissionService - userRoleService rbacService.RoleService + userPermService permSvc.PermissionService + userRoleService roleSvc.RoleService userService service.UserService userController controller.UserController ) func getUserRoutes(router fiber.Router) { - userPermService = rbacService.NewPermissionService() - userRoleService = rbacService.NewRoleService(userPermService) + userPermService = permSvc.NewPermissionService() + userRoleService = roleSvc.NewRoleService(userPermService) userService = service.NewUserService(userRoleService) userController = controller.NewUserController(userService) jwtHandler := middleware.NewJWTHandler() diff --git a/controller/development/dev_controller.go b/controller/development/dev_controller.go index beef701..602bc85 100644 --- a/controller/development/dev_controller.go +++ b/controller/development/dev_controller.go @@ -11,26 +11,51 @@ import ( "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 } type DevControllerImpl struct { - fileSvc fileService.UploadFile + fileSvc fileService.FileService redis *redis.Client db *gorm.DB } @@ -44,7 +69,7 @@ func NewDevControllerImpl() DevController { devImplOnce.Do(func() { devImpl = &DevControllerImpl{ fileSvc: fileService.NewFileService(), - redis: connector.LoadRedisDatabase(), + redis: connector.LoadRedisCache(), db: connector.LoadDatabase(), } }) @@ -52,7 +77,7 @@ func NewDevControllerImpl() DevController { return devImpl } -func (ctr DevControllerImpl) PingDatabase(c *fiber.Ctx) error { +func (ctr *DevControllerImpl) PingDatabase(c *fiber.Ctx) error { db := ctr.db if db == nil { return response.Error(c, "failed db is nil") @@ -71,10 +96,10 @@ func (ctr DevControllerImpl) PingDatabase(c *fiber.Ctx) error { return response.CreateResponse(c, fiber.StatusOK, true, "success ping-sql-db", nil) } -func (ctr DevControllerImpl) PingRedis(c *fiber.Ctx) error { +func (ctr *DevControllerImpl) PingRedis(c *fiber.Ctx) error { redis := ctr.redis if redis == nil { - return response.Error(c, "redis nil value") + return response.Error(c, constants.RedisNil) } for i := 0; i < 5; i++ { status := redis.Ping() @@ -86,7 +111,7 @@ func (ctr DevControllerImpl) PingRedis(c *fiber.Ctx) error { return response.CreateResponse(c, fiber.StatusOK, true, "success ping-redis", nil) } -func (ctr DevControllerImpl) Panic(c *fiber.Ctx) error { +func (ctr *DevControllerImpl) Panic(c *fiber.Ctx) error { defer func() error { r := recover() if r != nil { @@ -95,13 +120,13 @@ func (ctr DevControllerImpl) Panic(c *fiber.Ctx) error { } return nil }() - panic("Panic message") // message should string + panic("your panic message") // should string } -func (ctr DevControllerImpl) StoringToRedis(c *fiber.Ctx) error { +func (ctr *DevControllerImpl) StoringToRedis(c *fiber.Ctx) error { redis := ctr.redis if redis == nil { - return response.Error(c, "redis nil value") + return response.Error(c, constants.RedisNil) } redisStatus := redis.Set("example-key", "example-value", 50*time.Minute) if redisStatus.Err() != nil { @@ -112,10 +137,10 @@ func (ctr DevControllerImpl) StoringToRedis(c *fiber.Ctx) error { return response.SuccessCreated(c, nil) } -func (ctr DevControllerImpl) GetFromRedis(c *fiber.Ctx) error { +func (ctr *DevControllerImpl) GetFromRedis(c *fiber.Ctx) error { redis := ctr.redis if redis == nil { - return response.Error(c, "redis nil value") + return response.Error(c, constants.RedisNil) } redisStatus := redis.Get("example-key") if redisStatus.Err() != nil { @@ -131,15 +156,15 @@ func (ctr DevControllerImpl) GetFromRedis(c *fiber.Ctx) error { return response.SuccessLoaded(c, res) } -func (ctr DevControllerImpl) CheckNewRole(c *fiber.Ctx) error { +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 { +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 { +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()) @@ -147,7 +172,7 @@ func (ctr DevControllerImpl) UploadFile(c *fiber.Ctx) error { if file == nil { return response.BadRequest(c, "file is nil or not found") } - mimeType := file.Header.Get("Content-Type") + mimeType := file.Header.Get(fiber.HeaderContentType) if mimeType != "application/pdf" { return response.BadRequest(c, "only PDF file are allowed for upload") } @@ -156,29 +181,29 @@ func (ctr DevControllerImpl) UploadFile(c *fiber.Ctx) error { return response.BadRequest(c, "file size exceeds the maximum allowed (3MB)") } - fileUrl, uploadErr := ctr.fileSvc.UploadFile(file) + 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, "internal server error: "+uploadErr.Error()) + return response.Error(c, constants.ServerErr+uploadErr.Error()) } return response.SuccessCreated(c, map[string]any{ - "file_url": fileUrl, + "file_url": fileURL, }) } -func (ctr DevControllerImpl) RemoveFile(c *fiber.Ctx) error { +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, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } validate := validator.New() if err := validate.Struct(&fileName); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } removeErr := ctr.fileSvc.RemoveFile(fileName.FileName) @@ -187,19 +212,19 @@ func (ctr DevControllerImpl) RemoveFile(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+removeErr.Error()) + return response.Error(c, constants.ServerErr+removeErr.Error()) } return response.SuccessNoContent(c) } -func (ctr DevControllerImpl) GetFilesList(c *fiber.Ctx) error { +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, "internal server error: "+getErr.Error()) + return response.Error(c, constants.ServerErr+getErr.Error()) } return response.SuccessLoaded(c, resp) } diff --git a/controller/development/dev_controller_test.go b/controller/development/dev_controller_test.go index debb596..ea08ea7 100644 --- a/controller/development/dev_controller_test.go +++ b/controller/development/dev_controller_test.go @@ -21,7 +21,7 @@ func init() { env.ReadConfig("./../../.env") connector.LoadDatabase() - connector.LoadRedisDatabase() + connector.LoadRedisCache() } func TestNewDevControllerImpl(t *testing.T) { @@ -67,7 +67,7 @@ func TestNewDevControllerImpl(t *testing.T) { } } -func Test_Methods(t *testing.T) { +func TestMethods(t *testing.T) { c := helper.NewFiberCtx() ctr := NewDevControllerImpl() if ctr == nil || c == nil { diff --git a/controller/permission/permission_controller.go b/controller/permission/permission_controller.go index 8a985bf..1a54d0c 100644 --- a/controller/permission/permission_controller.go +++ b/controller/permission/permission_controller.go @@ -9,15 +9,25 @@ import ( "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/rbac" + 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 } @@ -39,14 +49,14 @@ func NewPermissionController(service service.PermissionService) PermissionContro return permissionControllerImpl } -func (ctr PermissionControllerImpl) Create(c *fiber.Ctx) error { +func (ctr *PermissionControllerImpl) Create(c *fiber.Ctx) error { var permission model.PermissionCreate if err := c.BodyParser(&permission); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } validate := validator.New() if err := validate.Struct(&permission); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -56,7 +66,7 @@ func (ctr PermissionControllerImpl) Create(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+createErr.Error()) + return response.Error(c, constants.ServerErr+createErr.Error()) } data := map[string]any{ "id": id, @@ -64,10 +74,10 @@ func (ctr PermissionControllerImpl) Create(c *fiber.Ctx) error { return response.SuccessCreated(c, data) } -func (ctr PermissionControllerImpl) Get(c *fiber.Ctx) error { +func (ctr *PermissionControllerImpl) Get(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil || id <= 0 { - return response.BadRequest(c, "invalid id") + return response.BadRequest(c, constants.InvalidID) } ctx := c.Context() @@ -77,12 +87,12 @@ func (ctr PermissionControllerImpl) Get(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+getErr.Error()) + return response.Error(c, constants.ServerErr+getErr.Error()) } return response.SuccessLoaded(c, permission) } -func (ctr PermissionControllerImpl) GetAll(c *fiber.Ctx) error { +func (ctr *PermissionControllerImpl) GetAll(c *fiber.Ctx) error { request := base.RequestGetAll{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 20), @@ -96,7 +106,7 @@ func (ctr PermissionControllerImpl) GetAll(c *fiber.Ctx) error { ctx := c.Context() permissions, total, getErr := ctr.service.GetAll(ctx, request) if getErr != nil { - return response.Error(c, "internal server error: "+getErr.Error()) + return response.Error(c, constants.ServerErr+getErr.Error()) } data := make([]interface{}, len(permissions)) @@ -114,19 +124,19 @@ func (ctr PermissionControllerImpl) GetAll(c *fiber.Ctx) error { return response.SuccessLoaded(c, responseData) } -func (ctr PermissionControllerImpl) Update(c *fiber.Ctx) error { +func (ctr *PermissionControllerImpl) Update(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil || id <= 0 { - return response.BadRequest(c, "invalid id") + return response.BadRequest(c, constants.InvalidID) } var permission model.PermissionUpdate permission.ID = id if err := c.BodyParser(&permission); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } validate := validator.New() if err := validate.Struct(&permission); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -136,15 +146,15 @@ func (ctr PermissionControllerImpl) Update(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+updateErr.Error()) + return response.Error(c, constants.ServerErr+updateErr.Error()) } return response.SuccessNoContent(c) } -func (ctr PermissionControllerImpl) Delete(c *fiber.Ctx) error { +func (ctr *PermissionControllerImpl) Delete(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil || id <= 0 { - return response.BadRequest(c, "invalid id") + return response.BadRequest(c, constants.InvalidID) } ctx := c.Context() @@ -154,7 +164,7 @@ func (ctr PermissionControllerImpl) Delete(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+deleteErr.Error()) + 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 index d0ed666..285fa0c 100644 --- a/controller/permission/permission_controller_test.go +++ b/controller/permission/permission_controller_test.go @@ -20,6 +20,7 @@ import ( "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" @@ -27,7 +28,8 @@ import ( userController "github.com/Lukmanern/gost/controller/user" userRepository "github.com/Lukmanern/gost/repository/user" - service "github.com/Lukmanern/gost/service/rbac" + service "github.com/Lukmanern/gost/service/permission" + roleService "github.com/Lukmanern/gost/service/role" userService "github.com/Lukmanern/gost/service/user" ) @@ -35,37 +37,37 @@ var ( userRepo userRepository.UserRepository permService service.PermissionService permController PermissionController - appUrl string + appURL string ) func init() { env.ReadConfig("./../../.env") config := env.Configuration() - appUrl = config.AppUrl + appURL = config.AppURL connector.LoadDatabase() - connector.LoadRedisDatabase() + connector.LoadRedisCache() userRepo = userRepository.NewUserRepository() permService = service.NewPermissionService() permController = NewPermissionController(permService) } -func Test_Perm_NewPermissionController(t *testing.T) { +func TestPermNewPermissionController(t *testing.T) { permSvc := service.NewPermissionService() permCtr := NewPermissionController(permSvc) if permSvc == nil || permCtr == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } } -func Test_Perm_Create(t *testing.T) { +func TestPermCreate(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } userID, userToken := createUserAndToken() @@ -113,7 +115,7 @@ func Test_Perm_Create(t *testing.T) { jwtHandler := middleware.NewJWTHandler() for _, tc := range testCases { c := helper.NewFiberCtx() - c.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", tc.token)) + 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 { @@ -166,12 +168,12 @@ func Test_Perm_Create(t *testing.T) { } } -func Test_Perm_Get(t *testing.T) { +func TestPermGet(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } testCases := []struct { @@ -207,7 +209,7 @@ func Test_Perm_Get(t *testing.T) { app.Get("/permission/:id", permController.Get) resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -233,12 +235,12 @@ func Test_Perm_Get(t *testing.T) { } } -func Test_Perm_GetAll(t *testing.T) { +func TestPermGetAll(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } testCases := []struct { @@ -278,7 +280,7 @@ func Test_Perm_GetAll(t *testing.T) { app.Get("/permission", permController.GetAll) resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -307,12 +309,12 @@ func Test_Perm_GetAll(t *testing.T) { } } -func Test_Perm_Update(t *testing.T) { +func TestPermUpdate(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } // create 1 permission @@ -385,19 +387,19 @@ func Test_Perm_Update(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := fmt.Sprintf(appUrl+"permission/%d", tc.permID) + url := fmt.Sprintf(appURL+"permission/%d", tc.permID) req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(jsonObject)) if err != nil { - t.Error("should not error", err.Error()) + 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("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -422,12 +424,12 @@ func Test_Perm_Update(t *testing.T) { } } -func Test_Perm_Delete(t *testing.T) { +func TestPermDelete(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } // create 1 permission @@ -471,17 +473,17 @@ func Test_Perm_Delete(t *testing.T) { } for _, tc := range testCases { - url := appUrl + "permission/" + strconv.Itoa(tc.permID) + url := appURL + "permission/" + strconv.Itoa(tc.permID) req, httpReqErr := http.NewRequest(http.MethodDelete, url, nil) if httpReqErr != nil || req == nil { - t.Fatal("should not 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("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -492,7 +494,7 @@ func Test_Perm_Delete(t *testing.T) { func createUserAndToken() (userID int, token string) { permService := service.NewPermissionService() - roleService := service.NewRoleService(permService) + roleService := roleService.NewRoleService(permService) userSvc := userService.NewUserService(roleService) userCtr := userController.NewUserController(userSvc) @@ -500,7 +502,7 @@ func createUserAndToken() (userID int, token string) { ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - log.Fatal("should not nil") + log.Fatal(constants.ShouldNotNil) } createdUser := model.UserRegister{ @@ -527,7 +529,7 @@ func createUserAndToken() (userID int, token string) { Email: userByID.Email, }) if verifyErr != nil { - log.Fatal("should not error") + log.Fatal(constants.ShouldNotErr) } userByID = nil userByID, getErr = userRepo.GetByID(ctx, userID) diff --git a/controller/role/role_controller.go b/controller/role/role_controller.go index 6718ba4..f794326 100644 --- a/controller/role/role_controller.go +++ b/controller/role/role_controller.go @@ -9,16 +9,30 @@ import ( "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/rbac" + service "github.com/Lukmanern/gost/service/role" ) type RoleController interface { + + // Create func creates a new role Create(c *fiber.Ctx) error + + // Connect func connects a role with some permissions + // and storing data in role_has_permissions table Connect(c *fiber.Ctx) error + + // Get func gets a role Get(c *fiber.Ctx) error + + // GetAll func gets some roles GetAll(c *fiber.Ctx) error + + // Update func updates a role Update(c *fiber.Ctx) error + + // Delete func deletes a role Delete(c *fiber.Ctx) error } @@ -40,10 +54,10 @@ func NewRoleController(service service.RoleService) RoleController { return roleControllerImpl } -func (ctr RoleControllerImpl) Create(c *fiber.Ctx) error { +func (ctr *RoleControllerImpl) Create(c *fiber.Ctx) error { var role model.RoleCreate if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } // hashmap idCheckers := make(map[int]bool) @@ -58,7 +72,7 @@ func (ctr RoleControllerImpl) Create(c *fiber.Ctx) error { } validate := validator.New() if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -68,7 +82,7 @@ func (ctr RoleControllerImpl) Create(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+createErr.Error()) + return response.Error(c, constants.ServerErr+createErr.Error()) } data := map[string]any{ "id": id, @@ -76,10 +90,10 @@ func (ctr RoleControllerImpl) Create(c *fiber.Ctx) error { return response.SuccessCreated(c, data) } -func (ctr RoleControllerImpl) Connect(c *fiber.Ctx) error { +func (ctr *RoleControllerImpl) Connect(c *fiber.Ctx) error { var role model.RoleConnectToPermissions if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } // hashmap idCheckers := make(map[int]bool) @@ -94,7 +108,7 @@ func (ctr RoleControllerImpl) Connect(c *fiber.Ctx) error { } validate := validator.New() if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -104,15 +118,15 @@ func (ctr RoleControllerImpl) Connect(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+connectErr.Error()) + return response.Error(c, constants.ServerErr+connectErr.Error()) } return response.SuccessCreated(c, "role and permissions success connected") } -func (ctr RoleControllerImpl) Get(c *fiber.Ctx) error { +func (ctr *RoleControllerImpl) Get(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil || id <= 0 { - return response.BadRequest(c, "invalid id") + return response.BadRequest(c, constants.InvalidID) } ctx := c.Context() @@ -122,12 +136,12 @@ func (ctr RoleControllerImpl) Get(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+getErr.Error()) + return response.Error(c, constants.ServerErr+getErr.Error()) } return response.SuccessLoaded(c, role) } -func (ctr RoleControllerImpl) GetAll(c *fiber.Ctx) error { +func (ctr *RoleControllerImpl) GetAll(c *fiber.Ctx) error { request := base.RequestGetAll{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 20), @@ -141,7 +155,7 @@ 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, "internal server error: "+getErr.Error()) + return response.Error(c, constants.ServerErr+getErr.Error()) } data := make([]interface{}, len(roles)) @@ -159,19 +173,19 @@ func (ctr RoleControllerImpl) GetAll(c *fiber.Ctx) error { return response.SuccessLoaded(c, responseData) } -func (ctr RoleControllerImpl) Update(c *fiber.Ctx) error { +func (ctr *RoleControllerImpl) Update(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil || id <= 0 { - return response.BadRequest(c, "invalid id") + return response.BadRequest(c, constants.InvalidID) } var role model.RoleUpdate role.ID = id if err := c.BodyParser(&role); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } validate := validator.New() if err := validate.Struct(&role); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -181,15 +195,15 @@ func (ctr RoleControllerImpl) Update(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+updateErr.Error()) + return response.Error(c, constants.ServerErr+updateErr.Error()) } return response.SuccessNoContent(c) } -func (ctr RoleControllerImpl) Delete(c *fiber.Ctx) error { +func (ctr *RoleControllerImpl) Delete(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil || id <= 0 { - return response.BadRequest(c, "invalid id") + return response.BadRequest(c, constants.InvalidID) } ctx := c.Context() @@ -199,7 +213,7 @@ func (ctr RoleControllerImpl) Delete(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+deleteErr.Error()) + return response.Error(c, constants.ServerErr+deleteErr.Error()) } return response.SuccessNoContent(c) } diff --git a/controller/role/role_controller_test.go b/controller/role/role_controller_test.go index 2565741..de4cbb9 100644 --- a/controller/role/role_controller_test.go +++ b/controller/role/role_controller_test.go @@ -15,44 +15,46 @@ import ( "github.com/Lukmanern/gost/database/connector" "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/constants" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" "github.com/Lukmanern/gost/internal/response" "github.com/gofiber/fiber/v2" permissionController "github.com/Lukmanern/gost/controller/permission" - service "github.com/Lukmanern/gost/service/rbac" + permSvc "github.com/Lukmanern/gost/service/permission" + service "github.com/Lukmanern/gost/service/role" ) var ( - permService service.PermissionService + permService permSvc.PermissionService roleService service.RoleService roleController RoleController permController permissionController.PermissionController - appUrl string + appURL string ) func init() { env.ReadConfig("./../../.env") config := env.Configuration() - appUrl = config.AppUrl + appURL = config.AppURL connector.LoadDatabase() - connector.LoadRedisDatabase() + connector.LoadRedisCache() - permService = service.NewPermissionService() + permService = permSvc.NewPermissionService() permController = permissionController.NewPermissionController(permService) roleService = service.NewRoleService(permService) roleController = NewRoleController(roleService) } -func Test_Role_Create(t *testing.T) { +func TestRoleCreate(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } permIDs := make([]int, 0) @@ -146,19 +148,19 @@ func Test_Role_Create(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + "role" + url := appURL + "role" req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) app := fiber.New() app.Post("/role", roleController.Create) resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -174,28 +176,28 @@ func Test_Role_Create(t *testing.T) { if !ok1 { t.Error("should ok1") } - anyId, ok2 := data["id"] + anyID, ok2 := data["id"] if !ok2 { t.Error("should ok2") } - intId, ok3 := anyId.(float64) + intID, ok3 := anyID.(float64) if !ok3 { t.Error("should ok3") } - deleteErr := roleService.Delete(ctx, int(intId)) + deleteErr := roleService.Delete(ctx, int(intID)) if deleteErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } } } } -func Test_Role_Connect(t *testing.T) { +func TestRoleConnect(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } permIDs := make([]int, 0) @@ -286,19 +288,19 @@ func Test_Role_Connect(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + "role/connect" + url := appURL + "role/connect" req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) app := fiber.New() app.Post("/role/connect", roleController.Connect) resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -321,12 +323,12 @@ func Test_Role_Connect(t *testing.T) { } } -func Test_Role_Get(t *testing.T) { +func TestRoleGet(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } permIDs := make([]int, 0) @@ -394,17 +396,17 @@ func Test_Role_Get(t *testing.T) { } for _, tc := range testCases { - url := fmt.Sprintf(appUrl+"role/%d", tc.roleID) + url := fmt.Sprintf(appURL+"role/%d", tc.roleID) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) app := fiber.New() app.Get("/role/:id", roleController.Get) resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -417,12 +419,12 @@ func Test_Role_Get(t *testing.T) { } } -func Test_Role_GetAll(t *testing.T) { +func TestRoleGetAll(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } permIDs := make([]int, 0) @@ -485,17 +487,17 @@ func Test_Role_GetAll(t *testing.T) { } for _, tc := range testCases { - url := appUrl + "role?" + tc.params + url := appURL + "role?" + tc.params req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - t.Error("should not error", err.Error()) + 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("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -508,12 +510,12 @@ func Test_Role_GetAll(t *testing.T) { } } -func Test_Role_Update(t *testing.T) { +func TestRoleUpdate(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } permIDs := make([]int, 0) @@ -610,19 +612,19 @@ func Test_Role_Update(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := fmt.Sprintf(appUrl+"role/%d", tc.roleID) + url := fmt.Sprintf(appURL+"role/%d", tc.roleID) req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(jsonObject)) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) app := fiber.New() app.Put("/role/:id", roleController.Update) resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -647,12 +649,12 @@ func Test_Role_Update(t *testing.T) { } } -func Test_Role_Delete(t *testing.T) { +func TestRoleDelete(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := permController if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } permIDs := make([]int, 0) @@ -720,17 +722,17 @@ func Test_Role_Delete(t *testing.T) { } for _, tc := range testCases { - url := fmt.Sprintf(appUrl+"role/%d", tc.roleID) + url := fmt.Sprintf(appURL+"role/%d", tc.roleID) req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) app := fiber.New() app.Delete("/role/:id", roleController.Delete) resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go index 0c8a5b2..4e860b2 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -9,21 +9,49 @@ import ( "github.com/gofiber/fiber/v2" "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/constants" "github.com/Lukmanern/gost/internal/middleware" "github.com/Lukmanern/gost/internal/response" service "github.com/Lukmanern/gost/service/user" ) type UserController interface { + // Register function register user account, + // than send verification-code to email Register(c *fiber.Ctx) error + + // AccountActivation function activates user account with + // verification code that has been sended to the user's email AccountActivation(c *fiber.Ctx) error + + // DeleteUserByVerification function deletes user data if the + // user account is not yet verified. This implies that the email + // owner hasn't actually registered the email, indicating that + // the user who registered may be making typing errors or may + // be a hacker attempting to get the verification code. DeleteAccountActivation(c *fiber.Ctx) error + + // ForgetPassword function send + // verification code into user's email ForgetPassword(c *fiber.Ctx) error + + // ResetPassword func resets password by creating + // new password by email and verification code ResetPassword(c *fiber.Ctx) error + + // Login func gives token and access to user Login(c *fiber.Ctx) error + + // Logout func stores user's token into Redis Logout(c *fiber.Ctx) error + + // UpdatePassword func updates user's password UpdatePassword(c *fiber.Ctx) error + + // UpdateProfile func updates user's profile data UpdateProfile(c *fiber.Ctx) error + + // MyProfile func shows user's profile data MyProfile(c *fiber.Ctx) error } @@ -46,15 +74,15 @@ func NewUserController(service service.UserService) UserController { return userAuthController } -func (ctr UserControllerImpl) Register(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) Register(c *fiber.Ctx) error { var user model.UserRegister if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + 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, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -64,7 +92,7 @@ func (ctr UserControllerImpl) Register(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+regisErr.Error()) + return response.Error(c, constants.ServerErr+regisErr.Error()) } message := "Account success created. please check " + user.Email + " " @@ -75,14 +103,14 @@ func (ctr UserControllerImpl) Register(c *fiber.Ctx) error { return response.CreateResponse(c, fiber.StatusCreated, true, message, data) } -func (ctr UserControllerImpl) AccountActivation(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { var user model.UserVerificationCode if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } validate := validator.New() if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() err := ctr.service.Verification(ctx, user) @@ -91,21 +119,21 @@ func (ctr UserControllerImpl) AccountActivation(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+err.Error()) + return response.Error(c, constants.ServerErr+err.Error()) } message := "Thank you for your confirmation. Your account is active now, you can login." return response.CreateResponse(c, fiber.StatusOK, true, message, nil) } -func (ctr UserControllerImpl) DeleteAccountActivation(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) DeleteAccountActivation(c *fiber.Ctx) error { var verifyData model.UserVerificationCode if err := c.BodyParser(&verifyData); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } validate := validator.New() if err := validate.Struct(&verifyData); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() err := ctr.service.DeleteUserByVerification(ctx, verifyData) @@ -114,23 +142,23 @@ func (ctr UserControllerImpl) DeleteAccountActivation(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+err.Error()) + return response.Error(c, constants.ServerErr+err.Error()) } message := "Your data is already deleted, thank you for your confirmation." return response.CreateResponse(c, fiber.StatusOK, true, message, nil) } -func (ctr UserControllerImpl) Login(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { var user model.UserLogin - // user.IP = c.IP() // Todo : uncomment this line in production + // user.IP = c.IP() // Note : uncomment this line in production if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } userIP := net.ParseIP(user.IP) if userIP == nil { - return response.BadRequest(c, "invalid json body: invalid user ip address") + return response.BadRequest(c, constants.InvalidBody+"invalid user ip address") } counter, _ := ctr.service.FailedLoginCounter(userIP.String(), false) ipBlockMsg := "Your IP has been blocked by system. Please try again in 1 or 2 Hour" @@ -140,7 +168,7 @@ func (ctr UserControllerImpl) Login(c *fiber.Ctx) error { validate := validator.New() if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -154,7 +182,7 @@ func (ctr UserControllerImpl) Login(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+loginErr.Error()) + return response.Error(c, constants.ServerErr+loginErr.Error()) } data := map[string]any{ @@ -164,26 +192,26 @@ func (ctr UserControllerImpl) Login(c *fiber.Ctx) error { return response.CreateResponse(c, fiber.StatusOK, true, "success login", data) } -func (ctr UserControllerImpl) Logout(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) Logout(c *fiber.Ctx) error { userClaims, ok := c.Locals("claims").(*middleware.Claims) if !ok || userClaims == nil { return response.Unauthorized(c) } logoutErr := ctr.service.Logout(c) if logoutErr != nil { - return response.Error(c, "internal server error: "+logoutErr.Error()) + 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 { +func (ctr *UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { var user model.UserForgetPassword if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } validate := validator.New() if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -193,21 +221,21 @@ func (ctr UserControllerImpl) ForgetPassword(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+forgetErr.Error()) + return response.Error(c, constants.ServerErr+forgetErr.Error()) } message := "success sending link for reset password to email, check your email inbox" return response.CreateResponse(c, fiber.StatusAccepted, true, message, nil) } -func (ctr UserControllerImpl) ResetPassword(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) ResetPassword(c *fiber.Ctx) error { var user model.UserResetPassword if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } validate := validator.New() if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } if user.NewPassword != user.NewPasswordConfirm { return response.BadRequest(c, "password confirmation not match") @@ -220,14 +248,14 @@ func (ctr UserControllerImpl) ResetPassword(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+resetErr.Error()) + return response.Error(c, constants.ServerErr+resetErr.Error()) } message := "your password already updated, you can login with your new password, thank you" return response.CreateResponse(c, fiber.StatusAccepted, true, message, nil) } -func (ctr UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { userClaims, ok := c.Locals("claims").(*middleware.Claims) if !ok || userClaims == nil { return response.Unauthorized(c) @@ -235,13 +263,13 @@ func (ctr UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { var user model.UserPasswordUpdate if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } user.ID = userClaims.ID validate := validator.New() if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } if user.NewPassword != user.NewPasswordConfirm { return response.BadRequest(c, "new password confirmation is wrong") @@ -257,13 +285,13 @@ func (ctr UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+updateErr.Error()) + return response.Error(c, constants.ServerErr+updateErr.Error()) } return response.SuccessNoContent(c) } -func (ctr UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { userClaims, ok := c.Locals("claims").(*middleware.Claims) if !ok || userClaims == nil { return response.Unauthorized(c) @@ -271,12 +299,12 @@ func (ctr UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { var user model.UserProfileUpdate if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } user.ID = userClaims.ID validate := validator.New() if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -286,13 +314,13 @@ func (ctr UserControllerImpl) UpdateProfile(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+updateErr.Error()) + return response.Error(c, constants.ServerErr+updateErr.Error()) } return response.SuccessNoContent(c) } -func (ctr UserControllerImpl) MyProfile(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) @@ -305,7 +333,7 @@ func (ctr UserControllerImpl) MyProfile(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+getErr.Error()) + return response.Error(c, constants.ServerErr+getErr.Error()) } return response.SuccessLoaded(c, userProfile) } diff --git a/controller/user/user_controller_test.go b/controller/user/user_controller_test.go index bd0ac8b..c107a85 100644 --- a/controller/user/user_controller_test.go +++ b/controller/user/user_controller_test.go @@ -10,18 +10,18 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "golang.org/x/text/cases" - "golang.org/x/text/language" "github.com/Lukmanern/gost/database/connector" "github.com/Lukmanern/gost/domain/entity" "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/constants" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" "github.com/Lukmanern/gost/internal/middleware" "github.com/Lukmanern/gost/internal/response" repository "github.com/Lukmanern/gost/repository/user" - rbacService "github.com/Lukmanern/gost/service/rbac" + permService "github.com/Lukmanern/gost/service/permission" + roleService "github.com/Lukmanern/gost/service/role" service "github.com/Lukmanern/gost/service/user" ) @@ -29,43 +29,43 @@ var ( userSvc service.UserService userCtr UserController userRepo repository.UserRepository - appUrl string + appURL string ) func init() { env.ReadConfig("./../../.env") config := env.Configuration() - appUrl = config.AppUrl + appURL = config.AppURL connector.LoadDatabase() - r := connector.LoadRedisDatabase() + r := connector.LoadRedisCache() r.FlushAll() // clear all key:value in redis - permService := rbacService.NewPermissionService() - roleService := rbacService.NewRoleService(permService) + permService := permService.NewPermissionService() + roleService := roleService.NewRoleService(permService) userSvc = service.NewUserService(roleService) userCtr = NewUserController(userSvc) userRepo = repository.NewUserRepository() } func TestNewUserController(t *testing.T) { - permService := rbacService.NewPermissionService() - roleService := rbacService.NewRoleService(permService) + permService := permService.NewPermissionService() + roleService := roleService.NewRoleService(permService) userService := service.NewUserService(roleService) userController := NewUserController(userService) if userController == nil || userService == nil || roleService == nil || permService == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } } -func Test_Register(t *testing.T) { +func TestRegister(t *testing.T) { // unaudit c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } c.Method(http.MethodPost) c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -192,12 +192,12 @@ func Test_Register(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(&tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + endp + url := appURL + endp req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) if httpReqErr != nil || req == nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -206,7 +206,7 @@ func Test_Register(t *testing.T) { req.Close = true resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -217,7 +217,7 @@ func Test_Register(t *testing.T) { respModel := response.Response{} decodeErr := json.NewDecoder(resp.Body).Decode(&respModel) if decodeErr != nil { - t.Error("should not error", decodeErr) + t.Error(constants.ShouldNotErr, decodeErr) } } @@ -226,7 +226,7 @@ func Test_Register(t *testing.T) { if getErr != nil || userByEmail == nil { t.Fatal("should success whilte create and get user") } - if userByEmail.Name != cases.Title(language.Und).String(tc.payload.Name) { + if userByEmail.Name != helper.ToTitle(tc.payload.Name) { t.Error("name should equal") } @@ -239,13 +239,13 @@ func Test_Register(t *testing.T) { } -func Test_AccountActivation(t *testing.T) { +func TestAccountActivation(t *testing.T) { // unaudit c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } c.Method(http.MethodPost) c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -313,12 +313,12 @@ func Test_AccountActivation(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(&tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + endp + url := appURL + endp req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) if httpReqErr != nil || req == nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -327,7 +327,7 @@ func Test_AccountActivation(t *testing.T) { req.Close = true resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -350,13 +350,13 @@ func Test_AccountActivation(t *testing.T) { } } -func Test_DeleteAccountActivation(t *testing.T) { +func TestDeleteAccountActivation(t *testing.T) { // unaudit c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } c.Method(http.MethodPost) c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -424,12 +424,12 @@ func Test_DeleteAccountActivation(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(&tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + endp + url := appURL + endp req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) if httpReqErr != nil || req == nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -438,7 +438,7 @@ func Test_DeleteAccountActivation(t *testing.T) { req.Close = true resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -462,13 +462,13 @@ func Test_DeleteAccountActivation(t *testing.T) { } } -func Test_ForgetPassword(t *testing.T) { +func TestForgetPassword(t *testing.T) { // unaudit c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } c.Method(http.MethodPost) c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -553,12 +553,12 @@ func Test_ForgetPassword(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(&tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + endp + url := appURL + endp req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) if httpReqErr != nil || req == nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -567,7 +567,7 @@ func Test_ForgetPassword(t *testing.T) { req.Close = true resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -576,13 +576,13 @@ func Test_ForgetPassword(t *testing.T) { } } -func Test_ResetPassword(t *testing.T) { +func TestResetPassword(t *testing.T) { // unaudit c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } c.Method(http.MethodPost) c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -610,7 +610,7 @@ func Test_ResetPassword(t *testing.T) { Email: userByID.Email, }) if verifyErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } // value reset @@ -629,7 +629,7 @@ func Test_ResetPassword(t *testing.T) { } forgetPassErr := userSvc.ForgetPassword(ctx, userForgetPasswd) if forgetPassErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } // value reset @@ -661,6 +661,7 @@ func Test_ResetPassword(t *testing.T) { caseName: "success reset password", respCode: http.StatusAccepted, payload: &model.UserResetPassword{ + Email: userByID.Email, Code: *userByID.VerificationCode, NewPassword: "newPassword", NewPasswordConfirm: "newPassword", @@ -670,6 +671,7 @@ func Test_ResetPassword(t *testing.T) { caseName: "failed reset password: password not match", respCode: http.StatusBadRequest, payload: &model.UserResetPassword{ + Email: userByID.Email, Code: *userByID.VerificationCode, NewPassword: "newPassword", NewPasswordConfirm: "newPasswordNotMatch", @@ -679,6 +681,7 @@ func Test_ResetPassword(t *testing.T) { caseName: "failed reset password: verification code too short", respCode: http.StatusBadRequest, payload: &model.UserResetPassword{ + Email: helper.RandomEmail(), Code: "short", NewPassword: "newPassword", NewPasswordConfirm: "newPasswordNotMatch", @@ -691,12 +694,12 @@ func Test_ResetPassword(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(&tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + endp + url := appURL + endp req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) if httpReqErr != nil || req == nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -705,7 +708,7 @@ func Test_ResetPassword(t *testing.T) { req.Close = true resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -726,13 +729,13 @@ func Test_ResetPassword(t *testing.T) { } } -func Test_Login(t *testing.T) { +func TestLogin(t *testing.T) { // unaudit c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } c.Method(http.MethodPost) c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -768,13 +771,13 @@ func Test_Login(t *testing.T) { // create active user createdActiveUser := entity.User{} func() { - createdUser_2 := model.UserRegister{ + createdUser2 := model.UserRegister{ Name: helper.RandomString(10), Email: helper.RandomEmail(), Password: helper.RandomString(10), RoleID: 1, // admin } - userID, createErr := userSvc.Register(ctx, createdUser_2) + userID, createErr := userSvc.Register(ctx, createdUser2) if createErr != nil || userID <= 0 { t.Fatal("should success create user, user failed to create") } @@ -793,7 +796,7 @@ func Test_Login(t *testing.T) { Email: userByID.Email, }) if verifyErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } userByID = nil userByID, getErr = userRepo.GetByID(ctx, userID) @@ -802,7 +805,7 @@ func Test_Login(t *testing.T) { } createdActiveUser = *userByID - createdActiveUser.Password = createdUser_2.Password + createdActiveUser.Password = createdUser2.Password }() defer userRepo.Delete(ctx, createdActiveUser.ID) @@ -891,12 +894,12 @@ func Test_Login(t *testing.T) { log.Println(":::::::" + tc.caseName) jsonObject, err := json.Marshal(&tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + endp + url := appURL + endp req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) if httpReqErr != nil || req == nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -905,7 +908,7 @@ func Test_Login(t *testing.T) { req.Close = true resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { @@ -932,12 +935,12 @@ func Test_Login(t *testing.T) { log.Println(":::::::" + testCase.caseName) jsonObject, err := json.Marshal(&testCase.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + endp + url := appURL + endp req, httpReqErr := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonObject)) if httpReqErr != nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -946,7 +949,7 @@ func Test_Login(t *testing.T) { req.Close = true resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != testCase.respCode { @@ -954,9 +957,9 @@ func Test_Login(t *testing.T) { } } - redis := connector.LoadRedisDatabase() + redis := connector.LoadRedisCache() if redis == nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } value := redis.Get("failed-login-" + clientIP).Val() if value != "5" { @@ -964,13 +967,13 @@ func Test_Login(t *testing.T) { } } -func Test_Logout(t *testing.T) { +func TestLogout(t *testing.T) { // unaudit c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } // create inactive user @@ -998,7 +1001,7 @@ func Test_Logout(t *testing.T) { Email: userByID.Email, }) if verifyErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } userByID = nil userByID, getErr = userRepo.GetByID(ctx, userID) @@ -1051,7 +1054,7 @@ func Test_Logout(t *testing.T) { jwtHandler := middleware.NewJWTHandler() for _, tc := range testCases { c := helper.NewFiberCtx() - c.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", userToken)) + 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 { @@ -1083,12 +1086,12 @@ func Test_Logout(t *testing.T) { } } -func Test_UpdatePassword(t *testing.T) { +func TestUpdatePassword(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } // create inactive user @@ -1116,7 +1119,7 @@ func Test_UpdatePassword(t *testing.T) { Email: userByID.Email, }) if verifyErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } userByID = nil userByID, getErr = userRepo.GetByID(ctx, userID) @@ -1200,7 +1203,7 @@ func Test_UpdatePassword(t *testing.T) { jwtHandler := middleware.NewJWTHandler() for _, tc := range testCases { c := helper.NewFiberCtx() - c.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", userToken)) + 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) @@ -1232,13 +1235,13 @@ func Test_UpdatePassword(t *testing.T) { } } -func Test_UpdateProfile(t *testing.T) { +func TestUpdateProfile(t *testing.T) { // unaudit c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } // create inactive user @@ -1266,7 +1269,7 @@ func Test_UpdateProfile(t *testing.T) { Email: userByID.Email, }) if verifyErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } userByID = nil userByID, getErr = userRepo.GetByID(ctx, userID) @@ -1336,7 +1339,7 @@ func Test_UpdateProfile(t *testing.T) { jwtHandler := middleware.NewJWTHandler() for _, tc := range testCases { c := helper.NewFiberCtx() - c.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", userToken)) + 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) @@ -1358,22 +1361,22 @@ func Test_UpdateProfile(t *testing.T) { if resp.StatusCode() == http.StatusNoContent { userByID, err := userRepo.GetByID(ctx, userID) if err != nil || userByID == nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } - if userByID.Name != cases.Title(language.Und).String(tc.payload.Name) { + if userByID.Name != helper.ToTitle(tc.payload.Name) { t.Error("shoudl equal") } } } } -func Test_MyProfile(t *testing.T) { +func TestMyProfile(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() ctr := userCtr if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } // create inactive user @@ -1401,7 +1404,7 @@ func Test_MyProfile(t *testing.T) { Email: userByID.Email, }) if verifyErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } userByID = nil userByID, getErr = userRepo.GetByID(ctx, userID) @@ -1454,7 +1457,7 @@ func Test_MyProfile(t *testing.T) { jwtHandler := middleware.NewJWTHandler() for _, tc := range testCases { c := helper.NewFiberCtx() - c.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", userToken)) + 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 { diff --git a/controller/user_management/user_management_controller.go b/controller/user_management/user_management_controller.go index 5b9e20d..8032c72 100644 --- a/controller/user_management/user_management_controller.go +++ b/controller/user_management/user_management_controller.go @@ -13,15 +13,25 @@ import ( "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 } @@ -35,15 +45,15 @@ func NewUserManagementController(userService service.UserManagementService) User } } -func (ctr UserManagementControllerImpl) Create(c *fiber.Ctx) error { +func (ctr *UserManagementControllerImpl) Create(c *fiber.Ctx) error { var user model.UserCreate if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + 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, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -53,7 +63,7 @@ func (ctr UserManagementControllerImpl) Create(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+createErr.Error()) + return response.Error(c, constants.ServerErr+createErr.Error()) } data := map[string]any{ "id": id, @@ -61,10 +71,10 @@ func (ctr UserManagementControllerImpl) Create(c *fiber.Ctx) error { return response.SuccessCreated(c, data) } -func (ctr UserManagementControllerImpl) Get(c *fiber.Ctx) error { +func (ctr *UserManagementControllerImpl) Get(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil || id <= 0 { - return response.BadRequest(c, "invalid id") + return response.BadRequest(c, constants.InvalidID) } ctx := c.Context() @@ -74,12 +84,12 @@ func (ctr UserManagementControllerImpl) Get(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+getErr.Error()) + return response.Error(c, constants.ServerErr+getErr.Error()) } return response.SuccessLoaded(c, userProfile) } -func (ctr UserManagementControllerImpl) GetAll(c *fiber.Ctx) error { +func (ctr *UserManagementControllerImpl) GetAll(c *fiber.Ctx) error { request := base.RequestGetAll{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 20), @@ -93,7 +103,7 @@ func (ctr UserManagementControllerImpl) GetAll(c *fiber.Ctx) error { ctx := c.Context() users, total, getErr := ctr.service.GetAll(ctx, request) if getErr != nil { - return response.Error(c, "internal server error: "+getErr.Error()) + return response.Error(c, constants.ServerErr+getErr.Error()) } data := make([]interface{}, len(users)) @@ -111,19 +121,19 @@ func (ctr UserManagementControllerImpl) GetAll(c *fiber.Ctx) error { return response.SuccessLoaded(c, responseData) } -func (ctr UserManagementControllerImpl) Update(c *fiber.Ctx) error { +func (ctr *UserManagementControllerImpl) Update(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil || id <= 0 { - return response.BadRequest(c, "invalid id") + return response.BadRequest(c, constants.InvalidID) } var user model.UserProfileUpdate user.ID = id if err := c.BodyParser(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } validate := validator.New() if err := validate.Struct(&user); err != nil { - return response.BadRequest(c, "invalid json body: "+err.Error()) + return response.BadRequest(c, constants.InvalidBody+err.Error()) } ctx := c.Context() @@ -133,15 +143,15 @@ func (ctr UserManagementControllerImpl) Update(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+updateErr.Error()) + return response.Error(c, constants.ServerErr+updateErr.Error()) } return response.SuccessNoContent(c) } -func (ctr UserManagementControllerImpl) Delete(c *fiber.Ctx) error { +func (ctr *UserManagementControllerImpl) Delete(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil || id <= 0 { - return response.BadRequest(c, "invalid id") + return response.BadRequest(c, constants.InvalidID) } ctx := c.Context() @@ -151,7 +161,7 @@ func (ctr UserManagementControllerImpl) Delete(c *fiber.Ctx) error { if ok { return response.CreateResponse(c, fiberErr.Code, false, fiberErr.Message, nil) } - return response.Error(c, "internal server error: "+deleteErr.Error()) + 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 index 26353f5..aa5fd96 100644 --- a/controller/user_management/user_management_controller_test.go +++ b/controller/user_management/user_management_controller_test.go @@ -15,11 +15,10 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "golang.org/x/text/cases" - "golang.org/x/text/language" "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" @@ -31,27 +30,27 @@ import ( var ( userDevService service.UserManagementService userDevController controller.UserManagementController - appUrl string + appURL string ) func init() { env.ReadConfig("./../../.env") config := env.Configuration() - appUrl = config.AppUrl + appURL = config.AppURL connector.LoadDatabase() - r := connector.LoadRedisDatabase() + r := connector.LoadRedisCache() r.FlushAll() // clear all key:value in redis userDevService = service.NewUserManagementService() userDevController = controller.NewUserManagementController(userDevService) } -func Test_Create(t *testing.T) { +func TestCreate(t *testing.T) { c := helper.NewFiberCtx() ctr := userDevController if ctr == nil || c == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } c.Method(http.MethodPost) c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -148,13 +147,13 @@ func Test_Create(t *testing.T) { for _, tc := range testCases { jsonObject, marshalErr := json.Marshal(&tc.payload) if marshalErr != nil { - t.Error("should not error", marshalErr.Error()) + t.Error(constants.ShouldNotErr, marshalErr.Error()) } c.Request().SetBody(jsonObject) createErr := ctr.Create(c) if createErr != nil { - t.Error("should not erro", createErr) + t.Error(constants.ShouldNotErr, createErr) } else if tc.payload == nil { continue } @@ -168,25 +167,25 @@ func Test_Create(t *testing.T) { } if !tc.wantErr { if userByEMail == nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } else { deleteErr := userDevService.Delete(ctx, userByEMail.ID) if deleteErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } } - if userByEMail.Name != cases.Title(language.Und).String(tc.payload.Name) { - t.Error("should equal") + if userByEMail.Name != helper.ToTitle(tc.payload.Name) { + t.Error(constants.ShouldEqual) } } } } -func Test_Get(t *testing.T) { +func TestGet(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() if c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } createdUser := model.UserCreate{ @@ -250,34 +249,34 @@ func Test_Get(t *testing.T) { app.Get("/user-management/:id", userDevController.Get) resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { - t.Error("should equal") + t.Error(constants.ShouldEqual) } if !tc.wantErr { respModel := response.Response{} decodeErr := json.NewDecoder(resp.Body).Decode(&respModel) if decodeErr != nil { - t.Error("should not error", decodeErr) + t.Error(constants.ShouldNotErr, decodeErr) } if tc.response.Message != respModel.Message && tc.response.Message != "" { - t.Error("should equal") + t.Error(constants.ShouldEqual) } if respModel.Success != tc.response.Success { - t.Error("should equal") + t.Error(constants.ShouldEqual) } } } } -func Test_GetAll(t *testing.T) { +func TestGetAll(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() if c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } userIDs := make([]int, 0) @@ -331,21 +330,21 @@ func Test_GetAll(t *testing.T) { app.Get("/user-management", userDevController.GetAll) resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error", err.Error()) + t.Fatal(constants.ShouldNotErr, err.Error()) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { - t.Error("should equal") + t.Error(constants.ShouldEqual) } if !tc.wantErr { body := response.Response{} bytes, err := io.ReadAll(resp.Body) if err != nil { - t.Fatal("should not error", err.Error()) + t.Fatal(constants.ShouldNotErr, err.Error()) } err = json.Unmarshal(bytes, &body) if err != nil { - t.Fatal("should not error", err.Error()) + t.Fatal(constants.ShouldNotErr, err.Error()) } if !body.Success { t.Fatal("should be success") @@ -357,12 +356,12 @@ func Test_GetAll(t *testing.T) { } } -func Test_Update(t *testing.T) { +func TestUpdate(t *testing.T) { c := helper.NewFiberCtx() ctr := userDevController ctx := c.Context() if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } c.Method(http.MethodPut) c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -444,12 +443,12 @@ func Test_Update(t *testing.T) { log.Println(tc.caseName) jsonObject, err := json.Marshal(&tc.payload) if err != nil { - t.Error("should not error", err.Error()) + t.Error(constants.ShouldNotErr, err.Error()) } - url := appUrl + "user-management/" + strconv.Itoa(tc.payload.ID) + 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("should not nil") + t.Fatal(constants.ShouldNotNil) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -458,28 +457,28 @@ func Test_Update(t *testing.T) { req.Close = true resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { - t.Error("should equal", resp.StatusCode) + 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("should not error", decodeErr) + t.Error(constants.ShouldNotErr, decodeErr) } } } } -func Test_Delete(t *testing.T) { +func TestDelete(t *testing.T) { c := helper.NewFiberCtx() ctr := userDevController ctx := c.Context() if ctr == nil || c == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } c.Method(http.MethodPut) c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -528,10 +527,10 @@ func Test_Delete(t *testing.T) { for _, tc := range testCases { log.Println(tc.caseName) - url := appUrl + "user-management/" + strconv.Itoa(tc.paramID) + url := appURL + "user-management/" + strconv.Itoa(tc.paramID) req, httpReqErr := http.NewRequest(http.MethodDelete, url, nil) if httpReqErr != nil || req == nil { - t.Fatal("should not nil") + t.Fatal(constants.ShouldNotNil) } req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) @@ -540,11 +539,11 @@ func Test_Delete(t *testing.T) { req.Close = true resp, err := app.Test(req, -1) if err != nil { - t.Fatal("should not error") + t.Fatal(constants.ShouldNotErr) } defer resp.Body.Close() if resp.StatusCode != tc.respCode { - t.Error("should equal", resp.StatusCode) + t.Error(constants.ShouldEqual, resp.StatusCode) } } diff --git a/database/connector/connector.go b/database/connector/connector.go index 89b8baa..2cc6df2 100644 --- a/database/connector/connector.go +++ b/database/connector/connector.go @@ -19,7 +19,9 @@ var ( redisDatastoreOnce sync.Once ) -// SQL Database +// LoadDatabase func read env intenal package and +// give database connection using gorm package, +// also pings the DB before the function end. func LoadDatabase() *gorm.DB { gormDatabaseOnce.Do(func() { // try to read env @@ -60,8 +62,10 @@ func LoadDatabase() *gorm.DB { return gormDatabase } -// Redis -func LoadRedisDatabase() *redis.Client { +// LoadRedisCache func read env intenal package and +// give redis connection using redis external package, +// also pings the DB before the function end. +func LoadRedisCache() *redis.Client { redisDatastoreOnce.Do(func() { env.ReadConfig("./.env") config := env.Configuration() diff --git a/database/connector/connector_test.go b/database/connector/connector_test.go index c08e071..ed9aa3b 100644 --- a/database/connector/connector_test.go +++ b/database/connector/connector_test.go @@ -18,8 +18,8 @@ func TestLoadDatabase(t *testing.T) { } } -func TestLoadRedisDatabase(t *testing.T) { - rds := LoadRedisDatabase() +func TestLoadRedisCache(t *testing.T) { + rds := LoadRedisCache() if rds == nil { t.Error("rds shouldn't nil") } diff --git a/database/migration/main.go b/database/migration/main.go index 05ddb40..66fa850 100644 --- a/database/migration/main.go +++ b/database/migration/main.go @@ -22,13 +22,14 @@ func setup() { config = env.Configuration() } -// be careful using this -// this will delete entire DB tables, -// and recreate from the beginning +// ⚠️ Do not run this on production. +// ⚠️ Warning: This script will drop all tables in Database. +// This script is designed to perform a complete reset of the database, +// which involves the deletion of all existing tables and recreating them from scratch. func main() { setup() log.Println("Start Migration") - defer log.Println("Finish Migration: success no error") + defer log.Println("Finish Migration: success with no error") // delete all tables if not in production if !config.GetAppInProduction() { @@ -61,6 +62,7 @@ func main() { } } +// dropAll func drops all tables that listed in entity.AllTables() func dropAll() { log.Println("Warning: dropping all tables in 9 seconds (CTRL+C to stop)") time.Sleep(10 * time.Second) @@ -71,6 +73,9 @@ func dropAll() { } } +// seeding func seed data like role, permission, +// and role_has_permissions tables. You can add +// more seed if you want. func seeding() { // Create a new transaction for seeding tx := db.Begin() @@ -86,7 +91,7 @@ func seeding() { } } for _, perm := range rbac.AllPermissions() { - perm.SetCreateTimes() + perm.SetCreateTime() perm.ID = 0 if createErr := tx.Create(&perm).Error; createErr != nil { tx.Rollback() diff --git a/devnote.md b/devnote.md deleted file mode 100644 index 2b566f1..0000000 --- a/devnote.md +++ /dev/null @@ -1,16 +0,0 @@ -# DevNote - -### Todo / OnDev - -1. Add new feature file management (upload and download) - -### Read List - -1. Database connection conf : https://www.alexedwards.net/blog/configuring-sqldb -2. GoFiber Test https://dev.to/koddr/go-fiber-by-examples-testing-the-application-1ldf -3. https://aleksei-kornev.medium.com/production-readiness-checklist-for-backend-applications-8d2b0c57ccec/ -4. https://last9.io/blog/deployment-readiness-checklists/ -5. https://github.com/gorrion-io/production-readiness-checklist/ -6. https://www.cockroachlabs.com/docs/cockroachcloud/production-checklist/ -7. https://www.alexedwards.net/blog/ci-with-go-and-github-actions -8. https://roadmap.sh/best-practices/api-security/ diff --git a/docs/Gost Project Docs.postman_collection.json b/docs/Gost Project Docs.postman_collection.json new file mode 100644 index 0000000..ee57c4e --- /dev/null +++ b/docs/Gost Project Docs.postman_collection.json @@ -0,0 +1,1419 @@ +{ + "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/domain/base/request.go b/domain/base/request.go index bd552ef..fad61e6 100644 --- a/domain/base/request.go +++ b/domain/base/request.go @@ -1,5 +1,6 @@ package base +// RequestGetAll struct used for request getAll controller funcs type RequestGetAll struct { Page int `query:"page"` Limit int `query:"limit"` diff --git a/domain/base/response.go b/domain/base/response.go index 628108c..e6f4717 100644 --- a/domain/base/response.go +++ b/domain/base/response.go @@ -6,6 +6,7 @@ type PageMeta struct { 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.go b/domain/base/time.go index e7f7909..acbe6f2 100644 --- a/domain/base/time.go +++ b/domain/base/time.go @@ -2,17 +2,25 @@ package base import "time" +// TimeFields struct used by almost all entity. +// This struct give stabillity for creating more and more entity/ struct/ table. +// This struct prevents developers from making typing errors / typo. 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"` } -func (att *TimeFields) SetCreateTimes() { +// SetCreateTime func fills created_at and updated_at fields +// This struct prevents developers from forgets or any common mistake. +func (att *TimeFields) SetCreateTime() { timeNow := time.Now() att.CreatedAt = &timeNow att.UpdatedAt = &timeNow } +// SetUpdateTime func fills updated_at fields +// This struct prevents developers from forgets +// or any common mistake. func (att *TimeFields) SetUpdateTime() { timeNow := time.Now() att.UpdatedAt = &timeNow diff --git a/domain/base/time_test.go b/domain/base/time_test.go index 5016179..73354b5 100644 --- a/domain/base/time_test.go +++ b/domain/base/time_test.go @@ -2,10 +2,9 @@ package base import ( "testing" - "time" ) -func TestTimeFields_SetCreateTimes(t *testing.T) { +func TestTimeFields_SetCreateTime(t *testing.T) { tests := []struct { name string att *TimeFields @@ -18,14 +17,10 @@ func TestTimeFields_SetCreateTimes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.att.SetCreateTimes() - currentTime := time.Now() + 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") } - if !tt.att.CreatedAt.Equal(currentTime) || !tt.att.UpdatedAt.Equal(currentTime) { - t.Errorf("Expected CreatedAt and UpdatedAt to be equal to the current time") - } }) } } @@ -44,13 +39,9 @@ func TestTimeFields_SetUpdateTime(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.att.SetUpdateTime() - currentTime := time.Now() if tt.att.UpdatedAt == nil { t.Errorf("Expected UpdatedAt to be set, but it is nil") } - if !tt.att.UpdatedAt.Equal(currentTime) { - t.Errorf("Expected UpdatedAt to be equal to the current time") - } }) } } diff --git a/domain/entity/all_entities.go b/domain/entity/all_entities.go index 848eb95..0105969 100644 --- a/domain/entity/all_entities.go +++ b/domain/entity/all_entities.go @@ -1,9 +1,22 @@ +// ⚠️ Don't forget to Add Your new +// Table Here : must sorted by developer. + +// Package entity contains all the structs that will be used to build tables in the database. package entity -// Don't forget to Add Your new -// Table Here : must sorted by developer. -func AllTables() []interface{} { - return []any{ +import "log" + +// Table interface is contract that make developer +// not forget to add TableName method for struct +type Table interface { + TableName() string +} + +// 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{}, @@ -13,4 +26,11 @@ func AllTables() []interface{} { // ... // Add more tables/structs } + for _, table := range allTables { + _, ok := table.(Table) + if !ok { + log.Fatal("please add TableName() func to all structs") + } + } + return allTables } diff --git a/domain/entity/all_entities_test.go b/domain/entity/all_entities_test.go index 3da956f..4aac3e5 100644 --- a/domain/entity/all_entities_test.go +++ b/domain/entity/all_entities_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/Lukmanern/gost/domain/base" + "github.com/Lukmanern/gost/internal/constants" ) func TestAllTablesName(t *testing.T) { @@ -39,9 +40,9 @@ func TestUserSetActivateAccount(t *testing.T) { user.SetActivateAccount() if user.VerificationCode != nil { - t.Error("should nil") + t.Error(constants.ShouldNil) } if user.ActivatedAt == nil { - t.Error("should nil") + t.Error(constants.ShouldNil) } } diff --git a/domain/entity/rbac.go b/domain/entity/role_permission.go similarity index 90% rename from domain/entity/rbac.go rename to domain/entity/role_permission.go index 6042aca..cacd618 100644 --- a/domain/entity/rbac.go +++ b/domain/entity/role_permission.go @@ -1,3 +1,6 @@ +// ⚠️ 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" diff --git a/domain/entity/user.go b/domain/entity/user.go index 5e304f3..fb7b47c 100644 --- a/domain/entity/user.go +++ b/domain/entity/user.go @@ -1,3 +1,6 @@ +// ⚠️ Don't forget to Add Your new Table +// at AllTables func at all_entities.go file + package entity import ( diff --git a/domain/model/rbac.go b/domain/model/role_permission.go similarity index 95% rename from domain/model/rbac.go rename to domain/model/role_permission.go index a327f5a..fd23d2f 100644 --- a/domain/model/rbac.go +++ b/domain/model/role_permission.go @@ -1,6 +1,5 @@ package model -// Role type RoleCreate struct { Name string `validate:"required,min=1,max=60" json:"name"` Description string `validate:"required,min=1,max=100" json:"description"` @@ -24,7 +23,6 @@ type RoleConnectToPermissions struct { PermissionsID []int `validate:"required" json:"permissions_id"` } -// Permission type PermissionCreate struct { Name string `validate:"required,min=1,max=60" json:"name"` Description string `validate:"required,min=1,max=100" json:"description"` diff --git a/domain/model/user.go b/domain/model/user.go index 589bf33..80f0457 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -25,6 +25,7 @@ type UserForgetPassword struct { } type UserResetPassword struct { + Email string `validate:"required,email,min=5,max=60" json:"email"` Code string `validate:"required,min=21,max=60" json:"code"` NewPassword string `validate:"required,min=8,max=30" json:"new_password"` NewPasswordConfirm string `validate:"required,min=8,max=30" json:"new_password_confirm"` diff --git a/go.mod b/go.mod index 2ef8537..23baf07 100644 --- a/go.mod +++ b/go.mod @@ -3,34 +3,26 @@ module github.com/Lukmanern/gost go 1.20 require ( + github.com/go-playground/validator/v10 v10.15.5 github.com/go-redis/redis v6.15.9+incompatible github.com/gofiber/fiber/v2 v2.50.0 - github.com/ilyakaznacheev/cleanenv v1.5.0 - gorm.io/gorm v1.25.4 -) - -require ( - github.com/go-playground/validator/v10 v10.15.5 github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/stretchr/testify v1.8.4 github.com/valyala/fasthttp v1.50.0 golang.org/x/crypto v0.14.0 - gorm.io/driver/postgres v1.5.3 -) - -require ( - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - golang.org/x/net v0.17.0 // indirect golang.org/x/text v0.13.0 + gorm.io/driver/postgres v1.5.3 + gorm.io/gorm v1.25.4 ) require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/google/uuid v1.3.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -40,6 +32,7 @@ require ( github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -50,6 +43,7 @@ require ( github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect diff --git a/internal/constants/constants.go b/internal/constants/constants.go new file mode 100644 index 0000000..94ab46a --- /dev/null +++ b/internal/constants/constants.go @@ -0,0 +1,19 @@ +// 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/env/env.go b/internal/env/env.go index 685e844..b722255 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -14,11 +14,10 @@ import ( type Config struct { AppName string `env:"APP_NAME"` AppInProduction bool `env:"APP_IN_PRODUCTION"` - AppKey string `env:"APP_SECRET_KEY"` AppAccessTokenTTL time.Duration `env:"APP_ACCESS_TOKEN_TTL"` AppPort int `env:"APP_PORT"` AppTimeZone string `env:"APP_TIME_ZONE"` - AppUrl string + AppURL string DatabaseHost string `env:"DB_HOST"` DatabasePort string `env:"DB_PORT"` @@ -57,6 +56,7 @@ var ( paths = []string{"", "./..", "./../..", "./../../.."} ) +// ReadConfig func check .env file and read the value. func ReadConfig(filePath string) *Config { cfgOnce.Do(func() { if _, err := os.Stat(filePath); os.IsNotExist(err) { @@ -75,6 +75,8 @@ func ReadConfig(filePath string) *Config { return &cfg } +// Configuration func set and update .env file +// and returning config it self. func Configuration() Config { if envFile == nil { log.Panic(`configuration file is not set. Call ReadConfig("path_to_file") first`) @@ -83,10 +85,11 @@ func Configuration() Config { if err != nil { log.Fatalf("Config error %s", err.Error()) } - cfg.setAppUrl() + cfg.setAppURL() return cfg } +// GetDatabaseURI func return string URI of database. func (c *Config) GetDatabaseURI() string { c.DatabaseURI = fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=%s", c.DatabaseHost, c.DatabaseUser, c.DatabasePassword, c.DatabaseName, c.DatabasePort, c.AppTimeZone, @@ -95,10 +98,19 @@ func (c *Config) GetDatabaseURI() string { return c.DatabaseURI } +// GetAppInProduction func return application condition is +// under development or production ready. func (c *Config) GetAppInProduction() bool { return c.AppInProduction } +// setAppURL func combine set AppURL with localhost and port. +func (c *Config) setAppURL() { + localAddr := fmt.Sprintf("http://127.0.0.1:%d/", c.AppPort) + c.AppURL = localAddr +} + +// GetPublicKey func gets PublicKey values. func (c *Config) GetPublicKey() []byte { PublicKeyReadOne.Do(func() { var foundPath string @@ -121,6 +133,7 @@ func (c *Config) GetPublicKey() []byte { return *PublicKey } +// GetPrivateKey func gets PrivateKey values. func (c *Config) GetPrivateKey() []byte { PrivateKeyReadOne.Do(func() { var foundPath string @@ -143,19 +156,14 @@ func (c *Config) GetPrivateKey() []byte { return *PrivateKey } -func (c *Config) setAppUrl() { - localAddr := fmt.Sprintf("http://127.0.0.1:%d/", c.AppPort) - c.AppUrl = localAddr -} - +// ShowConfig prints all fields that Config struct has func (c *Config) ShowConfig() { fmt.Printf("%-21s: %s\n", "AppName", c.AppName) fmt.Printf("%-21s: %v\n", "AppInProduction", c.AppInProduction) - fmt.Printf("%-21s: %s\n", "AppKey", c.AppKey) fmt.Printf("%-21s: %s\n", "AppAccessTokenTTL", c.AppAccessTokenTTL) fmt.Printf("%-21s: %d\n", "AppPort", c.AppPort) fmt.Printf("%-21s: %s\n", "AppTimeZone", c.AppTimeZone) - fmt.Printf("%-21s: %s\n", "AppUrl", c.AppUrl) + fmt.Printf("%-21s: %s\n", "AppURL", c.AppURL) fmt.Printf("%-21s: %s\n", "DatabaseHost", c.DatabaseHost) fmt.Printf("%-21s: %s\n", "DatabasePort", c.DatabasePort) @@ -174,4 +182,7 @@ func (c *Config) ShowConfig() { fmt.Printf("%-21s: %s\n", "SMTPEmail", c.SMTPEmail) fmt.Printf("%-21s: %s\n", "SMTPPassword", c.SMTPPassword) fmt.Printf("%-21s: %s\n", "ClientURL", c.ClientURL) + + // ... + // add more } diff --git a/internal/env/env_test.go b/internal/env/env_test.go index 22bf8fa..c627a4e 100644 --- a/internal/env/env_test.go +++ b/internal/env/env_test.go @@ -3,6 +3,8 @@ package env import ( "net/url" "testing" + + "github.com/stretchr/testify/assert" ) var ( @@ -14,74 +16,39 @@ func TestReadConfigAndConfiguration(t *testing.T) { ReadConfig(validPath) c := Configuration() - if c == config { - t.Error("Expected configuration to be initialized, but it is null") - } - - if c.AppKey == "" { - t.Error("AppKey is empty; it should have a valid value") - } - - if c.AppPort < 1 { - t.Error("AppPort is less than 1; it should be a positive integer") - } - - if c.DatabasePort == "" { - t.Error("DatabasePort is empty; it should have a valid value") - } - - if c.PublicKey == "" { - t.Error("PublicKey is empty; it should have a valid value") - } - - if c.PrivateKey == "" { - t.Error("PrivateKey is empty; it should have a valid value") - } - - if c.RedisURI == "" { - t.Error("RedisURI is empty; it should have a valid value") - } - - if c.SMTPPort < 1 { - t.Error("SMTPPort is less than 1; it should be a positive integer") - } + assert.NotEqual(t, c, config, "Expected configuration to be initialized, but it is null") + assert.True(t, c.AppPort >= 1, "AppPort is less than 1; it should be a positive integer") + assert.NotEmpty(t, c.DatabasePort, "DatabasePort is empty; it should have a valid value") + assert.NotEmpty(t, c.PublicKey, "PublicKey is empty; it should have a valid value") + assert.NotEmpty(t, c.PrivateKey, "PrivateKey is empty; it should have a valid value") + assert.NotEmpty(t, c.RedisURI, "RedisURI is empty; it should have a valid value") + assert.True(t, c.SMTPPort >= 1, "SMTPPort is less than 1; it should be a positive integer") } -func TestConfig_GetPublicKeyAndGetPrivateKey(t *testing.T) { +func TestConfigGetPublicKeyAndGetPrivateKey(t *testing.T) { defer func() { r := recover() - if r != nil { - t.Error("should not panic") - } + assert.Nil(t, r, "should not panic") }() ReadConfig(validPath) c := Configuration() - dbUri := c.GetDatabaseURI() - if len(dbUri) < 1 { - t.Error("should more long") - } + rawDbURI := "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=%s" + dbURI := c.GetDatabaseURI() + assert.True(t, len(dbURI) >= len(rawDbURI), "should be more long") isProd := c.GetAppInProduction() - if c.AppInProduction != isProd { - t.Error("Expected AppInProduction to match the configuration, but it doesn't") - } + assert.Equal(t, c.AppInProduction, isProd, "Expected AppInProduction to match the configuration, but it doesn't") pubKey := c.GetPublicKey() - if pubKey == nil { - t.Error("Public Key should not be nil") - } + assert.NotNil(t, pubKey, "Public Key should not be nil") privKey := c.GetPrivateKey() - if privKey == nil { - t.Error("Private Key should not be nil") - } + assert.NotNil(t, privKey, "Private Key should not be nil") c.ShowConfig() - c.setAppUrl() - _, parseUrlErr := url.Parse(c.AppUrl) - if parseUrlErr != nil { - t.Error("should not error while parsing url") - } + c.setAppURL() + _, parseURLErr := url.Parse(c.AppURL) + assert.NoError(t, parseURLErr, "should not error while parsing url") } diff --git a/internal/hash/hash_test.go b/internal/hash/hash_test.go index 8cc1cdc..9547614 100644 --- a/internal/hash/hash_test.go +++ b/internal/hash/hash_test.go @@ -2,6 +2,8 @@ package hash import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestGenerate(t *testing.T) { @@ -17,9 +19,7 @@ func TestGenerate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := Generate(tt.data) - if (err != nil) != tt.wantErr { - t.Errorf("Generate() error = %v, wantErr %v", err, tt.wantErr) - } + assert.Equal(t, (err != nil), tt.wantErr, "Generate() error") }) } } @@ -43,12 +43,8 @@ func TestVerify(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Verify(tt.hashedPassword, tt.password) - if (err != nil) != tt.wantErr { - t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr) - } - if got != tt.want { - t.Errorf("Verify() = %v, want %v", got, tt.want) - } + assert.Equal(t, (err != nil), tt.wantErr, "Verify() error") + assert.Equal(t, got, tt.want, "Verify() result") }) } } diff --git a/internal/helper/helper.go b/internal/helper/helper.go index b3779c6..ac7fa44 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -10,10 +10,16 @@ import ( "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) +// RandomString func generate random string +// used for testing and any needs. func RandomString(n uint) string { - letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + letterBytes := "abcdefghijklmnopqrstuvwxyz" + letterBytes += "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + letterBytes += "1234567890" b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] @@ -21,12 +27,14 @@ 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] += 1 + emailsMap[randEmail]++ } emails := make([]string, 0, len(emailsMap)) @@ -36,18 +44,16 @@ func RandomEmails(n uint) []string { return emails } +// RandomEmail func return a email +// used for testing and any needs. func RandomEmail() string { body := strings.ToLower(RandomString(7) + RandomString(7) + RandomString(7)) randEmail := body + "@gost.project" return randEmail } -// This used for testing handler : controller/ middleware/ any -func NewFiberCtx() *fiber.Ctx { - app := fiber.New() - return app.AcquireCtx(&fasthttp.RequestCtx{}) -} - +// RandomIPAddress func return a IP Address +// used for testing and any needs. func RandomIPAddress() string { source := rand.NewSource(time.Now().UnixNano()) rng := rand.New(source) @@ -60,6 +66,7 @@ func RandomIPAddress() string { return ip.String() } +// ValidateEmails func validates emails func ValidateEmails(emails ...string) error { for _, email := range emails { _, err := mail.ParseAddress(email) @@ -69,3 +76,16 @@ func ValidateEmails(emails ...string) error { } return nil } + +// NewFiberCtx func create new fiber.Ctx used for testing +// handler like controller and middleware. +func NewFiberCtx() *fiber.Ctx { + app := fiber.New() + return app.AcquireCtx(&fasthttp.RequestCtx{}) +} + +// ToTitle func make string to Title Case +// Example : Your name => Your Name +func ToTitle(s string) string { + return cases.Title(language.Und).String(s) +} diff --git a/internal/helper/helper_test.go b/internal/helper/helper_test.go index 2e8efc5..2ec1c68 100644 --- a/internal/helper/helper_test.go +++ b/internal/helper/helper_test.go @@ -4,30 +4,25 @@ import ( "net" "strings" "testing" + + "github.com/Lukmanern/gost/internal/constants" + "github.com/stretchr/testify/assert" ) func TestRandomString(t *testing.T) { for i := 0; i < 25; i++ { s := RandomString(uint(i)) - if len(s) != i { - t.Error("len of string should equal") - } + assert.Len(t, s, i, "length of string should equal") } } func TestRandomEmails(t *testing.T) { for i := 1; i <= 20; i++ { emails := RandomEmails(uint(i)) - if len(emails) != i { - t.Error("total of emails should equal") - } + assert.Len(t, emails, i, "total of emails should equal") for _, email := range emails { - if len(email) < 10 { - t.Error("length of an email should not less than 10") - } - if email != strings.ToLower(email) { - t.Error("email should lower by results") - } + 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") } } } @@ -35,20 +30,8 @@ func TestRandomEmails(t *testing.T) { func TestRandomEmail(t *testing.T) { for i := 0; i < 25; i++ { email := RandomEmail() - validateErr := ValidateEmails(email) - if validateErr != nil { - t.Error("should not error") - } - if len(email) < 25 { - t.Error("should more than 25") - } - } -} - -func TestNewFiberCtx(t *testing.T) { - c := NewFiberCtx() - if c == nil { - t.Error("should not nil") + assert.NoError(t, ValidateEmails(email), "should not error") + assert.GreaterOrEqual(t, len(email), 25, "should be more than 25") } } @@ -56,30 +39,54 @@ func TestRandomIPAddress(t *testing.T) { for i := 0; i < 20; i++ { ipRand := RandomIPAddress() ip := net.ParseIP(ipRand) - if ip == nil { - t.Error("should not nil") - } + assert.NotNil(t, ip, constants.ShouldNotNil) } } func TestValidateEmails(t *testing.T) { err1 := ValidateEmails("f", "a") - if err1 == nil { - t.Error("should err not nil") - } + assert.Error(t, err1, constants.ShouldErr) - err2 := ValidateEmails("validemail0987@gmail.com") - if err2 != nil { - t.Error("should err not nil") - } + 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) - err3 := ValidateEmails("validemail0987@gmail.com", "invalidemail0987@.gmail.com") - if err3 == nil { - t.Error("should err not nil") + err4 := ValidateEmails("validemail0987@gmail.com", "valid_email0987@gmail.com", "invalidemail0987@gmail.com.") + assert.Error(t, err4, constants.ShouldErr) +} + +func TestNewFiberCtx(t *testing.T) { + c := NewFiberCtx() + assert.NotNil(t, c, constants.ShouldNotNil) +} + +func TestToTitle(t *testing.T) { + payloads := []struct { + str string + isEqual bool + }{ + {"", true}, + {"Bca", true}, + {"A B C", true}, + {"Your Name", true}, + {"ABC", false}, + {"aa", false}, + {" bb", false}, + {" ccc", false}, + {"a ab cc", false}, + {"your -Name", false}, + {"-m'rning", false}, } - err4 := ValidateEmails("validemail0987@gmail.com", "validemail0987@gmail.com", "invalidemail0987@gmail.com.") - if err4 == nil { - t.Error("should err not nil") + for _, payload := range payloads { + res := ToTitle(payload.str) + + if res != payload.str && payload.isEqual { + assert.Failf(t, "should equal, but not at: %s got %s", payload.str, res) + } else if res == payload.str && !payload.isEqual { + assert.Failf(t, "should not equal, but equal at: %s got %s", payload.str, res) + } } } diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 88bb4f9..323d52a 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -17,12 +17,17 @@ import ( "github.com/Lukmanern/gost/internal/response" ) +// JWTHandler struct handles some key and redis connection +// The purpose of this is to handler and checking HTTP Header +// and/or checking is JWT blacklisted or not. See IsBlacklisted func. type JWTHandler struct { publicKey *rsa.PublicKey privateKey *rsa.PrivateKey cache *redis.Client } +// Claims struct will be generated as token,contains +// user data like ID, email, role and permissions. type Claims struct { ID int `json:"id"` Email string `json:"email"` @@ -31,6 +36,7 @@ type Claims struct { jwt.RegisteredClaims } +// NewJWTHandler func creates new JwtHandler struct func NewJWTHandler() *JWTHandler { env.ReadConfig("./.env") config := env.Configuration() @@ -46,7 +52,7 @@ func NewJWTHandler() *JWTHandler { if privateKeyErr != nil { log.Fatalln("jwt private key parser failed: please check in log file at ./log/log-files") } - newJWTHandler.cache = connector.LoadRedisDatabase() + newJWTHandler.cache = connector.LoadRedisCache() if newJWTHandler.privateKey == nil { log.Fatalln("jwt private keys are missed (nil)") @@ -60,7 +66,7 @@ func NewJWTHandler() *JWTHandler { return &newJWTHandler } -// This func used for login. +// 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") @@ -86,6 +92,9 @@ func (j *JWTHandler) GenerateJWT(id int, email, role string, permissions map[int return t, nil } +// InvalidateToken func stores (blacklistings) token to redis. +// After storing token in redis, the token is already blacklisted. +// This func is used in Logout feature. func (j JWTHandler) InvalidateToken(c *fiber.Ctx) error { cookie := extractToken(c) claims := Claims{} @@ -107,12 +116,15 @@ func (j JWTHandler) InvalidateToken(c *fiber.Ctx) error { return nil } +// IsBlacklisted func check the token/cookie is blacklisted or not. func (j JWTHandler) IsBlacklisted(cookie string) bool { status := j.cache.Get(cookie) val, _ := status.Result() return val != "" } +// IsAuthenticated func extracts token from context (fiber Ctx), +// check is blacklisted or not. And checks the expire time too. func (j JWTHandler) IsAuthenticated(c *fiber.Ctx) error { cookie := extractToken(c) if j.IsBlacklisted(cookie) { @@ -134,23 +146,9 @@ func (j JWTHandler) IsAuthenticated(c *fiber.Ctx) error { return c.Next() } -func (j JWTHandler) IsTokenValid(cookie string) bool { - claims := Claims{} - token, err := jwt.ParseWithClaims(cookie, &claims, func(jwtToken *jwt.Token) (interface{}, error) { - if _, ok := jwtToken.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fiber.NewError(fiber.StatusUnauthorized) - } - - return j.publicKey, nil - }) - if err != nil || !token.Valid { - return false - } - return true -} - +// extractToken func extracts token from fiber Ctx. func extractToken(c *fiber.Ctx) string { - bearerToken := c.Get("Authorization") + bearerToken := c.Get(fiber.HeaderAuthorization) // Normally Authorization HTTP header. onlyToken := strings.Split(bearerToken, " ") if len(onlyToken) == 2 { @@ -160,6 +158,7 @@ func extractToken(c *fiber.Ctx) string { return "" } +// GenerateClaims func generates claims struct from jwt. func (j JWTHandler) GenerateClaims(cookieToken string) *Claims { if j.IsBlacklisted(cookieToken) { return nil @@ -179,6 +178,14 @@ 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 { @@ -189,8 +196,10 @@ func BuildBitGroups(permIDs ...int) map[int]int { return groups } -func CheckHasPermission(endpointPermID int, userPermissions map[int]int) bool { - endpointBits := BuildBitGroups(endpointPermID) +// 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 { @@ -202,7 +211,7 @@ func CheckHasPermission(endpointPermID int, userPermissions map[int]int) bool { return true } -// type PermissionMap = map[uint8]uint8 +// 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 { @@ -221,6 +230,7 @@ func (j JWTHandler) HasPermission(c *fiber.Ctx, endpointPermID int) error { 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 { @@ -229,14 +239,16 @@ func (j JWTHandler) HasRole(c *fiber.Ctx, role string) error { return c.Next() } -// for handler or middleware +// 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) } } -// for handler or middleware +// 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 6e8a05e..280764d 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -7,10 +7,10 @@ import ( "testing" "time" + "github.com/Lukmanern/gost/internal/constants" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" "github.com/gofiber/fiber/v2" - "github.com/stretchr/testify/assert" "github.com/valyala/fasthttp" ) @@ -94,7 +94,7 @@ func TestGenerateClaims(t *testing.T) { } } -func TestJWTHandler_InvalidateToken(t *testing.T) { +func TestJWTHandlerInvalidateToken(t *testing.T) { jwtHandler := NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) if err != nil { @@ -111,14 +111,14 @@ func TestJWTHandler_InvalidateToken(t *testing.T) { t.Error("Should error: Expected error for no token") } - c.Request().Header.Add("Authorization", "Bearer "+token) + c.Request().Header.Add(fiber.HeaderAuthorization, "Bearer "+token) invalidErr2 := jwtHandler.InvalidateToken(c) if invalidErr2 != nil { t.Error("Expected no error for a valid token, but got an error.") } } -func TestJWTHandler_IsBlacklisted(t *testing.T) { +func TestJWTHandlerIsBlacklisted(t *testing.T) { jwtHandler := NewJWTHandler() cookie, err := jwtHandler.GenerateJWT(1000, helper.RandomEmail(), "example-role", @@ -152,7 +152,7 @@ func TestJWTHandler_IsBlacklisted(t *testing.T) { } } -func TestJWTHandler_IsAuthenticated(t *testing.T) { +func TestJWTHandlerIsAuthenticated(t *testing.T) { jwtHandler := NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) if err != nil { @@ -183,7 +183,7 @@ func TestJWTHandler_IsAuthenticated(t *testing.T) { jwtHandler3 := NewJWTHandler() app := fiber.New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().Header.Add("Authorization", " "+token) + c.Request().Header.Add(fiber.HeaderAuthorization, " "+token) c.Status(fiber.StatusUnauthorized) jwtHandler3.IsAuthenticated(c) if c.Context().Response.StatusCode() != fiber.StatusUnauthorized { @@ -192,27 +192,7 @@ func TestJWTHandler_IsAuthenticated(t *testing.T) { }() } -func TestJWTHandler_IsTokenValid(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") - } - if token == "" { - t.Error("error : token void") - } - - isValid := jwtHandler.IsTokenValid(token) - assert.True(t, isValid, "Valid token should be considered valid") - - isValid = jwtHandler.IsTokenValid("expiredToken") - assert.False(t, isValid, "Expired token should be considered invalid") - - isValid = jwtHandler.IsTokenValid("invalidToken") - assert.False(t, isValid, "Invalid token should be considered invalid") -} - -func TestJWTHandler_HasPermission(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 { @@ -224,14 +204,14 @@ func TestJWTHandler_HasPermission(t *testing.T) { app := fiber.New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().Header.Add("Authorization", "Bearer "+token) + c.Request().Header.Add(fiber.HeaderAuthorization, "Bearer "+token) jwtHandler.HasPermission(c, 25) if c.Response().Header.StatusCode() != fiber.StatusUnauthorized { t.Error("Should authorized") } } -func TestJWTHandler_HasRole(t *testing.T) { +func TestJWTHandlerHasRole(t *testing.T) { jwtHandler := NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) if err != nil { @@ -243,14 +223,14 @@ func TestJWTHandler_HasRole(t *testing.T) { app := fiber.New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().Header.Add("Authorization", "Bearer "+token) + c.Request().Header.Add(fiber.HeaderAuthorization, "Bearer "+token) jwtHandler.HasRole(c, "test-role") if c.Response().Header.StatusCode() != fiber.StatusUnauthorized { - t.Error("Should unauthorized") + t.Error(constants.Unauthorized) } } -func TestJWTHandler_CheckHasPermission(t *testing.T) { +func TestJWTHandlerCheckHasPermission(t *testing.T) { jwtHandler := NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) if err != nil { @@ -262,11 +242,11 @@ func TestJWTHandler_CheckHasPermission(t *testing.T) { err2 := jwtHandler.CheckHasPermission(9999) if err2 == nil { - t.Error("Should unauthorized") + t.Error(constants.Unauthorized) } } -func TestJWTHandler_CheckHasRole(t *testing.T) { +func TestJWTHandlerCheckHasRole(t *testing.T) { jwtHandler := NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Role, params.Per, params.Exp) if err != nil { @@ -278,11 +258,11 @@ func TestJWTHandler_CheckHasRole(t *testing.T) { err2 := jwtHandler.CheckHasRole("permission-1") if err2 == nil { - t.Error("Should unauthorized") + t.Error(constants.Unauthorized) } } -func Test_PermissionBitGroup(t *testing.T) { +func TestPermissionBitGroup(t *testing.T) { d := 8 testCases := []struct { input int @@ -335,7 +315,7 @@ func Test_PermissionBitGroup(t *testing.T) { } } -func Test_CheckHasPermission(t *testing.T) { +func TestCheckHasPermission(t *testing.T) { // user perms permIDs := make([]int, 0) for i := 1; i <= 19; i++ { diff --git a/internal/rbac/permission.go b/internal/rbac/permission.go index 06da6ce..0e55c0e 100644 --- a/internal/rbac/permission.go +++ b/internal/rbac/permission.go @@ -6,8 +6,10 @@ import ( "github.com/Lukmanern/gost/domain/entity" ) -// Don't forget to run Test_AllPermissions -// to audit +// 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 @@ -32,8 +34,8 @@ func AllPermissions() []entity.Permission { if perm.ID < 1 || len(perm.Name) <= 1 { log.Fatal("permission name too short or invalid id at:", perm) } - checkIDs[perm.ID] += 1 - checkNames[perm.Name] += 1 + 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) } @@ -72,5 +74,5 @@ var ( // add more permissions // Rule : // Name should unique - // ID +1 from before + // ID +1 from the ID before ) diff --git a/internal/rbac/rbac_test.go b/internal/rbac/rbac_test.go index 9085585..1de87d2 100644 --- a/internal/rbac/rbac_test.go +++ b/internal/rbac/rbac_test.go @@ -2,63 +2,47 @@ package rbac import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestAllPermissions(t *testing.T) { - for _, permission := range AllPermissions() { - if permission.Name == "" { - t.Error("permission name should not string-nil") - } + 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") } } -// This Test is to make sure -// that permission value is unique func TestCountPermissions(t *testing.T) { hashMapPermissions := make(map[string]int, 0) for _, permission := range AllPermissions() { - hashMapPermissions[permission.Name] += 1 - if hashMapPermissions[permission.Name] > 1 { - t.Error("should 1, not more, non-unique permission detected : ", permission.Name) - } + hashMapPermissions[permission.Name]++ + assert.LessOrEqual(t, hashMapPermissions[permission.Name], 1, "should be 1, not more; non-unique permission detected: %s", permission.Name) } - if len(hashMapPermissions) != len(AllPermissions()) { - t.Error("should equal len, non-unique permission detected") - } + assert.Equal(t, len(hashMapPermissions), len(AllPermissions()), "should have equal length; non-unique permission detected") } -func Test_AllPermissions(t *testing.T) { - defer func() { - r := recover() - if r != nil { - t.Error("should not panic, but got:", r) - } - }() - - AllPermissions() -} - -func Test_AllRoles(t *testing.T) { - for _, role := range AllRoles() { - if role.Name == "" { - t.Error("name should not string-nil") - } +func TestAllRoles(t *testing.T) { + roles := AllRoles() + for _, role := range roles { + assert.NotEmpty(t, role.Name, "name should not be empty") } } -func Test_CountRoles(t *testing.T) { +func TestCountRoles(t *testing.T) { hashMapRoles := make(map[string]int, 0) for _, role := range AllRoles() { - hashMapRoles[role.Name] += 1 - if hashMapRoles[role.Name] > 1 { - t.Error("should 1, not more, non-unique role (role:name) detected : ", role.Name) - } + hashMapRoles[role.Name]++ + assert.LessOrEqual(t, hashMapRoles[role.Name], 1, "should be 1, not more; non-unique role (role:name) detected: %s", role.Name) } - if len(hashMapRoles) != len(AllRoles()) { - t.Error("should equal len, non-unique role detected") - } + 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 index c448e3c..0953d81 100644 --- a/internal/rbac/role.go +++ b/internal/rbac/role.go @@ -2,11 +2,15 @@ package rbac import "github.com/Lukmanern/gost/domain/entity" -// for migration and seeder +// 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 } @@ -16,7 +20,7 @@ func AllRoles() []entity.Role { newRoleEntity := entity.Role{ Name: name, } - newRoleEntity.SetCreateTimes() + newRoleEntity.SetCreateTime() roles = append(roles, newRoleEntity) } @@ -26,6 +30,7 @@ func AllRoles() []entity.Role { const ( RoleAdmin = "admin" RoleUser = "user" + // ... // add more here ) diff --git a/internal/response/response.go b/internal/response/response.go index bc70ada..eb7447d 100644 --- a/internal/response/response.go +++ b/internal/response/response.go @@ -6,6 +6,9 @@ import ( "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"` diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 01eb0a8..0000000 --- a/main_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "testing" - "time" -) - -func Test_1(t *testing.T) { - defer func() { - r := recover() - if r != nil { - t.Error("should not panic", r) - } - }() - - go main() - time.Sleep(3 * time.Second) -} diff --git a/repository/rbac/permission_repository.go b/repository/permission/permission_repository.go similarity index 69% rename from repository/rbac/permission_repository.go rename to repository/permission/permission_repository.go index aaa79a0..fb1fc94 100644 --- a/repository/rbac/permission_repository.go +++ b/repository/permission/permission_repository.go @@ -11,21 +11,30 @@ import ( ) 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 { - permissionTableName string - db *gorm.DB + db *gorm.DB } var ( - permissionTableName string = "permissions" permissionRepositoryImpl *PermissionRepositoryImpl permissionRepositoryImplOnce sync.Once ) @@ -33,14 +42,13 @@ var ( func NewPermissionRepository() PermissionRepository { permissionRepositoryImplOnce.Do(func() { permissionRepositoryImpl = &PermissionRepositoryImpl{ - permissionTableName: permissionTableName, - db: connector.LoadDatabase(), + db: connector.LoadDatabase(), } }) return permissionRepositoryImpl } -func (repo PermissionRepositoryImpl) Create(ctx context.Context, permission entity.Permission) (id int, err error) { +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 { @@ -57,7 +65,7 @@ func (repo PermissionRepositoryImpl) Create(ctx context.Context, permission enti return id, nil } -func (repo PermissionRepositoryImpl) GetByID(ctx context.Context, id int) (permission *entity.Permission, err error) { +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 { @@ -66,7 +74,7 @@ func (repo PermissionRepositoryImpl) GetByID(ctx context.Context, id int) (permi return permission, nil } -func (repo PermissionRepositoryImpl) GetByName(ctx context.Context, name string) (permission *entity.Permission, err error) { +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 { @@ -75,7 +83,7 @@ func (repo PermissionRepositoryImpl) GetByName(ctx context.Context, name string) return permission, nil } -func (repo PermissionRepositoryImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (permissions []entity.Permission, total int, err error) { +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 ?" @@ -95,7 +103,7 @@ func (repo PermissionRepositoryImpl) GetAll(ctx context.Context, filter base.Req return permissions, total, nil } -func (repo PermissionRepositoryImpl) Update(ctx context.Context, permission entity.Permission) (err error) { +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) @@ -118,7 +126,7 @@ func (repo PermissionRepositoryImpl) Update(ctx context.Context, permission enti return err } -func (repo PermissionRepositoryImpl) Delete(ctx context.Context, id int) (err error) { +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 { diff --git a/repository/rbac/permission_repository_test.go b/repository/permission/permission_repository_test.go similarity index 94% rename from repository/rbac/permission_repository_test.go rename to repository/permission/permission_repository_test.go index 4dbd05e..2493a8b 100644 --- a/repository/rbac/permission_repository_test.go +++ b/repository/permission/permission_repository_test.go @@ -24,8 +24,7 @@ func init() { timeNow = time.Now() ctx = context.Background() permissionRepoImpl = PermissionRepositoryImpl{ - permissionTableName: permissionTableName, - db: connector.LoadDatabase(), + db: connector.LoadDatabase(), } } @@ -54,7 +53,7 @@ func TestNewPermissionRepository(t *testing.T) { } } -func TestPermissionRepositoryImpl_Create(t *testing.T) { +func TestPermissionRepositoryImplCreate(t *testing.T) { permission := createOnePermission(t, "create-same-name") if permission == nil { t.Error("failed creating permission : permission is nil") @@ -98,19 +97,19 @@ func TestPermissionRepositoryImpl_Create(t *testing.T) { t.Errorf("create() do not panic") } }() - gotId, err := tt.repo.Create(tt.args.ctx, tt.args.permission) + 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 { + if gotID <= 0 { t.Errorf("ID should be positive") } }) } } -func TestPermissionRepositoryImpl_GetByID(t *testing.T) { +func TestPermissionRepositoryImplGetByID(t *testing.T) { permission := createOnePermission(t, "TestGetByID") if permission == nil { t.Error("failed creating permission : permission is nil") @@ -162,7 +161,7 @@ func TestPermissionRepositoryImpl_GetByID(t *testing.T) { } } -func TestPermissionRepositoryImpl_GetByName(t *testing.T) { +func TestPermissionRepositoryImplGetByName(t *testing.T) { permission := createOnePermission(t, "TestGetByName") if permission == nil { t.Error("failed creating permission : permission is nil") @@ -215,7 +214,7 @@ func TestPermissionRepositoryImpl_GetByName(t *testing.T) { } } -func TestPermissionRepositoryImpl_GetAll(t *testing.T) { +func TestPermissionRepositoryImplGetAll(t *testing.T) { permissions := make([]entity.Permission, 0) for i := 0; i < 10; i++ { permission := createOnePermission(t, "TestGetAll-"+strconv.Itoa(i)) @@ -285,7 +284,7 @@ func TestPermissionRepositoryImpl_GetAll(t *testing.T) { } } -func TestPermissionRepositoryImpl_Update(t *testing.T) { +func TestPermissionRepositoryImplUpdate(t *testing.T) { permission := createOnePermission(t, "TestUpdateByID") if permission == nil { t.Error("failed creating permission : permission is nil") @@ -349,7 +348,7 @@ func TestPermissionRepositoryImpl_Update(t *testing.T) { } } -func TestPermissionRepositoryImpl_Delete(t *testing.T) { +func TestPermissionRepositoryImplDelete(t *testing.T) { permission := createOnePermission(t, "TestDeleteByID") if permission == nil { t.Error("failed creating permission : permission is nil") diff --git a/repository/rbac/role_repository.go b/repository/role/role_repository.go similarity index 71% rename from repository/rbac/role_repository.go rename to repository/role/role_repository.go index 0d9d633..c1f4d96 100644 --- a/repository/rbac/role_repository.go +++ b/repository/role/role_repository.go @@ -11,22 +11,33 @@ import ( ) type RoleRepository interface { + // Create adds a new role to the repository with specified permissions. Create(ctx context.Context, role entity.Role, permissionsID []int) (id int, err error) + + // ConnectToPermission associates a role with specified permissions. ConnectToPermission(ctx context.Context, roleID int, permissionsID []int) (err error) + + // GetByID retrieves a role by its unique identifier. GetByID(ctx context.Context, id int) (role *entity.Role, err error) + + // GetByName retrieves a role by its name. GetByName(ctx context.Context, name string) (role *entity.Role, err error) + + // GetAll retrieves all roles based on a filter for pagination. GetAll(ctx context.Context, filter base.RequestGetAll) (roles []entity.Role, total int, err error) + + // Update modifies role information in the repository. Update(ctx context.Context, role entity.Role) (err error) + + // Delete removes a role from the repository by its ID. Delete(ctx context.Context, id int) (err error) } type RoleRepositoryImpl struct { - roleTableName string - db *gorm.DB + db *gorm.DB } var ( - roleTableName string = "roles" roleRepositoryImpl *RoleRepositoryImpl roleRepositoryImplOnce sync.Once ) @@ -34,14 +45,13 @@ var ( func NewRoleRepository() RoleRepository { roleRepositoryImplOnce.Do(func() { roleRepositoryImpl = &RoleRepositoryImpl{ - roleTableName: roleTableName, - db: connector.LoadDatabase(), + db: connector.LoadDatabase(), } }) 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, permissionsID []int) (id int, err error) { err = repo.db.Transaction(func(tx *gorm.DB) error { res := tx.Create(&role) if res.Error != nil { @@ -69,7 +79,7 @@ func (repo RoleRepositoryImpl) Create(ctx context.Context, role entity.Role, per return id, nil } -func (repo RoleRepositoryImpl) ConnectToPermission(ctx context.Context, roleID int, permissionsID []int) (err error) { +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) @@ -96,7 +106,7 @@ func (repo RoleRepositoryImpl) ConnectToPermission(ctx context.Context, roleID i return nil } -func (repo RoleRepositoryImpl) GetByID(ctx context.Context, id int) (role *entity.Role, err error) { +func (repo *RoleRepositoryImpl) GetByID(ctx context.Context, id int) (role *entity.Role, err error) { role = &entity.Role{} result := repo.db.Where("id = ?", id).Preload("Permissions").First(&role) if result.Error != nil { @@ -105,7 +115,7 @@ func (repo RoleRepositoryImpl) GetByID(ctx context.Context, id int) (role *entit return role, nil } -func (repo RoleRepositoryImpl) GetByName(ctx context.Context, name string) (role *entity.Role, err error) { +func (repo *RoleRepositoryImpl) GetByName(ctx context.Context, name string) (role *entity.Role, err error) { role = &entity.Role{} result := repo.db.Where("name = ?", name).Preload("Permissions").First(&role) if result.Error != nil { @@ -114,7 +124,7 @@ func (repo RoleRepositoryImpl) GetByName(ctx context.Context, name string) (role 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 base.RequestGetAll) (roles []entity.Role, total int, err error) { var count int64 args := []interface{}{"%" + filter.Keyword + "%"} cond := "name LIKE ?" @@ -134,7 +144,7 @@ func (repo RoleRepositoryImpl) GetAll(ctx context.Context, filter base.RequestGe return roles, total, nil } -func (repo RoleRepositoryImpl) Update(ctx context.Context, role entity.Role) (err error) { +func (repo *RoleRepositoryImpl) Update(ctx context.Context, role entity.Role) (err error) { err = repo.db.Transaction(func(tx *gorm.DB) error { var oldData entity.Role result := tx.Where("id = ?", role.ID).First(&oldData) @@ -157,7 +167,7 @@ func (repo RoleRepositoryImpl) Update(ctx context.Context, role entity.Role) (er return err } -func (repo RoleRepositoryImpl) Delete(ctx context.Context, id int) (err error) { +func (repo *RoleRepositoryImpl) Delete(ctx context.Context, id int) (err error) { deleted := entity.Role{} result := repo.db.Where("id = ?", id).Delete(&deleted) if result.Error != nil { diff --git a/repository/rbac/role_repository_test.go b/repository/role/role_repository_test.go similarity index 93% rename from repository/rbac/role_repository_test.go rename to repository/role/role_repository_test.go index dde5834..8835806 100644 --- a/repository/rbac/role_repository_test.go +++ b/repository/role/role_repository_test.go @@ -16,8 +16,8 @@ import ( var ( roleRepoImpl RoleRepositoryImpl permissionsID []int - // timeNow and ctx is declared - // at permission_repository_test file + timeNow time.Time + ctx context.Context ) func init() { @@ -28,8 +28,7 @@ func init() { ctx = context.Background() roleRepoImpl = RoleRepositoryImpl{ - roleTableName: roleTableName, - db: connector.LoadDatabase(), + db: connector.LoadDatabase(), } permissionsID = []int{1, 2, 3, 4, 5} @@ -59,7 +58,7 @@ func TestNewRoleRepository(t *testing.T) { } } -func TestRoleRepositoryImpl_Create(t *testing.T) { +func TestCreate(t *testing.T) { role := createOneRole(t, "create-same-name") if role == nil { t.Error("failed creating role : role is nil") @@ -103,19 +102,19 @@ func TestRoleRepositoryImpl_Create(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, tt.args.permissionsID) if (err != nil) != tt.wantErr { t.Errorf("RoleRepositoryImpl.Create() error = %v, wantErr %v", err, tt.wantErr) return } - if gotId <= 0 { + if gotID <= 0 { t.Errorf("ID should be positive") } }) } } -func TestRoleRepositoryImpl_ConnectToPermission(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") @@ -172,7 +171,7 @@ func TestRoleRepositoryImpl_ConnectToPermission(t *testing.T) { } } -func TestRoleRepositoryImpl_GetByID(t *testing.T) { +func TestGetByID(t *testing.T) { role := createOneRole(t, "TestGetByID") if role == nil { t.Error("failed creating role : role is nil") @@ -224,7 +223,7 @@ func TestRoleRepositoryImpl_GetByID(t *testing.T) { } } -func TestRoleRepositoryImpl_GetByName(t *testing.T) { +func TestGetByName(t *testing.T) { role := createOneRole(t, "TestGetByName") if role == nil { t.Error("failed creating role : role is nil") @@ -276,7 +275,7 @@ func TestRoleRepositoryImpl_GetByName(t *testing.T) { } } -func TestRoleRepositoryImpl_GetAll(t *testing.T) { +func TestGetAll(t *testing.T) { roles := make([]entity.Role, 0) for i := 0; i < 10; i++ { role := createOneRole(t, "TestGetAll"+strconv.Itoa(i)) @@ -345,7 +344,7 @@ func TestRoleRepositoryImpl_GetAll(t *testing.T) { } } -func TestRoleRepositoryImpl_Update(t *testing.T) { +func TestUpdate(t *testing.T) { role := createOneRole(t, "TestUpdateByID") if role == nil { t.Error("failed creating role : role is nil") @@ -408,7 +407,7 @@ func TestRoleRepositoryImpl_Update(t *testing.T) { } } -func TestRoleRepositoryImpl_Delete(t *testing.T) { +func TestDelete(t *testing.T) { role := createOneRole(t, "TestDeleteByID") if role == nil { t.Error("failed creating role : role is nil") diff --git a/repository/user/user_repository.go b/repository/user/user_repository.go index b45ab06..51e36f8 100644 --- a/repository/user/user_repository.go +++ b/repository/user/user_repository.go @@ -14,23 +14,36 @@ import ( ) type UserRepository interface { + // Create adds a new user to the repository with a specified role. Create(ctx context.Context, user entity.User, roleID int) (id int, err error) + + // GetByID retrieves a user by their unique identifier. GetByID(ctx context.Context, id int) (user *entity.User, err error) + + // GetByEmail retrieves a user by their email address. GetByEmail(ctx context.Context, email string) (user *entity.User, err error) + + // GetByConditions retrieves a user based on specified conditions. GetByConditions(ctx context.Context, conds map[string]any) (user *entity.User, err error) + + // GetAll retrieves all users based on a filter for pagination. GetAll(ctx context.Context, filter base.RequestGetAll) (users []entity.User, total int, err error) + + // Update modifies user information in the repository. Update(ctx context.Context, user entity.User) (err error) + + // Delete removes a user from the repository by their ID. Delete(ctx context.Context, id int) (err error) + + // UpdatePassword updates a user's password in the repository. UpdatePassword(ctx context.Context, id int, passwordHashed string) (err error) } type UserRepositoryImpl struct { - userTableName string - db *gorm.DB + db *gorm.DB } var ( - userTableName string = "users" userRepositoryImpl *UserRepositoryImpl userRepositoryImplOnce sync.Once ) @@ -38,14 +51,13 @@ var ( func NewUserRepository() UserRepository { userRepositoryImplOnce.Do(func() { userRepositoryImpl = &UserRepositoryImpl{ - userTableName: userTableName, - db: connector.LoadDatabase(), + db: connector.LoadDatabase(), } }) 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, roleID int) (id int, err error) { err = repo.db.Transaction(func(tx *gorm.DB) error { if res := tx.Create(&user); res.Error != nil { tx.Rollback() @@ -68,7 +80,7 @@ func (repo UserRepositoryImpl) Create(ctx context.Context, user entity.User, rol return id, nil } -func (repo UserRepositoryImpl) GetByID(ctx context.Context, id int) (user *entity.User, err error) { +func (repo *UserRepositoryImpl) GetByID(ctx context.Context, id int) (user *entity.User, err error) { user = &entity.User{} result := repo.db.Where("id = ?", id).Preload("Roles.Permissions").First(&user) if result.Error != nil { @@ -77,7 +89,7 @@ func (repo UserRepositoryImpl) GetByID(ctx context.Context, id int) (user *entit return user, nil } -func (repo UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (user *entity.User, err error) { +func (repo *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (user *entity.User, err error) { user = &entity.User{} result := repo.db.Where("email = ?", email).Preload("Roles.Permissions").First(&user) if result.Error != nil { @@ -86,7 +98,7 @@ func (repo UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (us return user, nil } -func (repo UserRepositoryImpl) GetByConditions(ctx context.Context, conds map[string]any) (user *entity.User, err error) { +func (repo *UserRepositoryImpl) GetByConditions(ctx context.Context, conds map[string]any) (user *entity.User, err error) { // this func is easy-contain-vunarable by default user = &entity.User{} query := repo.db @@ -100,7 +112,7 @@ func (repo UserRepositoryImpl) GetByConditions(ctx context.Context, conds map[st 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 base.RequestGetAll) (users []entity.User, total int, err error) { var count int64 args := []interface{}{"%" + filter.Keyword + "%"} cond := "name LIKE ?" @@ -120,7 +132,7 @@ func (repo UserRepositoryImpl) GetAll(ctx context.Context, filter base.RequestGe return users, total, nil } -func (repo UserRepositoryImpl) Update(ctx context.Context, user entity.User) (err error) { +func (repo *UserRepositoryImpl) Update(ctx context.Context, user entity.User) (err error) { err = repo.db.Transaction(func(tx *gorm.DB) error { var oldData entity.User result := tx.Where("id = ?", user.ID).First(&oldData) @@ -142,7 +154,7 @@ func (repo UserRepositoryImpl) Update(ctx context.Context, user entity.User) (er return err } -func (repo UserRepositoryImpl) Delete(ctx context.Context, id int) (err error) { +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 { @@ -151,7 +163,7 @@ func (repo UserRepositoryImpl) Delete(ctx context.Context, id int) (err error) { return nil } -func (repo UserRepositoryImpl) UpdatePassword(ctx context.Context, id int, passwordHashed string) (err error) { +func (repo *UserRepositoryImpl) UpdatePassword(ctx context.Context, id int, passwordHashed string) (err error) { err = repo.db.Transaction(func(tx *gorm.DB) error { var user entity.User result := tx.Where("id = ?", id).First(&user) diff --git a/repository/user/user_repository_test.go b/repository/user/user_repository_test.go index af7ba74..1a6dada 100644 --- a/repository/user/user_repository_test.go +++ b/repository/user/user_repository_test.go @@ -30,7 +30,7 @@ func TestNewUserRepository(t *testing.T) { } } -func TestUserRepositoryImpl_Create(t *testing.T) { +func TestUserRepositoryImplCreate(t *testing.T) { userRepositoryImpl := UserRepositoryImpl{ db: connector.LoadDatabase(), } @@ -82,17 +82,17 @@ func TestUserRepositoryImpl_Create(t *testing.T) { 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) + 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 - } else { - 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) } + 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 @@ -111,7 +111,7 @@ func TestUserRepositoryImpl_Create(t *testing.T) { } } -func TestUserRepositoryImpl_GetByID(t *testing.T) { +func TestUserRepositoryImplGetByID(t *testing.T) { // create user user := entity.User{ Name: "validname", @@ -181,7 +181,7 @@ func TestUserRepositoryImpl_GetByID(t *testing.T) { } } -func TestUserRepositoryImpl_GetByEmail(t *testing.T) { +func TestUserRepositoryImplGetByEmail(t *testing.T) { // create user user := entity.User{ Name: "validname", @@ -251,7 +251,7 @@ func TestUserRepositoryImpl_GetByEmail(t *testing.T) { } } -func TestUserRepositoryImpl_GetAll(t *testing.T) { +func TestUserRepositoryImplGetAll(t *testing.T) { // create user allUsersID := make([]int, 0) userRepositoryImpl := UserRepositoryImpl{ @@ -336,7 +336,7 @@ func TestUserRepositoryImpl_GetAll(t *testing.T) { } } -func TestUserRepositoryImpl_Update(t *testing.T) { +func TestUserRepositoryImplUpdate(t *testing.T) { // create user user := entity.User{ Name: "validname", @@ -399,7 +399,7 @@ func TestUserRepositoryImpl_Update(t *testing.T) { } } -func TestUserRepositoryImpl_Delete(t *testing.T) { +func TestUserRepositoryImplDelete(t *testing.T) { userRepository := NewUserRepository() if userRepository == nil { t.Error("shouldn't nil") @@ -415,7 +415,7 @@ func TestUserRepositoryImpl_Delete(t *testing.T) { } } -func TestUserRepositoryImpl_UpdatePassword(t *testing.T) { +func TestUserRepositoryImplUpdatePassword(t *testing.T) { // create user user := entity.User{ Name: "validname", @@ -489,7 +489,7 @@ func TestUserRepositoryImpl_UpdatePassword(t *testing.T) { } } -func TestUserRepositoryImpl_GetByConditions(t *testing.T) { +func TestUserRepositoryImplGetByConditions(t *testing.T) { user := entity.User{ Name: "validname", Email: "valid10@email.com", diff --git a/scripts/generate_keys.sh b/scripts/generate_keys.sh new file mode 100644 index 0000000..d771f74 --- /dev/null +++ b/scripts/generate_keys.sh @@ -0,0 +1,7 @@ +# 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 new file mode 100644 index 0000000..73e853f --- /dev/null +++ b/scripts/update_module.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Step 1: Remove .git directory +rm -rf .git + +# Step 2: Search and replace "github.com/Lukmanern/gost" with "github.com/YourUsername/YourRepoName" +find . -type f -exec sed -i 's/github\.com\/Lukmanern\/gost/github\.com\/YourUsername\/YourRepoName/g' {} + + +echo "Finish! .git directory removed and search/replace completed." diff --git a/service/email/email_service.go b/service/email/email_service.go index 87508ca..e503d93 100644 --- a/service/email/email_service.go +++ b/service/email/email_service.go @@ -11,9 +11,12 @@ import ( ) type EmailService interface { + // SendMail func sends message with subject to some emails address. SendMail(emails []string, subject, message string) error } +// EmailServiceImpl struct contains all the +// SMTP needs for sending emails. // SMTP => Simple Mail Transfer Protocol type EmailServiceImpl struct { Server string @@ -48,7 +51,7 @@ func NewEmailService() EmailService { return emailService } -func (svc EmailServiceImpl) SendMail(emails []string, subject, message string) error { +func (svc *EmailServiceImpl) SendMail(emails []string, subject, message string) error { validateErr := helper.ValidateEmails(emails...) if validateErr != nil { return validateErr diff --git a/service/email/email_service_test.go b/service/email/email_service_test.go index ee6160c..ee4553b 100644 --- a/service/email/email_service_test.go +++ b/service/email/email_service_test.go @@ -12,10 +12,10 @@ func init() { env.ReadConfig("./../../.env") connector.LoadDatabase() - connector.LoadRedisDatabase() + connector.LoadRedisCache() } -func Test_SendEmail(t *testing.T) { +func TestSendEmail(t *testing.T) { emailService := NewEmailService() if emailService == nil { t.Error("should not nil") diff --git a/service/file/file_service.go b/service/file/file_service.go index d5d18dd..88f8aa3 100644 --- a/service/file/file_service.go +++ b/service/file/file_service.go @@ -8,6 +8,7 @@ import ( "mime/multipart" "net/http" "strconv" + "sync" "github.com/Lukmanern/gost/internal/env" "github.com/gofiber/fiber/v2" @@ -21,13 +22,18 @@ type FileReponse struct { } `json:"metadata"` } -type UploadFile interface { +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 client struct { +type FileServiceImpl struct { PublicURL string ListFilesURL string UploadURL string @@ -35,19 +41,27 @@ type client struct { Token string } -func NewFileService() UploadFile { - config := env.Configuration() - baseURL := config.BucketURL + "/storage/v1/object/" - return &client{ - PublicURL: baseURL + "public/" + config.BucketName + "/", - ListFilesURL: baseURL + "list/" + config.BucketName, - UploadURL: baseURL + config.BucketName + "/", - DeleteURL: baseURL + config.BucketName, - Token: config.BucketToken, - } +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 *client) UploadFile(fileHeader *multipart.FileHeader) (fileURL string, err error) { +func (c FileServiceImpl) UploadFile(fileHeader *multipart.FileHeader) (fileURL string, err error) { fileName := fileHeader.Filename file, headerErr := fileHeader.Open() if headerErr != nil { @@ -72,10 +86,10 @@ func (c *client) UploadFile(fileHeader *multipart.FileHeader) (fileURL string, e return "", newReqErr } - request.Header.Set("Authorization", "Bearer "+c.Token) - request.Header.Set("Content-Type", writer.FormDataContentType()) - client := &http.Client{} - respUpload, doErr := client.Do(request) + 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 } @@ -90,7 +104,7 @@ func (c *client) UploadFile(fileHeader *multipart.FileHeader) (fileURL string, e if reqErr != nil { return "", reqErr } - respTestGet, doErr := client.Do(reqTestGet) + respTestGet, doErr := FileServiceImpl.Do(reqTestGet) if doErr != nil { return "", doErr } @@ -101,7 +115,7 @@ func (c *client) UploadFile(fileHeader *multipart.FileHeader) (fileURL string, e return link, nil } -func (c *client) RemoveFile(fileName string) (err error) { +func (c FileServiceImpl) RemoveFile(fileName string) (err error) { body := map[string]interface{}{ "prefixes": fileName, } @@ -114,10 +128,10 @@ func (c *client) RemoveFile(fileName string) (err error) { return err } - request.Header.Set("Authorization", "Bearer "+c.Token) - request.Header.Set("Content-Type", "application/json") - client := &http.Client{} - response, err := client.Do(request) + 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) } @@ -140,7 +154,7 @@ func (c *client) RemoveFile(fileName string) (err error) { return nil } -func (c *client) GetFilesList() (files []map[string]any, err error) { +func (c FileServiceImpl) GetFilesList() (files []map[string]any, err error) { type sortBy struct { Column string `json:"column"` Order string `json:"order"` @@ -171,10 +185,10 @@ func (c *client) GetFilesList() (files []map[string]any, err error) { return nil, err } - request.Header.Set("Authorization", "Bearer "+c.Token) - request.Header.Set("Content-Type", "application/json") - client := &http.Client{} - response, err := client.Do(request) + 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) } diff --git a/service/rbac/permission_service.go b/service/permission/permission_service.go similarity index 69% rename from service/rbac/permission_service.go rename to service/permission/permission_service.go index 8608eb7..31e742d 100644 --- a/service/rbac/permission_service.go +++ b/service/permission/permission_service.go @@ -11,14 +11,23 @@ import ( "github.com/Lukmanern/gost/domain/base" "github.com/Lukmanern/gost/domain/entity" "github.com/Lukmanern/gost/domain/model" - repository "github.com/Lukmanern/gost/repository/rbac" + 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) } @@ -31,6 +40,8 @@ var ( permissionServiceImplOnce sync.Once ) +const permNotFound = "permission/s not found" + func NewPermissionService() PermissionService { permissionServiceImplOnce.Do(func() { permissionServiceImpl = &PermissionServiceImpl{ @@ -40,7 +51,7 @@ func NewPermissionService() PermissionService { return permissionServiceImpl } -func (svc PermissionServiceImpl) Create(ctx context.Context, permission model.PermissionCreate) (id int, err error) { +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) @@ -51,7 +62,7 @@ func (svc PermissionServiceImpl) Create(ctx context.Context, permission model.Pe Name: permission.Name, Description: permission.Description, } - entityPermission.SetCreateTimes() + entityPermission.SetCreateTime() id, err = svc.repository.Create(ctx, entityPermission) if err != nil { return 0, err @@ -59,16 +70,16 @@ func (svc PermissionServiceImpl) Create(ctx context.Context, permission model.Pe return id, nil } -func (svc PermissionServiceImpl) GetByID(ctx context.Context, id int) (permission *model.PermissionResponse, err error) { +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, "permission not found") + return nil, fiber.NewError(fiber.StatusNotFound, permNotFound) } return nil, getErr } if permissionEntity == nil { - return nil, fiber.NewError(fiber.StatusNotFound, "permission not found") + return nil, fiber.NewError(fiber.StatusNotFound, permNotFound) } permission = &model.PermissionResponse{ @@ -79,7 +90,7 @@ func (svc PermissionServiceImpl) GetByID(ctx context.Context, id int) (permissio return permission, nil } -func (svc PermissionServiceImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (permissions []model.PermissionResponse, total int, err error) { +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 @@ -98,7 +109,7 @@ func (svc PermissionServiceImpl) GetAll(ctx context.Context, filter base.Request return permissions, total, nil } -func (svc PermissionServiceImpl) Update(ctx context.Context, data model.PermissionUpdate) (err error) { +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 { @@ -111,12 +122,12 @@ func (svc PermissionServiceImpl) Update(ctx context.Context, data model.Permissi permissionByID, getErr := svc.repository.GetByID(ctx, data.ID) if getErr != nil { if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, "permission not found") + return fiber.NewError(fiber.StatusNotFound, permNotFound) } return getErr } if permissionByID == nil { - return fiber.NewError(fiber.StatusNotFound, "permission not found") + return fiber.NewError(fiber.StatusNotFound, permNotFound) } entityRole := entity.Permission{ @@ -132,16 +143,16 @@ func (svc PermissionServiceImpl) Update(ctx context.Context, data model.Permissi return nil } -func (svc PermissionServiceImpl) Delete(ctx context.Context, id int) (err error) { +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, "permission not found") + return fiber.NewError(fiber.StatusNotFound, permNotFound) } return getErr } if permission == nil { - return fiber.NewError(fiber.StatusNotFound, "permission not found") + return fiber.NewError(fiber.StatusNotFound, permNotFound) } err = svc.repository.Delete(ctx, id) if err != nil { diff --git a/service/rbac/permission_service_test.go b/service/permission/permission_service_test.go similarity index 86% rename from service/rbac/permission_service_test.go rename to service/permission/permission_service_test.go index b917607..e62aff0 100644 --- a/service/rbac/permission_service_test.go +++ b/service/permission/permission_service_test.go @@ -11,6 +11,7 @@ import ( "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" ) @@ -20,13 +21,13 @@ func init() { env.ReadConfig("./../../.env") connector.LoadDatabase() - connector.LoadRedisDatabase() + connector.LoadRedisCache() } func TestNewPermissionService(t *testing.T) { svc := NewPermissionService() if svc == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } } @@ -37,12 +38,12 @@ func TestNewPermissionService(t *testing.T) { // -> delete // -> get by id -func TestSuccessCRUD_Permission(t *testing.T) { +func TestSuccessCrudPermission(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() svc := NewPermissionService() if svc == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } modelPerm := model.PermissionCreate{ Name: strings.ToLower(helper.RandomString(10)), @@ -76,7 +77,7 @@ func TestSuccessCRUD_Permission(t *testing.T) { } updateErr := svc.Update(ctx, updatePermModel) if updateErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } // value reset @@ -92,7 +93,7 @@ func TestSuccessCRUD_Permission(t *testing.T) { deleteErr := svc.Delete(ctx, permID) if deleteErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } // value reset @@ -104,12 +105,12 @@ func TestSuccessCRUD_Permission(t *testing.T) { } } -func TestFailedCRUD_Permission(t *testing.T) { +func TestFailedCrudPermission(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() svc := NewPermissionService() if svc == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } modelPerm := model.PermissionCreate{ Name: strings.ToLower(helper.RandomString(10)), @@ -135,11 +136,11 @@ func TestFailedCRUD_Permission(t *testing.T) { } updateErr := svc.Update(ctx, updatePermModel) if updateErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } deleteErr := svc.Delete(ctx, -10) if deleteErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } } diff --git a/service/rbac/role_service.go b/service/role/role_service.go similarity index 67% rename from service/rbac/role_service.go rename to service/role/role_service.go index 1a191d8..5969f5b 100644 --- a/service/rbac/role_service.go +++ b/service/role/role_service.go @@ -11,21 +11,34 @@ import ( "github.com/Lukmanern/gost/domain/base" "github.com/Lukmanern/gost/domain/entity" "github.com/Lukmanern/gost/domain/model" - repository "github.com/Lukmanern/gost/repository/rbac" + repository "github.com/Lukmanern/gost/repository/role" + permService "github.com/Lukmanern/gost/service/permission" ) type RoleService interface { + + // Create func create one role. Create(ctx context.Context, data model.RoleCreate) (id int, err error) + + // ConnectPermissions func connect one role with one or more permissions. ConnectPermissions(ctx context.Context, data model.RoleConnectToPermissions) (err error) + + // GetByID func get one role. GetByID(ctx context.Context, id int) (role *entity.Role, err error) + + // GetAll func get some roles. GetAll(ctx context.Context, filter base.RequestGetAll) (roles []model.RoleResponse, total int, err error) + + // Update func update one role. Update(ctx context.Context, data model.RoleUpdate) (err error) + + // Delete func delete one role. Delete(ctx context.Context, id int) (err error) } type RoleServiceImpl struct { repository repository.RoleRepository - servicePermission PermissionService + servicePermission permService.PermissionService } var ( @@ -33,7 +46,9 @@ var ( roleServiceImplOnce sync.Once ) -func NewRoleService(servicePermission PermissionService) RoleService { +const roleNotFound = "role/s not found" + +func NewRoleService(servicePermission permService.PermissionService) RoleService { roleServiceImplOnce.Do(func() { roleServiceImpl = &RoleServiceImpl{ repository: repository.NewRoleRepository(), @@ -43,7 +58,7 @@ func NewRoleService(servicePermission PermissionService) RoleService { return roleServiceImpl } -func (svc RoleServiceImpl) Create(ctx context.Context, data model.RoleCreate) (id int, err error) { +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) @@ -60,7 +75,7 @@ func (svc RoleServiceImpl) Create(ctx context.Context, data model.RoleCreate) (i Name: data.Name, Description: data.Description, } - entityRole.SetCreateTimes() + entityRole.SetCreateTime() id, err = svc.repository.Create(ctx, entityRole, data.PermissionsID) if err != nil { return 0, err @@ -68,16 +83,16 @@ func (svc RoleServiceImpl) Create(ctx context.Context, data model.RoleCreate) (i return id, nil } -func (svc RoleServiceImpl) ConnectPermissions(ctx context.Context, data model.RoleConnectToPermissions) (err error) { +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, "role not found") + return fiber.NewError(fiber.StatusNotFound, roleNotFound) } return getErr } if role == nil { - return fiber.NewError(fiber.StatusNotFound, "role not found") + return fiber.NewError(fiber.StatusNotFound, roleNotFound) } for _, id := range data.PermissionsID { permission, getErr := svc.servicePermission.GetByID(ctx, id) @@ -93,21 +108,21 @@ func (svc RoleServiceImpl) ConnectPermissions(ctx context.Context, data model.Ro return nil } -func (svc RoleServiceImpl) GetByID(ctx context.Context, id int) (role *entity.Role, err error) { +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, "role not found") + return nil, fiber.NewError(fiber.StatusNotFound, roleNotFound) } return nil, err } if role == nil { - return nil, fiber.NewError(fiber.StatusNotFound, "role not found") + return nil, fiber.NewError(fiber.StatusNotFound, roleNotFound) } return role, nil } -func (svc RoleServiceImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (roles []model.RoleResponse, total int, err error) { +func (svc *RoleServiceImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (roles []model.RoleResponse, total int, err error) { roleEntities, total, err := svc.repository.GetAll(ctx, filter) if err != nil { return nil, 0, err @@ -125,7 +140,7 @@ func (svc RoleServiceImpl) GetAll(ctx context.Context, filter base.RequestGetAll return roles, total, nil } -func (svc RoleServiceImpl) Update(ctx context.Context, data model.RoleUpdate) (err error) { +func (svc *RoleServiceImpl) Update(ctx context.Context, data model.RoleUpdate) (err error) { data.Name = strings.ToLower(data.Name) roleByName, getErr := svc.repository.GetByName(ctx, data.Name) if getErr != nil && getErr != gorm.ErrRecordNotFound { @@ -138,12 +153,12 @@ func (svc RoleServiceImpl) Update(ctx context.Context, data model.RoleUpdate) (e roleByID, getErr := svc.repository.GetByID(ctx, data.ID) if getErr != nil { if getErr == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, "role not found") + return fiber.NewError(fiber.StatusNotFound, roleNotFound) } return getErr } if roleByID == nil { - return fiber.NewError(fiber.StatusNotFound, "role not found") + return fiber.NewError(fiber.StatusNotFound, roleNotFound) } entityRole := entity.Role{ @@ -159,16 +174,16 @@ func (svc RoleServiceImpl) Update(ctx context.Context, data model.RoleUpdate) (e return nil } -func (svc RoleServiceImpl) Delete(ctx context.Context, id int) (err error) { +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, "role not found") + return fiber.NewError(fiber.StatusNotFound, roleNotFound) } return getErr } if role == nil { - return fiber.NewError(fiber.StatusNotFound, "role not found") + return fiber.NewError(fiber.StatusNotFound, roleNotFound) } err = svc.repository.Delete(ctx, id) if err != nil { diff --git a/service/rbac/role_service_test.go b/service/role/role_service_test.go similarity index 75% rename from service/rbac/role_service_test.go rename to service/role/role_service_test.go index d374948..1b54a19 100644 --- a/service/rbac/role_service_test.go +++ b/service/role/role_service_test.go @@ -11,8 +11,10 @@ import ( "github.com/Lukmanern/gost/database/connector" "github.com/Lukmanern/gost/domain/base" "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/constants" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" + permService "github.com/Lukmanern/gost/service/permission" ) func init() { @@ -20,30 +22,29 @@ func init() { env.ReadConfig("./../../.env") connector.LoadDatabase() - connector.LoadRedisDatabase() + connector.LoadRedisCache() } func TestNewRoleService(t *testing.T) { - permSvc := NewPermissionService() + permSvc := permService.NewPermissionService() svc := NewRoleService(permSvc) if svc == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } } // create 1 role, create 4 permissions // trying to connect - -func TestSuccessCRUD_Role(t *testing.T) { +func TestSuccessCrudRole(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() - permSvc := NewPermissionService() + permSvc := permService.NewPermissionService() if permSvc == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } svc := NewRoleService(permSvc) if svc == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } modelRole := model.RoleCreate{ @@ -55,7 +56,7 @@ func TestSuccessCRUD_Role(t *testing.T) { t.Error("should not error and id should more than zero") } - // save the id for delete the perms + // Save the ID for deleting the permissions permsID := make([]int, 0) for i := 0; i < 3; i++ { modelPerm := model.PermissionCreate{ @@ -64,7 +65,7 @@ func TestSuccessCRUD_Role(t *testing.T) { } permID, createErr := permSvc.Create(ctx, modelPerm) if createErr != nil || permID < 1 { - t.Error("should not error and permID should more than one") + t.Error("should not error and permID should be more than one") } permsID = append(permsID, permID) @@ -77,14 +78,14 @@ func TestSuccessCRUD_Role(t *testing.T) { } }() - // success connect + // Success connect modelConnect := model.RoleConnectToPermissions{ RoleID: roleID, PermissionsID: permsID, } connectErr := svc.ConnectPermissions(ctx, modelConnect) if connectErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } roleByID, getErr := svc.GetByID(ctx, roleID) @@ -92,12 +93,12 @@ func TestSuccessCRUD_Role(t *testing.T) { t.Error("should not error and role not nil") } if len(roleByID.Permissions) != len(permsID) { - t.Error("total of permissions connected by role should equal") + t.Error("total of permissions connected by role should be equal") } roles, total, getAllErr := svc.GetAll(ctx, base.RequestGetAll{Limit: 10, Page: 1}) if len(roles) < 1 || total < 1 || getAllErr != nil { - t.Error("should more than or equal one and not error at all") + t.Error("should be more than or equal to one and not error at all") } updateRoleModel := model.RoleUpdate{ @@ -107,44 +108,44 @@ func TestSuccessCRUD_Role(t *testing.T) { } updateErr := svc.Update(ctx, updateRoleModel) if updateErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } - // value reset + // 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 nil") + t.Error("should not error and roleByID should not be nil") } if roleByID.Name != updateRoleModel.Name || roleByID.Description != updateRoleModel.Description { - t.Error("name and desc should same") + t.Error("name and description should be the same") } deleteErr := svc.Delete(ctx, roleID) if deleteErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } - // value reset + // Value reset roleByID = nil getErr = nil roleByID, getErr = svc.GetByID(ctx, roleID) if getErr == nil || roleByID != nil { - t.Error("should error and roleByID should nil") + t.Error("should error and roleByID should be nil") } } -func TestFailedCRUD_Roles(t *testing.T) { +func TestFailedCrudRoles(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() - permSvc := NewPermissionService() + permSvc := permService.NewPermissionService() if permSvc == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } svc := NewRoleService(permSvc) if svc == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } // failed create: permissions not found @@ -181,7 +182,7 @@ func TestFailedCRUD_Roles(t *testing.T) { } connectErr := svc.ConnectPermissions(ctx, modelConnectFailed) if connectErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } modelConnectFailed = model.RoleConnectToPermissions{ @@ -191,7 +192,7 @@ func TestFailedCRUD_Roles(t *testing.T) { connectErr = nil connectErr = svc.ConnectPermissions(ctx, modelConnectFailed) if connectErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } // failed update @@ -202,12 +203,12 @@ func TestFailedCRUD_Roles(t *testing.T) { } updateErr := svc.Update(ctx, updateRoleModel) if updateErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } // failed delete deleteErr := svc.Delete(ctx, -1) if deleteErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } } diff --git a/service/user/user_service.go b/service/user/user_service.go index 77fe87d..89de676 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -10,33 +10,64 @@ import ( "github.com/go-redis/redis" "github.com/gofiber/fiber/v2" - "golang.org/x/text/cases" - "golang.org/x/text/language" "gorm.io/gorm" "github.com/Lukmanern/gost/database/connector" "github.com/Lukmanern/gost/domain/entity" "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/constants" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/hash" "github.com/Lukmanern/gost/internal/helper" "github.com/Lukmanern/gost/internal/middleware" repository "github.com/Lukmanern/gost/repository/user" emailService "github.com/Lukmanern/gost/service/email" - roleService "github.com/Lukmanern/gost/service/rbac" + roleService "github.com/Lukmanern/gost/service/role" ) type UserService interface { + + // Register function register user account, than send verification-code to email Register(ctx context.Context, user model.UserRegister) (id int, err error) + + // Verification function activates user account with + // verification code that has been sended to the user's email Verification(ctx context.Context, verifyData model.UserVerificationCode) (err error) + + // DeleteUserByVerification function deletes user data if the user account is not yet verified. + // This implies that the email owner hasn't actually registered the email, indicating that + // the user who registered may be making typing errors or may be a hacker attempting to get + // the verification code. DeleteUserByVerification(ctx context.Context, verifyData model.UserVerificationCode) (err error) + + // FailedLoginCounter function counts failed login attempts and stores them in Redis. + // After the N-th attempt to log in with the same IP address results in continuous failures, + // the system will impose a 50-minute ban. During this period, login requests (refer to + // the login function in the user controller) will not be processed. FailedLoginCounter(userIP string, increment bool) (counter int, err error) + + // Login func give user token/ jwt for auth header. Login(ctx context.Context, user model.UserLogin) (token string, err error) + + // Logout function stores the user's active token in Redis, effectively + // blacklisting the token. This ensures that the token cannot be reused + // for authentication (refer to the IsBlacklisted function in internal/middleware). Logout(c *fiber.Ctx) (err error) + + // ForgetPassword func send verification code into user's email ForgetPassword(ctx context.Context, user model.UserForgetPassword) (err error) + + // ResetPassword func resets password by creating + // new password by email and verification code ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) + + // UpdatePassword func updates user's password UpdatePassword(ctx context.Context, user model.UserPasswordUpdate) (err error) + + // UpdateProfile func updates user's profile data UpdateProfile(ctx context.Context, user model.UserProfileUpdate) (err error) + + // MyProfile func shows user's profile data MyProfile(ctx context.Context, id int) (profile model.UserProfile, err error) } @@ -60,14 +91,14 @@ func NewUserService(roleService roleService.RoleService) UserService { repository: repository.NewUserRepository(), emailService: emailService.NewEmailService(), jwtHandler: middleware.NewJWTHandler(), - redis: connector.LoadRedisDatabase(), + redis: connector.LoadRedisCache(), } }) return userAuthService } -func (svc UserServiceImpl) Register(ctx context.Context, user model.UserRegister) (id int, err error) { +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) @@ -98,7 +129,7 @@ func (svc UserServiceImpl) Register(ctx context.Context, user model.UserRegister if getByCodeErr != nil || userGetByCode == nil { break } - counter += 1 + counter++ if counter >= 150 { return 0, errors.New("failed generating verification code") } @@ -110,21 +141,21 @@ func (svc UserServiceImpl) Register(ctx context.Context, user model.UserRegister if hashErr == nil { break } - counter += 1 + counter++ if counter >= 150 { return 0, errors.New("failed hashing user password") } } userEntity := entity.User{ - Name: cases.Title(language.Und).String(user.Name), + Name: helper.ToTitle(user.Name), Email: user.Email, Password: passwordHashed, VerificationCode: &verifCode, ActivatedAt: nil, } // set created_at and updated_at equal to now - userEntity.SetCreateTimes() + userEntity.SetCreateTime() id, err = svc.repository.Create(ctx, userEntity, user.RoleID) if err != nil { return 0, err @@ -148,7 +179,7 @@ func (svc UserServiceImpl) Register(ctx context.Context, user model.UserRegister return id, nil } -func (svc UserServiceImpl) Verification(ctx context.Context, verifyData model.UserVerificationCode) (err error) { +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, @@ -171,7 +202,7 @@ func (svc UserServiceImpl) Verification(ctx context.Context, verifyData model.Us return nil } -func (svc UserServiceImpl) DeleteUserByVerification(ctx context.Context, verifyData model.UserVerificationCode) (err error) { +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, @@ -191,7 +222,7 @@ func (svc UserServiceImpl) DeleteUserByVerification(ctx context.Context, verifyD return nil } -func (svc UserServiceImpl) FailedLoginCounter(userIP string, increment bool) (counter int, err error) { +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) @@ -206,18 +237,18 @@ func (svc UserServiceImpl) FailedLoginCounter(userIP string, increment bool) (co return counter, nil } -func (svc UserServiceImpl) Login(ctx context.Context, user model.UserLogin) (token string, err error) { +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, "data not found") + return "", fiber.NewError(fiber.StatusNotFound, constants.NotFound) } return "", err } if userEntity == nil { - return "", fiber.NewError(fiber.StatusNotFound, "data not found") + return "", fiber.NewError(fiber.StatusNotFound, constants.NotFound) } res, verfiryErr := hash.Verify(userEntity.Password, user.Password) @@ -255,7 +286,7 @@ func (svc UserServiceImpl) Login(ctx context.Context, user model.UserLogin) (tok return token, nil } -func (svc UserServiceImpl) Logout(c *fiber.Ctx) (err error) { +func (svc *UserServiceImpl) Logout(c *fiber.Ctx) (err error) { err = svc.jwtHandler.InvalidateToken(c) if err != nil { return errors.New("problem invalidating token") @@ -264,16 +295,16 @@ func (svc UserServiceImpl) Logout(c *fiber.Ctx) (err error) { return nil } -func (svc UserServiceImpl) ForgetPassword(ctx context.Context, user model.UserForgetPassword) (err error) { +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, "data not found") + return fiber.NewError(fiber.StatusNotFound, constants.NotFound) } return err } if userEntity == nil { - return fiber.NewError(fiber.StatusNotFound, "data not found") + 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." @@ -292,7 +323,7 @@ func (svc UserServiceImpl) ForgetPassword(ctx context.Context, user model.UserFo if getByCodeErr != nil || userGetByCode == nil { break } - counter += 1 + counter++ if counter >= 150 { return errors.New("failed generating verification code") } @@ -324,13 +355,14 @@ func (svc UserServiceImpl) ForgetPassword(ctx context.Context, user model.UserFo return nil } -func (svc UserServiceImpl) ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) { +func (svc *UserServiceImpl) ResetPassword(ctx context.Context, user model.UserResetPassword) (err error) { userByCode, err := svc.repository.GetByConditions(ctx, map[string]any{ + "email =": user.Email, "verification_code =": user.Code, }) if err != nil { if err == gorm.ErrRecordNotFound { - return fiber.NewError(fiber.StatusNotFound, "data not found") + return fiber.NewError(fiber.StatusNotFound, constants.NotFound) } return err } @@ -353,13 +385,14 @@ func (svc UserServiceImpl) ResetPassword(ctx context.Context, user model.UserRes if hashErr == nil { break } - counter += 1 + counter++ if counter >= 150 { return errors.New("failed hashing user password") } } userByCode.VerificationCode = nil + userByCode.SetUpdateTime() updateErr := svc.repository.Update(ctx, *userByCode) if updateErr != nil { return updateErr @@ -372,11 +405,11 @@ func (svc UserServiceImpl) ResetPassword(ctx context.Context, user model.UserRes return nil } -func (svc UserServiceImpl) UpdatePassword(ctx context.Context, user model.UserPasswordUpdate) (err error) { +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, "data not found") + return fiber.NewError(fiber.StatusNotFound, constants.NotFound) } return err } @@ -402,7 +435,7 @@ func (svc UserServiceImpl) UpdatePassword(ctx context.Context, user model.UserPa if hashErr == nil { break } - counter += 1 + counter++ if counter >= 150 { return errors.New("failed hashing user password") } @@ -415,7 +448,7 @@ func (svc UserServiceImpl) UpdatePassword(ctx context.Context, user model.UserPa return nil } -func (svc UserServiceImpl) MyProfile(ctx context.Context, id int) (profile model.UserProfile, err error) { +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 { @@ -440,22 +473,22 @@ func (svc UserServiceImpl) MyProfile(ctx context.Context, id int) (profile model return profile, nil } -func (svc UserServiceImpl) UpdateProfile(ctx context.Context, user model.UserProfileUpdate) (err error) { +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, "data not found") + return fiber.NewError(fiber.StatusNotFound, constants.NotFound) } return err } if userByID == nil { - return fiber.NewError(fiber.StatusNotFound, "data not found") + return fiber.NewError(fiber.StatusNotFound, constants.NotFound) } userEntity := entity.User{ ID: user.ID, - Name: cases.Title(language.Und).String(user.Name), + Name: helper.ToTitle(user.Name), VerificationCode: userByID.VerificationCode, ActivatedAt: userByID.ActivatedAt, } diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go index 6451f94..00aa5c0 100644 --- a/service/user/user_service_test.go +++ b/service/user/user_service_test.go @@ -8,16 +8,16 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "golang.org/x/text/cases" - "golang.org/x/text/language" "github.com/Lukmanern/gost/database/connector" "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/constants" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" "github.com/Lukmanern/gost/internal/middleware" repository "github.com/Lukmanern/gost/repository/user" - rbacService "github.com/Lukmanern/gost/service/rbac" + permService "github.com/Lukmanern/gost/service/permission" + roleService "github.com/Lukmanern/gost/service/role" ) func init() { @@ -25,34 +25,34 @@ func init() { env.ReadConfig("./../../.env") connector.LoadDatabase() - connector.LoadRedisDatabase() + connector.LoadRedisCache() } func TestNewUserService(t *testing.T) { - permSvc := rbacService.NewPermissionService() - roleSvc := rbacService.NewRoleService(permSvc) + permSvc := permService.NewPermissionService() + roleSvc := roleService.NewRoleService(permSvc) svc := NewUserService(roleSvc) if svc == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } } -func Test_SuccessRegister(t *testing.T) { +func TestSuccessRegister(t *testing.T) { defer func() { - connector.LoadRedisDatabase().FlushAll() + connector.LoadRedisCache().FlushAll() }() - permSvc := rbacService.NewPermissionService() - roleSvc := rbacService.NewRoleService(permSvc) + permSvc := permService.NewPermissionService() + roleSvc := roleService.NewRoleService(permSvc) svc := NewUserService(roleSvc) c := helper.NewFiberCtx() ctx := c.Context() if svc == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } userRepo := repository.NewUserRepository() if userRepo == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } modelUserRegis := model.UserRegister{ @@ -74,16 +74,16 @@ func Test_SuccessRegister(t *testing.T) { if getErr != nil || userByID == nil { t.Error("should not error and id should not nil") } - if userByID.Name != cases.Title(language.Und).String(modelUserRegis.Name) || + 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("should not nil") + t.Error(constants.ShouldNotNil) } if userByID.ActivatedAt != nil { - t.Error("should nil") + t.Error(constants.ShouldNil) } // failed login : account is created, @@ -131,7 +131,7 @@ func Test_SuccessRegister(t *testing.T) { Email: userByID.Email, }) if verifErr != nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } // value reset @@ -142,10 +142,10 @@ func Test_SuccessRegister(t *testing.T) { t.Error("should not error and id should not nil") } if userByID.VerificationCode != nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } if userByID.ActivatedAt == nil { - t.Error("should nil") + t.Error(constants.ShouldNil) } // reset value @@ -162,9 +162,6 @@ func Test_SuccessRegister(t *testing.T) { } jwtHandler := middleware.NewJWTHandler() - if !jwtHandler.IsTokenValid(token) { - t.Error("token should valid") - } if jwtHandler.IsBlacklisted(token) { t.Error("should not in black-list") } @@ -174,7 +171,7 @@ func Test_SuccessRegister(t *testing.T) { } forgetPwErr := svc.ForgetPassword(ctx, modelUserForgetPasswd) if forgetPwErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } // value reset @@ -185,21 +182,22 @@ func Test_SuccessRegister(t *testing.T) { t.Error("should not error and id should not nil") } if userByID.VerificationCode == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } if userByID.ActivatedAt == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } passwd := helper.RandomString(12) modelUserResetPasswd := model.UserResetPassword{ + Email: userByID.Email, Code: *userByID.VerificationCode, NewPassword: passwd, NewPasswordConfirm: passwd, } resetErr := svc.ResetPassword(ctx, modelUserResetPasswd) if resetErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } // reset value, login failed @@ -237,7 +235,7 @@ func Test_SuccessRegister(t *testing.T) { } updatePasswdErr := svc.UpdatePassword(ctx, modelUserUpdatePasswd) if updatePasswdErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } // reset value, login success @@ -259,14 +257,14 @@ func Test_SuccessRegister(t *testing.T) { } updateProfileErr := svc.UpdateProfile(ctx, modelUserUpdate) if updateProfileErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } profile, getErr := svc.MyProfile(ctx, userID) if getErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } - if profile.Name != cases.Title(language.Und).String(modelUserUpdate.Name) { + if profile.Name != helper.ToTitle(modelUserUpdate.Name) { t.Error("should equal") } @@ -278,22 +276,22 @@ func Test_SuccessRegister(t *testing.T) { } } -func Test_FailedRegister(t *testing.T) { +func TestFailedRegister(t *testing.T) { defer func() { - connector.LoadRedisDatabase().FlushAll() + connector.LoadRedisCache().FlushAll() }() - permSvc := rbacService.NewPermissionService() - roleSvc := rbacService.NewRoleService(permSvc) + permSvc := permService.NewPermissionService() + roleSvc := roleService.NewRoleService(permSvc) svc := NewUserService(roleSvc) c := helper.NewFiberCtx() ctx := c.Context() if svc == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } userRepo := repository.NewUserRepository() if userRepo == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } modelUserRegis := model.UserRegister{ @@ -316,7 +314,7 @@ func Test_FailedRegister(t *testing.T) { Email: "wrongEmail", }) if verifErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } fiberErr, ok := verifErr.(*fiber.Error) if ok { @@ -330,7 +328,7 @@ func Test_FailedRegister(t *testing.T) { Email: "wrongEmail", }) if deleteUserErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } fiberErr, ok = deleteUserErr.(*fiber.Error) if ok { @@ -344,47 +342,47 @@ func Test_FailedRegister(t *testing.T) { IP: helper.RandomIPAddress(), }) if loginErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } forgetErr := svc.ForgetPassword(ctx, model.UserForgetPassword{Email: "wrong_email@gost.project"}) if forgetErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } verifyErr := svc.ResetPassword(ctx, model.UserResetPassword{Code: "wrong-code"}) if verifyErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } updatePasswdErr := svc.UpdatePassword(ctx, model.UserPasswordUpdate{ID: -1}) if updatePasswdErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } _, getErr := svc.MyProfile(ctx, -10) if getErr == nil { - t.Error("should error") + t.Error(constants.ShouldErr) } } -func Test_Banned_IP_Address(t *testing.T) { +func TestBannedIPAddress(t *testing.T) { defer func() { - connector.LoadRedisDatabase().FlushAll() + connector.LoadRedisCache().FlushAll() }() - permSvc := rbacService.NewPermissionService() - roleSvc := rbacService.NewRoleService(permSvc) + permSvc := permService.NewPermissionService() + roleSvc := roleService.NewRoleService(permSvc) svc := NewUserService(roleSvc) c := helper.NewFiberCtx() ctx := c.Context() if svc == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } for i := 1; i <= 15; i++ { counter, err := svc.FailedLoginCounter(helper.RandomIPAddress(), true) if err != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } if i >= 4 { if counter == i { diff --git a/service/user_management/user_management_service.go b/service/user_management/user_management_service.go index fd8f7aa..e70ae6d 100644 --- a/service/user_management/user_management_service.go +++ b/service/user_management/user_management_service.go @@ -11,23 +11,35 @@ import ( "sync" "github.com/gofiber/fiber/v2" - "golang.org/x/text/cases" - "golang.org/x/text/language" "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) } @@ -50,7 +62,7 @@ func NewUserManagementService() UserManagementService { return userManagementService } -func (svc UserManagementServiceImpl) Create(ctx context.Context, user model.UserCreate) (id int, err error) { +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") @@ -63,11 +75,11 @@ func (svc UserManagementServiceImpl) Create(ctx context.Context, user model.User } userEntity := entity.User{ - Name: cases.Title(language.Und).String(user.Name), + Name: helper.ToTitle(user.Name), Email: user.Email, Password: passwordHashed, } - userEntity.SetCreateTimes() + userEntity.SetCreateTime() roleID := entity.USER if user.IsAdmin { @@ -81,11 +93,11 @@ func (svc UserManagementServiceImpl) Create(ctx context.Context, user model.User return id, nil } -func (svc UserManagementServiceImpl) GetByID(ctx context.Context, id int) (user *model.UserResponse, err error) { +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, "data not found") + return nil, fiber.NewError(fiber.StatusNotFound, constants.NotFound) } return nil, err } @@ -97,12 +109,12 @@ func (svc UserManagementServiceImpl) GetByID(ctx context.Context, id int) (user return user, nil } -func (svc UserManagementServiceImpl) GetByEmail(ctx context.Context, email string) (user *model.UserResponse, err error) { +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, "data not found") + return nil, fiber.NewError(fiber.StatusNotFound, constants.NotFound) } return nil, getErr } @@ -114,7 +126,7 @@ func (svc UserManagementServiceImpl) GetByEmail(ctx context.Context, email strin return user, nil } -func (svc UserManagementServiceImpl) GetAll(ctx context.Context, filter base.RequestGetAll) (users []model.UserResponse, total int, err error) { +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 @@ -132,21 +144,21 @@ func (svc UserManagementServiceImpl) GetAll(ctx context.Context, filter base.Req return users, total, nil } -func (svc UserManagementServiceImpl) Update(ctx context.Context, user model.UserProfileUpdate) (err error) { +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, "data not found") + return fiber.NewError(fiber.StatusNotFound, constants.NotFound) } return getErr } if getUser == nil { - return fiber.NewError(fiber.StatusNotFound, "data not found") + return fiber.NewError(fiber.StatusNotFound, constants.NotFound) } userEntity := entity.User{ ID: user.ID, - Name: cases.Title(language.Und).String(user.Name), + Name: helper.ToTitle(user.Name), // ... // add more fields } @@ -159,16 +171,16 @@ func (svc UserManagementServiceImpl) Update(ctx context.Context, user model.User return nil } -func (svc UserManagementServiceImpl) Delete(ctx context.Context, id int) (err error) { +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, "data not found") + return fiber.NewError(fiber.StatusNotFound, constants.NotFound) } return getErr } if getUser == nil { - return fiber.NewError(fiber.StatusNotFound, "data not found") + return fiber.NewError(fiber.StatusNotFound, constants.NotFound) } err = svc.repository.Delete(ctx, id) diff --git a/service/user_management/user_management_service_test.go b/service/user_management/user_management_service_test.go index 29bb429..71b601b 100644 --- a/service/user_management/user_management_service_test.go +++ b/service/user_management/user_management_service_test.go @@ -10,6 +10,7 @@ import ( "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" @@ -20,13 +21,13 @@ func init() { env.ReadConfig("./../../.env") connector.LoadDatabase() - connector.LoadRedisDatabase() + connector.LoadRedisCache() } func TestNewUserManagementService(t *testing.T) { svc := NewUserManagementService() if svc == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } } @@ -39,12 +40,12 @@ func TestNewUserManagementService(t *testing.T) { // -> get by id (checking) // -> get by email (checking) -func TestSuccessCRUD(t *testing.T) { +func TestSuccessCrud(t *testing.T) { c := helper.NewFiberCtx() ctx := c.Context() svc := NewUserManagementService() if svc == nil || ctx == nil { - t.Error("should not nil") + t.Error(constants.ShouldNotNil) } userModel := model.UserCreate{ @@ -61,8 +62,8 @@ func TestSuccessCRUD(t *testing.T) { svc.Delete(ctx, userID) }() - userByID, getByIdErr := svc.GetByID(ctx, userID) - if 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 != userModel.Name || userByID.Email != userModel.Email { @@ -88,14 +89,14 @@ func TestSuccessCRUD(t *testing.T) { } updateErr := svc.Update(ctx, updateUserData) if updateErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } // reset value - getByIdErr = nil + getByIDErr = nil userByID = nil - userByID, getByIdErr = svc.GetByID(ctx, userID) - if 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 { @@ -104,17 +105,17 @@ func TestSuccessCRUD(t *testing.T) { deleteErr := svc.Delete(ctx, userID) if deleteErr != nil { - t.Error("should not error") + t.Error(constants.ShouldNotErr) } // reset value - getByIdErr = nil + getByIDErr = nil userByID = nil - userByID, getByIdErr = svc.GetByID(ctx, userID) - if 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) + fiberErr, ok := getByIDErr.(*fiber.Error) if ok { if fiberErr.Code != fiber.StatusNotFound { t.Error("should error 404") diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index e160c35..0000000 --- a/tests/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/log -/logs -*.log -*.logs \ No newline at end of file diff --git a/tests/user_dev_test.go b/tests/user_dev_test.go deleted file mode 100644 index d051bd1..0000000 --- a/tests/user_dev_test.go +++ /dev/null @@ -1,186 +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 test - -import ( - "encoding/json" - "log" - "net/http" - "reflect" - "strings" - "testing" - "time" - - "github.com/Lukmanern/gost/application" - "github.com/Lukmanern/gost/database/connector" - "github.com/Lukmanern/gost/domain/model" - "github.com/Lukmanern/gost/internal/env" - "github.com/Lukmanern/gost/internal/helper" - "github.com/Lukmanern/gost/internal/response" - "github.com/gofiber/fiber/v2" - "golang.org/x/text/cases" - "golang.org/x/text/language" - - 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 - dbURI := config.GetDatabaseURI() - privKey := config.GetPrivateKey() - pubKey := config.GetPublicKey() - if dbURI == "" || privKey == nil || pubKey == nil { - log.Fatal("Database URI or keys aren't valid") - } - - connector.LoadDatabase() - r := connector.LoadRedisDatabase() - r.FlushAll() // clear all key:value in redis - - userDevService = service.NewUserManagementService() - userDevController = controller.NewUserManagementController(userDevService) -} - -func Test_Create(t *testing.T) { - go application.RunApp() - time.Sleep(4 * time.Second) - - ctr := userDevController - if ctr == nil { - t.Error("should not nil") - } - c := helper.NewFiberCtx() - if ctr == nil || c == nil { - t.Error("should not error") - } - - ctx := c.Context() - if ctx == nil { - t.Error("should not nil") - } - modelUserCreate := model.UserCreate{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - IsAdmin: true, - } - userID, createErr := userDevService.Create(ctx, modelUserCreate) - if createErr != nil || userID < 1 { - t.Error("should not error and got id more or equal than 1") - } - defer func() { - userDevService.Delete(ctx, userID) - }() - - testCases := []struct { - CaseName string - payload model.UserCreate - resp response.Response - wantHttpCode int - }{ - { - CaseName: "failed create: email has already used", - payload: model.UserCreate{ - Name: helper.RandomString(10), - Email: modelUserCreate.Email, - Password: helper.RandomString(11), - IsAdmin: true, - }, - resp: response.Response{ - Data: nil, - Success: false, - Message: "", - }, - wantHttpCode: http.StatusBadRequest, - }, - { - CaseName: "success create", - payload: model.UserCreate{ - Name: helper.RandomString(10), - Email: helper.RandomEmail(), - Password: helper.RandomString(11), - IsAdmin: true, - }, - resp: response.Response{ - Data: nil, - Success: true, - Message: response.MessageSuccessCreated, - }, - wantHttpCode: http.StatusCreated, - }, - } - - for _, tc := range testCases { - jsonObject, err := json.Marshal(&tc.payload) - if err != nil { - t.Error("should not error", err.Error()) - } - req, httpReqErr := http.NewRequest(http.MethodPost, appUrl+"user-management/create", strings.NewReader(string(jsonObject))) - if httpReqErr != nil { - t.Fatal("should not nil") - } - req.Close = true - req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - client := &http.Client{ - Transport: &http.Transport{}, - } - resp, clientErr := client.Do(req) - if clientErr != nil { - t.Fatalf("HTTP request failed: %v", clientErr) - } - defer resp.Body.Close() - if resp.StatusCode != tc.wantHttpCode { - t.Error("should equal") - } - - respModel := response.Response{} - decodeErr := json.NewDecoder(resp.Body).Decode(&respModel) - if decodeErr != nil { - t.Error("should not error", decodeErr) - } - - if tc.resp.Success != respModel.Success { - t.Fatal("should equal") - } - if tc.resp.Message != "" { - if tc.resp.Message != respModel.Message { - t.Error("should equal") - } - } - if tc.resp.Data != nil { - if !reflect.DeepEqual(tc.resp.Data, respModel.Data) { - t.Error("should equal") - } - } - if respModel.Success { - userByEmail, getErr := userDevService.GetByEmail(ctx, tc.payload.Email) - if userByEmail.Name != cases.Title(language.Und).String(tc.payload.Name) { - t.Error("should equal") - } - if getErr != nil { - t.Error("should not error", getErr) - } - deleteErr := userDevService.Delete(ctx, userByEmail.ID) - if deleteErr != nil { - t.Error("should not error", deleteErr) - } - } - } -} - -// Create(c *fiber.Ctx) error -// Get(c *fiber.Ctx) error -// GetAll(c *fiber.Ctx) error -// Update(c *fiber.Ctx) error -// Delete(c *fiber.Ctx) error