Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,45 @@ public static void deleteAllCategories(@Nonnull final ProjectApiRoot ctpClient)
});
}

/**
* Deletes categories from CTP projects defined by the {@code ctpClient} that match any of the
* supplied slugs in the specified locale. This method is useful for cleaning up categories that
* may not have keys set (which prevents them from being properly tracked by {@link
* #deleteAllCategories(ProjectApiRoot)}).
*
* @param ctpClient defines the CTP project to delete the categories from.
* @param locale the locale to use when matching slugs.
* @param slugs the list of slugs to match for deletion.
*/
public static void deleteCategoriesBySlug(
@Nonnull final ProjectApiRoot ctpClient,
@Nonnull final Locale locale,
@Nonnull final List<String> slugs) {
slugs.forEach(
slug -> {
ctpClient
.categories()
.get()
.addWhere("slug(" + locale.getLanguage() + "=:slug)")
.addPredicateVar("slug", slug)
.execute()
.toCompletableFuture()
.join()
.getBody()
.getResults()
.forEach(
category ->
ctpClient
.categories()
.withId(category.getId())
.delete()
.withVersion(category.getVersion())
.execute()
.toCompletableFuture()
.join());
});
}

private static List<Category> sortCategoriesByLeastAncestors(
@Nonnull final List<Category> categories) {
categories.sort(Comparator.comparingInt(category -> category.getAncestors().size()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
package com.commercetools.sync.integration.commons.utils;

import static org.assertj.core.api.Assertions.assertThat;

import com.commercetools.api.models.category.Category;
import com.commercetools.api.models.category.CategoryDraft;
import com.commercetools.api.models.category.CategoryDraftBuilder;
import com.commercetools.api.models.common.LocalizedString;
import java.util.List;
import java.util.Locale;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Integration tests for {@link CategoryITUtils} utility methods that require actual CTP API
* interactions.
*/
class CategoryITUtilsIT {

/** Delete all categories and types from target project before running tests. */
@BeforeAll
static void setup() {
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_TARGET_CLIENT);
ITUtils.deleteTypes(TestClientUtils.CTP_TARGET_CLIENT);
}

/** Clean up before each test to ensure a fresh state. */
@BeforeEach
void setupTest() {
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_TARGET_CLIENT);
}

/** Cleans up the target test data that were built in this test class. */
@AfterAll
static void tearDown() {
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_TARGET_CLIENT);
ITUtils.deleteTypes(TestClientUtils.CTP_TARGET_CLIENT);
}

@Test
void deleteCategoriesBySlug_WithExistingCategories_ShouldDeleteOnlyMatchingSlugs() {
// preparation - create 4 categories with different slugs
final CategoryDraft category1 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category 1"))
.slug(LocalizedString.of(Locale.ENGLISH, "test-slug-1"))
.key("key1")
.build();

final CategoryDraft category2 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category 2"))
.slug(LocalizedString.of(Locale.ENGLISH, "test-slug-2"))
.key("key2")
.build();

final CategoryDraft category3 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category 3"))
.slug(LocalizedString.of(Locale.ENGLISH, "test-slug-3"))
.key("key3")
.build();

final CategoryDraft category4 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category 4"))
.slug(LocalizedString.of(Locale.ENGLISH, "other-slug"))
.key("key4")
.build();

TestClientUtils.CTP_TARGET_CLIENT.categories().create(category1).executeBlocking();
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category2).executeBlocking();
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category3).executeBlocking();
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category4).executeBlocking();

// test - delete categories with slugs test-slug-1 and test-slug-2
CategoryITUtils.deleteCategoriesBySlug(
TestClientUtils.CTP_TARGET_CLIENT, Locale.ENGLISH, List.of("test-slug-1", "test-slug-2"));

// assertion - verify only 2 categories remain (test-slug-3 and other-slug)
final List<Category> remainingCategories =
TestClientUtils.CTP_TARGET_CLIENT
.categories()
.get()
.execute()
.toCompletableFuture()
.join()
.getBody()
.getResults();

