From b5979903bd1efd1dfd37a73ce8fe283f1dfe1796 Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Fri, 11 Jul 2025 01:24:48 +0200 Subject: [PATCH 1/2] fix: Ensure proper deletion of categories with RESTRICT constraint and update all fields in category updates --- .../infrastructure/repository/gorm/category_repository.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/infrastructure/repository/gorm/category_repository.go b/internal/infrastructure/repository/gorm/category_repository.go index 97550be..1e64c8c 100644 --- a/internal/infrastructure/repository/gorm/category_repository.go +++ b/internal/infrastructure/repository/gorm/category_repository.go @@ -23,7 +23,7 @@ func (c *CategoryRepository) Create(category *entity.Category) error { func (c *CategoryRepository) Delete(categoryID uint) error { // Note: This will fail if there are products in this category due to RESTRICT constraint // which is the intended behavior for data integrity - return c.db.Delete(&entity.Category{}, categoryID).Error + return c.db.Unscoped().Delete(&entity.Category{}, categoryID).Error } // GetByID implements repository.CategoryRepository. @@ -61,7 +61,8 @@ func (c *CategoryRepository) List() ([]*entity.Category, error) { // Update implements repository.CategoryRepository. func (c *CategoryRepository) Update(category *entity.Category) error { - return c.db.Save(category).Error + // Use Select with "*" and Omit to ensure all fields are updated, including nil values + return c.db.Select("*").Omit("created_at").Save(category).Error } // NewCategoryRepository creates a new GORM-based CategoryRepository From a1f73e26579bf45410c491ed477167fc54718b3b Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Fri, 11 Jul 2025 01:25:39 +0200 Subject: [PATCH 2/2] feat: Enhance UpdateCategory to handle ParentID logic and add corresponding tests --- .../application/usecase/category_usecase.go | 33 +++- .../usecase/category_usecase_test.go | 45 ++++++ .../repository/gorm/category_repository.go | 4 +- .../gorm/category_repository_test.go | 150 ++++++++++++++++++ 4 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 internal/infrastructure/repository/gorm/category_repository_test.go diff --git a/internal/application/usecase/category_usecase.go b/internal/application/usecase/category_usecase.go index 9e050bd..622df38 100644 --- a/internal/application/usecase/category_usecase.go +++ b/internal/application/usecase/category_usecase.go @@ -81,7 +81,7 @@ type UpdateCategory struct { CategoryID uint `json:"category_id" validate:"required"` Name string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` Description string `json:"description,omitempty" validate:"max=1000"` - ParentID *uint `json:"parent_id,omitempty"` // Optional parent category ID + ParentID *uint `json:"parent_id,omitempty"` // Optional parent category ID (0 means remove parent) } // UpdateCategory updates an existing category @@ -92,14 +92,24 @@ func (uc *CategoryUseCase) UpdateCategory(input UpdateCategory) (*entity.Categor return nil, fmt.Errorf("failed to get category: %w", err) } - // Validate parent category exists if parentID is provided + // Handle ParentID logic: if ParentID is provided and is 0, set it to nil (remove parent) + var actualParentID *uint if input.ParentID != nil { + if *input.ParentID == 0 { + actualParentID = nil + } else { + actualParentID = input.ParentID + } + } + + // Validate parent category exists if parentID is provided and not 0 + if actualParentID != nil { // Check for circular reference (category cannot be its own parent) - if *input.ParentID == input.CategoryID { + if *actualParentID == input.CategoryID { return nil, errors.New("category cannot be its own parent") } - parent, err := uc.categoryRepo.GetByID(*input.ParentID) + parent, err := uc.categoryRepo.GetByID(*actualParentID) if err != nil { return nil, fmt.Errorf("parent category not found: %w", err) } @@ -116,7 +126,7 @@ func (uc *CategoryUseCase) UpdateCategory(input UpdateCategory) (*entity.Categor category.Description = input.Description } if input.ParentID != nil { - category.ParentID = input.ParentID + category.ParentID = actualParentID } // Save updated category @@ -134,7 +144,18 @@ func (uc *CategoryUseCase) UpdateCategory(input UpdateCategory) (*entity.Categor return nil, fmt.Errorf("failed to update category: %w", err) } - return category, nil + // Clear associations if ParentID was set to nil + if input.ParentID != nil && actualParentID == nil { + category.Parent = nil + } + + // Refetch the category to get the updated data with proper associations + updatedCategory, err := uc.categoryRepo.GetByID(category.ID) + if err != nil { + return nil, fmt.Errorf("failed to fetch updated category: %w", err) + } + + return updatedCategory, nil } // DeleteCategory deletes a category by ID diff --git a/internal/application/usecase/category_usecase_test.go b/internal/application/usecase/category_usecase_test.go index b1c2f47..c56f62b 100644 --- a/internal/application/usecase/category_usecase_test.go +++ b/internal/application/usecase/category_usecase_test.go @@ -129,3 +129,48 @@ func TestCategoryUseCase_DeleteCategory_WithProducts(t *testing.T) { assert.Equal(t, "cannot delete category with child categories", err.Error()) }) } + +func TestCategoryUseCase_UpdateCategory_ParentIDZero(t *testing.T) { + t.Run("should remove parent when parent_id is 0", func(t *testing.T) { + // Setup + db := testutil.SetupTestDB(t) + defer testutil.CleanupTestDB(t, db) + + categoryRepo := gorm.NewCategoryRepository(db) + productRepo := gorm.NewProductRepository(db) + categoryUseCase := NewCategoryUseCase(categoryRepo, productRepo) + + // Create parent category + parentCategory, err := categoryUseCase.CreateCategory(CreateCategory{ + Name: "Parent Category", + Description: "Parent category for test", + ParentID: nil, + }) + require.NoError(t, err) + + // Create child category with parent + childCategory, err := categoryUseCase.CreateCategory(CreateCategory{ + Name: "Child Category", + Description: "Child category for test", + ParentID: &parentCategory.ID, + }) + require.NoError(t, err) + + // Verify initial state + assert.NotNil(t, childCategory.ParentID) + assert.Equal(t, parentCategory.ID, *childCategory.ParentID) + + // Update child to remove parent using parent_id: 0 + zeroID := uint(0) + _, err = categoryUseCase.UpdateCategory(UpdateCategory{ + CategoryID: childCategory.ID, + ParentID: &zeroID, + }) + require.NoError(t, err) + + // Verify in database directly (this is the most reliable test) + fetchedCategory, err := categoryRepo.GetByID(childCategory.ID) + require.NoError(t, err) + assert.Nil(t, fetchedCategory.ParentID, "ParentID should be nil after setting to 0") + }) +} diff --git a/internal/infrastructure/repository/gorm/category_repository.go b/internal/infrastructure/repository/gorm/category_repository.go index 1e64c8c..18dca44 100644 --- a/internal/infrastructure/repository/gorm/category_repository.go +++ b/internal/infrastructure/repository/gorm/category_repository.go @@ -61,8 +61,8 @@ func (c *CategoryRepository) List() ([]*entity.Category, error) { // Update implements repository.CategoryRepository. func (c *CategoryRepository) Update(category *entity.Category) error { - // Use Select with "*" and Omit to ensure all fields are updated, including nil values - return c.db.Select("*").Omit("created_at").Save(category).Error + // Use explicit field updates to ensure parent_id is properly updated when nil + return c.db.Model(category).Select("name", "description", "parent_id").Updates(category).Error } // NewCategoryRepository creates a new GORM-based CategoryRepository diff --git a/internal/infrastructure/repository/gorm/category_repository_test.go b/internal/infrastructure/repository/gorm/category_repository_test.go new file mode 100644 index 0000000..2862931 --- /dev/null +++ b/internal/infrastructure/repository/gorm/category_repository_test.go @@ -0,0 +1,150 @@ +package gorm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zenfulcode/commercify/internal/domain/entity" + "github.com/zenfulcode/commercify/testutil" +) + +func TestCategoryRepository_UpdateParentID(t *testing.T) { + // Setup + db := testutil.SetupTestDB(t) + defer testutil.CleanupTestDB(t, db) + + repo := NewCategoryRepository(db) + + t.Run("should update ParentID from nil to valid ID", func(t *testing.T) { + // Create parent category + parentCategory, err := entity.NewCategory("Parent Category", "Parent description", nil) + require.NoError(t, err) + err = repo.Create(parentCategory) + require.NoError(t, err) + + // Create child category without parent + childCategory, err := entity.NewCategory("Child Category", "Child description", nil) + require.NoError(t, err) + err = repo.Create(childCategory) + require.NoError(t, err) + + // Verify initial state + assert.Nil(t, childCategory.ParentID) + + // Update child to have parent + childCategory.ParentID = &parentCategory.ID + err = repo.Update(childCategory) + require.NoError(t, err) + + // Fetch from database to verify + updated, err := repo.GetByID(childCategory.ID) + require.NoError(t, err) + assert.NotNil(t, updated.ParentID) + assert.Equal(t, parentCategory.ID, *updated.ParentID) + }) + + t.Run("should update ParentID from valid ID to nil using 0", func(t *testing.T) { + // Create parent category + parentCategory, err := entity.NewCategory("Parent Category 2", "Parent description", nil) + require.NoError(t, err) + err = repo.Create(parentCategory) + require.NoError(t, err) + + // Create child category with parent + childCategory, err := entity.NewCategory("Child Category 2", "Child description", &parentCategory.ID) + require.NoError(t, err) + err = repo.Create(childCategory) + require.NoError(t, err) + + // Verify initial state + assert.NotNil(t, childCategory.ParentID) + assert.Equal(t, parentCategory.ID, *childCategory.ParentID) + + // Update child to remove parent (simulate sending parent_id: 0 from API) + childCategory.ParentID = nil + err = repo.Update(childCategory) + require.NoError(t, err) + + // Fetch from database to verify + updated, err := repo.GetByID(childCategory.ID) + require.NoError(t, err) + assert.Nil(t, updated.ParentID) + }) + + t.Run("should update ParentID from one valid ID to another", func(t *testing.T) { + // Create parent categories + parentCategory1, err := entity.NewCategory("Parent Category 3", "Parent description", nil) + require.NoError(t, err) + err = repo.Create(parentCategory1) + require.NoError(t, err) + + parentCategory2, err := entity.NewCategory("Parent Category 4", "Parent description", nil) + require.NoError(t, err) + err = repo.Create(parentCategory2) + require.NoError(t, err) + + // Create child category with first parent + childCategory, err := entity.NewCategory("Child Category 3", "Child description", &parentCategory1.ID) + require.NoError(t, err) + err = repo.Create(childCategory) + require.NoError(t, err) + + // Verify initial state + assert.NotNil(t, childCategory.ParentID) + assert.Equal(t, parentCategory1.ID, *childCategory.ParentID) + + // Update child to have second parent + childCategory.ParentID = &parentCategory2.ID + err = repo.Update(childCategory) + require.NoError(t, err) + + // Fetch from database to verify + updated, err := repo.GetByID(childCategory.ID) + require.NoError(t, err) + assert.NotNil(t, updated.ParentID) + assert.Equal(t, parentCategory2.ID, *updated.ParentID) + }) +} + +func TestCategoryRepository_UpdateParentIDToNil(t *testing.T) { + // Setup + db := testutil.SetupTestDB(t) + defer testutil.CleanupTestDB(t, db) + + repo := NewCategoryRepository(db) + + t.Run("should update ParentID to nil when explicitly set", func(t *testing.T) { + // Create parent category + parentCategory, err := entity.NewCategory("Parent Category", "Parent description", nil) + require.NoError(t, err) + err = repo.Create(parentCategory) + require.NoError(t, err) + + // Create child category with parent + childCategory, err := entity.NewCategory("Child Category", "Child description", &parentCategory.ID) + require.NoError(t, err) + err = repo.Create(childCategory) + require.NoError(t, err) + + // Verify initial state + initial, err := repo.GetByID(childCategory.ID) + require.NoError(t, err) + assert.NotNil(t, initial.ParentID) + assert.Equal(t, parentCategory.ID, *initial.ParentID) + + // Update child to remove parent by setting ParentID to nil + childCategory.ParentID = nil + t.Logf("Before update: childCategory.ParentID = %v", childCategory.ParentID) + + err = repo.Update(childCategory) + require.NoError(t, err) + + // Verify the update worked by fetching from database + updated, err := repo.GetByID(childCategory.ID) + require.NoError(t, err) + t.Logf("After update: updated.ParentID = %v", updated.ParentID) + assert.Nil(t, updated.ParentID, "ParentID should be nil after update") + }) +}