diff --git a/pom.xml b/pom.xml index 4e7b3104d..228b5b9f5 100644 --- a/pom.xml +++ b/pom.xml @@ -445,6 +445,9 @@ ${basedir}/ramls/holdings-record.json ${basedir}/ramls/holdings-records-source.json ${basedir}/ramls/mappingMetadataDto.json + ${basedir}/ramls/holdings_update_ownership.json + ${basedir}/ramls/items_update_ownership.json + ${basedir}/ramls/update_ownership_response.json ${basedir}/ramls/instance-ingress-event.json org.folio diff --git a/ramls/update_ownership_response.json b/ramls/update_ownership_response.json new file mode 100644 index 000000000..9ef38d0ef --- /dev/null +++ b/ramls/update_ownership_response.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Holder for errors during item/holdingsRecord update ownership", + "type": "object", + "properties": { + "notUpdatedEntities": { + "description": "Item/HoldingsRecord errors", + "type": "array", + "items": { + "type": "object", + "properties": { + "entityId": { + "$ref": "uuid.json", + "description": "Id of item/holdingsRecord" + }, + "errorMessage": { + "type": "string", + "description": "Error message" + } + } + } + } + }, + "additionalProperties": false +} diff --git a/src/main/java/org/folio/inventory/InventoryVerticle.java b/src/main/java/org/folio/inventory/InventoryVerticle.java index 26af54824..80f2bdae9 100644 --- a/src/main/java/org/folio/inventory/InventoryVerticle.java +++ b/src/main/java/org/folio/inventory/InventoryVerticle.java @@ -70,7 +70,7 @@ public void start(Promise started) { new ItemsByHoldingsRecordId(storage, client).register(router); new InventoryConfigApi().register(router); new TenantApi().register(router); - new UpdateOwnershipApi(storage, client).register(router); + new UpdateOwnershipApi(storage, client, consortiumService).register(router); Handler> onHttpServerStart = result -> { if (result.succeeded()) { diff --git a/src/main/java/org/folio/inventory/resources/MoveApi.java b/src/main/java/org/folio/inventory/resources/MoveApi.java index 14216ddb0..68990f155 100644 --- a/src/main/java/org/folio/inventory/resources/MoveApi.java +++ b/src/main/java/org/folio/inventory/resources/MoveApi.java @@ -2,20 +2,21 @@ import static java.util.stream.Collectors.toList; import static org.folio.inventory.support.JsonArrayHelper.toListOfStrings; -import static org.folio.inventory.support.http.server.JsonResponse.success; +import static org.folio.inventory.support.MoveApiUtil.createHoldingsRecordsFetchClient; +import static org.folio.inventory.support.MoveApiUtil.createHoldingsStorageClient; +import static org.folio.inventory.support.MoveApiUtil.createHttpClient; +import static org.folio.inventory.support.MoveApiUtil.createItemStorageClient; +import static org.folio.inventory.support.MoveApiUtil.createItemsFetchClient; +import static org.folio.inventory.support.MoveApiUtil.respond; import static org.folio.inventory.support.http.server.JsonResponse.unprocessableEntity; import static org.folio.inventory.validation.MoveValidator.holdingsMoveHasRequiredFields; import static org.folio.inventory.validation.MoveValidator.itemsMoveHasRequiredFields; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -import org.apache.commons.collections4.ListUtils; import org.folio.HoldingsRecord; import org.folio.inventory.common.WebContext; import org.folio.inventory.domain.HoldingsRecordCollection; @@ -23,20 +24,17 @@ import org.folio.inventory.domain.items.ItemCollection; import org.folio.inventory.storage.Storage; import org.folio.inventory.storage.external.CollectionResourceClient; -import org.folio.inventory.storage.external.CqlQuery; import org.folio.inventory.storage.external.MultipleRecordsFetchClient; import org.folio.inventory.support.ItemUtil; -import org.folio.inventory.support.http.client.OkapiHttpClient; +import org.folio.inventory.support.MoveApiUtil; import org.folio.inventory.support.http.server.JsonResponse; import org.folio.inventory.support.http.server.ServerErrorResponse; import org.folio.inventory.support.http.server.ValidationError; import io.vertx.core.http.HttpClient; -import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.handler.BodyHandler; public class MoveApi extends AbstractInventoryResource { @@ -44,12 +42,6 @@ public class MoveApi extends AbstractInventoryResource { public static final String TO_INSTANCE_ID = "toInstanceId"; public static final String ITEM_IDS = "itemIds"; public static final String HOLDINGS_RECORD_IDS = "holdingsRecordIds"; - public static final String ITEM_STORAGE = "/item-storage/items"; - public static final String ITEMS_PROPERTY = "items"; - public static final String HOLDINGS_RECORDS_PROPERTY = "holdingsRecords"; - public static final String HOLDINGS_STORAGE = "/holdings-storage/holdings"; - private static final String HOLDINGS_ITEMS_PROPERTY = "holdingsItems"; - private static final String BARE_HOLDINGS_ITEMS_PROPERTY = "bareHoldingsItems"; public MoveApi(final Storage storage, final HttpClient client) { super(storage, client); @@ -84,10 +76,10 @@ private void moveItems(RoutingContext routingContext) { .thenAccept(holding -> { if (holding != null) { try { - final var itemsStorageClient = createItemStorageClient(routingContext, context); + final var itemsStorageClient = createItemStorageClient(createHttpClient(client, routingContext, context), context); final var itemsFetchClient = createItemsFetchClient(itemsStorageClient); - itemsFetchClient.find(itemIdsToUpdate, this::fetchByIdCql) + itemsFetchClient.find(itemIdsToUpdate, MoveApiUtil::fetchByIdCql) .thenAccept(jsons -> { List itemsToUpdate = updateHoldingsRecordIdForItems(toHoldingsRecordId, jsons); updateItems(routingContext, context, itemIdsToUpdate, itemsToUpdate); @@ -129,11 +121,11 @@ private void moveHoldings(RoutingContext routingContext) { return; } try { - CollectionResourceClient holdingsStorageClient = createHoldingsStorageClient(createHttpClient(routingContext, context), + CollectionResourceClient holdingsStorageClient = createHoldingsStorageClient(createHttpClient(client, routingContext, context), context); MultipleRecordsFetchClient holdingsRecordFetchClient = createHoldingsRecordsFetchClient(holdingsStorageClient); - holdingsRecordFetchClient.find(holdingsRecordsIdsToUpdate, this::fetchByIdCql) + holdingsRecordFetchClient.find(holdingsRecordsIdsToUpdate, MoveApiUtil::fetchByIdCql) .thenAccept(jsons -> { List holdingsRecordsToUpdate = updateInstanceIdForHoldings(toInstanceId, jsons); updateHoldings(routingContext, context, holdingsRecordsIdsToUpdate, holdingsRecordsToUpdate); @@ -176,18 +168,14 @@ private void updateItems(RoutingContext routingContext, WebContext context, List } private List updateInstanceIdForHoldings(String toInstanceId, List jsons) { + jsons.forEach(MoveApiUtil::removeExtraRedundantFields); + return jsons.stream() - .peek(this::removeExtraRedundantFields) .map(json -> json.mapTo(HoldingsRecord.class)) .map(holding -> holding.withInstanceId(toInstanceId)) .collect(toList()); } - private void removeExtraRedundantFields(JsonObject json) { - json.remove(HOLDINGS_ITEMS_PROPERTY); - json.remove(BARE_HOLDINGS_ITEMS_PROPERTY); - } - private void updateHoldings(RoutingContext routingContext, WebContext context, List idsToUpdate, List holdingsToUpdate) { HoldingsRecordCollection storageHoldingsRecordsCollection = storage.getHoldingsRecordCollection(context); @@ -204,66 +192,4 @@ private void updateHoldings(RoutingContext routingContext, WebContext context, L .collect(toList())) .thenAccept(updatedIds -> respond(routingContext, idsToUpdate, updatedIds)); } - - private void respond(RoutingContext routingContext, List itemIdsToUpdate, List updatedItemIds) { - List nonUpdatedIds = ListUtils.subtract(itemIdsToUpdate, updatedItemIds); - HttpServerResponse response = routingContext.response(); - if (nonUpdatedIds.isEmpty()) { - successWithEmptyIds(response); - } else { - successWithIds(response, nonUpdatedIds); - } - } - - private OkapiHttpClient createHttpClient(RoutingContext routingContext, WebContext context) throws MalformedURLException { - return new OkapiHttpClient(WebClient.wrap(client), context, - exception -> ServerErrorResponse.internalError(routingContext.response(), - String.format("Failed to contact storage module: %s", exception.toString()))); - } - - private CollectionResourceClient createStorageClient(OkapiHttpClient client, WebContext context, String storageUrl) - throws MalformedURLException { - - return new CollectionResourceClient(client, new URL(context.getOkapiLocation() + storageUrl)); - } - - private CollectionResourceClient createItemStorageClient( - RoutingContext routingContext, WebContext context) throws MalformedURLException { - - return createStorageClient(createHttpClient(routingContext, context), - context, ITEM_STORAGE); - } - - private CollectionResourceClient createHoldingsStorageClient(OkapiHttpClient client, WebContext context) - throws MalformedURLException { - return createStorageClient(client, context, HOLDINGS_STORAGE); - } - - private CqlQuery fetchByIdCql(List ids) { - return CqlQuery.exactMatchAny("id", ids); - } - - private MultipleRecordsFetchClient createFetchClient(CollectionResourceClient client, String propertyName) { - return MultipleRecordsFetchClient.builder() - .withCollectionPropertyName(propertyName) - .withExpectedStatus(200) - .withCollectionResourceClient(client) - .build(); - } - - private MultipleRecordsFetchClient createItemsFetchClient(CollectionResourceClient client) { - return createFetchClient(client, ITEMS_PROPERTY); - } - - private MultipleRecordsFetchClient createHoldingsRecordsFetchClient(CollectionResourceClient client) { - return createFetchClient(client, HOLDINGS_RECORDS_PROPERTY); - } - - private void successWithIds(HttpServerResponse response, List ids) { - success(response, new JsonObject().put("nonUpdatedIds", ids)); - } - - private void successWithEmptyIds(HttpServerResponse response) { - successWithIds(response, new ArrayList<>()); - } } diff --git a/src/main/java/org/folio/inventory/resources/UpdateOwnershipApi.java b/src/main/java/org/folio/inventory/resources/UpdateOwnershipApi.java index 339fb8886..0c295d7ce 100644 --- a/src/main/java/org/folio/inventory/resources/UpdateOwnershipApi.java +++ b/src/main/java/org/folio/inventory/resources/UpdateOwnershipApi.java @@ -1,28 +1,304 @@ package org.folio.inventory.resources; +import io.vertx.core.Promise; import io.vertx.core.http.HttpClient; +import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; +import org.apache.commons.collections4.ListUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.HoldingsRecord; +import org.folio.HoldingsUpdateOwnership; +import org.folio.NotUpdatedEntity; +import org.folio.inventory.common.Context; +import org.folio.inventory.common.WebContext; +import org.folio.inventory.consortium.services.ConsortiumService; +import org.folio.inventory.domain.HoldingsRecordCollection; +import org.folio.inventory.domain.items.Item; +import org.folio.inventory.domain.items.ItemCollection; +import org.folio.inventory.exceptions.BadRequestException; +import org.folio.inventory.exceptions.NotFoundException; import org.folio.inventory.storage.Storage; +import org.folio.inventory.storage.external.CollectionResourceClient; +import org.folio.inventory.storage.external.MultipleRecordsFetchClient; +import org.folio.inventory.support.ItemUtil; +import org.folio.inventory.support.MoveApiUtil; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static org.folio.inventory.dataimport.handlers.matching.util.EventHandlingUtil.constructContext; +import static org.folio.inventory.domain.instances.InstanceSource.CONSORTIUM_FOLIO; +import static org.folio.inventory.domain.instances.InstanceSource.CONSORTIUM_MARC; +import static org.folio.inventory.support.EndpointFailureHandler.handleFailure; +import static org.folio.inventory.support.MoveApiUtil.createHoldingsRecordsFetchClient; +import static org.folio.inventory.support.MoveApiUtil.createHoldingsStorageClient; +import static org.folio.inventory.support.MoveApiUtil.createHttpClient; +import static org.folio.inventory.support.MoveApiUtil.createItemStorageClient; +import static org.folio.inventory.support.MoveApiUtil.createItemsFetchClient; +import static org.folio.inventory.support.MoveApiUtil.respond; +import static org.folio.inventory.support.http.server.JsonResponse.unprocessableEntity; +import static org.folio.inventory.validation.UpdateOwnershipValidator.updateOwnershipHasRequiredFields; public class UpdateOwnershipApi extends AbstractInventoryResource { - public UpdateOwnershipApi(Storage storage, HttpClient client) { + private static final Logger LOGGER = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + public static final String INSTANCE_NOT_SHARED = "Instance with id: %s is not shared"; + public static final String INSTANCE_NOT_FOUND_AT_SOURCE_TENANT = "Instance with id: %s not found at source tenant, tenant: %s"; + public static final String TENANT_NOT_IN_CONSORTIA = "%s tenant is not in consortia"; + public static final String HOLDINGS_NOT_FOUND = "HoldingsRecord with id: %s not found on tenant: %s"; + public static final String LOG_UPDATE_HOLDINGS_OWNERSHIP = "updateHoldingsOwnership:: %s"; + + private final ConsortiumService consortiumService; + + public UpdateOwnershipApi(Storage storage, HttpClient client, ConsortiumService consortiumService) { super(storage, client); + this.consortiumService = consortiumService; } @Override public void register(Router router) { router.post("/inventory/items/update-ownership") - .handler(this::updateItemsOwnership); + .handler(this::processUpdateItemsOwnership); router.post("/inventory/holdings/update-ownership") - .handler(this::updateHoldingsOwnership); + .handler(this::processUpdateHoldingsOwnership); + } + + private void processUpdateHoldingsOwnership(RoutingContext routingContext) { + try { + final var context = new WebContext(routingContext); + final var updateOwnershipRequest = routingContext.body().asJsonObject(); + + final var validationError = updateOwnershipHasRequiredFields(context.getTenantId(), updateOwnershipRequest, HoldingsUpdateOwnership.class); + + if (validationError.isPresent()) { + unprocessableEntity(routingContext.response(), validationError.get()); + return; + } + var holdingsUpdateOwnership = updateOwnershipRequest.mapTo(HoldingsUpdateOwnership.class); + + List notUpdatedEntities = new ArrayList<>(); + + LOGGER.info("updateHoldingsOwnership:: Started updating ownership of holdings record: {}, to tenant: {}", holdingsUpdateOwnership.getHoldingsRecordIds(), + holdingsUpdateOwnership.getTargetTenantId()); + + consortiumService.getConsortiumConfiguration(context).toCompletionStage().toCompletableFuture() + .thenCompose(consortiumConfigurationOptional -> { + if (consortiumConfigurationOptional.isPresent()) { + return storage.getInstanceCollection(context) + .findById(holdingsUpdateOwnership.getToInstanceId()) + .thenCompose(instance -> { + if (instance != null) { + if (instance.getSource().equals(CONSORTIUM_MARC.getValue()) || instance.getSource().equals(CONSORTIUM_FOLIO.getValue())) { + Context targetTenantContext = constructContext(holdingsUpdateOwnership.getTargetTenantId(), context.getToken(), context.getOkapiLocation()); + return updateOwnershipOfHoldingsRecords(holdingsUpdateOwnership, notUpdatedEntities, routingContext, context, targetTenantContext); + } else { + String instanceNotSharedErrorMessage = String.format(INSTANCE_NOT_SHARED, holdingsUpdateOwnership.getToInstanceId()); + LOGGER.warn(String.format(LOG_UPDATE_HOLDINGS_OWNERSHIP, instanceNotSharedErrorMessage)); + return CompletableFuture.failedFuture(new BadRequestException(instanceNotSharedErrorMessage)); + } + } else { + String instanceNotFoundErrorMessage = String.format(INSTANCE_NOT_FOUND_AT_SOURCE_TENANT, holdingsUpdateOwnership.getToInstanceId(), context.getTenantId()); + LOGGER.warn(String.format(LOG_UPDATE_HOLDINGS_OWNERSHIP, instanceNotFoundErrorMessage)); + return CompletableFuture.failedFuture(new NotFoundException(instanceNotFoundErrorMessage)); + } + }); + } + String notInConsortiaErrorMessage = String.format(TENANT_NOT_IN_CONSORTIA, context.getTenantId()); + LOGGER.warn(String.format(LOG_UPDATE_HOLDINGS_OWNERSHIP, notInConsortiaErrorMessage)); + return CompletableFuture.failedFuture(new BadRequestException(notInConsortiaErrorMessage)); + }) + .thenAccept(v -> respond(routingContext, notUpdatedEntities)) + .exceptionally(throwable -> { + LOGGER.warn("updateHoldingsOwnership:: Error during update ownership of holdings {}, to tenant: {}", + holdingsUpdateOwnership.getHoldingsRecordIds(), holdingsUpdateOwnership.getTargetTenantId(), throwable); + handleFailure(throwable, routingContext); + return null; + }); + } catch (Exception e) { + LOGGER.warn("updateHoldingsOwnership:: Error during update ownership of holdings", e); + handleFailure(e, routingContext); + } + } + + private void processUpdateItemsOwnership(RoutingContext routingContext) { + // should be implemented in MODINV-955 + } + + private CompletableFuture> updateOwnershipOfHoldingsRecords(HoldingsUpdateOwnership holdingsUpdateOwnership, + List notUpdatedEntities, RoutingContext routingContext, + WebContext context, Context targetTenantContext) { + try { + LOGGER.info("updateOwnershipOfHoldingsRecords:: Updating ownership of holdingsRecord: {}, to tenant: {}", + holdingsUpdateOwnership.getHoldingsRecordIds(), targetTenantContext.getTenantId()); + + CollectionResourceClient holdingsStorageClient = createHoldingsStorageClient(createHttpClient(client, routingContext, context), + context); + MultipleRecordsFetchClient holdingsRecordFetchClient = createHoldingsRecordsFetchClient(holdingsStorageClient); + + HoldingsRecordCollection sourceTenantHoldingsRecordCollection = storage.getHoldingsRecordCollection(context); + HoldingsRecordCollection targetTenantHoldingsRecordCollection = storage.getHoldingsRecordCollection(targetTenantContext); + + return holdingsRecordFetchClient.find(holdingsUpdateOwnership.getHoldingsRecordIds(), MoveApiUtil::fetchByIdCql) + .thenCompose(jsons -> { + LOGGER.info("updateOwnershipOfHoldingsRecords:: Found holdings to update ownership: {}", jsons); + processNotFoundInstances(holdingsUpdateOwnership.getHoldingsRecordIds(), notUpdatedEntities, context, jsons); + if (!jsons.isEmpty()) { + return createHoldings(jsons, notUpdatedEntities, holdingsUpdateOwnership.getToInstanceId(), targetTenantHoldingsRecordCollection) + .thenCompose(createdHoldings -> { + LOGGER.info("updateOwnershipOfHoldingsRecords:: Created holdings: {}, for tenant: {}", createdHoldings, targetTenantContext.getTenantId()); + List createdHoldingsIds = createdHoldings.stream().map(HoldingsRecord::getId).toList(); + + return transferAttachedItems(createdHoldingsIds, notUpdatedEntities, routingContext, context, targetTenantContext) + .thenCompose(itemIds -> + deleteHoldings(getHoldingsToDelete(notUpdatedEntities, createdHoldings), notUpdatedEntities, sourceTenantHoldingsRecordCollection)); + }); + } + return CompletableFuture.completedFuture(new ArrayList<>()); + }); + } catch (Exception e) { + LOGGER.warn("updateOwnershipOfHoldingsRecords:: Error during update ownership of holdings {}, to tenant: {}", + holdingsUpdateOwnership.getHoldingsRecordIds(), holdingsUpdateOwnership.getTargetTenantId(), e); + return CompletableFuture.failedFuture(e); + } + } + + private CompletableFuture> transferAttachedItems(List holdingsRecordIds, List notUpdatedEntities, + RoutingContext routingContext, WebContext context, Context targetTenantContext) { + try { + LOGGER.info("transferAttachedItems:: Transfer items of holdingsRecordIds: {}, to tenant: {}", + holdingsRecordIds, targetTenantContext.getTenantId()); + + CollectionResourceClient itemsStorageClient = createItemStorageClient(createHttpClient(client, routingContext, context), context); + MultipleRecordsFetchClient itemsFetchClient = createItemsFetchClient(itemsStorageClient); + + ItemCollection sourceTenantItemCollection = storage.getItemCollection(context); + ItemCollection targetTenantItemCollection = storage.getItemCollection(targetTenantContext); + + return itemsFetchClient.find(holdingsRecordIds, MoveApiUtil::fetchByHoldingsRecordIdCql) + .thenCompose(jsons -> { + LOGGER.info("transferAttachedItems:: Found items to transfer: {}", jsons); + if (!jsons.isEmpty()) { + return createItems(jsons, notUpdatedEntities, targetTenantItemCollection) + .thenCompose(items -> deleteItems(items, notUpdatedEntities, sourceTenantItemCollection)); + } + return CompletableFuture.completedFuture(new ArrayList<>()); + }); + } catch (Exception e) { + LOGGER.warn("transferAttachedItems:: Error during transfer attached items for holdings {}, to tenant: {}", + holdingsRecordIds, targetTenantContext.getTenantId(), e); + return CompletableFuture.failedFuture(e); + } + } + + private CompletableFuture> createItems(List jsons, List notUpdatedEntities, ItemCollection itemCollection) { + List itemRecordsToUpdateOwnership = jsons.stream() + .map(json -> ItemUtil.fromStoredItemRepresentation(json).withHrid(null)) + .toList(); + + List> createFutures = itemRecordsToUpdateOwnership.stream() + .map(item -> + itemCollection.add(item) + .exceptionally(e -> { + LOGGER.warn("createHoldings:: Error during creating item with id: {} for holdingsRecord with id: {}", item.getId(), item.getHoldingId(), e); + notUpdatedEntities.add(new NotUpdatedEntity().withEntityId(item.getHoldingId()).withErrorMessage(e.getMessage())); + throw new CompletionException(e); + })) + .toList(); + + return CompletableFuture.allOf(createFutures.toArray(new CompletableFuture[0])) + .handle((vVoid, throwable) -> createFutures.stream() + .filter(future -> !future.isCompletedExceptionally()) + .map(CompletableFuture::join) + .toList()); + } + + private CompletableFuture> createHoldings(List jsons, List notUpdatedEntities, String instanceId, + HoldingsRecordCollection holdingsRecordCollection) { + jsons.forEach(MoveApiUtil::removeExtraRedundantFields); + + List holdingsRecordsToUpdateOwnership = jsons.stream() + .map(json -> json.mapTo(HoldingsRecord.class).withHrid(null)) + .filter(holdingsRecord -> holdingsRecord.getInstanceId().equals(instanceId)) + .toList(); + + List> createFutures = holdingsRecordsToUpdateOwnership.stream() + .map(holdingRecord -> + holdingsRecordCollection.add(holdingRecord) + .exceptionally(e -> { + LOGGER.warn("createHoldings:: Error during creating holdingsRecord with id: {}", holdingRecord.getId(), e); + notUpdatedEntities.add(new NotUpdatedEntity().withEntityId(holdingRecord.getId()).withErrorMessage(e.getMessage())); + throw new CompletionException(e); + })) + .toList(); + + return CompletableFuture.allOf(createFutures.toArray(new CompletableFuture[0])) + .handle((vVoid, throwable) -> createFutures.stream() + .filter(future -> !future.isCompletedExceptionally()) + .map(CompletableFuture::join) + .toList()); + } + + private CompletableFuture> deleteHoldings(List holdingsRecords, List notUpdatedEntities, + HoldingsRecordCollection holdingsRecordCollection) { + List> deleteFutures = holdingsRecords.stream() + .map(holdingsRecord -> { + Promise promise = Promise.promise(); + holdingsRecordCollection.delete(holdingsRecord.getId(), success -> promise.complete(holdingsRecord.getId()), + failure -> { + LOGGER.warn("deleteHoldings:: Error during deleting holdingsRecord with id: {}, status code: {}, reason: {}", + holdingsRecord.getId(), failure.getStatusCode(), failure.getReason()); + + notUpdatedEntities.add(new NotUpdatedEntity().withEntityId(holdingsRecord.getId()).withErrorMessage(failure.getReason())); + promise.fail(failure.getReason()); + }); + return promise.future().toCompletionStage().toCompletableFuture(); + }).toList(); + + return CompletableFuture.allOf(deleteFutures.toArray(new CompletableFuture[0])) + .handle((vVoid, throwable) -> deleteFutures.stream() + .filter(future -> !future.isCompletedExceptionally()) + .map(CompletableFuture::join) + .toList()); + } + + private CompletableFuture> deleteItems(List items, List notUpdatedEntities, ItemCollection itemCollection) { + List> deleteFutures = items.stream() + .map(item -> { + Promise promise = Promise.promise(); + itemCollection.delete(item.getId(), success -> promise.complete(item.getId()), + failure -> { + LOGGER.warn("deleteItems:: Error during deleting item with id: {} for holdingsRecord with id {}, status code: {}, reason: {}", + item.getId(), item.getHoldingId(), failure.getStatusCode(), failure.getReason()); + + notUpdatedEntities.add(new NotUpdatedEntity().withEntityId(item.getHoldingId()).withErrorMessage(failure.getReason())); + promise.fail(failure.getReason()); + }); + return promise.future().toCompletionStage().toCompletableFuture(); + }).toList(); + + return CompletableFuture.allOf(deleteFutures.toArray(new CompletableFuture[0])) + .handle((vVoid, throwable) -> deleteFutures.stream() + .filter(future -> !future.isCompletedExceptionally()) + .map(CompletableFuture::join) + .toList()); } - private void updateHoldingsOwnership(RoutingContext routingContext) { - // should be implemented in MODINV-1031 + private void processNotFoundInstances(List holdingsRecordIds, List notUpdatedEntities, WebContext context, List jsons) { + List foundIds = jsons.stream().map(json -> json.getString("id")).toList(); + List notFoundIds = ListUtils.subtract(holdingsRecordIds, foundIds); + notFoundIds.forEach(id -> { + String errorMessage = String.format(HOLDINGS_NOT_FOUND, id, context.getTenantId()); + LOGGER.warn(String.format("processNotFoundInstances:: %s", errorMessage)); + notUpdatedEntities.add(new NotUpdatedEntity().withEntityId(id).withErrorMessage(errorMessage)); + }); } - private void updateItemsOwnership(RoutingContext routingContext) { - // should be implemented in MODINV-1031 + private List getHoldingsToDelete(List notUpdatedEntities, List createdHoldings) { + List notUpdatedHoldingsIds = notUpdatedEntities.stream().map(NotUpdatedEntity::getEntityId).toList(); + return createdHoldings.stream().filter(holdingsRecord -> !notUpdatedHoldingsIds.contains(holdingsRecord.getId())).toList(); } } diff --git a/src/main/java/org/folio/inventory/support/MoveApiUtil.java b/src/main/java/org/folio/inventory/support/MoveApiUtil.java new file mode 100644 index 000000000..af6051a21 --- /dev/null +++ b/src/main/java/org/folio/inventory/support/MoveApiUtil.java @@ -0,0 +1,110 @@ +package org.folio.inventory.support; + +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.client.WebClient; +import org.apache.commons.collections4.ListUtils; +import org.folio.NotUpdatedEntity; +import org.folio.UpdateOwnershipResponse; +import org.folio.inventory.common.WebContext; +import org.folio.inventory.storage.external.CollectionResourceClient; +import org.folio.inventory.storage.external.CqlQuery; +import org.folio.inventory.storage.external.MultipleRecordsFetchClient; +import org.folio.inventory.support.http.client.OkapiHttpClient; +import org.folio.inventory.support.http.server.ServerErrorResponse; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import static org.folio.inventory.support.http.server.JsonResponse.success; + +public final class MoveApiUtil { + public static final String HOLDINGS_STORAGE = "/holdings-storage/holdings"; + public static final String HOLDINGS_RECORDS_PROPERTY = "holdingsRecords"; + public static final String TARGET_TENANT_ID = "targetTenantId"; + public static final String ITEM_STORAGE = "/item-storage/items"; + public static final String ITEMS_PROPERTY = "items"; + private static final String HOLDINGS_ITEMS_PROPERTY = "holdingsItems"; + private static final String BARE_HOLDINGS_ITEMS_PROPERTY = "bareHoldingsItems"; + + private MoveApiUtil() { } + + public static OkapiHttpClient createHttpClient(HttpClient client, RoutingContext routingContext, WebContext context) throws MalformedURLException { + return new OkapiHttpClient(WebClient.wrap(client), context, + exception -> ServerErrorResponse.internalError(routingContext.response(), + String.format("Failed to contact storage module: %s", exception.toString()))); + } + + + private static MultipleRecordsFetchClient createFetchClient(CollectionResourceClient client, String propertyName) { + return MultipleRecordsFetchClient.builder() + .withCollectionPropertyName(propertyName) + .withExpectedStatus(200) + .withCollectionResourceClient(client) + .build(); + } + + public static CollectionResourceClient createStorageClient(OkapiHttpClient client, WebContext context, String storageUrl) + throws MalformedURLException { + + return new CollectionResourceClient(client, new URL(context.getOkapiLocation() + storageUrl)); + } + + public static CollectionResourceClient createHoldingsStorageClient(OkapiHttpClient client, WebContext context) + throws MalformedURLException { + return createStorageClient(client, context, HOLDINGS_STORAGE); + } + + public static CollectionResourceClient createItemStorageClient(OkapiHttpClient client, WebContext context) + throws MalformedURLException { + return createStorageClient(client, context, ITEM_STORAGE); + } + + public static MultipleRecordsFetchClient createHoldingsRecordsFetchClient(CollectionResourceClient client) { + return createFetchClient(client, HOLDINGS_RECORDS_PROPERTY); + } + + public static MultipleRecordsFetchClient createItemsFetchClient(CollectionResourceClient client) { + return createFetchClient(client, ITEMS_PROPERTY); + } + + public static CqlQuery fetchByIdCql(List ids) { + return CqlQuery.exactMatchAny("id", ids); + } + + public static CqlQuery fetchByHoldingsRecordIdCql(List ids) { + return CqlQuery.exactMatchAny("holdingsRecordId", ids); + } + + public static void successWithEmptyIds(HttpServerResponse response) { + successWithIds(response, new ArrayList<>()); + } + + public static void successWithIds(HttpServerResponse response, List ids) { + success(response, new JsonObject().put("nonUpdatedIds", ids)); + } + + public static void removeExtraRedundantFields(JsonObject json) { + json.remove(HOLDINGS_ITEMS_PROPERTY); + json.remove(BARE_HOLDINGS_ITEMS_PROPERTY); + } + + public static void respond(RoutingContext routingContext, List itemIdsToUpdate, List updatedItemIds) { + List nonUpdatedIds = ListUtils.subtract(itemIdsToUpdate, updatedItemIds); + HttpServerResponse response = routingContext.response(); + if (nonUpdatedIds.isEmpty()) { + successWithEmptyIds(response); + } else { + successWithIds(response, nonUpdatedIds); + } + } + + public static void respond(RoutingContext routingContext, List notUpdatedEntities) { + HttpServerResponse response = routingContext.response(); + success(response, JsonObject.mapFrom(new UpdateOwnershipResponse().withNotUpdatedEntities(notUpdatedEntities))); + } +} diff --git a/src/main/java/org/folio/inventory/validation/UpdateOwnershipValidator.java b/src/main/java/org/folio/inventory/validation/UpdateOwnershipValidator.java new file mode 100644 index 000000000..bd5afdb55 --- /dev/null +++ b/src/main/java/org/folio/inventory/validation/UpdateOwnershipValidator.java @@ -0,0 +1,31 @@ +package org.folio.inventory.validation; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import org.folio.inventory.support.http.server.ValidationError; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.folio.inventory.support.MoveApiUtil.TARGET_TENANT_ID; + +public final class UpdateOwnershipValidator { + private UpdateOwnershipValidator() { } + + public static Optional updateOwnershipHasRequiredFields(String sourceTenant, JsonObject updateOwnershipRequest, Class updateOwnershipClass) { + List requiredFields = Arrays.stream(updateOwnershipClass.getDeclaredFields()).map(Field::getName).toList(); + for (String field: requiredFields) { + var value = updateOwnershipRequest.getValue(field); + if (value == null || (value instanceof JsonArray jsonArray && jsonArray.isEmpty())) { + return Optional.of(new ValidationError(field + " is a required field", field, null)); + } + } + String targetTenantId = updateOwnershipRequest.getString(TARGET_TENANT_ID); + if (sourceTenant.equals(targetTenantId)) { + return Optional.of(new ValidationError("targetTenantId field cannot be equal to source tenant id", TARGET_TENANT_ID, targetTenantId)); + } + return Optional.empty(); + } +} diff --git a/src/test/java/api/ApiTestSuite.java b/src/test/java/api/ApiTestSuite.java index 2373dc0fd..4f3c967bc 100644 --- a/src/test/java/api/ApiTestSuite.java +++ b/src/test/java/api/ApiTestSuite.java @@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import api.holdings.HoldingsUpdateOwnershipApiTest; import org.folio.inventory.InventoryVerticle; import org.folio.inventory.common.VertxAssistant; import org.folio.inventory.consortium.util.ConsortiumUtil; @@ -68,12 +69,14 @@ BoundWithTests.class, TenantApiTest.class, AdminApiTest.class, - InventoryConfigApiTest.class + InventoryConfigApiTest.class, + HoldingsUpdateOwnershipApiTest.class }) public class ApiTestSuite { public static final int INVENTORY_VERTICLE_TEST_PORT = 9603; public static final String TENANT_ID = "test_tenant"; public static final String CONSORTIA_TENANT_ID = "consortium"; + public static final String COLLEGE_TENANT_ID = "college"; public static final UUID ID_FOR_FAILURE = UUID.fromString("fa45a95b-38a3-430b-8f34-548ca005a176"); public static final UUID ID_FOR_OPTIMISTIC_LOCKING_FAILURE = UUID.fromString("40900409-0409-4444-8888-409000000409"); diff --git a/src/test/java/api/holdings/HoldingsUpdateOwnershipApiTest.java b/src/test/java/api/holdings/HoldingsUpdateOwnershipApiTest.java index 28f8145e1..85c48a826 100644 --- a/src/test/java/api/holdings/HoldingsUpdateOwnershipApiTest.java +++ b/src/test/java/api/holdings/HoldingsUpdateOwnershipApiTest.java @@ -6,17 +6,21 @@ import api.support.InstanceApiClient; import api.support.builders.HoldingRequestBuilder; import api.support.builders.HoldingsRecordUpdateOwnershipRequestBuilder; +import api.support.builders.ItemRequestBuilder; +import io.vertx.core.http.HttpMethod; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import junitparams.JUnitParamsRunner; import org.apache.http.HttpStatus; +import org.folio.inventory.domain.items.ItemStatusName; import org.folio.inventory.support.http.client.Response; +import org.joda.time.DateTime; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import support.fakes.EndpointFailureDescriptor; import java.net.MalformedURLException; import java.util.List; @@ -25,9 +29,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import static api.ApiTestSuite.ID_FOR_FAILURE; import static api.ApiTestSuite.createConsortiumTenant; import static api.support.InstanceSamples.smallAngryPlanet; +import static org.folio.inventory.domain.instances.InstanceSource.CONSORTIUM_FOLIO; +import static org.folio.inventory.domain.instances.InstanceSource.FOLIO; +import static org.folio.inventory.support.ItemUtil.HOLDINGS_RECORD_ID; import static org.folio.inventory.support.http.ContentType.APPLICATION_JSON; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -35,7 +41,6 @@ import static org.hamcrest.Matchers.equalTo; import static support.matchers.ResponseMatchers.hasValidationError; -@Ignore @RunWith(JUnitParamsRunner.class) public class HoldingsUpdateOwnershipApiTest extends ApiTests { private static final String INSTANCE_ID = "instanceId"; @@ -55,32 +60,220 @@ public void canUpdateHoldingsOwnershipToDifferentTenant() throws MalformedURLExc UUID instanceId = UUID.randomUUID(); JsonObject instance = smallAngryPlanet(instanceId); - InstanceApiClient.createInstance(okapiClient, instance); - InstanceApiClient.createInstance(consortiumOkapiClient, instance); + InstanceApiClient.createInstance(okapiClient, instance.put("source", CONSORTIUM_FOLIO.getValue())); + InstanceApiClient.createInstance(consortiumOkapiClient, instance.put("source", FOLIO.getValue())); + + final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); + final UUID createHoldingsRecord2 = createHoldingForInstance(instanceId); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, + new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); + assertThat(new JsonObject(postHoldingsUpdateOwnershipResponse.getBody()).getJsonArray("notUpdatedEntities").size(), is(0)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = collegeHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, sourceTenantHoldingsRecord1.getStatusCode()); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + + Response sourceTenantHoldingsRecord2 = holdingsStorageClient.getById(createHoldingsRecord2); + Response targetTenantHoldingsRecord2 = collegeHoldingsStorageClient.getById(createHoldingsRecord2); + + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, sourceTenantHoldingsRecord2.getStatusCode()); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord2.getJson().getString(INSTANCE_ID)); + Assert.assertNull(targetTenantHoldingsRecord2.getJson().getString("hrid")); + } + + @Test + public void canUpdateHoldingsOwnershipWithRelatedItemsToDifferentTenant() throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { + UUID instanceId = UUID.randomUUID(); + JsonObject instance = smallAngryPlanet(instanceId); + String itemHrId = "it0000001"; + + InstanceApiClient.createInstance(okapiClient, instance.put("source", CONSORTIUM_FOLIO.getValue())); + InstanceApiClient.createInstance(consortiumOkapiClient, instance.put("source", FOLIO.getValue())); final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); final UUID createHoldingsRecord2 = createHoldingForInstance(instanceId); + final var firstItem = itemsClient.create( + new ItemRequestBuilder() + .forHolding(createHoldingsRecord1) + .withBarcode("645398607547") + .withStatus(ItemStatusName.AVAILABLE.value())); + + final var secondItem = itemsClient.create( + new ItemRequestBuilder() + .forHolding(createHoldingsRecord2) + .withHrid(itemHrId) + .withBarcode("645398607546") + .withStatus(ItemStatusName.AVAILABLE.value())); + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, - new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); - assertThat(new JsonObject(postHoldingsUpdateOwnershipResponse.getBody()).getJsonArray("nonUpdatedIds").size(), is(0)); + assertThat(new JsonObject(postHoldingsUpdateOwnershipResponse.getBody()).getJsonArray("notUpdatedEntities").size(), is(0)); assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + // Verify Holdings ownership updated Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); - Response targetTenantHoldingsRecord1 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = collegeHoldingsStorageClient.getById(createHoldingsRecord1); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, sourceTenantHoldingsRecord1.getStatusCode()); Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); - Response sourceTenantHoldingsRecord2 = holdingsStorageClient.getById(createHoldingsRecord1); - Response targetTenantHoldingsRecord2 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + Response sourceTenantHoldingsRecord2 = holdingsStorageClient.getById(createHoldingsRecord2); + Response targetTenantHoldingsRecord2 = collegeHoldingsStorageClient.getById(createHoldingsRecord2); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, sourceTenantHoldingsRecord2.getStatusCode()); Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord2.getJson().getString(INSTANCE_ID)); + + // Verify related Items ownership updated + Response sourceTenantItem1 = itemsClient.getById(firstItem.getId()); + Response targetTenantItem1 = collegeItemsClient.getById(firstItem.getId()); + + assertThat(HttpStatus.SC_NOT_FOUND, is(sourceTenantItem1.getStatusCode())); + assertThat(targetTenantItem1.getJson().getString(HOLDINGS_RECORD_ID), is(createHoldingsRecord1.toString())); + + Response sourceTenantItem2 = itemsClient.getById(secondItem.getId()); + Response targetTenantItem2 = collegeItemsClient.getById(secondItem.getId()); + + assertThat(HttpStatus.SC_NOT_FOUND, is(sourceTenantItem2.getStatusCode())); + assertThat(targetTenantItem2.getJson().getString(HOLDINGS_RECORD_ID), is(createHoldingsRecord2.toString())); + Assert.assertNotEquals(targetTenantItem2.getJson().getString("hrid"), itemHrId); + } + + @Test + public void canUpdateHoldingsOwnershipIfErrorUpdatingRelatedItemsToDifferentTenant() throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { + UUID instanceId = UUID.randomUUID(); + JsonObject instance = smallAngryPlanet(instanceId); + + InstanceApiClient.createInstance(okapiClient, instance.put("source", CONSORTIUM_FOLIO.getValue())); + InstanceApiClient.createInstance(consortiumOkapiClient, instance.put("source", FOLIO.getValue())); + + final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); + + final var firstItem = itemsClient.create( + new ItemRequestBuilder() + .forHolding(createHoldingsRecord1) + .withBarcode("645398607547") + .withStatus(ItemStatusName.AVAILABLE.value())); + + final JsonObject expectedErrorResponse = new JsonObject().put("message", "Server error"); + collegeItemsClient.emulateFailure( + new EndpointFailureDescriptor() + .setFailureExpireDate(DateTime.now().plusSeconds(2).toDate()) + .setStatusCode(500) + .setContentType("application/json") + .setBody(expectedErrorResponse.toString()) + .setMethod(HttpMethod.POST.name())); + + holdingsStorageClient.emulateFailure( + new EndpointFailureDescriptor() + .setFailureExpireDate(DateTime.now().plusSeconds(2).toDate()) + .setStatusCode(500) + .setContentType("application/json") + .setBody(expectedErrorResponse.toString()) + .setMethod(HttpMethod.DELETE.name())); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, + new JsonArray(List.of(createHoldingsRecord1.toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + collegeItemsClient.disableFailureEmulation(); + holdingsStorageClient.disableFailureEmulation(); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); + + JsonArray notUpdatedEntitiesIds = postHoldingsUpdateOwnershipResponse.getJson() + .getJsonArray("notUpdatedEntities"); + + assertThat(notUpdatedEntitiesIds.size(), is(1)); + assertThat(notUpdatedEntitiesIds.getJsonObject(0).getString("entityId"), equalTo(createHoldingsRecord1.toString())); + assertThat(notUpdatedEntitiesIds.getJsonObject(0).getString("errorMessage"), containsString(expectedErrorResponse.toString())); + + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + // Verify Holdings ownership updated + Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = collegeHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(instanceId.toString(), sourceTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + + // Verify related Item ownership not updated + Response sourceTenantItem1 = itemsClient.getById(firstItem.getId()); + Response targetTenantItem1 = collegeItemsClient.getById(firstItem.getId()); + + assertThat(HttpStatus.SC_NOT_FOUND, is(targetTenantItem1.getStatusCode())); + assertThat(sourceTenantItem1.getJson().getString(HOLDINGS_RECORD_ID), is(createHoldingsRecord1.toString())); + } + + @Test + public void canUpdateHoldingsOwnershipIfErrorDeletingRelatedItemsToDifferentTenant() throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { + UUID instanceId = UUID.randomUUID(); + JsonObject instance = smallAngryPlanet(instanceId); + + InstanceApiClient.createInstance(okapiClient, instance.put("source", CONSORTIUM_FOLIO.getValue())); + InstanceApiClient.createInstance(consortiumOkapiClient, instance.put("source", FOLIO.getValue())); + + final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); + + final var firstItem = itemsClient.create( + new ItemRequestBuilder() + .forHolding(createHoldingsRecord1) + .withBarcode("645398607547") + .withStatus(ItemStatusName.AVAILABLE.value())); + + final JsonObject expectedErrorResponse = new JsonObject().put("message", "Server error"); + collegeItemsClient.emulateFailure( + new EndpointFailureDescriptor() + .setFailureExpireDate(DateTime.now().plusSeconds(2).toDate()) + .setStatusCode(500) + .setContentType("application/json") + .setBody(expectedErrorResponse.toString()) + .setMethod(HttpMethod.DELETE.name())); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, + new JsonArray(List.of(createHoldingsRecord1.toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + collegeItemsClient.disableFailureEmulation(); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); + + JsonArray notUpdatedEntitiesIds = postHoldingsUpdateOwnershipResponse.getJson() + .getJsonArray("notUpdatedEntities"); + + assertThat(notUpdatedEntitiesIds.size(), is(1)); + assertThat(notUpdatedEntitiesIds.getJsonObject(0).getString("entityId"), equalTo(createHoldingsRecord1.toString())); + assertThat(notUpdatedEntitiesIds.getJsonObject(0).getString("errorMessage"), containsString(expectedErrorResponse.toString())); + + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + // Verify Holdings ownership updated + Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = collegeHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(instanceId.toString(), sourceTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + + // Verify related Item ownership not updated + Response sourceTenantItem1 = itemsClient.getById(firstItem.getId()); + Response targetTenantItem1 = collegeItemsClient.getById(firstItem.getId()); + + assertThat(targetTenantItem1.getJson().getString(HOLDINGS_RECORD_ID), is(createHoldingsRecord1.toString())); + assertThat(sourceTenantItem1.getJson().getString(HOLDINGS_RECORD_ID), is(createHoldingsRecord1.toString())); } @Test @@ -88,8 +281,8 @@ public void shouldReportErrorsWhenOnlySomeRequestedHoldingsRecordsCouldNotBeUpda UUID instanceId = UUID.randomUUID(); JsonObject instance = smallAngryPlanet(instanceId); - InstanceApiClient.createInstance(okapiClient, instance); - InstanceApiClient.createInstance(consortiumOkapiClient, instance); + InstanceApiClient.createInstance(okapiClient, instance.put("source", CONSORTIUM_FOLIO.getValue())); + InstanceApiClient.createInstance(consortiumOkapiClient, instance.put("source", FOLIO.getValue())); final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); final UUID createHoldingsRecord2 = UUID.randomUUID(); @@ -97,27 +290,28 @@ public void shouldReportErrorsWhenOnlySomeRequestedHoldingsRecordsCouldNotBeUpda Assert.assertNotEquals(createHoldingsRecord1, createHoldingsRecord2); JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, - new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); - List notFoundIds = postHoldingsUpdateOwnershipResponse.getJson() - .getJsonArray("nonUpdatedIds") - .getList(); + JsonArray notFoundIds = postHoldingsUpdateOwnershipResponse.getJson() + .getJsonArray("notUpdatedEntities"); assertThat(notFoundIds.size(), is(1)); - assertThat(notFoundIds.get(0), equalTo(createHoldingsRecord2.toString())); + assertThat(notFoundIds.getJsonObject(0).getString("entityId"), equalTo(createHoldingsRecord2.toString())); + assertThat(notFoundIds.getJsonObject(0).getString("errorMessage"), + equalTo(String.format("HoldingsRecord with id: %s not found on tenant: %s", createHoldingsRecord2, ApiTestSuite.TENANT_ID))); Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); - Response targetTenantHoldingsRecord1 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = collegeHoldingsStorageClient.getById(createHoldingsRecord1); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, sourceTenantHoldingsRecord1.getStatusCode()); assertThat(instanceId.toString(), equalTo(targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID))); - Response targetTenantHoldingsRecord2 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord2 = collegeHoldingsStorageClient.getById(createHoldingsRecord2); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, targetTenantHoldingsRecord2.getStatusCode()); } @@ -125,7 +319,7 @@ public void shouldReportErrorsWhenOnlySomeRequestedHoldingsRecordsCouldNotBeUpda public void cannotUpdateHoldingsRecordsOwnershipToUnspecifiedInstance() throws InterruptedException, MalformedURLException, TimeoutException, ExecutionException { JsonObject holdingsRecordUpdateOwnershipWithoutToInstanceId = new HoldingsRecordUpdateOwnershipRequestBuilder(null, - new JsonArray(List.of(UUID.randomUUID())), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + new JsonArray(List.of(UUID.randomUUID())), ApiTestSuite.COLLEGE_TENANT_ID).create(); Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipWithoutToInstanceId); @@ -149,7 +343,23 @@ public void cannotUpdateHoldingsRecordsOwnershipToUnspecifiedTenant() assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); assertThat(postHoldingsUpdateOwnershipResponse, hasValidationError( - "tenantId is a required field", "toInstanceId", null + "targetTenantId is a required field", "targetTenantId", null + )); + } + + @Test + public void cannotUpdateHoldingsRecordOwnershipToSameTenant() + throws MalformedURLException, InterruptedException, ExecutionException, TimeoutException { + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(UUID.randomUUID(), + new JsonArray(List.of(UUID.randomUUID().toString())), ApiTestSuite.TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(422)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + assertThat(postHoldingsUpdateOwnershipResponse, hasValidationError( + "targetTenantId field cannot be equal to source tenant id", "targetTenantId", ApiTestSuite.TENANT_ID )); } @@ -157,7 +367,7 @@ public void cannotUpdateHoldingsRecordsOwnershipToUnspecifiedTenant() public void cannotUpdateUnspecifiedHoldingsRecordsOwnership() throws MalformedURLException, InterruptedException, ExecutionException, TimeoutException { JsonObject holdingsRecordUpdateOwnershipWithoutHoldingsRecordIds = new HoldingsRecordUpdateOwnershipRequestBuilder(UUID.randomUUID(), - new JsonArray(), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + new JsonArray(), ApiTestSuite.COLLEGE_TENANT_ID).create(); Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipWithoutHoldingsRecordIds); @@ -165,15 +375,29 @@ public void cannotUpdateUnspecifiedHoldingsRecordsOwnership() assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); assertThat(postHoldingsUpdateOwnershipResponse, hasValidationError( - "Holdings record ids aren't specified", "holdingsRecordIds", null + "holdingsRecordIds is a required field", "holdingsRecordIds", null )); } @Test - public void cannotUpdateHoldingsRecordOwnershipOfNonExistedInstance() + public void cannotUpdateHoldingsRecordOwnershipIfTenantNotInConsortium() throws MalformedURLException, InterruptedException, ExecutionException, TimeoutException { + userTenantsClient.deleteAll(); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(UUID.randomUUID(), + new JsonArray(List.of(UUID.randomUUID().toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(400)); + + assertThat(postHoldingsUpdateOwnershipResponse.getBody(), containsString("tenant is not in consortia")); createConsortiumTenant(); + } + @Test + public void cannotUpdateHoldingsRecordOwnershipOfNonExistedInstance() + throws MalformedURLException, InterruptedException, ExecutionException, TimeoutException { UUID instanceId = UUID.randomUUID(); JsonObject instance = smallAngryPlanet(instanceId); @@ -185,54 +409,121 @@ public void cannotUpdateHoldingsRecordOwnershipOfNonExistedInstance() final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); JsonObject holdingsRecordUpdateOwnershipWithoutHoldingsRecordIds = new HoldingsRecordUpdateOwnershipRequestBuilder(invalidInstanceId, - new JsonArray(List.of(createHoldingsRecord1)), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + new JsonArray(List.of(createHoldingsRecord1)), ApiTestSuite.COLLEGE_TENANT_ID).create(); Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipWithoutHoldingsRecordIds); - assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(422)); - assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(404)); - assertThat(postHoldingsUpdateOwnershipResponse.getBody(), containsString("errors")); + assertThat(postHoldingsUpdateOwnershipResponse.getBody(), containsString("not found")); assertThat(postHoldingsUpdateOwnershipResponse.getBody(), containsString(invalidInstanceId.toString())); } @Test - public void canUpdateHoldingsRecordOwnershipDueToHoldingsRecordUpdateError() throws InterruptedException, MalformedURLException, TimeoutException, ExecutionException { + public void cannotUpdateHoldingsRecordOwnershipOfNonSharedInstance() + throws MalformedURLException, InterruptedException, ExecutionException, TimeoutException { UUID instanceId = UUID.randomUUID(); JsonObject instance = smallAngryPlanet(instanceId); InstanceApiClient.createInstance(okapiClient, instance); - InstanceApiClient.createInstance(consortiumOkapiClient, instance); final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); - final UUID createHoldingsRecord2 = createHoldingForInstance(ID_FOR_FAILURE, instanceId); + final UUID createHoldingsRecord2 = createHoldingForInstance(instanceId); JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, - new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); - List nonUpdatedIdsIds = postHoldingsUpdateOwnershipResponse.getJson() - .getJsonArray("nonUpdatedIds") - .getList(); + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(400)); - assertThat(nonUpdatedIdsIds.size(), is(1)); - assertThat(nonUpdatedIdsIds.get(0), equalTo(ID_FOR_FAILURE.toString())); + assertThat(postHoldingsUpdateOwnershipResponse.getBody(), containsString(String.format("Instance with id: %s is not shared", instanceId))); + } + + @Test + public void cannotUpdateHoldingsRecordOwnershipDueToHoldingsRecordCreateError() throws InterruptedException, MalformedURLException, TimeoutException, ExecutionException { + UUID instanceId = UUID.randomUUID(); + JsonObject instance = smallAngryPlanet(instanceId); + + InstanceApiClient.createInstance(okapiClient, instance.put("source", CONSORTIUM_FOLIO.getValue())); + InstanceApiClient.createInstance(consortiumOkapiClient, instance.put("source", FOLIO.getValue())); + + final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); + + final JsonObject expectedErrorResponse = new JsonObject().put("message", "Server error"); + collegeHoldingsStorageClient.emulateFailure( + new EndpointFailureDescriptor() + .setFailureExpireDate(DateTime.now().plusSeconds(2).toDate()) + .setStatusCode(500) + .setContentType("application/json") + .setBody(expectedErrorResponse.toString()) + .setMethod(HttpMethod.POST.name())); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, + new JsonArray(List.of(createHoldingsRecord1.toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + collegeHoldingsStorageClient.disableFailureEmulation(); + + JsonArray notUpdatedEntitiesIds = postHoldingsUpdateOwnershipResponse.getJson() + .getJsonArray("notUpdatedEntities"); + + assertThat(notUpdatedEntitiesIds.size(), is(1)); + assertThat(notUpdatedEntitiesIds.getJsonObject(0).getString("entityId"), equalTo(createHoldingsRecord1.toString())); + assertThat(notUpdatedEntitiesIds.getJsonObject(0).getString("errorMessage"), containsString(expectedErrorResponse.toString())); assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); - Response targetTenantHoldingsRecord1 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = collegeHoldingsStorageClient.getById(createHoldingsRecord1); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, sourceTenantHoldingsRecord1.getStatusCode()); - Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + Assert.assertEquals(instanceId.toString(), sourceTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, targetTenantHoldingsRecord1.getStatusCode()); + } + + @Test + public void cannotUpdateHoldingsRecordOwnershipDueToHoldingsRecordDeleteError() throws InterruptedException, MalformedURLException, TimeoutException, ExecutionException { + UUID instanceId = UUID.randomUUID(); + JsonObject instance = smallAngryPlanet(instanceId); - Response sourceTenantHoldingsRecord2 = holdingsStorageClient.getById(createHoldingsRecord1); - Response targetTenantHoldingsRecord2 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + InstanceApiClient.createInstance(okapiClient, instance.put("source", CONSORTIUM_FOLIO.getValue())); + InstanceApiClient.createInstance(consortiumOkapiClient, instance.put("source", FOLIO.getValue())); - Assert.assertEquals(instanceId.toString(), sourceTenantHoldingsRecord2.getJson().getString(INSTANCE_ID)); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, targetTenantHoldingsRecord2.getStatusCode()); + final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); + + final JsonObject expectedErrorResponse = new JsonObject().put("message", "Server error"); + collegeHoldingsStorageClient.emulateFailure( + new EndpointFailureDescriptor() + .setFailureExpireDate(DateTime.now().plusSeconds(2).toDate()) + .setStatusCode(500) + .setContentType("application/json") + .setBody(expectedErrorResponse.toString()) + .setMethod(HttpMethod.DELETE.name())); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, + new JsonArray(List.of(createHoldingsRecord1.toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + collegeHoldingsStorageClient.disableFailureEmulation(); + + JsonArray notUpdatedEntitiesIds = postHoldingsUpdateOwnershipResponse.getJson() + .getJsonArray("notUpdatedEntities"); + + assertThat(notUpdatedEntitiesIds.size(), is(1)); + assertThat(notUpdatedEntitiesIds.getJsonObject(0).getString("entityId"), equalTo(createHoldingsRecord1.toString())); + assertThat(notUpdatedEntitiesIds.getJsonObject(0).getString("errorMessage"), containsString(expectedErrorResponse.toString())); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = collegeHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(instanceId.toString(), sourceTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); } @Test @@ -240,8 +531,8 @@ public void canUpdateHoldingsRecordOwnershipToDifferentInstanceWithExtraRedundan UUID instanceId = UUID.randomUUID(); JsonObject instance = smallAngryPlanet(instanceId); - InstanceApiClient.createInstance(okapiClient, instance); - InstanceApiClient.createInstance(consortiumOkapiClient, instance); + InstanceApiClient.createInstance(okapiClient, instance.put("source", CONSORTIUM_FOLIO.getValue())); + InstanceApiClient.createInstance(consortiumOkapiClient, instance.put("source", FOLIO.getValue())); JsonObject firstJsonHoldingsAsRequest = new HoldingRequestBuilder().forInstance(instanceId).create(); final UUID createHoldingsRecord1 = holdingsStorageClient.create(firstJsonHoldingsAsRequest @@ -256,22 +547,22 @@ public void canUpdateHoldingsRecordOwnershipToDifferentInstanceWithExtraRedundan .getId(); JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, - new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.COLLEGE_TENANT_ID).create(); Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); - assertThat(new JsonObject(postHoldingsUpdateOwnershipResponse.getBody()).getJsonArray("nonUpdatedIds").size(), is(0)); + assertThat(new JsonObject(postHoldingsUpdateOwnershipResponse.getBody()).getJsonArray("notUpdatedEntities").size(), is(0)); assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); - Response targetTenantHoldingsRecord1 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = collegeHoldingsStorageClient.getById(createHoldingsRecord1); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, sourceTenantHoldingsRecord1.getStatusCode()); Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); - Response sourceTenantHoldingsRecord2 = holdingsStorageClient.getById(createHoldingsRecord1); - Response targetTenantHoldingsRecord2 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + Response sourceTenantHoldingsRecord2 = holdingsStorageClient.getById(createHoldingsRecord2); + Response targetTenantHoldingsRecord2 = collegeHoldingsStorageClient.getById(createHoldingsRecord2); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, sourceTenantHoldingsRecord2.getStatusCode()); Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord2.getJson().getString(INSTANCE_ID)); @@ -280,18 +571,11 @@ public void canUpdateHoldingsRecordOwnershipToDifferentInstanceWithExtraRedundan private Response updateHoldingsRecordsOwnership(JsonObject holdingsRecordUpdateOwnershipRequestBody) throws MalformedURLException, InterruptedException, ExecutionException, TimeoutException { final var postHoldingRecordsUpdateOwnershipCompleted = okapiClient.post( ApiRoot.updateHoldingsRecordsOwnership(), holdingsRecordUpdateOwnershipRequestBody); - return postHoldingRecordsUpdateOwnershipCompleted.toCompletableFuture().get(5, TimeUnit.SECONDS); + return postHoldingRecordsUpdateOwnershipCompleted.toCompletableFuture().get(50, TimeUnit.SECONDS); } private UUID createHoldingForInstance(UUID instanceId) { - return holdingsStorageClient.create(new HoldingRequestBuilder().forInstance(instanceId)) + return holdingsStorageClient.create(new HoldingRequestBuilder().withHrId("hol0000001").forInstance(instanceId)) .getId(); } - - private UUID createHoldingForInstance(UUID id, UUID instanceId) { - JsonObject obj = new HoldingRequestBuilder().forInstance(instanceId).create(); - obj.put("id", id.toString()); - holdingsStorageClient.create(obj); - return id; - } } diff --git a/src/test/java/api/support/ApiTests.java b/src/test/java/api/support/ApiTests.java index b23394342..c1300a26b 100644 --- a/src/test/java/api/support/ApiTests.java +++ b/src/test/java/api/support/ApiTests.java @@ -19,6 +19,7 @@ public abstract class ApiTests { private static boolean runningOnOwn; protected static OkapiHttpClient okapiClient; protected static OkapiHttpClient consortiumOkapiClient; + protected static OkapiHttpClient collegeOkapiClient; protected final ResourceClient holdingsStorageClient; protected final ResourceClient holdingsSourceStorageClient; protected final ResourceClient itemsStorageClient; @@ -35,6 +36,8 @@ public abstract class ApiTests { protected final ResourceClient sourceRecordStorageClient; protected final ResourceClient consortiumItemsClient; protected final ResourceClient consortiumHoldingsStorageClient; + protected final ResourceClient collegeItemsClient; + protected final ResourceClient collegeHoldingsStorageClient; protected final InstanceRelationshipTypeFixture instanceRelationshipTypeFixture; protected final MarkItemFixture markItemFixture; @@ -59,6 +62,9 @@ public ApiTests() { consortiumHoldingsStorageClient = ResourceClient.forHoldingsStorage(consortiumOkapiClient); consortiumItemsClient = ResourceClient.forItemsStorage(consortiumOkapiClient); + + collegeHoldingsStorageClient = ResourceClient.forHoldingsStorage(collegeOkapiClient); + collegeItemsClient = ResourceClient.forItemsStorage(collegeOkapiClient); } @BeforeClass @@ -76,6 +82,7 @@ public static void before() okapiClient = ApiTestSuite.createOkapiHttpClient(); consortiumOkapiClient = ApiTestSuite.createOkapiHttpClient(ApiTestSuite.CONSORTIA_TENANT_ID); + collegeOkapiClient = ApiTestSuite.createOkapiHttpClient(ApiTestSuite.COLLEGE_TENANT_ID); } @AfterClass diff --git a/src/test/java/api/support/builders/HoldingRequestBuilder.java b/src/test/java/api/support/builders/HoldingRequestBuilder.java index cff460332..f8a587bae 100644 --- a/src/test/java/api/support/builders/HoldingRequestBuilder.java +++ b/src/test/java/api/support/builders/HoldingRequestBuilder.java @@ -20,6 +20,7 @@ public class HoldingRequestBuilder extends AbstractBuilder { private final String callNumberTypeId; private final UUID sourceId; private final List administrativeNotes; + private final String hrId; public HoldingRequestBuilder() { this( @@ -31,6 +32,7 @@ public HoldingRequestBuilder() { null, null, FOLIO_SOURCE_HOLDINGS_ID, + null, null); } @@ -43,7 +45,8 @@ private HoldingRequestBuilder( String callNumberPrefix, String callNumberTypeId, UUID sourceId, - List administrativeNotes) { + List administrativeNotes, + String hrId) { this.instanceId = instanceId; this.permanentLocationId = permanentLocationId; @@ -54,6 +57,7 @@ private HoldingRequestBuilder( this.callNumberTypeId = callNumberTypeId; this.sourceId = sourceId; this.administrativeNotes = administrativeNotes; + this.hrId = hrId; } @Override @@ -72,6 +76,7 @@ public JsonObject create() { holding.put("callNumberSuffix", callNumberSuffix); holding.put("callNumberTypeId", callNumberTypeId); holding.put("sourceId", sourceId); + holding.put("hrid", hrId); return holding; } @@ -85,7 +90,8 @@ private HoldingRequestBuilder withPermanentLocation(UUID permanentLocationId) { this.callNumberPrefix, this.callNumberTypeId, this.sourceId, - this.administrativeNotes); + this.administrativeNotes, + this.hrId); } private HoldingRequestBuilder withTemporaryLocation(UUID temporaryLocationId) { @@ -98,7 +104,8 @@ private HoldingRequestBuilder withTemporaryLocation(UUID temporaryLocationId) { this.callNumberPrefix, this.callNumberTypeId, this.sourceId, - this.administrativeNotes); + this.administrativeNotes, + this.hrId); } public JsonObject createFolioHoldingsSource() { @@ -135,7 +142,8 @@ public HoldingRequestBuilder forInstance(UUID instanceId) { this.callNumberPrefix, this.callNumberTypeId, this.sourceId, - this.administrativeNotes); + this.administrativeNotes, + this.hrId); } public HoldingRequestBuilder withCallNumber(String callNumber) { @@ -148,7 +156,8 @@ public HoldingRequestBuilder withCallNumber(String callNumber) { this.callNumberPrefix, this.callNumberTypeId, this.sourceId, - this.administrativeNotes); + this.administrativeNotes, + this.hrId); } public HoldingRequestBuilder withCallNumberSuffix(String suffix) { @@ -161,7 +170,8 @@ public HoldingRequestBuilder withCallNumberSuffix(String suffix) { this.callNumberPrefix, this.callNumberTypeId, this.sourceId, - this.administrativeNotes); + this.administrativeNotes, + this.hrId); } public HoldingRequestBuilder withCallNumberPrefix(String prefix) { @@ -174,7 +184,8 @@ public HoldingRequestBuilder withCallNumberPrefix(String prefix) { prefix, this.callNumberTypeId, this.sourceId, - this.administrativeNotes); + this.administrativeNotes, + this.hrId); } public HoldingRequestBuilder withCallNumberTypeId(String callNumberTypeId) { @@ -187,7 +198,8 @@ public HoldingRequestBuilder withCallNumberTypeId(String callNumberTypeId) { this.callNumberPrefix, callNumberTypeId, this.sourceId, - this.administrativeNotes); + this.administrativeNotes, + this.hrId); } public HoldingRequestBuilder withMarcSource() { @@ -200,7 +212,8 @@ public HoldingRequestBuilder withMarcSource() { this.callNumberPrefix, this.callNumberTypeId, MARC_SOURCE_HOLDINGS_ID, - this.administrativeNotes); + this.administrativeNotes, + this.hrId); } public HoldingRequestBuilder withAdministrativeNotes(List administrativeNotes) { @@ -213,6 +226,21 @@ public HoldingRequestBuilder withAdministrativeNotes(List administrative this.callNumberPrefix, this.callNumberTypeId, this.sourceId, - administrativeNotes); + administrativeNotes, + this.hrId); + } + + public HoldingRequestBuilder withHrId(String hrId) { + return new HoldingRequestBuilder( + this.instanceId, + this.permanentLocationId, + this.temporaryLocationId, + this.callNumber, + this.callNumberSuffix, + this.callNumberPrefix, + this.callNumberTypeId, + this.sourceId, + this.administrativeNotes, + hrId); } } diff --git a/src/test/java/api/support/builders/HoldingsRecordUpdateOwnershipRequestBuilder.java b/src/test/java/api/support/builders/HoldingsRecordUpdateOwnershipRequestBuilder.java index 2c4e22e0a..fe8e8009d 100644 --- a/src/test/java/api/support/builders/HoldingsRecordUpdateOwnershipRequestBuilder.java +++ b/src/test/java/api/support/builders/HoldingsRecordUpdateOwnershipRequestBuilder.java @@ -6,9 +6,9 @@ import java.util.UUID; -import static api.ApiTestSuite.TENANT_ID; import static org.folio.inventory.resources.MoveApi.HOLDINGS_RECORD_IDS; import static org.folio.inventory.resources.MoveApi.TO_INSTANCE_ID; +import static org.folio.inventory.support.MoveApiUtil.TARGET_TENANT_ID; public class HoldingsRecordUpdateOwnershipRequestBuilder extends AbstractBuilder { @@ -27,7 +27,7 @@ public JsonObject create() { includeWhenPresent(holdingsRecordUpdateOwnershipRequest, TO_INSTANCE_ID, toInstanceId); includeWhenPresent(holdingsRecordUpdateOwnershipRequest, HOLDINGS_RECORD_IDS, holdingsRecordsIds); - includeWhenPresent(holdingsRecordUpdateOwnershipRequest, TENANT_ID, tenantId); + includeWhenPresent(holdingsRecordUpdateOwnershipRequest, TARGET_TENANT_ID, tenantId); return holdingsRecordUpdateOwnershipRequest; } diff --git a/src/test/java/support/fakes/FakeStorageModuleBuilder.java b/src/test/java/support/fakes/FakeStorageModuleBuilder.java index 2ed59cf63..9ce8f1a18 100644 --- a/src/test/java/support/fakes/FakeStorageModuleBuilder.java +++ b/src/test/java/support/fakes/FakeStorageModuleBuilder.java @@ -24,7 +24,7 @@ public class FakeStorageModuleBuilder { private final List recordPreProcessors; FakeStorageModuleBuilder() { - this(null, null, List.of(ApiTestSuite.TENANT_ID, ApiTestSuite.CONSORTIA_TENANT_ID), new ArrayList<>(), true, "", + this(null, null, List.of(ApiTestSuite.TENANT_ID, ApiTestSuite.CONSORTIA_TENANT_ID, ApiTestSuite.COLLEGE_TENANT_ID), new ArrayList<>(), true, "", new ArrayList<>(), new HashMap<>(), Collections.emptyList()); }