package com.commercetools.sync.integration.ctpprojectsource.categories; import com.commercetools.sync.categories.CategorySync; import com.commercetools.sync.categories.CategorySyncOptions; import com.commercetools.sync.categories.CategorySyncOptionsBuilder; import com.commercetools.sync.categories.helpers.CategorySyncStatistics; import com.commercetools.sync.commons.exceptions.ReferenceResolutionException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.sphere.sdk.categories.Category; import io.sphere.sdk.categories.CategoryDraft; import io.sphere.sdk.categories.CategoryDraftBuilder; import io.sphere.sdk.categories.commands.CategoryCreateCommand; import io.sphere.sdk.client.ErrorResponseException; import io.sphere.sdk.models.LocalizedString; import io.sphere.sdk.models.errors.DuplicateFieldError; import io.sphere.sdk.types.CustomFieldsDraft; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import static com.commercetools.sync.categories.utils.CategoryReferenceReplacementUtils.buildCategoryQuery; import static com.commercetools.sync.categories.utils.CategoryReferenceReplacementUtils.replaceCategoriesReferenceIdsWithKeys; import static com.commercetools.sync.commons.asserts.statistics.AssertionsForStatistics.assertThat; import static com.commercetools.sync.commons.utils.SyncUtils.batchElements; import static com.commercetools.sync.integration.commons.utils.CategoryITUtils.OLD_CATEGORY_CUSTOM_TYPE_KEY; import static com.commercetools.sync.integration.commons.utils.CategoryITUtils.createCategories; import static com.commercetools.sync.integration.commons.utils.CategoryITUtils.createCategoriesCustomType; import static com.commercetools.sync.integration.commons.utils.CategoryITUtils.createChildren; import static com.commercetools.sync.integration.commons.utils.CategoryITUtils.deleteAllCategories; import static com.commercetools.sync.integration.commons.utils.CategoryITUtils.getCategoryDrafts; import static com.commercetools.sync.integration.commons.utils.CategoryITUtils.getCategoryDraftsWithPrefix; import static com.commercetools.sync.integration.commons.utils.CategoryITUtils.getCustomFieldsDraft; import static com.commercetools.sync.integration.commons.utils.CategoryITUtils.syncBatches; import static com.commercetools.sync.integration.commons.utils.ITUtils.createCustomFieldsJsonMap; import static com.commercetools.sync.integration.commons.utils.ITUtils.deleteTypesFromTargetAndSource; import static com.commercetools.sync.integration.commons.utils.SphereClientUtils.CTP_SOURCE_CLIENT; import static com.commercetools.sync.integration.commons.utils.SphereClientUtils.CTP_TARGET_CLIENT; import static java.lang.String.format; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; class CategorySyncIT { private CategorySync categorySync; private List<String> callBackErrorResponses = new ArrayList<>(); private List<Throwable> callBackExceptions = new ArrayList<>(); private List<String> callBackWarningResponses = new ArrayList<>(); /** * Delete all categories and types from source and target project. Then create custom types for source and target * CTP project categories. */ @BeforeAll static void setup() { deleteAllCategories(CTP_TARGET_CLIENT); deleteAllCategories(CTP_SOURCE_CLIENT); deleteTypesFromTargetAndSource(); createCategoriesCustomType(OLD_CATEGORY_CUSTOM_TYPE_KEY, Locale.ENGLISH, "anyName", CTP_TARGET_CLIENT); createCategoriesCustomType(OLD_CATEGORY_CUSTOM_TYPE_KEY, Locale.ENGLISH, "anyName", CTP_SOURCE_CLIENT); } /** * Deletes Categories and Types from source and target CTP projects, then it populates target CTP project with * category test data. */ @BeforeEach void setupTest() { deleteAllCategories(CTP_TARGET_CLIENT); deleteAllCategories(CTP_SOURCE_CLIENT); createCategories(CTP_TARGET_CLIENT, getCategoryDrafts(null, 2)); callBackErrorResponses = new ArrayList<>(); callBackExceptions = new ArrayList<>(); callBackWarningResponses = new ArrayList<>(); final CategorySyncOptions categorySyncOptions = CategorySyncOptionsBuilder .of(CTP_TARGET_CLIENT) .errorCallback((errorMessage, exception) -> { callBackErrorResponses.add(errorMessage); callBackExceptions.add(exception); }) .warningCallback((warningMessage) -> callBackWarningResponses.add(warningMessage)) .build(); categorySync = new CategorySync(categorySyncOptions); } /** * Cleans up the target and source test data that were built in this test class. */ @AfterAll static void tearDown() { deleteAllCategories(CTP_TARGET_CLIENT); deleteAllCategories(CTP_SOURCE_CLIENT); deleteTypesFromTargetAndSource(); } @Test void syncDrafts_withChangesOnly_ShouldUpdateCategories() { createCategories(CTP_SOURCE_CLIENT, getCategoryDraftsWithPrefix(Locale.ENGLISH, "new", null, 2)); final List<Category> categories = CTP_SOURCE_CLIENT.execute(buildCategoryQuery()) .toCompletableFuture().join().getResults(); // Put the keys in the reference ids to prepare for reference resolution final List<CategoryDraft> categoryDrafts = replaceCategoriesReferenceIdsWithKeys(categories); final CategorySyncStatistics syncStatistics = categorySync.sync(categoryDrafts).toCompletableFuture().join(); assertThat(syncStatistics).hasValues(2, 0, 2, 0); assertThat(callBackErrorResponses).isEmpty(); assertThat(callBackExceptions).isEmpty(); assertThat(callBackWarningResponses).isEmpty(); } @Test void syncDrafts_withNewCategories_ShouldCreateCategories() { createCategories(CTP_SOURCE_CLIENT, getCategoryDraftsWithPrefix(Locale.ENGLISH, "new", null, 3)); final List<Category> categories = CTP_SOURCE_CLIENT.execute(buildCategoryQuery()) .toCompletableFuture().join().getResults(); // Put the keys in the reference ids to prepare for reference resolution final List<CategoryDraft> categoryDrafts = replaceCategoriesReferenceIdsWithKeys(categories); final CategorySyncStatistics syncStatistics = categorySync.sync(categoryDrafts).toCompletableFuture().join(); assertThat(syncStatistics).hasValues(3, 1, 2, 0, 0); assertThat(callBackErrorResponses).isEmpty(); assertThat(callBackExceptions).isEmpty(); assertThat(callBackWarningResponses).isEmpty(); } @Disabled("TODO - GITHUB ISSUE#138: Test should be adjusted after reference resolution refactoring") @Test void syncDrafts_WithUpdatedCategoriesWithoutReferenceKeys_ShouldNotSyncCategories() { createCategories(CTP_SOURCE_CLIENT, getCategoryDraftsWithPrefix(Locale.ENGLISH, "new", null, 2)); final List<Category> categories = CTP_SOURCE_CLIENT.execute(buildCategoryQuery()) .toCompletableFuture().join().getResults(); final List<CategoryDraft> categoryDrafts = categories.stream() .map(category -> CategoryDraftBuilder.of(category).build()) .collect(toList()); final CategorySyncStatistics syncStatistics = categorySync.sync(categoryDrafts).toCompletableFuture().join(); assertThat(syncStatistics).hasValues(2, 0, 0, 2, 0); assertThat(callBackErrorResponses).hasSize(2); final String key1 = categoryDrafts.get(0).getKey(); assertThat(callBackErrorResponses.get(0)).isEqualTo(format("Failed to process the CategoryDraft with" + " key:'%s'. Reason: %s: Failed to resolve custom type reference on " + "CategoryDraft with key:'%s'. " + "Reason: Found a UUID in the id field. Expecting a key without a UUID value. If you want to allow" + " UUID values for reference keys, please use the allowUuidKeys(true) option in the sync options.", key1, ReferenceResolutionException.class.getCanonicalName(), key1)); final String key2 = categoryDrafts.get(1).getKey(); assertThat(callBackErrorResponses.get(1)).isEqualTo(format("Failed to process the CategoryDraft with" + " key:'%s'. Reason: %s: Failed to resolve custom type reference on " + "CategoryDraft with key:'%s'. Reason: " + "Found a UUID in the id field. Expecting a key without a UUID value. If you want to allow UUID values" + " for reference keys, please use the allowUuidKeys(true) option in the sync options.", key2, ReferenceResolutionException.class.getCanonicalName(), key2)); assertThat(callBackExceptions).hasSize(2); assertThat(callBackExceptions.get(0)).isInstanceOf(CompletionException.class); assertThat(callBackExceptions.get(0).getCause()).isInstanceOf(ReferenceResolutionException.class); assertThat(callBackExceptions.get(1)).isInstanceOf(CompletionException.class); assertThat(callBackExceptions.get(1).getCause()).isInstanceOf(ReferenceResolutionException.class); assertThat(callBackWarningResponses).isEmpty(); } @Test @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") // https://github.com/findbugsproject/findbugs/issues/79 void syncDrafts_withNewShuffledBatchOfCategories_ShouldCreateCategories() { //-----------------Test Setup------------------------------------ // Delete all categories in target project deleteAllCategories(CTP_TARGET_CLIENT); // Create a total of 130 categories in the source project final List<Category> subFamily = createChildren(5, null, "root", CTP_SOURCE_CLIENT); for (final Category child : subFamily) { final List<Category> subsubFamily = createChildren(5, child, child.getName().get(Locale.ENGLISH), CTP_SOURCE_CLIENT); for (final Category subChild : subsubFamily) { createChildren(4, subChild, subChild.getName().get(Locale.ENGLISH), CTP_SOURCE_CLIENT); } } //--------------------------------------------------------------- // Fetch categories from source project final List<Category> categories = CTP_SOURCE_CLIENT.execute(buildCategoryQuery()) .toCompletableFuture().join().getResults(); // Put the keys in the reference ids to prepare for reference resolution final List<CategoryDraft> categoryDrafts = replaceCategoriesReferenceIdsWithKeys(categories); // Make sure there is no hierarchical order Collections.shuffle(categoryDrafts); // Simulate batches of categories where not all parent references are supplied at once. final List<List<CategoryDraft>> batches = batchElements(categoryDrafts, 13); final CategorySyncStatistics syncStatistics = syncBatches(categorySync, batches, CompletableFuture.completedFuture(null)).toCompletableFuture().join(); assertThat(syncStatistics).hasValues(130, 130, 0, 0, 0); assertThat(callBackErrorResponses).isEmpty(); assertThat(callBackExceptions).isEmpty(); assertThat(callBackWarningResponses).isEmpty(); } @Test @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") // https://github.com/findbugsproject/findbugs/issues/79 void syncDrafts_withExistingShuffledCategoriesWithChangingCategoryHierarchy_ShouldUpdateCategories() { //-----------------Test Setup------------------------------------ // Delete all categories in target project deleteAllCategories(CTP_TARGET_CLIENT); // Create a total of 130 categories in the target project final List<Category> subFamily = createChildren(5, null, "root", CTP_TARGET_CLIENT); for (final Category child : subFamily) { final List<Category> subsubFamily = createChildren(5, child, child.getName().get(Locale.ENGLISH), CTP_TARGET_CLIENT); for (final Category subChild : subsubFamily) { createChildren(4, subChild, subChild.getName().get(Locale.ENGLISH), CTP_TARGET_CLIENT); } } //--------------------------------------------------------------- // Create a total of 130 categories in the source project final List<Category> sourceSubFamily = createChildren(5, null, "root", CTP_SOURCE_CLIENT); for (final Category child : sourceSubFamily) { final List<Category> subsubFamily = createChildren(5, sourceSubFamily.get(0), child.getName().get(Locale.ENGLISH), CTP_SOURCE_CLIENT); for (final Category subChild : subsubFamily) { createChildren(4, sourceSubFamily.get(0), subChild.getName().get(Locale.ENGLISH), CTP_SOURCE_CLIENT); } } //--------------------------------------------------------------- // Fetch categories from source project final List<Category> categories = CTP_SOURCE_CLIENT.execute(buildCategoryQuery()) .toCompletableFuture().join().getResults(); // Put the keys in the reference ids to prepare for reference resolution final List<CategoryDraft> categoryDrafts = replaceCategoriesReferenceIdsWithKeys(categories); Collections.shuffle(categoryDrafts); final List<List<CategoryDraft>> batches = batchElements(categoryDrafts, 13); final CategorySyncStatistics syncStatistics = syncBatches(categorySync, batches, CompletableFuture.completedFuture(null)).toCompletableFuture().join(); assertThat(syncStatistics).hasValues(130, 0, 120, 0, 0); assertThat(callBackErrorResponses).isEmpty(); assertThat(callBackExceptions).isEmpty(); assertThat(callBackWarningResponses).isEmpty(); } @Test @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") // https://github.com/findbugsproject/findbugs/issues/79 void syncDrafts_withExistingCategoriesThatChangeParents_ShouldUpdateCategories() { //-----------------Test Setup------------------------------------ // Delete all categories in target project deleteAllCategories(CTP_TARGET_CLIENT); // Create a total of 3 categories in the target project (2 roots and 1 child to the first root) final List<Category> subFamily = createChildren(2, null, "root", CTP_TARGET_CLIENT); final Category firstRoot = subFamily.get(0); createChildren(1, firstRoot, "child", CTP_TARGET_CLIENT); //--------------------------------------------------------------- // Create a total of 2 categories in the source project (2 roots and 1 child to the second root) final List<Category> sourceSubFamily = createChildren(2, null, "root", CTP_SOURCE_CLIENT); final Category secondRoot = sourceSubFamily.get(1); createChildren(1, secondRoot, "child", CTP_SOURCE_CLIENT); //--------------------------------------------------------------- // Fetch categories from source project final List<Category> categories = CTP_SOURCE_CLIENT.execute(buildCategoryQuery()) .toCompletableFuture().join().getResults(); // Put the keys in the reference ids to prepare for reference resolution final List<CategoryDraft> categoryDrafts = replaceCategoriesReferenceIdsWithKeys(categories); Collections.shuffle(categoryDrafts); final List<List<CategoryDraft>> batches = batchElements(categoryDrafts, 1); final CategorySyncStatistics syncStatistics = syncBatches(categorySync, batches, CompletableFuture.completedFuture(null)).toCompletableFuture().join(); assertThat(syncStatistics).hasValues(3, 0, 1, 0, 0); assertThat(callBackErrorResponses).isEmpty(); assertThat(callBackExceptions).isEmpty(); assertThat(callBackWarningResponses).isEmpty(); } @Test @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") // https://github.com/findbugsproject/findbugs/issues/79 void syncDrafts_withANonExistingNewParent_ShouldUpdateCategories() { //-----------------Test Setup------------------------------------ // Delete all categories in target project deleteAllCategories(CTP_TARGET_CLIENT); // Create a total of 2 categories in the target project. final CategoryDraft parentDraft = CategoryDraftBuilder .of(LocalizedString.of(Locale.ENGLISH, "parent"), LocalizedString.of(Locale.ENGLISH, "parent")) .key("parent") .custom(CustomFieldsDraft.ofTypeKeyAndJson(OLD_CATEGORY_CUSTOM_TYPE_KEY, createCustomFieldsJsonMap())) .build(); final Category parentCreated = CTP_TARGET_CLIENT.execute(CategoryCreateCommand.of(parentDraft)) .toCompletableFuture().join(); final CategoryDraft childDraft = CategoryDraftBuilder .of(LocalizedString.of(Locale.ENGLISH, "child"), LocalizedString.of(Locale.ENGLISH, "child")) .key("child") .parent(parentCreated.toResourceIdentifier()) .custom(CustomFieldsDraft.ofTypeKeyAndJson(OLD_CATEGORY_CUSTOM_TYPE_KEY, createCustomFieldsJsonMap())) .build(); CTP_TARGET_CLIENT.execute(CategoryCreateCommand.of(childDraft)).toCompletableFuture().join(); //------------------------------------------------------------------------------------------------------------ // Create a total of 2 categories in the source project final CategoryDraft sourceParentDraft = CategoryDraftBuilder .of(LocalizedString.of(Locale.ENGLISH, "new-parent"), LocalizedString.of(Locale.ENGLISH, "new-parent")) .key("new-parent") .custom(CustomFieldsDraft.ofTypeKeyAndJson(OLD_CATEGORY_CUSTOM_TYPE_KEY, createCustomFieldsJsonMap())) .build(); final Category sourceParentCreated = CTP_SOURCE_CLIENT.execute(CategoryCreateCommand.of(sourceParentDraft)) .toCompletableFuture().join(); final CategoryDraft sourceChildDraft = CategoryDraftBuilder .of(LocalizedString.of(Locale.ENGLISH, "child-new-name"), LocalizedString.of(Locale.ENGLISH, "child")) .key("child") .parent(sourceParentCreated.toResourceIdentifier()) .custom(CustomFieldsDraft.ofTypeKeyAndJson(OLD_CATEGORY_CUSTOM_TYPE_KEY, createCustomFieldsJsonMap())) .build(); CTP_SOURCE_CLIENT.execute(CategoryCreateCommand.of(sourceChildDraft)).toCompletableFuture().join(); //--------------------------------------------------------------- // Fetch categories from source project final List<Category> categories = CTP_SOURCE_CLIENT .execute(buildCategoryQuery().withSort(sorting -> sorting.createdAt().sort().asc())) .toCompletableFuture().join().getResults(); // Put the keys in the reference ids to prepare for reference resolution final List<CategoryDraft> categoryDrafts = replaceCategoriesReferenceIdsWithKeys(categories); // To simulate the new parent coming in a later draft Collections.reverse(categoryDrafts); final List<List<CategoryDraft>> batches = batchElements(categoryDrafts, 1); final CategorySyncStatistics syncStatistics = syncBatches(categorySync, batches, CompletableFuture.completedFuture(null)).toCompletableFuture().join(); assertThat(syncStatistics).hasValues(2, 1, 1, 0, 0); assertThat(callBackErrorResponses).isEmpty(); assertThat(callBackExceptions).isEmpty(); assertThat(callBackWarningResponses).isEmpty(); } @Test void syncDrafts_fromCategoriesWithoutKeys_ShouldNotUpdateCategories() { final CategoryDraft oldCategoryDraft1 = CategoryDraftBuilder .of(LocalizedString.of(Locale.ENGLISH, "cat1"), LocalizedString.of(Locale.ENGLISH, "furniture1")) .custom(getCustomFieldsDraft()) .key("newKey1") .build(); final CategoryDraft oldCategoryDraft2 = CategoryDraftBuilder .of(LocalizedString.of(Locale.ENGLISH, "cat2"), LocalizedString.of(Locale.ENGLISH, "furniture2")) .custom(getCustomFieldsDraft()) .key("newKey2") .build(); // Create two categories in the source with Keys. List<CompletableFuture<Category>> futureCreations = new ArrayList<>(); futureCreations.add(CTP_SOURCE_CLIENT.execute(CategoryCreateCommand.of(oldCategoryDraft1)) .toCompletableFuture()); futureCreations.add(CTP_SOURCE_CLIENT.execute(CategoryCreateCommand.of(oldCategoryDraft2)) .toCompletableFuture()); CompletableFuture.allOf(futureCreations.toArray(new CompletableFuture[futureCreations.size()])).join(); // Create two categories in the target without Keys. futureCreations = new ArrayList<>(); final CategoryDraft newCategoryDraft1 = CategoryDraftBuilder.of(oldCategoryDraft1).key(null).build(); final CategoryDraft newCategoryDraft2 = CategoryDraftBuilder.of(oldCategoryDraft2).key(null).build(); futureCreations.add(CTP_TARGET_CLIENT.execute(CategoryCreateCommand.of(newCategoryDraft1)) .toCompletableFuture()); futureCreations.add(CTP_TARGET_CLIENT.execute(CategoryCreateCommand.of(newCategoryDraft2)) .toCompletableFuture()); CompletableFuture.allOf(futureCreations.toArray(new CompletableFuture[futureCreations.size()])).join(); //--------- final List<Category> categories = CTP_SOURCE_CLIENT.execute(buildCategoryQuery()) .toCompletableFuture().join().getResults(); // Put the keys in the reference ids to prepare for reference resolution final List<CategoryDraft> categoryDrafts = replaceCategoriesReferenceIdsWithKeys(categories); final CategorySyncStatistics syncStatistics = categorySync.sync(categoryDrafts).toCompletableFuture().join(); assertThat(syncStatistics).hasValues(2, 0, 0, 2, 0); assertThat(callBackErrorResponses) .hasSize(2) .allSatisfy(errorMessage -> { assertThat(errorMessage).contains("\"code\" : \"DuplicateField\""); assertThat(errorMessage).contains("\"field\" : \"slug.en\""); }); assertThat(callBackExceptions) .hasSize(2) .allSatisfy(exception -> { assertThat(exception).isExactlyInstanceOf(ErrorResponseException.class); final ErrorResponseException errorResponse = ((ErrorResponseException)exception); final List<DuplicateFieldError> fieldErrors = errorResponse .getErrors() .stream() .map(sphereError -> { assertThat(sphereError.getCode()).isEqualTo(DuplicateFieldError.CODE); return sphereError.as(DuplicateFieldError.class); }) .collect(toList()); assertThat(fieldErrors).hasSize(1); assertThat(fieldErrors).allSatisfy(error -> assertThat(error.getField()).isEqualTo("slug.en")); }); assertThat(callBackWarningResponses) .hasSize(2) .allSatisfy(warningMessage -> assertThat(warningMessage) .matches("Category with id: '.*' has no key set. Keys are required for category matching.")); } }