From 385a0a23ea8d1e7d787941b2e8a09623fb1f1d40 Mon Sep 17 00:00:00 2001 From: LukmanE22 Date: Sat, 27 Jan 2024 14:46:17 +0700 Subject: [PATCH] add user controller test --- controller/role/role_controller.go | 10 +- controller/role/role_controller_test.go | 1 + controller/user/user_controller.go | 29 +- controller/user/user_controller_test.go | 864 ++++++++++++++++++++++++ domain/model/user.go | 48 +- internal/helper/helper.go | 12 + internal/middleware/middleware_test.go | 37 +- service/role/role_service.go | 10 +- service/user/user_service.go | 6 +- service/user/user_service_test.go | 4 +- 10 files changed, 936 insertions(+), 85 deletions(-) create mode 100644 controller/role/role_controller_test.go create mode 100644 controller/user/user_controller_test.go diff --git a/controller/role/role_controller.go b/controller/role/role_controller.go index efcbe43..6c46ebc 100644 --- a/controller/role/role_controller.go +++ b/controller/role/role_controller.go @@ -15,19 +15,11 @@ import ( ) type RoleController interface { - // Create func creates a new role + // auth + admin Create(c *fiber.Ctx) error - - // Get func gets a role Get(c *fiber.Ctx) error - - // GetAll func gets some roles GetAll(c *fiber.Ctx) error - - // Update func updates a role Update(c *fiber.Ctx) error - - // Delete func deletes a role Delete(c *fiber.Ctx) error } diff --git a/controller/role/role_controller_test.go b/controller/role/role_controller_test.go new file mode 100644 index 0000000..4ea4e28 --- /dev/null +++ b/controller/role/role_controller_test.go @@ -0,0 +1 @@ +package controller diff --git a/controller/user/user_controller.go b/controller/user/user_controller.go index 86130fd..fe0755e 100644 --- a/controller/user/user_controller.go +++ b/controller/user/user_controller.go @@ -10,6 +10,7 @@ import ( "github.com/Lukmanern/gost/domain/model" "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/helper" "github.com/Lukmanern/gost/internal/middleware" "github.com/Lukmanern/gost/internal/response" service "github.com/Lukmanern/gost/service/user" @@ -22,14 +23,14 @@ type UserController interface { Login(c *fiber.Ctx) error ForgetPassword(c *fiber.Ctx) error ResetPassword(c *fiber.Ctx) error - // auth+admin + // auth + admin GetAll(c *fiber.Ctx) error // auth MyProfile(c *fiber.Ctx) error Logout(c *fiber.Ctx) error UpdateProfile(c *fiber.Ctx) error UpdatePassword(c *fiber.Ctx) error - Delete(c *fiber.Ctx) error + DeleteAccount(c *fiber.Ctx) error } type UserControllerImpl struct { @@ -58,6 +59,9 @@ func (ctr *UserControllerImpl) Register(c *fiber.Ctx) error { } user.Email = strings.ToLower(user.Email) validate := validator.New() + if len(user.RoleIDs) < 1 { + return response.BadRequest(c, "please choose one or more role") + } if err := validate.Struct(&user); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } @@ -116,11 +120,10 @@ func (ctr *UserControllerImpl) AccountActivation(c *fiber.Ctx) error { func (ctr *UserControllerImpl) Login(c *fiber.Ctx) error { var user model.UserLogin - // user.IP = c.IP() // Note : uncomment this line in production if err := c.BodyParser(&user); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) } - + user.IP = helper.RandomIPAddress() // Todo : update to c.IP() validate := validator.New() if err := validate.Struct(&user); err != nil { return response.BadRequest(c, consts.InvalidJSONBody+err.Error()) @@ -345,6 +348,22 @@ func (ctr *UserControllerImpl) UpdatePassword(c *fiber.Ctx) error { return response.SuccessNoContent(c) } -func (ctr *UserControllerImpl) Delete(c *fiber.Ctx) error { +func (ctr *UserControllerImpl) DeleteAccount(c *fiber.Ctx) error { + userClaims, ok := c.Locals("claims").(*middleware.Claims) + if !ok || userClaims == nil { + return response.Unauthorized(c) + } + + ctx := c.Context() + err := ctr.service.DeleteAccount(ctx, userClaims.ID) + if err != nil { + fiberErr, ok := err.(*fiber.Error) + if ok { + return response.CreateResponse(c, fiberErr.Code, response.Response{ + Message: fiberErr.Message, Success: false, Data: nil, + }) + } + return response.Error(c, consts.ErrServer) + } return response.SuccessNoContent(c) } diff --git a/controller/user/user_controller_test.go b/controller/user/user_controller_test.go new file mode 100644 index 0000000..0213903 --- /dev/null +++ b/controller/user/user_controller_test.go @@ -0,0 +1,864 @@ +package controller + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + + "github.com/Lukmanern/gost/database/connector" + "github.com/Lukmanern/gost/domain/entity" + "github.com/Lukmanern/gost/domain/model" + "github.com/Lukmanern/gost/internal/consts" + "github.com/Lukmanern/gost/internal/env" + "github.com/Lukmanern/gost/internal/hash" + "github.com/Lukmanern/gost/internal/helper" + "github.com/Lukmanern/gost/internal/middleware" + "github.com/Lukmanern/gost/internal/response" + repository "github.com/Lukmanern/gost/repository/user" + service "github.com/Lukmanern/gost/service/user" +) + +const ( + headerTestName string = "at UserController Test" +) + +var ( + baseURL string + timeNow time.Time + adminRepo repository.UserRepository +) + +func init() { + envFilePath := "./../../.env" + env.ReadConfig(envFilePath) + config := env.Configuration() + baseURL = config.AppURL + timeNow = time.Now() + adminRepo = repository.NewUserRepository() + + connector.LoadDatabase() + r := connector.LoadRedisCache() + r.FlushAll() // clear all key:value in redis +} + +type testCase struct { + Name string + ResCode int + Payload any +} + +func TestUnauthorized(t *testing.T) { + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + + handlers := []func(c *fiber.Ctx) error{ + controller.GetAll, + controller.MyProfile, + controller.Logout, + controller.UpdateProfile, + controller.UpdatePassword, + controller.DeleteAccount, + } + for _, handler := range handlers { + c := helper.NewFiberCtx() + c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + handler(c) + res := c.Response() + assert.Equalf(t, res.StatusCode(), fiber.StatusUnauthorized, "Expected response code %d, but got %d", fiber.StatusUnauthorized, res.StatusCode()) + } +} + +func TestJSONParser(t *testing.T) { + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + fakeClaims := middleware.Claims{ + Email: helper.RandomEmail(), + Roles: map[string]uint8{"Full Access": 1}, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "999", + ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(5 * time.Minute)}, + NotBefore: &jwt.NumericDate{Time: time.Now()}, + IssuedAt: &jwt.NumericDate{Time: time.Now()}, + }, + } + + handlers := []func(c *fiber.Ctx) error{ + controller.Register, + controller.AccountActivation, + controller.Login, + controller.ForgetPassword, + controller.ResetPassword, + } + for _, handler := range handlers { + c := helper.NewFiberCtx() + c.Request().Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + c.Locals("claims", &fakeClaims) + handler(c) + res := c.Response() + expectCode := fiber.StatusBadRequest + assert.Equalf(t, res.StatusCode(), expectCode, "Expected response code %d, but got %d", expectCode, res.StatusCode()) + } +} + +func TestRegister(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer repository.Delete(ctx, validUser.ID) + + testCases := []testCase{ + { + Name: "Success Register -1", + ResCode: fiber.StatusCreated, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: helper.RandomString(11), + Email: helper.RandomEmail(), + Password: helper.RandomString(12), + }, + }, + { + Name: "Success Register -2", + ResCode: fiber.StatusCreated, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: helper.RandomString(10), + Email: helper.RandomEmail(), + Password: helper.RandomString(12), + }, + }, + { + Name: "Failed Register -1: email is already used", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: validUser.Name, + Email: validUser.Email, + Password: helper.RandomString(12), + }, + }, + { + Name: "Failed Register -2: invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: helper.RandomString(10), + Email: "invalid email", + Password: helper.RandomString(12), + }, + }, + { + Name: "Failed Register -3: password too short", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: []int{1, 2}, + Name: helper.RandomString(10), + Email: helper.RandomEmail(), + Password: "--", + }, + }, + { + Name: "Failed Register -4: no role id", + ResCode: fiber.StatusBadRequest, + Payload: model.UserRegister{ + RoleIDs: nil, + Name: helper.RandomString(10), + Email: helper.RandomEmail(), + Password: helper.RandomString(10), + }, + }, + } + + pathURL := "user/register" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, controller.Register) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode == fiber.StatusCreated { + payload, ok := tc.Payload.(model.UserRegister) + assert.True(t, ok, "should true", headerTestName) + log.Println(payload) + entityUser, getErr := repository.GetByEmail(ctx, payload.Email) + assert.NoError(t, getErr, consts.ShouldNotErr, headerTestName) + deleteErr := repository.Delete(ctx, entityUser.ID) + assert.NoError(t, deleteErr, consts.ShouldNotErr, headerTestName) + } + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestLogin(t *testing.T) { + // Initialize repository, service and controller + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + entityUser := createUser() + defer repository.Delete(ctx, entityUser.ID) + + testCases := []testCase{ + { + Name: "Success Login -1", + ResCode: fiber.StatusOK, + Payload: model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }, + }, + { + Name: "Success Login -2", + ResCode: fiber.StatusOK, + Payload: model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }, + }, + { + Name: "Failed Login -1 : invalid email", + ResCode: fiber.StatusBadRequest, + Payload: model.UserLogin{ + Email: "invalid-email-", + Password: entityUser.Password, + }, + }, + { + Name: "Failed Login -2 : data not found", + ResCode: fiber.StatusNotFound, + Payload: model.UserLogin{ + Email: "validemail@gost.project", + Password: entityUser.Password, + }, + }, + { + Name: "Failed Login -3 : password too short", + ResCode: fiber.StatusBadRequest, + Payload: model.UserLogin{ + Email: entityUser.Email, + Password: "--", + }, + }, + } + + pathURL := "user/login" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, controller.Login) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestLogout(t *testing.T) { + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + + tokens := make([]string, 1) + for i := range tokens { + tokens[i] = helper.GenerateToken() + } + + type testCase struct { + Name string + ResCode int + Token string + } + + testCases := []testCase{ + { + Name: "Failed Login -1: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "--", + }, + { + Name: "Failed Login -2: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "INVALID-TOKEN", + }, + } + + for i, token := range tokens { + testCases = append(testCases, testCase{ + Name: "Success Logout -" + strconv.Itoa(i+2), + ResCode: fiber.StatusNoContent, + Token: token, + }) + testCases = append(testCases, testCase{ + Name: "Failed Logout -" + strconv.Itoa(i+3), + ResCode: fiber.StatusUnauthorized, + Token: token, + }) + } + + pathURL := "user/logout" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPost, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Post(pathURL, jwtHandler.IsAuthenticated, controller.Logout) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestMyProfile(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + tokens := make([]string, 2) + for i := range tokens { + tokens[i] = helper.GenerateToken() + } + + entityUser := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }) + defer repository.Delete(ctx, entityUser.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) + + type testCase struct { + Name string + ResCode int + Token string + } + + testCases := []testCase{ + { + Name: "Success Get My Profile -1", + ResCode: fiber.StatusOK, + Token: validToken, + }, + { + Name: "Failed Get My Profile -1: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "--", + }, + { + Name: "Failed Get My Profile -2: invalid token", + ResCode: fiber.StatusUnauthorized, + Token: "INVALID-TOKEN", + }, + } + + for i, token := range tokens { + testCases = append(testCases, testCase{ + Name: "Failed Get My Profile -" + strconv.Itoa(i+3), + ResCode: fiber.StatusNotFound, + Token: token, + }) + } + + pathURL := "user/my-profile" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodGet, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Get(pathURL, jwtHandler.IsAuthenticated, controller.MyProfile) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestGetAll(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + token := helper.GenerateToken() + assert.True(t, token != "", consts.ShouldNotNil, headerTestName) + + type testCase struct { + Name string + Params string + ResCode int + WantErr bool + } + + testCases := []testCase{ + { + Name: "Success get all -1", + Params: "?limit=100&page=1", + ResCode: fiber.StatusOK, + WantErr: false, + }, + { + Name: "Success get all -2", + Params: "?limit=12&page=1", + ResCode: fiber.StatusOK, + WantErr: false, + }, + { + Name: "Failed get all: invalid limit", + Params: "?limit=-1&page=1", + ResCode: fiber.StatusBadRequest, + WantErr: true, + }, + { + Name: "Failed get all: invalid page", + Params: "?limit=1&page=-1", + ResCode: fiber.StatusBadRequest, + WantErr: true, + }, + { + Name: "Failed get all: invalid sort", + Params: "?limit=1&page=1&sort=invalid", // sort should name + ResCode: fiber.StatusInternalServerError, + WantErr: true, + }, + } + + pathURL := "user/" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodGet, URL+tc.Params, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Get(pathURL, jwtHandler.IsAuthenticated, controller.GetAll) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, res.StatusCode, tc.ResCode, consts.ShouldEqual, res.StatusCode, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestUpdateProfile(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + validUser := createUser() + defer service.DeleteAccount(ctx, validUser.ID) + + validToken, err := service.Login(ctx, model.UserLogin{ + Email: validUser.Email, + Password: validUser.Password, + }) + assert.NoError(t, err, consts.ShouldNil, err, headerTestName) + + type testCase struct { + Name string + ResCode int + Payload model.UserUpdate + Token string + } + + testCases := []testCase{ + { + Name: "Success Update Profile -1", + ResCode: fiber.StatusNoContent, + Payload: model.UserUpdate{ + Name: "test update name", + }, + Token: validToken, + }, + { + Name: "Failed Update Profile -1: Invalid Token", + ResCode: fiber.StatusUnauthorized, + Payload: model.UserUpdate{ + Name: "test update", + }, + Token: "invalid-token", + }, + { + Name: "Failed Update Profile -2: Name too short", + ResCode: fiber.StatusBadRequest, + Payload: model.UserUpdate{ + Name: "", + }, + Token: helper.GenerateToken(), // valid token + }, + } + + pathURL := "user/profile" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPut, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Put(pathURL, jwtHandler.IsAuthenticated, controller.UpdateProfile) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr, headerTestName) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode, tc.Name, headerTestName) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestUpdatePassword(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + fakeToken := helper.GenerateToken() + + entityUser := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }) + defer repository.Delete(ctx, entityUser.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) + + type testCase struct { + Name string + ResCode int + Payload model.UserPasswordUpdate + Token string + } + + testCases := []testCase{ + { + Name: "Success Update Password", + ResCode: fiber.StatusNoContent, + Token: validToken, + Payload: model.UserPasswordUpdate{ + OldPassword: entityUser.Password, + NewPassword: entityUser.Password + "00", + NewPasswordConfirm: entityUser.Password + "00", + }, + }, + { + Name: "Failed Update Password -1: user not found (invalid token)", + ResCode: fiber.StatusNotFound, + Token: fakeToken, + Payload: model.UserPasswordUpdate{ + OldPassword: entityUser.Password, + NewPassword: entityUser.Password + "00", + NewPasswordConfirm: entityUser.Password + "00", + }, + }, + { + Name: "Failed Update Password -2: old and new password is equal", + ResCode: fiber.StatusBadRequest, + Token: validToken, + Payload: model.UserPasswordUpdate{ + OldPassword: entityUser.Password, + NewPassword: entityUser.Password, + NewPasswordConfirm: entityUser.Password, + }, + }, + { + Name: "Failed Update Password -3: new and new password confirm is not equal", + ResCode: fiber.StatusBadRequest, + Token: fakeToken, + Payload: model.UserPasswordUpdate{ + OldPassword: entityUser.Password, + NewPassword: entityUser.Password + "000", + NewPasswordConfirm: entityUser.Password + "00", + }, + }, + { + Name: "Failed Update Password -4: password too short", + ResCode: fiber.StatusBadRequest, + Token: fakeToken, + Payload: model.UserPasswordUpdate{ + OldPassword: "", + NewPassword: "" + "000", + NewPasswordConfirm: "" + "00", + }, + }, + } + + pathURL := "user/update-password" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Marshal payload to JSON + jsonData, marshalErr := json.Marshal(&tc.Payload) + assert.NoError(t, marshalErr, consts.ShouldNotErr, marshalErr) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodPut, URL, bytes.NewReader(jsonData)) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Put(pathURL, jwtHandler.IsAuthenticated, controller.UpdatePassword) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} + +func TestDeleteAccount(t *testing.T) { + repository := repository.NewUserRepository() + assert.NotNil(t, repository, consts.ShouldNotNil, headerTestName) + service := service.NewUserService() + assert.NotNil(t, service, consts.ShouldNotNil, headerTestName) + controller := NewUserController(service) + assert.NotNil(t, controller, consts.ShouldNotNil, headerTestName) + jwtHandler := middleware.NewJWTHandler() + assert.NotNil(t, jwtHandler, consts.ShouldNotNil, headerTestName) + ctx := helper.NewFiberCtx().Context() + assert.NotNil(t, ctx, consts.ShouldNotNil, headerTestName) + + fakeToken := helper.GenerateToken() + + entityUser := createUser() + validToken, loginErr := service.Login(ctx, model.UserLogin{ + Email: entityUser.Email, + Password: entityUser.Password, + }) + defer repository.Delete(ctx, entityUser.ID) + assert.NoError(t, loginErr, consts.ShouldNotErr, headerTestName) + + type testCase struct { + Name string + ResCode int + Token string + } + + testCases := []testCase{ + { + Name: "Success Delete Account -1", + ResCode: fiber.StatusNoContent, + Token: validToken, + }, + { + Name: "Failed Delete Account -1: user not found (invalid token)", + ResCode: fiber.StatusNotFound, + Token: fakeToken, // fake but valid + }, + { + Name: "Failed Delete Account -2: user already deleted", + ResCode: fiber.StatusNotFound, + Token: validToken, // is deleted before + }, + } + + pathURL := "user" + URL := baseURL + pathURL + for _, tc := range testCases { + log.Println(tc.Name, headerTestName) + + // Create HTTP request + req := httptest.NewRequest(fiber.MethodDelete, URL, nil) + req.Header.Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", tc.Token)) + req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + // Set up Fiber app and handle the request with the controller + app := fiber.New() + app.Delete(pathURL, jwtHandler.IsAuthenticated, controller.DeleteAccount) + req.Close = true + + // run test + res, testErr := app.Test(req, -1) + assert.Nil(t, testErr, consts.ShouldNil, testErr) + defer res.Body.Close() + assert.Equal(t, tc.ResCode, res.StatusCode, consts.ShouldEqual, res.StatusCode) + + if res.StatusCode != fiber.StatusNoContent { + responseStruct := response.Response{} + err := json.NewDecoder(res.Body).Decode(&responseStruct) + assert.NoErrorf(t, err, "Failed to parse response JSON: %v", err) + } + } +} +func createUser() entity.User { + pw := helper.RandomString(15) + pwHashed, _ := hash.Generate(pw) + repo := adminRepo + ctx := helper.NewFiberCtx().Context() + data := entity.User{ + Name: helper.RandomString(15), + Email: helper.RandomEmail(), + Password: pwHashed, + ActivatedAt: &timeNow, + } + data.SetCreateTime() + id, err := repo.Create(ctx, data, []int{1, 2}) + if err != nil { + log.Fatal("Failed create user", headerTestName) + } + data.Password = pw + data.ID = id + return data +} diff --git a/domain/model/user.go b/domain/model/user.go index 9bf37df..43b6be1 100644 --- a/domain/model/user.go +++ b/domain/model/user.go @@ -2,16 +2,14 @@ package model import ( "time" - - "github.com/Lukmanern/gost/domain/entity" ) type User struct { - ID int `gorm:"type:bigserial;primaryKey" json:"id"` - Name string `gorm:"type:varchar(100) not null" json:"name"` - Email string `gorm:"type:varchar(100) not null unique" json:"email"` - Password string `gorm:"type:varchar(255) not null" json:"password"` - ActivatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"activated_at"` + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Password string `json:"password"` + ActivatedAt *time.Time `json:"activated_at"` } type UserRegister struct { @@ -32,20 +30,13 @@ type UserLogin struct { IP string `validate:"required,min=4,max=20" json:"ip"` } -// ID int `gorm:"type:bigserial;primaryKey" json:"id"` -// Name string `gorm:"type:varchar(100) not null" json:"name"` -// Email string `gorm:"type:varchar(100) not null unique" json:"email"` -// Password string `gorm:"type:varchar(255) not null" json:"password"` -// ActivatedAt *time.Time `gorm:"type:timestamp null;default:null" json:"activated_at"` -// Roles []Role `gorm:"many2many:user_has_roles" json:"roles"` - type UserUpdate struct { - ID int `gorm:"type:bigserial;primaryKey" json:"id"` - Name string `gorm:"type:varchar(100) not null" json:"name"` + ID int `validate:"required,numeric,min=1" json:"id"` + Name string `validate:"required,min=2,max=60" json:"name"` } type UserUpdateRoles struct { - ID int `gorm:"type:bigserial;primaryKey" json:"id"` + ID int `validate:"required,numeric,min=1" json:"id"` RoleIDs []int `validate:"required" json:"role_id"` } @@ -61,29 +52,8 @@ type UserResetPassword struct { } type UserPasswordUpdate struct { - ID int `validate:"required,numeric,min=1"` + ID int `validate:"required,numeric,min=1" json:"id"` OldPassword string `validate:"required,min=8,max=30" json:"old_password"` NewPassword string `validate:"required,min=8,max=30" json:"new_password"` NewPasswordConfirm string `validate:"required,min=8,max=30" json:"new_password_confirm"` } - -type UserProfile struct { - Email string - Name string - ActivatedAt *time.Time - Roles []string -} - -type UserResponse struct { - ID int - Name string - ActivatedAt *time.Time -} - -type UserResponseDetail struct { - ID int - Email string - Name string - ActivatedAt *time.Time - Roles []entity.Role -} diff --git a/internal/helper/helper.go b/internal/helper/helper.go index f8ad03a..9c750d6 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/Lukmanern/gost/internal/middleware" "github.com/XANi/loremipsum" "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" @@ -83,6 +84,17 @@ func ToTitle(s string) string { return cases.Title(language.Und).String(s) } +// Generate token for admin role : Full Access +func GenerateToken() string { + jwtHandler := middleware.NewJWTHandler() + expire := time.Now().Add(15 * time.Hour) + token, err := jwtHandler.GenerateJWT(GenerateRandomID(), RandomEmail(), map[string]uint8{"admin": 1}, expire) + if err != nil { + return "" + } + return token +} + func GenerateRandomID() int { rand.New(rand.NewSource(time.Now().UnixNano())) min := 9000000 diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index 026ebf4..513c2d6 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -1,4 +1,4 @@ -package middleware +package middleware_test import ( "testing" @@ -7,6 +7,7 @@ import ( "github.com/Lukmanern/gost/internal/consts" "github.com/Lukmanern/gost/internal/env" "github.com/Lukmanern/gost/internal/helper" + "github.com/Lukmanern/gost/internal/middleware" "github.com/gofiber/fiber/v2" ) @@ -36,19 +37,19 @@ func init() { } } -func TestNewJWTHandler(t *testing.T) { - jwtHandler := NewJWTHandler() - if jwtHandler.publicKey == nil { - t.Errorf("Public key parsing should have failed") - } +// func TestNewJWTHandler(t *testing.T) { +// jwtHandler := middleware.NewJWTHandler() +// if jwtHandler.publicKey == nil { +// t.Errorf("Public key parsing should have failed") +// } - if jwtHandler.privateKey == nil { - t.Errorf("Private key parsing should have failed") - } -} +// if jwtHandler.privateKey == nil { +// t.Errorf("Private key parsing should have failed") +// } +// } func TestGenerateClaims(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() token, err := jwtHandler.GenerateJWT(1, params.Email, params.Roles, params.Exp) if err != nil || token == "" { t.Fatal("should not error") @@ -80,7 +81,7 @@ func TestGenerateClaims(t *testing.T) { } func TestJWTHandlerInvalidateToken(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("error while generating token") @@ -102,7 +103,7 @@ func TestJWTHandlerInvalidateToken(t *testing.T) { } func TestJWTHandlerIsBlacklisted(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() cookie, err := jwtHandler.GenerateJWT(1000, helper.RandomEmail(), params.Roles, time.Now().Add(1*time.Hour)) @@ -115,7 +116,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { } tests := []struct { name string - j JWTHandler + j middleware.JWTHandler args args want bool }{ @@ -136,7 +137,7 @@ func TestJWTHandlerIsBlacklisted(t *testing.T) { } func TestJWTHandlerIsAuthenticated(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("error while generating token") @@ -146,7 +147,7 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { } func() { - jwtHandler1 := NewJWTHandler() + jwtHandler1 := middleware.NewJWTHandler() c := helper.NewFiberCtx() jwtHandler1.IsAuthenticated(c) c.Status(fiber.StatusUnauthorized) @@ -162,7 +163,7 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { t.Error("should not panic", r) } }() - jwtHandler3 := NewJWTHandler() + jwtHandler3 := middleware.NewJWTHandler() c := helper.NewFiberCtx() c.Request().Header.Add(fiber.HeaderAuthorization, " "+token) c.Status(fiber.StatusUnauthorized) @@ -174,7 +175,7 @@ func TestJWTHandlerIsAuthenticated(t *testing.T) { } func TestJWTHandlerCheckHasRole(t *testing.T) { - jwtHandler := NewJWTHandler() + jwtHandler := middleware.NewJWTHandler() token, err := jwtHandler.GenerateJWT(params.ID, params.Email, params.Roles, params.Exp) if err != nil { t.Error("Error while generating token:", err) diff --git a/service/role/role_service.go b/service/role/role_service.go index 6bb8fea..84e4ca5 100644 --- a/service/role/role_service.go +++ b/service/role/role_service.go @@ -16,19 +16,11 @@ import ( ) type RoleService interface { - // Create func create one role. + // auth + admin Create(ctx context.Context, data model.RoleCreate) (id int, err error) - - // GetByID func get one role. GetByID(ctx context.Context, id int) (role model.RoleResponse, err error) - - // GetAll func get some roles. GetAll(ctx context.Context, filter model.RequestGetAll) (roles []model.RoleResponse, total int, err error) - - // Update func update one role. Update(ctx context.Context, data model.RoleUpdate) (err error) - - // Delete func delete one role. Delete(ctx context.Context, id int) (err error) } diff --git a/service/user/user_service.go b/service/user/user_service.go index 24d5dbb..d193347 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -34,7 +34,7 @@ type UserService interface { Logout(c *fiber.Ctx) (err error) UpdateProfile(ctx context.Context, data model.UserUpdate) (err error) UpdatePassword(ctx context.Context, data model.UserPasswordUpdate) (err error) - Delete(ctx context.Context, id int) (err error) + DeleteAccount(ctx context.Context, id int) (err error) } type UserServiceImpl struct { @@ -80,7 +80,7 @@ func (svc *UserServiceImpl) Register(ctx context.Context, data model.UserRegiste } for _, roleID := range data.RoleIDs { - enttRole, err := svc.repository.GetByID(ctx, roleID) + enttRole, err := svc.roleRepo.GetByID(ctx, roleID) if err == gorm.ErrRecordNotFound { return 0, fiber.NewError(fiber.StatusNotFound, consts.NotFound) } @@ -215,7 +215,7 @@ func (svc *UserServiceImpl) UpdatePassword(ctx context.Context, data model.UserP return nil } -func (svc *UserServiceImpl) Delete(ctx context.Context, id int) (err error) { +func (svc *UserServiceImpl) DeleteAccount(ctx context.Context, id int) (err error) { user, getErr := svc.repository.GetByID(ctx, id) if getErr == gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusNotFound, consts.NotFound) diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go index 3fc6b6e..1a47760 100644 --- a/service/user/user_service_test.go +++ b/service/user/user_service_test.go @@ -102,7 +102,7 @@ func TestRegister(t *testing.T) { assert.NoError(t, getErr, consts.ShouldNotErr, tc.Name, headerTestName) assert.NotNil(t, user, consts.ShouldNotNil, tc.Name, headerTestName) - deleteErr := service.Delete(ctx, id) + deleteErr := service.DeleteAccount(ctx, id) assert.NoError(t, deleteErr, consts.ShouldNotErr, tc.Name, headerTestName) // value reset @@ -504,7 +504,7 @@ func TestDelete(t *testing.T) { for _, tc := range testCases { log.Println(tc.Name, headerTestName) - deleteErr := service.Delete(ctx, tc.ID) + deleteErr := service.DeleteAccount(ctx, tc.ID) if tc.WantErr { assert.Error(t, deleteErr, consts.ShouldErr, tc.Name, headerTestName) continue