From 3831c275fb7d0f21195367a1bc1cea70787f9880 Mon Sep 17 00:00:00 2001 From: Michael Osajeh Date: Thu, 25 Jul 2024 01:51:48 +0100 Subject: [PATCH 1/2] feat: add endpoint to fetch single product --- internal/models/product.go | 12 +++++++++++- pkg/controller/product/product.go | 20 ++++++++++++++++++++ pkg/router/product.go | 1 + services/product/product.go | 24 ++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/internal/models/product.go b/internal/models/product.go index adc20568..280dc73a 100644 --- a/internal/models/product.go +++ b/internal/models/product.go @@ -11,7 +11,7 @@ import ( type Product struct { ID string `gorm:"type:uuid;primaryKey" json:"product_id"` Name string `gorm:"column:name; type:varchar(255); not null" json:"name"` - Price float64 `gorm:"column:price; type:decimal(10,2);not null" json:"price"` + Price float64 `gorm:"column:price; type:decimal(10,2);not null; default:0" json:"price"` Description string `gorm:"column:description; type:text" json:"description"` OwnerID string `gorm:"type:uuid;" json:"owner_id"` Category []Category `gorm:"many2many:product_categories;;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"category"` @@ -42,3 +42,13 @@ func (p *Product) AddProductToCategory(db *gorm.DB, categories []interface{}) er } return nil } + +func (p *Product) GetProduct(db *gorm.DB, id string) (Product, error) { + var product Product + err := db.Preload("Category").Model(p).First(&product, "id = ?", id).Error + if err != nil { + return Product{}, err + } + + return product, nil +} diff --git a/pkg/controller/product/product.go b/pkg/controller/product/product.go index 20040c34..182ff8b9 100644 --- a/pkg/controller/product/product.go +++ b/pkg/controller/product/product.go @@ -52,3 +52,23 @@ func (base *Controller) CreateProduct(c *gin.Context) { c.JSON(code, rd) } + +func (base *Controller) GetProduct(c *gin.Context) { + productId := c.Param("product_id") + respData, code, err := product.GetProduct(productId, base.Db.Postgresql) + if err != nil { + resp := gin.H{"error": "Product not found"} + if code == http.StatusNotFound { + resp = gin.H{"error": "Invalid product ID"} + } + + rd := utility.BuildErrorResponse(code, "error", err.Error(), resp, nil) + c.JSON(code, rd) + return + } + + base.Logger.Info("Product found successfully") + rd := utility.BuildSuccessResponse(http.StatusOK, "Product found successfully", respData) + + c.JSON(code, rd) +} diff --git a/pkg/router/product.go b/pkg/router/product.go index f5bc1472..fd4ec850 100644 --- a/pkg/router/product.go +++ b/pkg/router/product.go @@ -19,6 +19,7 @@ func Product(r *gin.Engine, ApiVersion string, validator *validator.Validate, db productUrl := r.Group(fmt.Sprintf("%v", ApiVersion), middleware.Authorize(db.Postgresql)) { + productUrl.GET("/products/:product_id", product.GetProduct) productUrl.POST("/products", product.CreateProduct) } return r diff --git a/services/product/product.go b/services/product/product.go index 73129f4c..4cb25d0a 100644 --- a/services/product/product.go +++ b/services/product/product.go @@ -1,6 +1,7 @@ package product import ( + "errors" "net/http" "strings" @@ -37,6 +38,7 @@ func CreateProduct(req models.CreateProductRequestModel, db *gorm.DB, c *gin.Con } responseData = gin.H{ + "id": product.ID, "name": product.Name, "description": product.Description, "price": product.Price, @@ -44,3 +46,25 @@ func CreateProduct(req models.CreateProductRequestModel, db *gorm.DB, c *gin.Con } return responseData, http.StatusCreated, nil } + +func GetProduct(productId string, db *gorm.DB) (gin.H, int, error) { + product := models.Product{} + product, err := product.GetProduct(db, productId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, http.StatusNotFound, err + } + return nil, http.StatusInternalServerError, err + } + + responseData := gin.H{ + "id": product.ID, + "name": product.Name, + "description": product.Description, + "price": product.Price, + "categories": product.Category, + "created_at": product.CreatedAt, + "updated_at": product.UpdatedAt, + } + return responseData, http.StatusCreated, nil +} From 53b0cc511b41c801118518820ed1d8c1ed9bb3df Mon Sep 17 00:00:00 2001 From: Michael Osajeh Date: Thu, 25 Jul 2024 02:25:04 +0100 Subject: [PATCH 2/2] chore: add tests for fetch single product endpoint --- pkg/controller/product/product.go | 23 ++++-- services/product/product.go | 2 +- tests/test_product/product_test.go | 123 +++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 7 deletions(-) diff --git a/pkg/controller/product/product.go b/pkg/controller/product/product.go index 182ff8b9..4377f355 100644 --- a/pkg/controller/product/product.go +++ b/pkg/controller/product/product.go @@ -2,6 +2,7 @@ package product import ( "net/http" + "regexp" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" @@ -55,14 +56,24 @@ func (base *Controller) CreateProduct(c *gin.Context) { func (base *Controller) GetProduct(c *gin.Context) { productId := c.Param("product_id") + + matched, err := regexp.MatchString("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", productId) + if err != nil { + rd := utility.BuildErrorResponse(http.StatusInternalServerError, "error", err.Error(), "An unexpected error occured", nil) + c.JSON(http.StatusInternalServerError, rd) + return + } + + if !matched { + rd := utility.BuildErrorResponse(http.StatusBadRequest, "error", "", "Invalid product ID", nil) + c.JSON(http.StatusBadRequest, rd) + return + } + + respData, code, err := product.GetProduct(productId, base.Db.Postgresql) if err != nil { - resp := gin.H{"error": "Product not found"} - if code == http.StatusNotFound { - resp = gin.H{"error": "Invalid product ID"} - } - - rd := utility.BuildErrorResponse(code, "error", err.Error(), resp, nil) + rd := utility.BuildErrorResponse(code, "error", err.Error(), "Product not found", nil) c.JSON(code, rd) return } diff --git a/services/product/product.go b/services/product/product.go index 4cb25d0a..8c4202b3 100644 --- a/services/product/product.go +++ b/services/product/product.go @@ -66,5 +66,5 @@ func GetProduct(productId string, db *gorm.DB) (gin.H, int, error) { "created_at": product.CreatedAt, "updated_at": product.UpdatedAt, } - return responseData, http.StatusCreated, nil + return responseData, http.StatusOK, nil } diff --git a/tests/test_product/product_test.go b/tests/test_product/product_test.go index adaa96d4..a9c040bb 100644 --- a/tests/test_product/product_test.go +++ b/tests/test_product/product_test.go @@ -132,3 +132,126 @@ func TestProductCreate(t *testing.T) { } } + +func TestProductGet(t *testing.T) { + logger := tst.Setup() + gin.SetMode(gin.TestMode) + + validatorRef := validator.New() + db := storage.Connection() + requestURI := url.URL{Path: "/api/v1/products"} + currUUID := utility.GenerateUUID() + userSignUpData := models.CreateUserRequestModel{ + Email: fmt.Sprintf("johncarpenter%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) + + token := tst.GetLoginToken(t, r, auth, loginData) + + testProduct := models.CreateProductRequestModel{ + Name: "Nike SB", + Description: "One of the best, common and cloned nike product of all time", + Price: 190.33, + } + + product := product.Controller{Db: db, Validator: validatorRef, Logger: logger} + productUrl := r.Group(fmt.Sprintf("%v", "/api/v1"), middleware.Authorize(db.Postgresql)) + { + productUrl.POST("/products", product.CreateProduct) + + } + + var b bytes.Buffer + json.NewEncoder(&b).Encode(testProduct) + + req, err := http.NewRequest(http.MethodPost, requestURI.String(), &b) + if err != nil { + t.Fatal(err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + productCreateResp := tst.ParseResponse(rr) + productId := productCreateResp["data"].(map[string]interface{})["id"].(string) // don't mind this goofy ahh line + + tests := []struct { + Name string + productId string + ExpectedCode int + Message string + Headers map[string]string + }{ + { + Name: "Get product success", + ExpectedCode: http.StatusOK, + productId: productId, + Message: "", + Headers: map[string]string{ + "Content-Type": "application/json", + "Authorization": "Bearer " + token, + }, + }, { + Name: "Get product invalid id", + ExpectedCode: http.StatusBadRequest, + productId: "ohio rizzler", + Message: "Invalid product ID", + Headers: map[string]string{ + "Content-Type": "application/json", + "Authorization": "Bearer " + token, + }, + }, { + Name: "Get product product does not exist", + ExpectedCode: http.StatusNotFound, + productId: utility.GenerateUUID(), + Message: "Product not found", + Headers: map[string]string{ + "Content-Type": "application/json", + "Authorization": "Bearer " + token, + }, + }, + } + + for _, test := range tests { + r := gin.Default() + + productUrl := r.Group(fmt.Sprintf("%v", "/api/v1"), middleware.Authorize(db.Postgresql)) + { + productUrl.GET("/products/:product_id", product.GetProduct) + + } + + t.Run(test.Name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, requestURI.String()+"/"+test.productId, nil) + 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) + }) + + } + +}