Skip to content

Commit c0cf253

Browse files
Merge pull request #1234 from commercetools/bugfix/error_deleting_categories
Bugfix/error deleting categories
2 parents c8c4311 + 0c40651 commit c0cf253

File tree

3 files changed

+359
-0
lines changed

3 files changed

+359
-0
lines changed

src/integration-test/java/com/commercetools/sync/integration/commons/utils/CategoryITUtils.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,45 @@ public static void deleteAllCategories(@Nonnull final ProjectApiRoot ctpClient)
311311
});
312312
}
313313

314+
/**
315+
* Deletes categories from CTP projects defined by the {@code ctpClient} that match any of the
316+
* supplied slugs in the specified locale. This method is useful for cleaning up categories that
317+
* may not have keys set (which prevents them from being properly tracked by {@link
318+
* #deleteAllCategories(ProjectApiRoot)}).
319+
*
320+
* @param ctpClient defines the CTP project to delete the categories from.
321+
* @param locale the locale to use when matching slugs.
322+
* @param slugs the list of slugs to match for deletion.
323+
*/
324+
public static void deleteCategoriesBySlug(
325+
@Nonnull final ProjectApiRoot ctpClient,
326+
@Nonnull final Locale locale,
327+
@Nonnull final List<String> slugs) {
328+
slugs.forEach(
329+
slug -> {
330+
ctpClient
331+
.categories()
332+
.get()
333+
.addWhere("slug(" + locale.getLanguage() + "=:slug)")
334+
.addPredicateVar("slug", slug)
335+
.execute()
336+
.toCompletableFuture()
337+
.join()
338+
.getBody()
339+
.getResults()
340+
.forEach(
341+
category ->
342+
ctpClient
343+
.categories()
344+
.withId(category.getId())
345+
.delete()
346+
.withVersion(category.getVersion())
347+
.execute()
348+
.toCompletableFuture()
349+
.join());
350+
});
351+
}
352+
314353
private static List<Category> sortCategoriesByLeastAncestors(
315354
@Nonnull final List<Category> categories) {
316355
categories.sort(Comparator.comparingInt(category -> category.getAncestors().size()));
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
package com.commercetools.sync.integration.commons.utils;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.commercetools.api.models.category.Category;
6+
import com.commercetools.api.models.category.CategoryDraft;
7+
import com.commercetools.api.models.category.CategoryDraftBuilder;
8+
import com.commercetools.api.models.common.LocalizedString;
9+
import java.util.List;
10+
import java.util.Locale;
11+
import org.junit.jupiter.api.AfterAll;
12+
import org.junit.jupiter.api.BeforeAll;
13+
import org.junit.jupiter.api.BeforeEach;
14+
import org.junit.jupiter.api.Test;
15+
16+
/**
17+
* Integration tests for {@link CategoryITUtils} utility methods that require actual CTP API
18+
* interactions.
19+
*/
20+
class CategoryITUtilsIT {
21+
22+
/** Delete all categories and types from target project before running tests. */
23+
@BeforeAll
24+
static void setup() {
25+
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_TARGET_CLIENT);
26+
ITUtils.deleteTypes(TestClientUtils.CTP_TARGET_CLIENT);
27+
}
28+
29+
/** Clean up before each test to ensure a fresh state. */
30+
@BeforeEach
31+
void setupTest() {
32+
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_TARGET_CLIENT);
33+
}
34+
35+
/** Cleans up the target test data that were built in this test class. */
36+
@AfterAll
37+
static void tearDown() {
38+
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_TARGET_CLIENT);
39+
ITUtils.deleteTypes(TestClientUtils.CTP_TARGET_CLIENT);
40+
}
41+
42+
@Test
43+
void deleteCategoriesBySlug_WithExistingCategories_ShouldDeleteOnlyMatchingSlugs() {
44+
// preparation - create 4 categories with different slugs
45+
final CategoryDraft category1 =
46+
CategoryDraftBuilder.of()
47+
.name(LocalizedString.of(Locale.ENGLISH, "Category 1"))
48+
.slug(LocalizedString.of(Locale.ENGLISH, "test-slug-1"))
49+
.key("key1")
50+
.build();
51+
52+
final CategoryDraft category2 =
53+
CategoryDraftBuilder.of()
54+
.name(LocalizedString.of(Locale.ENGLISH, "Category 2"))
55+
.slug(LocalizedString.of(Locale.ENGLISH, "test-slug-2"))
56+
.key("key2")
57+
.build();
58+
59+
final CategoryDraft category3 =
60+
CategoryDraftBuilder.of()
61+
.name(LocalizedString.of(Locale.ENGLISH, "Category 3"))
62+
.slug(LocalizedString.of(Locale.ENGLISH, "test-slug-3"))
63+
.key("key3")
64+
.build();
65+
66+
final CategoryDraft category4 =
67+
CategoryDraftBuilder.of()
68+
.name(LocalizedString.of(Locale.ENGLISH, "Category 4"))
69+
.slug(LocalizedString.of(Locale.ENGLISH, "other-slug"))
70+
.key("key4")
71+
.build();
72+
73+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category1).executeBlocking();
74+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category2).executeBlocking();
75+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category3).executeBlocking();
76+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category4).executeBlocking();
77+
78+
// test - delete categories with slugs test-slug-1 and test-slug-2
79+
CategoryITUtils.deleteCategoriesBySlug(
80+
TestClientUtils.CTP_TARGET_CLIENT, Locale.ENGLISH, List.of("test-slug-1", "test-slug-2"));
81+
82+
// assertion - verify only 2 categories remain (test-slug-3 and other-slug)
83+
final List<Category> remainingCategories =
84+
TestClientUtils.CTP_TARGET_CLIENT
85+
.categories()
86+
.get()
87+
.execute()
88+
.toCompletableFuture()
89+
.join()
90+
.getBody()
91+
.getResults();
92+
93+
assertThat(remainingCategories).hasSize(2);
94+
assertThat(remainingCategories)
95+
.extracting(category -> category.getSlug().get(Locale.ENGLISH))
96+
.containsExactlyInAnyOrder("test-slug-3", "other-slug");
97+
assertThat(remainingCategories)
98+
.extracting(Category::getKey)
99+
.containsExactlyInAnyOrder("key3", "key4");
100+
}
101+
102+
@Test
103+
void deleteCategoriesBySlug_WithCategoriesWithoutKeys_ShouldDeleteSuccessfully() {
104+
// preparation - create categories without keys
105+
final CategoryDraft categoryWithoutKey1 =
106+
CategoryDraftBuilder.of()
107+
.name(LocalizedString.of(Locale.ENGLISH, "Category Without Key 1"))
108+
.slug(LocalizedString.of(Locale.ENGLISH, "no-key-slug-1"))
109+
.build();
110+
111+
final CategoryDraft categoryWithoutKey2 =
112+
CategoryDraftBuilder.of()
113+
.name(LocalizedString.of(Locale.ENGLISH, "Category Without Key 2"))
114+
.slug(LocalizedString.of(Locale.ENGLISH, "no-key-slug-2"))
115+
.build();
116+
117+
final CategoryDraft categoryWithKey =
118+
CategoryDraftBuilder.of()
119+
.name(LocalizedString.of(Locale.ENGLISH, "Category With Key"))
120+
.slug(LocalizedString.of(Locale.ENGLISH, "with-key-slug"))
121+
.key("with-key")
122+
.build();
123+
124+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryWithoutKey1).executeBlocking();
125+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryWithoutKey2).executeBlocking();
126+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryWithKey).executeBlocking();
127+
128+
// test - delete categories without keys by their slugs
129+
CategoryITUtils.deleteCategoriesBySlug(
130+
TestClientUtils.CTP_TARGET_CLIENT,
131+
Locale.ENGLISH,
132+
List.of("no-key-slug-1", "no-key-slug-2"));
133+
134+
// assertion - verify only the category with key remains
135+
final List<Category> remainingCategories =
136+
TestClientUtils.CTP_TARGET_CLIENT
137+
.categories()
138+
.get()
139+
.execute()
140+
.toCompletableFuture()
141+
.join()
142+
.getBody()
143+
.getResults();
144+
145+
assertThat(remainingCategories).hasSize(1);
146+
assertThat(remainingCategories.get(0).getSlug().get(Locale.ENGLISH)).isEqualTo("with-key-slug");
147+
assertThat(remainingCategories.get(0).getKey()).isEqualTo("with-key");
148+
}
149+
150+
@Test
151+
void deleteCategoriesBySlug_WithNonExistingSlugs_ShouldNotThrowException() {
152+
// preparation - create one category
153+
final CategoryDraft category =
154+
CategoryDraftBuilder.of()
155+
.name(LocalizedString.of(Locale.ENGLISH, "Category"))
156+
.slug(LocalizedString.of(Locale.ENGLISH, "existing-slug"))
157+
.key("existing-key")
158+
.build();
159+
160+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category).executeBlocking();
161+
162+
// test - try to delete categories with non-existing slugs
163+
CategoryITUtils.deleteCategoriesBySlug(
164+
TestClientUtils.CTP_TARGET_CLIENT,
165+
Locale.ENGLISH,
166+
List.of("non-existing-slug-1", "non-existing-slug-2"));
167+
168+
// assertion - verify the existing category was not affected
169+
final List<Category> remainingCategories =
170+
TestClientUtils.CTP_TARGET_CLIENT
171+
.categories()
172+
.get()
173+
.execute()
174+
.toCompletableFuture()
175+
.join()
176+
.getBody()
177+
.getResults();
178+
179+
assertThat(remainingCategories).hasSize(1);
180+
assertThat(remainingCategories.get(0).getSlug().get(Locale.ENGLISH)).isEqualTo("existing-slug");
181+
}
182+
183+
@Test
184+
void deleteCategoriesBySlug_WithEmptySlugList_ShouldNotDeleteAnything() {
185+
// preparation - create categories
186+
final CategoryDraft category1 =
187+
CategoryDraftBuilder.of()
188+
.name(LocalizedString.of(Locale.ENGLISH, "Category 1"))
189+
.slug(LocalizedString.of(Locale.ENGLISH, "slug-1"))
190+
.key("key1")
191+
.build();
192+
193+
final CategoryDraft category2 =
194+
CategoryDraftBuilder.of()
195+
.name(LocalizedString.of(Locale.ENGLISH, "Category 2"))
196+
.slug(LocalizedString.of(Locale.ENGLISH, "slug-2"))
197+
.key("key2")
198+
.build();
199+
200+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category1).executeBlocking();
201+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category2).executeBlocking();
202+
203+
// test - call with empty list
204+
CategoryITUtils.deleteCategoriesBySlug(
205+
TestClientUtils.CTP_TARGET_CLIENT, Locale.ENGLISH, List.of());
206+
207+
// assertion - verify both categories still exist
208+
final List<Category> remainingCategories =
209+
TestClientUtils.CTP_TARGET_CLIENT
210+
.categories()
211+
.get()
212+
.execute()
213+
.toCompletableFuture()
214+
.join()
215+
.getBody()
216+
.getResults();
217+
218+
assertThat(remainingCategories).hasSize(2);
219+
assertThat(remainingCategories)
220+
.extracting(category -> category.getSlug().get(Locale.ENGLISH))
221+
.containsExactlyInAnyOrder("slug-1", "slug-2");
222+
}
223+
224+
@Test
225+
void deleteCategoriesBySlug_WithDifferentLocale_ShouldDeleteMatchingCategories() {
226+
// preparation - create categories with German slugs
227+
final CategoryDraft categoryDe1 =
228+
CategoryDraftBuilder.of()
229+
.name(LocalizedString.of(Locale.GERMAN, "Kategorie 1"))
230+
.slug(LocalizedString.of(Locale.GERMAN, "deutsche-slug-1"))
231+
.key("de-key1")
232+
.build();
233+
234+
final CategoryDraft categoryDe2 =
235+
CategoryDraftBuilder.of()
236+
.name(LocalizedString.of(Locale.GERMAN, "Kategorie 2"))
237+
.slug(LocalizedString.of(Locale.GERMAN, "deutsche-slug-2"))
238+
.key("de-key2")
239+
.build();
240+
241+
final CategoryDraft categoryDe3 =
242+
CategoryDraftBuilder.of()
243+
.name(LocalizedString.of(Locale.GERMAN, "Kategorie 3"))
244+
.slug(LocalizedString.of(Locale.GERMAN, "andere-slug"))
245+
.key("de-key3")
246+
.build();
247+
248+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryDe1).executeBlocking();
249+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryDe2).executeBlocking();
250+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(categoryDe3).executeBlocking();
251+
252+
// test - delete categories with German slugs
253+
CategoryITUtils.deleteCategoriesBySlug(
254+
TestClientUtils.CTP_TARGET_CLIENT, Locale.GERMAN, List.of("deutsche-slug-1"));
255+
256+
// assertion - verify only 2 categories remain
257+
final List<Category> remainingCategories =
258+
TestClientUtils.CTP_TARGET_CLIENT
259+
.categories()
260+
.get()
261+
.execute()
262+
.toCompletableFuture()
263+
.join()
264+
.getBody()
265+
.getResults();
266+
267+
assertThat(remainingCategories).hasSize(2);
268+
assertThat(remainingCategories)
269+
.extracting(category -> category.getSlug().get(Locale.GERMAN))
270+
.containsExactlyInAnyOrder("deutsche-slug-2", "andere-slug");
271+
}
272+
273+
@Test
274+
void deleteCategoriesBySlug_WithDuplicateSlugsInList_ShouldHandleGracefully() {
275+
// preparation - create category
276+
final CategoryDraft category =
277+
CategoryDraftBuilder.of()
278+
.name(LocalizedString.of(Locale.ENGLISH, "Category"))
279+
.slug(LocalizedString.of(Locale.ENGLISH, "duplicate-slug"))
280+
.key("dup-key")
281+
.build();
282+
283+
TestClientUtils.CTP_TARGET_CLIENT.categories().create(category).executeBlocking();
284+
285+
// test - try to delete with duplicate slugs in the list
286+
CategoryITUtils.deleteCategoriesBySlug(
287+
TestClientUtils.CTP_TARGET_CLIENT,
288+
Locale.ENGLISH,
289+
List.of("duplicate-slug", "duplicate-slug", "duplicate-slug"));
290+
291+
// assertion - verify category was deleted (no error thrown)
292+
final List<Category> remainingCategories =
293+
TestClientUtils.CTP_TARGET_CLIENT
294+
.categories()
295+
.get()
296+
.execute()
297+
.toCompletableFuture()
298+
.join()
299+
.getBody()
300+
.getResults();
301+
302+
assertThat(remainingCategories).isEmpty();
303+
}
304+
}

