diff --git a/internal/app/application/services/box.go b/internal/app/application/services/box.go index ef59efd..d52a8b2 100644 --- a/internal/app/application/services/box.go +++ b/internal/app/application/services/box.go @@ -353,3 +353,24 @@ func (s *BoxService) DeleteWithTransactionsAndItemQuantities(boxID string) error return nil } + +func (s *BoxService) Update( + boxID string, + name string, + description *string, +) (*entities.Box, error) { + box, err := s.boxRepository.GetByID(boxID) + if err != nil { + return nil, err + } + + box.Name = name + box.Description = description + + err = s.boxRepository.Update(box) + if err != nil { + return nil, err + } + + return box, nil +} diff --git a/internal/app/application/services/box_test.go b/internal/app/application/services/box_test.go index e24a2d1..25bae48 100644 --- a/internal/app/application/services/box_test.go +++ b/internal/app/application/services/box_test.go @@ -738,3 +738,91 @@ func TestBoxServiceDeleteWithTransactionsAndItemQuantitiesErrorInBoxRepositoryOn itemRepository.AssertExpectations(t) roomRepository.AssertExpectations(t) } + +func TestBoxServiceUpdate(t *testing.T) { + boxRepository := new(stub.BoxRepositoryMock) + roomRepository := new(stub.RoomRepositoryMock) + itemRepository := new(stub.ItemRepositoryMock) + + boxService := NewBoxService(boxRepository, itemRepository, roomRepository) + + boxID := uuid.NewString() + name := "box" + description := "description" + + boxRepository.On("GetByID", boxID). + Return(&entities.Box{ + ID: boxID, + Name: name, + Description: &description, + }, nil) + boxRepository.On("Update", mock.AnythingOfType("*entities.Box")). + Return(nil) + + box, err := boxService.Update(boxID, name, &description) + + assert.NoError(t, err) + assert.NotNil(t, box) + assert.Equal(t, boxID, box.ID) + assert.Equal(t, name, box.Name) + assert.Equal(t, description, *box.Description) + boxRepository.AssertExpectations(t) + itemRepository.AssertExpectations(t) + roomRepository.AssertExpectations(t) +} + +func TestBoxServiceUpdateErrorInBoxRepositoryOnGetByID(t *testing.T) { + boxRepository := new(stub.BoxRepositoryMock) + roomRepository := new(stub.RoomRepositoryMock) + itemRepository := new(stub.ItemRepositoryMock) + + boxService := NewBoxService(boxRepository, itemRepository, roomRepository) + + boxID := uuid.NewString() + name := "box" + description := "description" + + mockError := errors.New("repository error") + boxRepository.On("GetByID", boxID). + Return(nil, mockError) + + box, err := boxService.Update(boxID, name, &description) + + assert.Error(t, err) + assert.Nil(t, box) + assert.EqualError(t, err, mockError.Error()) + boxRepository.AssertExpectations(t) + itemRepository.AssertExpectations(t) + roomRepository.AssertExpectations(t) +} + +func TestBoxServiceUpdateErrorInBoxRepositoryOnUpdate(t *testing.T) { + boxRepository := new(stub.BoxRepositoryMock) + roomRepository := new(stub.RoomRepositoryMock) + itemRepository := new(stub.ItemRepositoryMock) + + boxService := NewBoxService(boxRepository, itemRepository, roomRepository) + + boxID := uuid.NewString() + name := "box" + description := "description" + + mockError := errors.New("repository error") + boxRepository.On("GetByID", boxID). + Return(&entities.Box{ + ID: boxID, + Name: name, + Description: &description, + }, nil) + boxRepository.On("Update", mock.AnythingOfType("*entities.Box")). + Return(mockError) + + box, err := boxService.Update(boxID, name, &description) + + assert.Error(t, err) + assert.Nil(t, box) + assert.EqualError(t, err, mockError.Error()) + boxRepository.AssertExpectations(t) + itemRepository.AssertExpectations(t) + roomRepository.AssertExpectations(t) +} diff --git a/internal/app/domain/repositories/box.go b/internal/app/domain/repositories/box.go index df00610..5c62790 100644 --- a/internal/app/domain/repositories/box.go +++ b/internal/app/domain/repositories/box.go @@ -15,8 +15,10 @@ var ( ErrBoxRepositoryCanNotDeleteBoxItemsByBoxID = errors.New("can not delete box items by box id") ErrBoxRepositoryCanNotDeleteBoxTransactionsByBoxID = errors.New("can not delete box transactions by box id") ErrBoxRepositoryCanNotUpdateBoxItem = errors.New("can not update box item") - ErrorBoxRepositoryCanNotCountByQueryFilters = errors.New("can not count by query filters") - ErrorBoxRepositoryCanNotGetByQueryFilters = errors.New("can not get by query filters") + ErrBoxRepositoryCanNotCountByQueryFilters = errors.New("can not count by query filters") + ErrBoxRepositoryCanNotGetByQueryFilters = errors.New("can not get by query filters") + ErrBoxRepositoryCanNotGetByID = errors.New("can not get by id") + ErrBoxRepositoryCanNotUpdate = errors.New("can not update") ) type BoxRepository interface { @@ -31,4 +33,6 @@ type BoxRepository interface { DeleteBoxItemsByBoxID(boxID string) error DeleteBoxTransactionsByBoxID(boxID string) error Delete(id string) error + GetByID(id string) (*entities.Box, error) + Update(box *entities.Box) error } diff --git a/internal/app/infrastructure/controllers/update_box.go b/internal/app/infrastructure/controllers/update_box.go new file mode 100644 index 0000000..4d2b8c1 --- /dev/null +++ b/internal/app/infrastructure/controllers/update_box.go @@ -0,0 +1,57 @@ +package controllers + +import ( + "github.com/jibaru/home-inventory-api/m/internal/app/application/services" + "github.com/jibaru/home-inventory-api/m/internal/app/infrastructure/responses" + "github.com/labstack/echo/v4" + "net/http" +) + +type UpdateBoxController struct { + boxService *services.BoxService +} + +type UpdateBoxRequest struct { + Name string `json:"name"` + Description *string `json:"description"` + BoxID string `param:"boxID"` +} + +type UpdateBoxResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` +} + +func NewUpdateBoxController(boxService *services.BoxService) *UpdateBoxController { + return &UpdateBoxController{ + boxService, + } +} + +func (c *UpdateBoxController) Handle(ctx echo.Context) error { + request := UpdateBoxRequest{} + + err := (&echo.DefaultBinder{}).BindBody(ctx, &request) + if err != nil { + return ctx.JSON(http.StatusBadRequest, responses.NewMessageResponse(err.Error())) + } + + err = (&echo.DefaultBinder{}).BindPathParams(ctx, &request) + if err != nil { + return ctx.JSON(http.StatusBadRequest, responses.NewMessageResponse(err.Error())) + } + + box, err := c.boxService.Update(request.BoxID, request.Name, request.Description) + if err != nil { + return ctx.JSON(http.StatusBadRequest, responses.NewMessageResponse(err.Error())) + } + + return ctx.JSON(http.StatusOK, responses.NewDataResponse( + &UpdateBoxResponse{ + ID: box.ID, + Name: box.Name, + Description: box.Description, + }, + )) +} diff --git a/internal/app/infrastructure/http/server.go b/internal/app/infrastructure/http/server.go index 1a79ebd..76ba701 100644 --- a/internal/app/infrastructure/http/server.go +++ b/internal/app/infrastructure/http/server.go @@ -59,6 +59,7 @@ func RunServer( deleteBoxController := controllers.NewDeleteBoxController(boxService) deleteRoomController := controllers.NewDeleteRoomController(roomService) updateRoomController := controllers.NewUpdateRoomController(roomService) + updateBoxController := controllers.NewUpdateBoxController(boxService) loggerMiddleware := middlewares.NewLoggerMiddleware() needsAuthMiddleware := middlewares.NewNeedsAuthMiddleware(authService) @@ -85,6 +86,7 @@ func RunServer( authApi.DELETE("/boxes/:boxID", deleteBoxController.Handle) authApi.DELETE("/rooms/:roomID", deleteRoomController.Handle) authApi.PATCH("/rooms/:roomID", updateRoomController.Handle) + authApi.PATCH("/boxes/:boxID", updateBoxController.Handle) logger.LogError(e.Start(host + ":" + port)) } diff --git a/internal/app/infrastructure/repositories/gorm/box.go b/internal/app/infrastructure/repositories/gorm/box.go index 7379ad2..b042851 100644 --- a/internal/app/infrastructure/repositories/gorm/box.go +++ b/internal/app/infrastructure/repositories/gorm/box.go @@ -88,7 +88,7 @@ func (r *BoxRepository) GetByQueryFilters(queryFilter repositories.QueryFilter, if err != nil { logger.LogError(err) - return nil, repositories.ErrorBoxRepositoryCanNotGetByQueryFilters + return nil, repositories.ErrBoxRepositoryCanNotGetByQueryFilters } return boxes, nil @@ -103,7 +103,7 @@ func (r *BoxRepository) CountByQueryFilters(queryFilter repositories.QueryFilter if err != nil { logger.LogError(err) - return 0, repositories.ErrorBoxRepositoryCanNotCountByQueryFilters + return 0, repositories.ErrBoxRepositoryCanNotCountByQueryFilters } return count, nil @@ -138,3 +138,23 @@ func (r *BoxRepository) Delete(id string) error { return nil } + +func (r *BoxRepository) GetByID(id string) (*entities.Box, error) { + var box entities.Box + if err := r.db.Where("id = ?", id).First(&box).Error; err != nil { + logger.LogError(err) + return nil, repositories.ErrBoxRepositoryCanNotGetByID + } + + return &box, nil +} + +func (r *BoxRepository) Update(box *entities.Box) error { + if err := r.db.Save(box).Error; err != nil { + logger.LogError(err) + notifier.NotifyError(err) + return repositories.ErrBoxRepositoryCanNotUpdate + } + + return nil +} diff --git a/internal/app/infrastructure/repositories/gorm/box_test.go b/internal/app/infrastructure/repositories/gorm/box_test.go index cc3c494..088c4f9 100644 --- a/internal/app/infrastructure/repositories/gorm/box_test.go +++ b/internal/app/infrastructure/repositories/gorm/box_test.go @@ -442,7 +442,7 @@ func TestBoxRepositoryGetByQueryFiltersErrorBoxRepositoryCanNotGetByQueryFilters result, err := boxRepository.GetByQueryFilters(queryFilter, pageFilter) assert.Error(t, err) - assert.ErrorIs(t, err, repositories.ErrorBoxRepositoryCanNotGetByQueryFilters) + assert.ErrorIs(t, err, repositories.ErrBoxRepositoryCanNotGetByQueryFilters) assert.Nil(t, result) err = dbMock.ExpectationsWereMet() assert.NoError(t, err) @@ -642,3 +642,105 @@ func TestBoxRepositoryDeleteErrorBoxRepositoryCanNotDeleteBox(t *testing.T) { err = dbMock.ExpectationsWereMet() assert.NoError(t, err) } + +func TestBoxRepositoryGetByID(t *testing.T) { + db, dbMock := makeDBMock() + boxRepository := NewBoxRepository(db) + + boxID := uuid.NewString() + description := random.String(255, random.Alphanumeric) + box := &entities.Box{ + ID: boxID, + Name: random.String(100, random.Alphanumeric), + Description: &description, + RoomID: uuid.NewString(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `boxes` WHERE id = ? ORDER BY `boxes`.`id` LIMIT 1")). + WithArgs(boxID). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "description", "room_id", "created_at", "updated_at"}). + AddRow(box.ID, box.Name, *box.Description, box.RoomID, box.CreatedAt, box.UpdatedAt)) + + result, err := boxRepository.GetByID(boxID) + + assert.NoError(t, err) + assert.Equal(t, box, result) + err = dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} + +func TestBoxRepositoryGetByIDErrorBoxRepositoryCanNotGetByID(t *testing.T) { + db, dbMock := makeDBMock() + boxRepository := NewBoxRepository(db) + + boxID := uuid.NewString() + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `boxes` WHERE id = ? ORDER BY `boxes`.`id` LIMIT 1")). + WithArgs(boxID). + WillReturnError(errors.New("record not found")) + + result, err := boxRepository.GetByID(boxID) + + assert.Error(t, err) + assert.ErrorIs(t, err, repositories.ErrBoxRepositoryCanNotGetByID) + assert.Nil(t, result) + err = dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} + +func TestBoxRepositoryUpdate(t *testing.T) { + db, dbMock := makeDBMock() + boxRepository := NewBoxRepository(db) + + description := random.String(255, random.Alphanumeric) + box := &entities.Box{ + ID: uuid.NewString(), + Name: random.String(100, random.Alphanumeric), + Description: &description, + RoomID: uuid.NewString(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + dbMock.ExpectBegin() + dbMock.ExpectExec(regexp.QuoteMeta("UPDATE `boxes` SET `name`=?,`description`=?,`room_id`=?,`created_at`=?,`updated_at`=? WHERE `id` = ?")). + WithArgs(box.Name, *box.Description, box.RoomID, box.CreatedAt, sqlmock.AnyArg(), box.ID). + WillReturnResult(sqlmock.NewResult(1, 1)) + dbMock.ExpectCommit() + + err := boxRepository.Update(box) + + assert.NoError(t, err) + err = dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} + +func TestBoxRepositoryUpdateErrorBoxRepositoryCanNotUpdate(t *testing.T) { + db, dbMock := makeDBMock() + boxRepository := NewBoxRepository(db) + + description := random.String(255, random.Alphanumeric) + box := &entities.Box{ + ID: uuid.NewString(), + Name: random.String(100, random.Alphanumeric), + Description: &description, + RoomID: uuid.NewString(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + dbMock.ExpectBegin() + dbMock.ExpectExec(regexp.QuoteMeta("UPDATE `boxes` SET `name`=?,`description`=?,`room_id`=?,`created_at`=?,`updated_at`=? WHERE `id` = ?")). + WithArgs(box.Name, *box.Description, box.RoomID, box.CreatedAt, sqlmock.AnyArg(), box.ID). + WillReturnError(errors.New("database error")) + dbMock.ExpectRollback() + + err := boxRepository.Update(box) + + assert.Error(t, err) + assert.ErrorIs(t, err, repositories.ErrBoxRepositoryCanNotUpdate) + err = dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} diff --git a/internal/app/infrastructure/repositories/stub/box.go b/internal/app/infrastructure/repositories/stub/box.go index 5afc369..d468441 100644 --- a/internal/app/infrastructure/repositories/stub/box.go +++ b/internal/app/infrastructure/repositories/stub/box.go @@ -77,3 +77,18 @@ func (m *BoxRepositoryMock) Delete(id string) error { args := m.Called(id) return args.Error(0) } + +func (m *BoxRepositoryMock) GetByID(id string) (*entities.Box, error) { + args := m.Called(id) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*entities.Box), args.Error(1) +} + +func (m *BoxRepositoryMock) Update(box *entities.Box) error { + args := m.Called(box) + return args.Error(0) +}