assertThat(remainingCategories).hasSize(2);
assertThat(remainingCategories)
.extracting(category -> category.getSlug().get(Locale.ENGLISH))
.containsExactlyInAnyOrder("test-slug-3", "other-slug");
assertThat(remainingCategories)
.extracting(Category::getKey)
.containsExactlyInAnyOrder("key3", "key4");
}

@Test
void deleteCategoriesBySlug_WithCategoriesWithoutKeys_ShouldDeleteSuccessfully() {
// preparation - create categories without keys
final CategoryDraft categoryWithoutKey1 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category Without Key 1"))
.slug(LocalizedString.of(Locale.ENGLISH, "no-key-slug-1"))
.build();

final CategoryDraft categoryWithoutKey2 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category Without Key 2"))
.slug(LocalizedString.of(Locale.ENGLISH, "no-key-slug-2"))
.build();

final CategoryDraft categoryWithKey =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category With Key"))
.slug(LocalizedString.of(Locale.ENGLISH, "with-key-slug"))
.key("with-key")
.build();

TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryWithoutKey1).executeBlocking();
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryWithoutKey2).executeBlocking();
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryWithKey).executeBlocking();

// test - delete categories without keys by their slugs
CategoryITUtils.deleteCategoriesBySlug(
TestClientUtils.CTP_TARGET_CLIENT,
Locale.ENGLISH,
List.of("no-key-slug-1", "no-key-slug-2"));

// assertion - verify only the category with key remains
final List<Category> remainingCategories =
TestClientUtils.CTP_TARGET_CLIENT
.categories()
.get()
.execute()
.toCompletableFuture()
.join()
.getBody()
.getResults();

assertThat(remainingCategories).hasSize(1);
assertThat(remainingCategories.get(0).getSlug().get(Locale.ENGLISH)).isEqualTo("with-key-slug");
assertThat(remainingCategories.get(0).getKey()).isEqualTo("with-key");
}

@Test
void deleteCategoriesBySlug_WithNonExistingSlugs_ShouldNotThrowException() {
// preparation - create one category
final CategoryDraft category =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category"))
.slug(LocalizedString.of(Locale.ENGLISH, "existing-slug"))
.key("existing-key")
.build();

TestClientUtils.CTP_TARGET_CLIENT.categories().create(category).executeBlocking();

// test - try to delete categories with non-existing slugs
CategoryITUtils.deleteCategoriesBySlug(
TestClientUtils.CTP_TARGET_CLIENT,
Locale.ENGLISH,
List.of("non-existing-slug-1", "non-existing-slug-2"));

// assertion - verify the existing category was not affected
final List<Category> remainingCategories =
TestClientUtils.CTP_TARGET_CLIENT
.categories()
.get()
.execute()
.toCompletableFuture()
.join()
.getBody()
.getResults();

assertThat(remainingCategories).hasSize(1);
assertThat(remainingCategories.get(0).getSlug().get(Locale.ENGLISH)).isEqualTo("existing-slug");
}

@Test
void deleteCategoriesBySlug_WithEmptySlugList_ShouldNotDeleteAnything() {
// preparation - create categories
final CategoryDraft category1 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category 1"))
.slug(LocalizedString.of(Locale.ENGLISH, "slug-1"))
.key("key1")
.build();

final CategoryDraft category2 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category 2"))
.slug(LocalizedString.of(Locale.ENGLISH, "slug-2"))
.key("key2")
.build();

TestClientUtils.CTP_TARGET_CLIENT.categories().create(category1).executeBlocking();
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category2).executeBlocking();

// test - call with empty list
CategoryITUtils.deleteCategoriesBySlug(
TestClientUtils.CTP_TARGET_CLIENT, Locale.ENGLISH, List.of());

// assertion - verify both categories still exist
final List<Category> remainingCategories =
TestClientUtils.CTP_TARGET_CLIENT
.categories()
.get()
.execute()
.toCompletableFuture()
.join()
.getBody()
.getResults();

assertThat(remainingCategories).hasSize(2);
assertThat(remainingCategories)
.extracting(category -> category.getSlug().get(Locale.ENGLISH))
.containsExactlyInAnyOrder("slug-1", "slug-2");
}