src/integration-test/java/com/commercetools/sync/integration/ctpprojectsource/categories/CategorySyncIT.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ void setupTest() {
6868
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_TARGET_CLIENT);
6969
CategoryITUtils.deleteAllCategories(TestClientUtils.CTP_SOURCE_CLIENT);
7070

71+
// Clean up any categories without keys that deleteAllCategories() might have missed
72+
CategoryITUtils.deleteCategoriesBySlug(
73+
TestClientUtils.CTP_TARGET_CLIENT,
74+
Locale.ENGLISH,
75+
List.of("furniture1-project-source", "furniture2-project-source"));
76+
CategoryITUtils.deleteCategoriesBySlug(
77+
TestClientUtils.CTP_SOURCE_CLIENT,
78+
Locale.ENGLISH,
79+
List.of("furniture1-project-source", "furniture2-project-source"));
80+
7181
CategoryITUtils.ensureCategories(
7282
TestClientUtils.CTP_TARGET_CLIENT, CategoryITUtils.getCategoryDrafts(null, 2, true));
7383

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

499+
// Ensure TARGET is clean before creating categories without keys (defensive cleanup)
500+
CategoryITUtils.deleteCategoriesBySlug(
501+
TestClientUtils.CTP_TARGET_CLIENT,
502+
Locale.ENGLISH,
503+
List.of("furniture1-project-source", "furniture2-project-source"));
504+
489505
// Create two categories in the target without Keys.
490506
futureCreations = new ArrayList<>();
491507
final CategoryDraft newCategoryDraft1 =

0 commit comments

Comments
 (0)