INFRA-2911 | Product service, repo and handler (#373)

* INFRA-2911 | Product entity, modal and repository

* INFRA-2911 | Product service and handler

* INFRA-2911 | Product service test

* INFRA-2911 | Optimisations

* INFRA-2911 | Optimisations
This commit is contained in:
Shashank Shekhar
2024-02-19 18:57:10 +05:30
committed by GitHub
parent 36d590221c
commit aa8161ca8b
15 changed files with 542 additions and 10 deletions

View File

@@ -62,6 +62,7 @@ generatemocks:
cd $(CURDIR)/pkg/maverick && minimock -i IMaverickClient -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/service/incident_jira && minimock -i IncidentJiraService -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/model/incident_jira && minimock -i IncidentJiraRepository -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/model/product && minimock -i ProductRepository -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/service/google && minimock -i IDriveService -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/service/rca && minimock -i IRcaService -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/pkg/alertClient && minimock -i AlertClient -s _mock.go -o $(CURDIR)/mocks

View File

@@ -6,6 +6,7 @@ import (
"gorm.io/gorm"
"houston/model/incident"
"houston/model/log"
productModel "houston/model/product"
"houston/model/severity"
"houston/model/tag"
"houston/model/team"
@@ -20,6 +21,7 @@ import (
"houston/service/documentService"
"houston/service/google"
incidentService "houston/service/incident/impl"
"houston/service/products"
rcaService "houston/service/rca/impl"
"houston/service/slack"
tagService "houston/service/tag"
@@ -45,6 +47,7 @@ type houstonServices struct {
driveService google.IDriveService
calendarService *conference.CalendarService
tagService *tagService.TagService
productsService products.ProductService
}
var appContext *applicationContext
@@ -76,6 +79,7 @@ func InitializeServices() {
driveService: initDriveService(),
calendarService: initCalendarService(),
tagService: initTagService(),
productsService: initProductsService(),
}
}
@@ -156,6 +160,14 @@ func GetTagService() *tagService.TagService {
return services.tagService
}
func initProductsService() products.ProductService {
return products.NewProductService(productModel.NewProductRepo(GetDB()))
}
func GetProductsService() products.ProductService {
return services.productsService
}
func GetCalendarService() *conference.CalendarService {
return services.calendarService
}

View File

@@ -0,0 +1,153 @@
package handler
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"houston/appcontext"
stringUtil "houston/common/util/string"
"houston/logger"
productModel "houston/model/product"
"houston/service/products"
productRequest "houston/service/request/product"
common "houston/service/response/common"
"net/http"
"strings"
)
const (
productHandlerLogTag = "[product_handler]"
)
type ProductHandler struct {
gin *gin.Engine
service products.ProductService
}
func NewProductHandler(
gin *gin.Engine,
) *ProductHandler {
return &ProductHandler{
gin: gin,
service: appcontext.GetProductsService(),
}
}
func (handler *ProductHandler) HandleCreateProduct(c *gin.Context) {
var createProductRequest productRequest.CreateOrUpdatedProductRequest
if err := c.ShouldBindJSON(&createProductRequest); err != nil {
c.JSON(
http.StatusInternalServerError,
common.ErrorResponse(
errors.New("failed to process the request"), http.StatusInternalServerError, nil,
),
)
return
}
if err := validateCreateOrProductRequest(createProductRequest); err != nil {
logger.Error(fmt.Sprintf("%s received invalid request to create new product. %+v", productHandlerLogTag, err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
createdProductIds, err := handler.service.CreateProduct(createProductRequest.ProductName)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to create product. %+v", productHandlerLogTag, err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse(createdProductIds, http.StatusOK))
}
func (handler *ProductHandler) HandleGetAllProducts(c *gin.Context) {
productDTOs, err := handler.service.GetAllProducts()
if err != nil {
logger.Error(fmt.Sprintf("%s failed to get all products. %+v", productHandlerLogTag, err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse(productDTOs, http.StatusOK))
}
func (handler *ProductHandler) HandleGetProductByID(c *gin.Context) {
productId, err := getProductIdFromPath(c)
if err != nil {
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
productDTO, err := handler.service.GetProductByID(productId)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to get product by ID. %+v", productHandlerLogTag, err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse(productDTO, http.StatusOK))
}
func (handler *ProductHandler) HandleUpdateProduct(c *gin.Context) {
var updateProductRequest productRequest.CreateOrUpdatedProductRequest
if err := c.ShouldBindJSON(&updateProductRequest); err != nil {
c.JSON(
http.StatusInternalServerError,
common.ErrorResponse(
errors.New("failed to process the request"), http.StatusInternalServerError, nil,
),
)
return
}
if err := validateCreateOrProductRequest(updateProductRequest); err != nil {
logger.Error(fmt.Sprintf("%s received invalid request to update product. %+v", productHandlerLogTag, err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
productId, err := getProductIdFromPath(c)
if err != nil {
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
productDTO := &productModel.ProductDTO{
ProductID: productId,
ProductName: updateProductRequest.ProductName,
}
err = handler.service.UpdateProduct(productDTO)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to update product. %+v", productHandlerLogTag, err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse(nil, http.StatusOK))
}
func (handler *ProductHandler) HandleDeleteProductByID(c *gin.Context) {
productId, err := getProductIdFromPath(c)
if err != nil {
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
err = handler.service.DeleteProductByID(productId)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to delete product. %+v", productHandlerLogTag, err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse(nil, http.StatusOK))
}
func validateCreateOrProductRequest(request productRequest.CreateOrUpdatedProductRequest) error {
if strings.TrimSpace(request.ProductName) == "" {
return errors.New("product name can not be empty")
}
return nil
}
func getProductIdFromPath(c *gin.Context) (uint, error) {
productIdString := c.Param("id")
productId, err := stringUtil.StringToUint(productIdString)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to convert product id to uint. %+v", productHandlerLogTag, err))
return 0, fmt.Errorf("%s is not a valid product ID", productIdString)
}
return productId, nil
}

View File

@@ -55,6 +55,7 @@ func (s *Server) Handler(houstonGroup *gin.RouterGroup) {
s.rcaHandler(houstonGroup)
s.gin.Use(s.createMiddleware())
s.incidentClientHandlerV2(houstonGroup)
s.productsHandler(houstonGroup)
s.teamHandler(houstonGroup)
s.severityHandler(houstonGroup)
s.incidentHandler(houstonGroup)
@@ -142,6 +143,16 @@ func (s *Server) incidentClientHandler(houstonGroup *gin.RouterGroup) {
houstonGroup.POST("/create-incident", incidentHandler.CreateIncident)
}
func (s *Server) productsHandler(houstonGroup *gin.RouterGroup) {
productsHandler := handler.NewProductHandler(s.gin)
houstonGroup.POST("/products", s.authService.IfAdmin(productsHandler.HandleCreateProduct))
houstonGroup.PUT("/product/:id", s.authService.IfAdmin(productsHandler.HandleUpdateProduct))
houstonGroup.DELETE("/product/:id", s.authService.IfAdmin(productsHandler.HandleDeleteProductByID))
houstonGroup.GET("/products", productsHandler.HandleGetAllProducts)
houstonGroup.GET("/product/:id", productsHandler.HandleGetProductByID)
}
func (s *Server) incidentClientHandlerV2(houstonGroup *gin.RouterGroup) {
houstonGroup.Use(func(c *gin.Context) {
// Add your desired header key-value pair

View File

@@ -1,6 +1,10 @@
package string
import "strings"
import (
"fmt"
"strconv"
"strings"
)
func IsBlank(input string) bool {
trimmedInput := strings.TrimSpace(input)
@@ -19,3 +23,11 @@ func ExtractCommaSeparatedTrimmedArrayFromString(jiraText string) []string {
return stringArray
}
func StringToUint(input string) (uint, error) {
u64, err := strconv.ParseUint(input, 10, 32)
if err != nil {
return 0, fmt.Errorf("failed to convert string to uint: %s", err)
}
return uint(u64), nil
}

View File

@@ -0,0 +1,15 @@
-- Table Definition ----------------------------------------------
CREATE TABLE product
(
id BIGSERIAL PRIMARY KEY,
name text NOT NULL UNIQUE,
created_at timestamp with time zone,
updated_at timestamp with time zone
);
-- Indices -------------------------------------------------------
CREATE UNIQUE INDEX product_pkey ON product (id int8_ops);
CREATE UNIQUE INDEX product_name_key ON product (name text_ops);
CREATE INDEX idx_product_name ON product (name text_ops);

10
go.mod
View File

@@ -7,8 +7,6 @@ require (
github.com/aws/aws-sdk-go v1.44.262
github.com/aws/aws-sdk-go-v2/config v1.18.25
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9
github.com/chromedp/chromedp v0.9.1
github.com/gin-contrib/zap v0.1.0
github.com/gin-gonic/gin v1.9.1
github.com/gojuno/minimock/v3 v3.1.3
@@ -51,20 +49,14 @@ require (
github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
@@ -85,7 +77,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2
github.com/goccy/go-json v0.10.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect

16
model/product/entity.go Normal file
View File

@@ -0,0 +1,16 @@
package product
import (
"time"
)
type ProductEntity struct {
ID uint `gorm:"primarykey;column:id;autoIncrement"`
Name string `gorm:"column:name;not null;unique;index"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;default:null"`
}
func (ProductEntity) TableName() string {
return "product"
}

6
model/product/model.go Normal file
View File

@@ -0,0 +1,6 @@
package product
type ProductDTO struct {
ProductID uint `json:"product_id"`
ProductName string `json:"product_name"`
}

View File

@@ -0,0 +1,19 @@
package product
import (
"gorm.io/gorm"
)
type ProductRepository interface {
InsertProduct(productName string) (uint, error)
UpdateProduct(productEntity *ProductEntity) error
GetAllProducts() ([]ProductEntity, error)
GetProductById(productID uint) (*ProductEntity, error)
GetProductByName(productName string) (*ProductEntity, error)
DeleteProductByID(productID uint) error
DeleteProductByName(productName string) error
}
func NewProductRepo(db *gorm.DB) ProductRepository {
return &productRepositoryImpl{db: db}
}

View File

@@ -0,0 +1,85 @@
package product
import (
"gorm.io/gorm"
)
type productRepositoryImpl struct {
db *gorm.DB
}
func (repo *productRepositoryImpl) InsertProduct(productName string) (uint, error) {
var record = ProductEntity{
Name: productName,
}
result := repo.db.Create(&record)
if result.Error != nil {
return 0, result.Error
}
return record.ID, nil
}
func (repo *productRepositoryImpl) UpdateProduct(productEntity *ProductEntity) error {
result := repo.db.Save(productEntity)
if result.Error != nil {
return result.Error
}
return nil
}
func (repo *productRepositoryImpl) GetAllProducts() ([]ProductEntity, error) {
var entities []ProductEntity
result := repo.db.Find(&entities)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, nil
}
return entities, nil
}
func (repo *productRepositoryImpl) GetProductById(productID uint) (*ProductEntity, error) {
var entity ProductEntity
result := repo.db.First(&entity, "id = ?", productID)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, nil
}
return &entity, nil
}
func (repo *productRepositoryImpl) GetProductByName(productName string) (*ProductEntity, error) {
var entity ProductEntity
result := repo.db.First(&entity, "name = ?", productName)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, nil
}
return &entity, nil
}
func (repo *productRepositoryImpl) DeleteProductByID(productID uint) error {
result := repo.db.Delete(&ProductEntity{}, "id = ?", productID)
if result.Error != nil {
return result.Error
}
return nil
}
func (repo *productRepositoryImpl) DeleteProductByName(productName string) error {
result := repo.db.Delete(&ProductEntity{}, "name = ?", productName)
if result.Error != nil {
return result.Error
}
return nil
}

View File

@@ -0,0 +1,19 @@
package products
import (
productModel "houston/model/product"
)
type ProductService interface {
CreateProduct(productName string) (uint, error)
UpdateProduct(productDTO *productModel.ProductDTO) error
GetAllProducts() ([]productModel.ProductDTO, error)
GetProductByID(productID uint) (*productModel.ProductDTO, error)
GetProductByName(productName string) (*productModel.ProductDTO, error)
DeleteProductByID(productID uint) error
DeleteProductByName(productName string) error
}
func NewProductService(repo productModel.ProductRepository) ProductService {
return &productServiceImpl{repo}
}

View File

@@ -0,0 +1,71 @@
package products
import (
"errors"
productModel "houston/model/product"
)
type productServiceImpl struct {
repo productModel.ProductRepository
}
func (service *productServiceImpl) CreateProduct(productName string) (uint, error) {
existingProducts, err := service.repo.GetProductByName(productName)
if err != nil {
return 0, errors.New("error while processing the request")
}
if existingProducts != nil {
return 0, errors.New("product already exists")
}
return service.repo.InsertProduct(productName)
}
func (service *productServiceImpl) UpdateProduct(productDTO *productModel.ProductDTO) error {
entity, err := service.repo.GetProductById(productDTO.ProductID)
if err != nil {
return err
}
entity.Name = productDTO.ProductName
return service.repo.UpdateProduct(entity)
}
func (service *productServiceImpl) GetAllProducts() ([]productModel.ProductDTO, error) {
productEntities, err := service.repo.GetAllProducts()
if err != nil {
return nil, err
}
var productDTOs []productModel.ProductDTO
for _, entity := range productEntities {
productDTOs = append(productDTOs, productModel.ProductDTO{
ProductID: entity.ID,
ProductName: entity.Name,
})
}
return productDTOs, nil
}
func (service *productServiceImpl) GetProductByID(productID uint) (*productModel.ProductDTO, error) {
productEntity, err := service.repo.GetProductById(productID)
if err != nil {
return nil, err
}
return &productModel.ProductDTO{
ProductID: productEntity.ID,
ProductName: productEntity.Name,
}, nil
}
func (service *productServiceImpl) GetProductByName(productName string) (*productModel.ProductDTO, error) {
productEntity, err := service.repo.GetProductByName(productName)
if err != nil {
return nil, err
}
return &productModel.ProductDTO{
ProductID: productEntity.ID,
ProductName: productEntity.Name,
}, nil
}
func (service *productServiceImpl) DeleteProductByID(productID uint) error {
return service.repo.DeleteProductByID(productID)
}
func (service *productServiceImpl) DeleteProductByName(productName string) error {
return service.repo.DeleteProductByName(productName)
}

View File

@@ -0,0 +1,115 @@
package products
import (
"errors"
"github.com/gojuno/minimock/v3"
"github.com/spf13/viper"
"github.com/stretchr/testify/suite"
"houston/logger"
"houston/mocks"
repo "houston/model/product"
"testing"
)
type ProductServiceSuite struct {
suite.Suite
controller *minimock.Controller
repoMock *mocks.ProductRepositoryMock
service ProductService
}
func (suite *ProductServiceSuite) SetupTest() {
logger.InitLogger()
viper.Set("jira.link.max.length", 50)
suite.controller = minimock.NewController(suite.T())
suite.T().Cleanup(suite.controller.Finish)
suite.repoMock = mocks.NewProductRepositoryMock(suite.controller)
suite.service = NewProductService(suite.repoMock)
}
func TestProductService(t *testing.T) {
suite.Run(t, new(ProductServiceSuite))
}
func (suite *ProductServiceSuite) TestProductServiceImpl_CreateProduct() {
suite.repoMock.GetProductByNameMock.Return(nil, nil)
suite.repoMock.InsertProductMock.When("test").Then(1, nil)
_, err := suite.service.CreateProduct("test")
if err != nil {
suite.Fail("Create Product Failed", err)
}
}
func (suite *ProductServiceSuite) TestProductServiceImpl_UpdateProduct() {
suite.repoMock.GetProductByIdMock.When(uint(1)).Then(&repo.ProductEntity{}, nil)
suite.repoMock.UpdateProductMock.Return(nil)
err := suite.service.UpdateProduct(&repo.ProductDTO{ProductID: 1, ProductName: "test"})
if err != nil {
suite.Fail("Update Product Failed", err)
}
}
func (suite *ProductServiceSuite) TestProductServiceImpl_UpdateProductWithInvalidID() {
suite.repoMock.GetProductByIdMock.When(uint(1)).Then(nil, errors.New("invalid ID"))
err := suite.service.UpdateProduct(&repo.ProductDTO{ProductID: 1, ProductName: "test"})
if err == nil {
suite.Fail("Update Product with invalid ID Failed", err)
}
}
func (suite *ProductServiceSuite) TestProductServiceImpl_GetAllProducts() {
suite.repoMock.GetAllProductsMock.Return(&[]repo.ProductEntity{}, nil)
_, err := suite.service.GetAllProducts()
if err != nil {
suite.Fail("Get All Products Failed", err)
}
}
func (suite *ProductServiceSuite) TestProductServiceImpl_GetProductByID() {
suite.repoMock.GetProductByIdMock.When(uint(1)).Then(&repo.ProductEntity{}, nil)
_, err := suite.service.GetProductByID(1)
if err != nil {
suite.Fail("Get Product By ID Failed", err)
}
}
func (suite *ProductServiceSuite) TestProductServiceImpl_DeleteProductByID() {
suite.repoMock.DeleteProductByIDMock.When(uint(1)).Then(nil)
err := suite.service.DeleteProductByID(1)
if err != nil {
suite.Fail("Delete Product By ID Failed", err)
}
}
func (suite *ProductServiceSuite) TestProductServiceImpl_DeleteProductByName() {
suite.repoMock.DeleteProductByNameMock.When("test").Then(nil)
err := suite.service.DeleteProductByName("test")
if err != nil {
suite.Fail("Delete Product By Name Failed", err)
}
}
func (suite *ProductServiceSuite) TestProductServiceImpl_GetProductByName() {
suite.repoMock.GetProductByNameMock.When("test").Then(&repo.ProductEntity{}, nil)
_, err := suite.service.GetProductByName("test")
if err != nil {
suite.Fail("Get Product By Name Failed", err)
}
}
func (suite *ProductServiceSuite) TestProductServiceImpl_CreateProduct_ExistingProduct() {
suite.repoMock.GetProductByNameMock.Return(&repo.ProductEntity{ID: 1, Name: "test"}, nil)
_, err := suite.service.CreateProduct("test")
if err == nil {
suite.Fail("Create Product Failed", err)
}
}

View File

@@ -0,0 +1,5 @@
package product
type CreateOrUpdatedProductRequest struct {
ProductName string `json:"productName"`
}