@Test
void deleteCategoriesBySlug_WithDifferentLocale_ShouldDeleteMatchingCategories() {
// preparation - create categories with German slugs
final CategoryDraft categoryDe1 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.GERMAN, "Kategorie 1"))
.slug(LocalizedString.of(Locale.GERMAN, "deutsche-slug-1"))
.key("de-key1")
.build();

final CategoryDraft categoryDe2 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.GERMAN, "Kategorie 2"))
.slug(LocalizedString.of(Locale.GERMAN, "deutsche-slug-2"))
.key("de-key2")
.build();

final CategoryDraft categoryDe3 =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.GERMAN, "Kategorie 3"))
.slug(LocalizedString.of(Locale.GERMAN, "andere-slug"))
.key("de-key3")
.build();

TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryDe1).executeBlocking();
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryDe2).executeBlocking();
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryDe3).executeBlocking();

// test - delete categories with German slugs
CategoryITUtils.deleteCategoriesBySlug(
TestClientUtils.CTP_TARGET_CLIENT, Locale.GERMAN, List.of("deutsche-slug-1"));

// assertion - verify only 2 categories remain
final List<Category> remainingCategories =
TestClientUtils.CTP_TARGET_CLIENT
.categories()
.get()
.execute()
.toCompletableFuture()
.join()
.getBody()
.getResults();

assertThat(remainingCategories).hasSize(2);
assertThat(remainingCategories)
.extracting(category -> category.getSlug().get(Locale.GERMAN))
.containsExactlyInAnyOrder("deutsche-slug-2", "andere-slug");
}

@Test
void deleteCategoriesBySlug_WithDuplicateSlugsInList_ShouldHandleGracefully() {
// preparation - create category
final CategoryDraft category =
CategoryDraftBuilder.of()
.name(LocalizedString.of(Locale.ENGLISH, "Category"))
.slug(LocalizedString.of(Locale.ENGLISH, "duplicate-slug"))
.key("dup-key")
.build();

TestClientUtils.CTP_TARGET_CLIENT.categories().create(category).executeBlocking();

// test - try to delete with duplicate slugs in the list
CategoryITUtils.deleteCategoriesBySlug(
TestClientUtils.CTP_TARGET_CLIENT,
Locale.ENGLISH,
List.of("duplicate-slug", "duplicate-slug", "duplicate-slug"));

// assertion - verify category was deleted (no error thrown)
final List<Category> remainingCategories =
TestClientUtils.CTP_TARGET_CLIENT
.categories()
.get()
.execute()
.toCompletableFuture()
.join()
.getBody()
.getResults();

assertThat(remainingCategories).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ void setupTest() {
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_TARGET_CLIENT);
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_SOURCE_CLIENT);

// Clean up any categories without keys that deleteAllCategories() might have missed
CategoryITUtils.deleteCategoriesBySlug(
TestClientUtils.CTP_TARGET_CLIENT,
Locale.ENGLISH,
List.of("furniture1-project-source", "furniture2-project-source"));
CategoryITUtils.deleteCategoriesBySlug(
TestClientUtils.CTP_SOURCE_CLIENT,
Locale.ENGLISH,
List.of("furniture1-project-source", "furniture2-project-source"));

CategoryITUtils.ensureCategories(
TestClientUtils.CTP_TARGET_CLIENT, CategoryITUtils.getCategoryDrafts(null, 2, true));

Expand Down Expand Up @@ -486,6 +496,12 @@ void syncDrafts_fromCategoriesWithoutKeys_ShouldNotUpdateCategories() {
CompletableFuture.allOf(futureCreations.toArray(new CompletableFuture[futureCreations.size()]))
.join();

// Ensure TARGET is clean before creating categories without keys (defensive cleanup)
CategoryITUtils.deleteCategoriesBySlug(
TestClientUtils.CTP_TARGET_CLIENT,
Locale.ENGLISH,
List.of("furniture1-project-source", "furniture2-project-source"));

// Create two categories in the target without Keys.
futureCreations = new ArrayList<>();
final CategoryDraft newCategoryDraft1 =
Expand Down