From db8cc3d515140c9daf1777fe43a9043eba2a0846 Mon Sep 17 00:00:00 2001 From: Cyberguru1 Date: Tue, 6 Aug 2024 06:04:51 -0500 Subject: [PATCH 1/5] feat: implement update profile endpoint --- internal/models/profile.go | 43 ++++++++++++++++----- internal/models/user.go | 13 +++++++ pkg/controller/profile/profile.go | 64 +++++++++++++++++++++++++++++++ pkg/router/profile.go | 26 +++++++++++++ pkg/router/router.go | 1 + services/profile/profile.go | 35 +++++++++++++++++ 6 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 pkg/controller/profile/profile.go create mode 100644 pkg/router/profile.go create mode 100644 services/profile/profile.go diff --git a/internal/models/profile.go b/internal/models/profile.go index cc416765..04e8fd0a 100644 --- a/internal/models/profile.go +++ b/internal/models/profile.go @@ -1,19 +1,44 @@ package models import ( + "errors" "time" "gorm.io/gorm" + + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage/postgresql" ) type Profile struct { - ID string `gorm:"type:uuid;primary_key" json:"profile_id"` - FirstName string `gorm:"column:first_name; type:text; not null" json:"first_name"` - LastName string `gorm:"column:last_name; type:text;not null" json:"last_name"` - Phone string `gorm:"type:varchar(255)" json:"phone"` - AvatarURL string `gorm:"type:varchar(255)" json:"avatar_url"` - Userid string `gorm:"type:uuid;" json:"user_id"` - CreatedAt time.Time `gorm:"column:created_at; not null; autoCreateTime" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at; null; autoUpdateTime" json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + ID string `gorm:"type:uuid;primary_key" json:"profile_id"` + FirstName string `gorm:"column:first_name; type:text; not null" json:"first_name"` + LastName string `gorm:"column:last_name; type:text;not null" json:"last_name"` + Phone string `gorm:"type:varchar(255)" json:"phone"` + AvatarURL string `gorm:"type:varchar(255)" json:"avatar_url"` + Userid string `gorm:"type:uuid;" json:"user_id"` + SecondaryEmail string `gorm:"type:string;" json:"secondary_email"` + CreatedAt time.Time `gorm:"column:created_at; not null; autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at; null; autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +type UpdateProfileRequest struct { + SecondaryEmail string `json:"secondary_email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Phone string `json:"phone"` +} + +func (p *Profile) UpdateProfile(db *gorm.DB, req UpdateProfileRequest, profId string) error { + + result, err := postgresql.UpdateFields(db, &p, req, profId) + if err != nil { + return err + } + + if result.RowsAffected == 0 { + return errors.New("failed to update organisation") + } + + return nil } diff --git a/internal/models/user.go b/internal/models/user.go index 0cb0f9f3..f6648229 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -173,3 +173,16 @@ func (u *User) DeleteAUser(db *gorm.DB) error { return nil } + +func (u *User) GetProfileID(db *gorm.DB, userID string) (string, error) { + var user User + + query := db.Where("id = ?", userID) + query = postgresql.PreloadEntities(query, &user, "Profile") + + if err := query.First(&user).Error; err != nil { + return user.Profile.ID, err + } + + return user.Profile.ID, nil +} diff --git a/pkg/controller/profile/profile.go b/pkg/controller/profile/profile.go new file mode 100644 index 00000000..aeae561c --- /dev/null +++ b/pkg/controller/profile/profile.go @@ -0,0 +1,64 @@ +package profile + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/golang-jwt/jwt" + + "github.com/hngprojects/hng_boilerplate_golang_web/external/request" + "github.com/hngprojects/hng_boilerplate_golang_web/internal/models" + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage" + "github.com/hngprojects/hng_boilerplate_golang_web/services/profile" + "github.com/hngprojects/hng_boilerplate_golang_web/utility" +) + +type Controller struct { + Db *storage.Database + Validator *validator.Validate + Logger *utility.Logger + ExtReq request.ExternalRequest +} + +func (base *Controller) UpdateProfile(c *gin.Context) { + var ( + req = models.UpdateProfileRequest{} + ) + + err := c.ShouldBind(&req) + if err != nil { + rd := utility.BuildErrorResponse(http.StatusBadRequest, "error", "Failed to parse request body", err, nil) + c.JSON(http.StatusBadRequest, rd) + return + } + + err = base.Validator.Struct(&req) + if err != nil { + rd := utility.BuildErrorResponse(http.StatusUnprocessableEntity, "error", "Validation failed", utility.ValidationResponse(err, base.Validator), nil) + c.JSON(http.StatusUnprocessableEntity, rd) + return + } + + claims, exists := c.Get("userClaims") + if !exists { + rd := utility.BuildErrorResponse(http.StatusBadRequest, "error", "unable to get user claims", err, nil) + c.JSON(http.StatusBadRequest, rd) + return + } + + userClaims := claims.(jwt.MapClaims) + userId := userClaims["user_id"].(string) + + respData, code, err := profile.UpdateProfile(req, userId, base.Db.Postgresql) + if err != nil { + rd := utility.BuildErrorResponse(http.StatusBadRequest, "error", err.Error(), err, nil) + c.JSON(http.StatusBadRequest, rd) + return + } + + base.Logger.Info("Product updated successfully") + rd := utility.BuildSuccessResponse(http.StatusCreated, "Product updated successfully", respData) + + c.JSON(code, rd) +} diff --git a/pkg/router/profile.go b/pkg/router/profile.go new file mode 100644 index 00000000..77136e87 --- /dev/null +++ b/pkg/router/profile.go @@ -0,0 +1,26 @@ +package router + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + + "github.com/hngprojects/hng_boilerplate_golang_web/external/request" + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/controller/profile" + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/middleware" + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage" + "github.com/hngprojects/hng_boilerplate_golang_web/utility" +) + +func Profile(r *gin.Engine, ApiVersion string, validator *validator.Validate, db *storage.Database, logger *utility.Logger) *gin.Engine { + extReq := request.ExternalRequest{Logger: logger, Test: false} + product := profile.Controller{Db: db, Validator: validator, Logger: logger, ExtReq: extReq} + + profileUrl := r.Group(fmt.Sprintf("%v", ApiVersion), middleware.Authorize(db.Postgresql)) + { + profileUrl.PATCH("/profile", product.UpdateProfile) + } + + return r +} diff --git a/pkg/router/router.go b/pkg/router/router.go index 3fe7d3d5..ead41488 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -52,6 +52,7 @@ func Setup(logger *utility.Logger, validator *validator.Validate, db *storage.Da Template(r, ApiVersion, validator, db, logger) Python(r, ApiVersion, validator, db, logger) HelpCenter(r, ApiVersion, validator, db, logger) + Profile(r, ApiVersion, validator, db, logger) r.GET("/", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ diff --git a/services/profile/profile.go b/services/profile/profile.go new file mode 100644 index 00000000..cff8570c --- /dev/null +++ b/services/profile/profile.go @@ -0,0 +1,35 @@ +package profile + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "github.com/hngprojects/hng_boilerplate_golang_web/internal/models" +) + +func UpdateProfile(req models.UpdateProfileRequest, userId string, db *gorm.DB) (gin.H, int, error) { + + var ( + user models.User + profile models.Profile + ) + + profileId, err := user.GetProfileID(db, userId) + + if err != nil { + return gin.H{}, http.StatusNotFound, err + } + + err = profile.UpdateProfile(db, req, profileId) + + if err != nil { + return gin.H{}, http.StatusInternalServerError, err + } + + responseData := gin.H{ + "message": "Profile updated successfully", + } + return responseData, http.StatusOK, nil +} From 60a671759586c2be586b4feccd69e06261c487eb Mon Sep 17 00:00:00 2001 From: Cyberguru1 Date: Tue, 6 Aug 2024 06:24:06 -0500 Subject: [PATCH 2/5] chore: added test file for update profile endpoint --- tests/test_profile/profile_test.go | 131 +++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/test_profile/profile_test.go diff --git a/tests/test_profile/profile_test.go b/tests/test_profile/profile_test.go new file mode 100644 index 00000000..4594f0c5 --- /dev/null +++ b/tests/test_profile/profile_test.go @@ -0,0 +1,131 @@ +package tests + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + + "github.com/hngprojects/hng_boilerplate_golang_web/internal/models" + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/controller/auth" + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/controller/profile" + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/middleware" + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage" + tst "github.com/hngprojects/hng_boilerplate_golang_web/tests" + "github.com/hngprojects/hng_boilerplate_golang_web/utility" +) + +func TestProfileUpdate(t *testing.T) { + logger := tst.Setup() + gin.SetMode(gin.TestMode) + + validatorRef := validator.New() + db := storage.Connection() + requestURI := url.URL{Path: "/api/v1/profile"} + currUUID := utility.GenerateUUID() + userSignUpData := models.CreateUserRequestModel{ + Email: fmt.Sprintf("emmanueluser%v@qa.team", currUUID), + PhoneNumber: fmt.Sprintf("+234%v", utility.GetRandomNumbersInRange(7000000000, 9099999999)), + FirstName: "test", + LastName: "user", + Password: "password", + UserName: fmt.Sprintf("test_username%v", currUUID), + } + loginData := models.LoginRequestModel{ + Email: userSignUpData.Email, + Password: userSignUpData.Password, + } + + auth := auth.Controller{Db: db, Validator: validatorRef, Logger: logger} + r := gin.Default() + tst.SignupUser(t, r, auth, userSignUpData, false) + + token := tst.GetLoginToken(t, r, auth, loginData) + + tests := []struct { + Name string + RequestBody models.UpdateProfileRequest + ExpectedCode int + Message string + Headers map[string]string + Method string + }{ + { + Name: "Successful profile update", + RequestBody: models.UpdateProfileRequest{ + FirstName: "not_mr_test", + // LastName: "not_Mr_user", + Phone: fmt.Sprintf("+234%v", utility.GetRandomNumbersInRange(7000000000, 9099999999)), + SecondaryEmail: fmt.Sprintf("wakandaNo%v@qa.team", currUUID), + }, + ExpectedCode: http.StatusOK, + Message: "Profile updated successfully", + Headers: map[string]string{ + "Content-Type": "application/json", + "Authorization": "Bearer " + token, + }, + Method: http.MethodPatch, + }, + } + + profile := profile.Controller{Db: db, Validator: validatorRef, Logger: logger} + + for _, test := range tests { + r := gin.Default() + + profileUrl := r.Group(fmt.Sprintf("%v", "/api/v1"), middleware.Authorize(db.Postgresql)) + { + profileUrl.PATCH("/profile", profile.UpdateProfile) + + } + + t.Run(test.Name, func(t *testing.T) { + var b bytes.Buffer + err := json.NewEncoder(&b).Encode(test.RequestBody) + + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest(test.Method, requestURI.String(), &b) + if err != nil { + t.Fatal(err) + } + + for i, v := range test.Headers { + req.Header.Set(i, v) + } + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + tst.AssertStatusCode(t, rr.Code, test.ExpectedCode) + + data := tst.ParseResponse(rr) + + fmt.Println(data) + + code := int(data["status_code"].(float64)) + tst.AssertStatusCode(t, code, test.ExpectedCode) + + if test.Message != "" { + message := data["message"] + if message != nil { + tst.AssertResponseMessage(t, message.(string), test.Message) + } else { + tst.AssertResponseMessage(t, "", test.Message) + } + + } + + }) + + } + +} From 93eb36dd4f0f160043bd0e58e1547de30830bf2d Mon Sep 17 00:00:00 2001 From: Cyberguru1 Date: Tue, 6 Aug 2024 06:33:02 -0500 Subject: [PATCH 3/5] chore: added test file for update profile endpoint --- tests/test_profile/profile_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_profile/profile_test.go b/tests/test_profile/profile_test.go index 4594f0c5..e5595af5 100644 --- a/tests/test_profile/profile_test.go +++ b/tests/test_profile/profile_test.go @@ -59,8 +59,8 @@ func TestProfileUpdate(t *testing.T) { { Name: "Successful profile update", RequestBody: models.UpdateProfileRequest{ - FirstName: "not_mr_test", - // LastName: "not_Mr_user", + FirstName: "not_mr_test", + LastName: "not_Mr_user", Phone: fmt.Sprintf("+234%v", utility.GetRandomNumbersInRange(7000000000, 9099999999)), SecondaryEmail: fmt.Sprintf("wakandaNo%v@qa.team", currUUID), }, @@ -109,8 +109,6 @@ func TestProfileUpdate(t *testing.T) { data := tst.ParseResponse(rr) - fmt.Println(data) - code := int(data["status_code"].(float64)) tst.AssertStatusCode(t, code, test.ExpectedCode) @@ -123,7 +121,6 @@ func TestProfileUpdate(t *testing.T) { } } - }) } From e8b772eee903282cd75013171b3cc16edc35e781 Mon Sep 17 00:00:00 2001 From: Cyberguru1 Date: Tue, 6 Aug 2024 07:06:31 -0500 Subject: [PATCH 4/5] chore: added test file for update profile endpoint --- pkg/controller/profile/profile.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/controller/profile/profile.go b/pkg/controller/profile/profile.go index aeae561c..c56a40ef 100644 --- a/pkg/controller/profile/profile.go +++ b/pkg/controller/profile/profile.go @@ -57,8 +57,8 @@ func (base *Controller) UpdateProfile(c *gin.Context) { return } - base.Logger.Info("Product updated successfully") - rd := utility.BuildSuccessResponse(http.StatusCreated, "Product updated successfully", respData) + base.Logger.Info("Profile updated successfully") + rd := utility.BuildSuccessResponse(http.StatusOK, "Profile updated successfully", respData) c.JSON(code, rd) } From b6f54495fc4508d3a2dea39ef2100eb3cb01ea38 Mon Sep 17 00:00:00 2001 From: Cyberguru1 Date: Tue, 6 Aug 2024 07:06:50 -0500 Subject: [PATCH 5/5] chore: updated test file for update profile endpoint --- pkg/controller/profile/profile.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/controller/profile/profile.go b/pkg/controller/profile/profile.go index c56a40ef..5cdb8113 100644 --- a/pkg/controller/profile/profile.go +++ b/pkg/controller/profile/profile.go @@ -62,3 +62,4 @@ func (base *Controller) UpdateProfile(c *gin.Context) { c.JSON(code, rd) } +