Skip to content

Commit

Permalink
Merge pull request #275 from hngprojects/feat/update-profile-endpoint
Browse files Browse the repository at this point in the history
Feat: Update profile endpoint
  • Loading branch information
Micah-Shallom authored Aug 6, 2024
2 parents aad3bfa + 1c030ae commit 51a7971
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 9 deletions.
43 changes: 34 additions & 9 deletions internal/models/profile.go
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 13 additions & 0 deletions internal/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
65 changes: 65 additions & 0 deletions pkg/controller/profile/profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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("Profile updated successfully")
rd := utility.BuildSuccessResponse(http.StatusOK, "Profile updated successfully", respData)

c.JSON(code, rd)
}

26 changes: 26 additions & 0 deletions pkg/router/profile.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions pkg/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Contact(r, ApiVersion, validator, db, logger)

r.GET("/", func(c *gin.Context) {
Expand Down
35 changes: 35 additions & 0 deletions services/profile/profile.go
Original file line number Diff line number Diff line change
@@ -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
}
128 changes: 128 additions & 0 deletions tests/test_profile/profile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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%[email protected]", 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%[email protected]", 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)

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)
}

}
})

}

}

0 comments on commit 51a7971

Please sign in to comment.