From 27243af27dbd7792ee6d85f93cf5a20bd8a8f08a Mon Sep 17 00:00:00 2001 From: Roman_Chernetskyi Date: Fri, 24 May 2024 11:59:33 +0300 Subject: [PATCH] [MODINV-1029] Declare an interface to update ownership of Holdings and Items --- descriptors/ModuleDescriptor-template.json | 40 ++- ramls/holdings_update_ownership.json | 28 ++ ramls/inventory-update-ownership.raml | 66 ++++ ramls/items_update_ownership.json | 28 ++ .../folio/inventory/InventoryVerticle.java | 2 + .../resources/UpdateOwnershipApi.java | 28 ++ src/test/java/api/ApiTestSuite.java | 24 +- .../java/api/InstanceRelationshipsTest.java | 19 +- .../HoldingsUpdateOwnershipApiTest.java | 297 ++++++++++++++++++ .../api/items/ItemUpdateOwnershipApiTest.java | 26 ++ src/test/java/api/support/ApiRoot.java | 6 + src/test/java/api/support/ApiTests.java | 5 + ...gsRecordUpdateOwnershipRequestBuilder.java | 34 ++ .../java/support/fakes/FakeStorageModule.java | 4 +- .../fakes/FakeStorageModuleBuilder.java | 22 +- 15 files changed, 598 insertions(+), 31 deletions(-) create mode 100644 ramls/holdings_update_ownership.json create mode 100644 ramls/inventory-update-ownership.raml create mode 100644 ramls/items_update_ownership.json create mode 100644 src/main/java/org/folio/inventory/resources/UpdateOwnershipApi.java create mode 100644 src/test/java/api/holdings/HoldingsUpdateOwnershipApiTest.java create mode 100644 src/test/java/api/items/ItemUpdateOwnershipApiTest.java create mode 100644 src/test/java/api/support/builders/HoldingsRecordUpdateOwnershipRequestBuilder.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 39b727800..8fa9f03c0 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -536,6 +536,32 @@ } ] }, + { + "id": "inventory-update-ownership", + "version": "0.1", + "handlers": [ + { + "methods": ["POST"], + "pathPattern": "/inventory/items/update-ownership", + "permissionsRequired": ["inventory.items.update-ownership.item.post"], + "modulePermissions": [ + "inventory-storage.items.item.put", + "inventory-storage.items.collection.get", + "inventory-storage.holdings.item.get" + ] + }, + { + "methods": ["POST"], + "pathPattern": "/inventory/holdings/update-ownership", + "permissionsRequired": ["inventory.holdings.update-ownership.item.post"], + "modulePermissions": [ + "inventory-storage.holdings.item.put", + "inventory-storage.holdings.collection.get", + "inventory-storage.instances.item.get" + ] + } + ] + }, { "id": "_tenant", "version": "1.2", @@ -706,6 +732,11 @@ "displayName": "Inventory - mark an item as unknown", "description": "Mark an item as unknown" }, + { + "permissionName": "inventory.items.update-ownership.item.post", + "displayName": "Inventory - update an item ownership", + "description": "Update an item ownership" + }, { "permissionName": "inventory.holdings.move.item.post", @@ -742,6 +773,11 @@ "displayName": "Inventory - modify holdings", "description": "Modify individual instance" }, + { + "permissionName": "inventory.holdings.update-ownership.item.post", + "displayName": "Inventory - update holdings ownership", + "description": "Update holdings record ownership" + }, { "permissionName": "inventory.instances.item.get", "displayName": "Inventory - get individual instance", @@ -803,7 +839,9 @@ "inventory.items.item.mark-in-process-non-requestable.post", "inventory.items.item.mark-missing.post", "inventory.items.move.item.post", - "inventory.holdings.move.item.post" + "inventory.holdings.move.item.post", + "inventory.items.update-ownership.item.post", + "inventory.holdings.update-ownership.item.post" ] }, { diff --git a/ramls/holdings_update_ownership.json b/ramls/holdings_update_ownership.json new file mode 100644 index 000000000..41d6a9c2d --- /dev/null +++ b/ramls/holdings_update_ownership.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Ids holder for updating ownership of the holdings records", + "type": "object", + "properties": { + "toInstanceId": { + "description": "Id of the instance which contains holdings to update ownership.", + "$ref": "uuid.json" + }, + "holdingsRecordIds": { + "description": "Ids of the holdings to update ownership.", + "type": "array", + "items": { + "$ref": "uuid.json" + } + }, + "targetTenantId": { + "description": "Id of the tenant to update ownership.", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "toInstanceId", + "holdingsRecordIds", + "targetTenantId" + ] +} diff --git a/ramls/inventory-update-ownership.raml b/ramls/inventory-update-ownership.raml new file mode 100644 index 000000000..c81864eb0 --- /dev/null +++ b/ramls/inventory-update-ownership.raml @@ -0,0 +1,66 @@ +#%RAML 1.0 +title: Inventory Update Ownership API +version: v0.1 +protocols: [ HTTP, HTTPS ] +baseUri: http://localhost + +documentation: + - title: "Inventory Update Ownership API" + content: API for updating ownership of holdings records and items between ECS tenants + +types: + errors: !include raml-util/schemas/errors.schema + items_update_ownership: !include items_update_ownership.json + holdings_update_ownership: !include holdings_update_ownership.json + move_response: !include move_response.json + +traits: + language: !include raml-util/traits/language.raml + validate: !include raml-util/traits/validation.raml + +/inventory/items/update-ownership: + displayName: Items Update Ownership + post: + is: [validate] + body: + application/json: + type: items_update_ownership + responses: + 200: + description: "Items ownership updated to another tenant" + body: + application/json: + type: move_response + 422: + description: "Validation error" + body: + application/json: + type: errors + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" +/inventory/holdings/update-ownership: + displayName: Holdings Record Update Ownership + post: + is: [validate] + body: + application/json: + type: holdings_update_ownership + responses: + 200: + description: "Holdings record ownership updated to another tenant" + body: + application/json: + type: move_response + 422: + description: "Validation error" + body: + application/json: + type: errors + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" diff --git a/ramls/items_update_ownership.json b/ramls/items_update_ownership.json new file mode 100644 index 000000000..c6c6fe2f5 --- /dev/null +++ b/ramls/items_update_ownership.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Ids holder for updating ownership of the items", + "type": "object", + "properties": { + "toHoldingsRecordId": { + "description": "Id of the holding to which the items are updating ownership.", + "$ref": "uuid.json" + }, + "itemIds": { + "description": "Ids of the items to update ownership.", + "type": "array", + "items": { + "$ref": "uuid.json" + } + }, + "targetTenantId": { + "description": "Id of the tenant to update ownership.", + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "toHoldingsRecordId", + "itemIds", + "targetTenantId" + ] +} diff --git a/src/main/java/org/folio/inventory/InventoryVerticle.java b/src/main/java/org/folio/inventory/InventoryVerticle.java index 08a08b54b..8079714c6 100644 --- a/src/main/java/org/folio/inventory/InventoryVerticle.java +++ b/src/main/java/org/folio/inventory/InventoryVerticle.java @@ -19,6 +19,7 @@ import org.folio.inventory.resources.ItemsByHoldingsRecordId; import org.folio.inventory.resources.MoveApi; import org.folio.inventory.resources.TenantApi; +import org.folio.inventory.resources.UpdateOwnershipApi; import org.folio.inventory.storage.Storage; import io.vertx.core.AbstractVerticle; @@ -69,6 +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); Handler> onHttpServerStart = result -> { if (result.succeeded()) { diff --git a/src/main/java/org/folio/inventory/resources/UpdateOwnershipApi.java b/src/main/java/org/folio/inventory/resources/UpdateOwnershipApi.java new file mode 100644 index 000000000..4e94941a7 --- /dev/null +++ b/src/main/java/org/folio/inventory/resources/UpdateOwnershipApi.java @@ -0,0 +1,28 @@ +package org.folio.inventory.resources; + +import io.vertx.core.http.HttpClient; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import org.folio.inventory.storage.Storage; + +public class UpdateOwnershipApi extends AbstractInventoryResource { + public UpdateOwnershipApi(Storage storage, HttpClient client) { + super(storage, client); + } + + @Override + public void register(Router router) { + router.post("/inventory/items/update-ownership") + .handler(this::updateItemsOwnership); + router.post("/inventory/holdings/update-ownership") + .handler(this::updateHoldingsOwnership); + } + + private void updateHoldingsOwnership(RoutingContext routingContext) { + + } + + private void updateItemsOwnership(RoutingContext routingContext) { + + } +} diff --git a/src/test/java/api/ApiTestSuite.java b/src/test/java/api/ApiTestSuite.java index ca5dc7344..2373dc0fd 100644 --- a/src/test/java/api/ApiTestSuite.java +++ b/src/test/java/api/ApiTestSuite.java @@ -79,6 +79,8 @@ public class ApiTestSuite { public static final String TOKEN = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInRlbmFudCI6ImRlbW9fdGVuYW50In0.29VPjLI6fLJzxQW0UhQ0jsvAn8xHz501zyXAxRflXfJ9wuDzT8TDf-V75PjzD7fe2kHjSV2dzRXbstt3BTtXIQ"; public static final String USER_ID = "7e115dfb-d1d6-46ac-b2dc-2b3e74cda694"; + public static final String CENTRAL_TENANT_ID_FIELD = "centralTenantId"; + public static final String CONSORTIUM_ID_FIELD = "consortiumId"; private static String bookMaterialTypeId; private static String dvdMaterialTypeId; @@ -218,10 +220,16 @@ public static String getBibliographyNatureOfContentTermId() { public static OkapiHttpClient createOkapiHttpClient() throws MalformedURLException { + return createOkapiHttpClient(TENANT_ID); + } + + public static OkapiHttpClient createOkapiHttpClient(String tenantId) + throws MalformedURLException { + return new OkapiHttpClient( vertxAssistant.getVertx(), - new URL(storageOkapiUrl()), TENANT_ID, TOKEN, USER_ID, null, - it -> System.out.println(String.format("Request failed: %s", it.toString()))); + new URL(storageOkapiUrl()), tenantId, TOKEN, USER_ID, null, + it -> System.out.printf("Request failed: %s%n", it.toString())); } public static String storageOkapiUrl() { @@ -449,6 +457,18 @@ private static void createNatureOfContentTerms() ); } + public static void createConsortiumTenant() throws MalformedURLException { + String expectedConsortiumId = UUID.randomUUID().toString(); + + JsonObject userTenantsCollection = new JsonObject() + .put(ApiTestSuite.CENTRAL_TENANT_ID_FIELD, ApiTestSuite.CONSORTIA_TENANT_ID) + .put(ApiTestSuite.CONSORTIUM_ID_FIELD, expectedConsortiumId); + + ResourceClient client = ResourceClient.forUserTenants(createOkapiHttpClient()); + + client.create(userTenantsCollection); + } + private static void createIdentifierTypes() throws MalformedURLException, InterruptedException, diff --git a/src/test/java/api/InstanceRelationshipsTest.java b/src/test/java/api/InstanceRelationshipsTest.java index 172cf5c66..c120a1ef2 100644 --- a/src/test/java/api/InstanceRelationshipsTest.java +++ b/src/test/java/api/InstanceRelationshipsTest.java @@ -1,5 +1,6 @@ package api; +import static api.ApiTestSuite.createConsortiumTenant; import static api.support.InstanceSamples.nod; import static api.support.InstanceSamples.smallAngryPlanet; import static io.vertx.core.json.JsonObject.mapFrom; @@ -34,8 +35,6 @@ public class InstanceRelationshipsTest extends ApiTests { private static final String PARENT_INSTANCES = "parentInstances"; - private static final String CENTRAL_TENANT_ID_FIELD = "centralTenantId"; - private static final String CONSORTIUM_ID_FIELD = "consortiumId"; @After public void disableFailureEmulationAndClearConsortia() throws Exception { @@ -490,7 +489,7 @@ public void canUpdateInstanceWithChildInstancesWhenParentInstancesAlreadySet() t public void cannotLinkLocalInstanceToSharedInstance() throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { UUID parentId = UUID.randomUUID(); - initConsortiumTenant(); + createConsortiumTenant(); final JsonObject parentRelationship = createParentRelationship(parentId.toString(), instanceRelationshipTypeFixture.boundWith().getId()); @@ -503,8 +502,8 @@ public void cannotLinkLocalInstanceToSharedInstance() throws MalformedURLExcepti } @Test - public void canCreateInstanceWithParentInstancesWhenConsortiaEnabled() { - initConsortiumTenant(); + public void canCreateInstanceWithParentInstancesWhenConsortiaEnabled() throws MalformedURLException { + createConsortiumTenant(); final IndividualResource parentInstance = instancesClient.create(nod(UUID.randomUUID())); @@ -518,16 +517,6 @@ public void canCreateInstanceWithParentInstancesWhenConsortiaEnabled() { is(parentRelationship)); } - private void initConsortiumTenant() { - String expectedConsortiumId = UUID.randomUUID().toString(); - - JsonObject userTenantsCollection = new JsonObject() - .put(CENTRAL_TENANT_ID_FIELD, ApiTestSuite.CONSORTIA_TENANT_ID) - .put(CONSORTIUM_ID_FIELD, expectedConsortiumId); - - userTenantsClient.create(userTenantsCollection); - } - private JsonObject createParentRelationship(String superInstanceId, String relationshipType) { return mapFrom(new InstanceRelationshipToParent(UUID.randomUUID().toString(), superInstanceId, relationshipType)); diff --git a/src/test/java/api/holdings/HoldingsUpdateOwnershipApiTest.java b/src/test/java/api/holdings/HoldingsUpdateOwnershipApiTest.java new file mode 100644 index 000000000..852832c08 --- /dev/null +++ b/src/test/java/api/holdings/HoldingsUpdateOwnershipApiTest.java @@ -0,0 +1,297 @@ +package api.holdings; + +import api.ApiTestSuite; +import api.support.ApiRoot; +import api.support.ApiTests; +import api.support.InstanceApiClient; +import api.support.builders.HoldingRequestBuilder; +import api.support.builders.HoldingsRecordUpdateOwnershipRequestBuilder; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import junitparams.JUnitParamsRunner; +import org.apache.http.HttpStatus; +import org.folio.inventory.support.http.client.Response; +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 java.net.MalformedURLException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +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.support.http.ContentType.APPLICATION_JSON; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +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"; + + @Before + public void initConsortia() throws Exception { + createConsortiumTenant(); + } + + @After + public void clearConsortia() throws Exception { + userTenantsClient.deleteAll(); + } + + @Test + public void canUpdateHoldingsOwnershipToDifferentTenant() throws MalformedURLException, ExecutionException, InterruptedException, 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(instanceId); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, + new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); + assertThat(new JsonObject(postHoldingsUpdateOwnershipResponse.getBody()).getJsonArray("nonUpdatedIds").size(), is(0)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(sourceTenantHoldingsRecord1.getStatusCode(), HttpStatus.SC_NOT_FOUND); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + + Response sourceTenantHoldingsRecord2 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord2 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(sourceTenantHoldingsRecord2.getStatusCode(), HttpStatus.SC_NOT_FOUND); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord2.getJson().getString(INSTANCE_ID)); + } + + @Test + public void shouldReportErrorsWhenOnlySomeRequestedHoldingsRecordsCouldNotBeUpdated() throws InterruptedException, MalformedURLException, TimeoutException, ExecutionException { + UUID instanceId = UUID.randomUUID(); + JsonObject instance = smallAngryPlanet(instanceId); + + InstanceApiClient.createInstance(okapiClient, instance); + InstanceApiClient.createInstance(consortiumOkapiClient, instance); + + final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); + final UUID createHoldingsRecord2 = UUID.randomUUID(); + + Assert.assertNotEquals(createHoldingsRecord1, createHoldingsRecord2); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, + new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.CONSORTIA_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(); + + assertThat(notFoundIds.size(), is(1)); + assertThat(notFoundIds.get(0), equalTo(createHoldingsRecord2.toString())); + + Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(sourceTenantHoldingsRecord1.getStatusCode(), HttpStatus.SC_NOT_FOUND); + assertThat(instanceId.toString(), equalTo(targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID))); + + Response targetTenantHoldingsRecord2 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + Assert.assertEquals(targetTenantHoldingsRecord2.getStatusCode(), HttpStatus.SC_NOT_FOUND); + } + + @Test + public void cannotUpdateHoldingsRecordsOwnershipToUnspecifiedInstance() + throws InterruptedException, MalformedURLException, TimeoutException, ExecutionException { + JsonObject holdingsRecordUpdateOwnershipWithoutToInstanceId = new HoldingsRecordUpdateOwnershipRequestBuilder(null, + new JsonArray(List.of(UUID.randomUUID())), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipWithoutToInstanceId); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(422)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + assertThat(postHoldingsUpdateOwnershipResponse, hasValidationError( + "toInstanceId is a required field", "toInstanceId", null + )); + } + + @Test + public void cannotUpdateHoldingsRecordsOwnershipToUnspecifiedTenant() + throws InterruptedException, MalformedURLException, TimeoutException, ExecutionException { + JsonObject holdingsRecordUpdateOwnershipWithoutTenantId = new HoldingsRecordUpdateOwnershipRequestBuilder(UUID.randomUUID(), + new JsonArray(List.of(UUID.randomUUID())), null).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipWithoutTenantId); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(422)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + assertThat(postHoldingsUpdateOwnershipResponse, hasValidationError( + "tenantId is a required field", "toInstanceId", null + )); + } + + @Test + public void cannotUpdateUnspecifiedHoldingsRecordsOwnership() + throws MalformedURLException, InterruptedException, ExecutionException, TimeoutException { + JsonObject holdingsRecordUpdateOwnershipWithoutHoldingsRecordIds = new HoldingsRecordUpdateOwnershipRequestBuilder(UUID.randomUUID(), + new JsonArray(), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipWithoutHoldingsRecordIds); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(422)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + assertThat(postHoldingsUpdateOwnershipResponse, hasValidationError( + "Holdings record ids aren't specified", "holdingsRecordIds", null + )); + } + + @Test + public void cannotUpdateHoldingsRecordOwnershipOfNonExistedInstance() + throws MalformedURLException, InterruptedException, ExecutionException, TimeoutException { + createConsortiumTenant(); + + UUID instanceId = UUID.randomUUID(); + JsonObject instance = smallAngryPlanet(instanceId); + + UUID invalidInstanceId = UUID.randomUUID(); + + InstanceApiClient.createInstance(okapiClient, instance); + InstanceApiClient.createInstance(consortiumOkapiClient, instance); + + final UUID createHoldingsRecord1 = createHoldingForInstance(instanceId); + + JsonObject holdingsRecordUpdateOwnershipWithoutHoldingsRecordIds = new HoldingsRecordUpdateOwnershipRequestBuilder(invalidInstanceId, + new JsonArray(List.of(createHoldingsRecord1)), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipWithoutHoldingsRecordIds); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(422)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + assertThat(postHoldingsUpdateOwnershipResponse.getBody(), containsString("errors")); + assertThat(postHoldingsUpdateOwnershipResponse.getBody(), containsString(invalidInstanceId.toString())); + } + + @Test + public void canUpdateHoldingsRecordOwnershipDueToHoldingsRecordUpdateError() throws InterruptedException, MalformedURLException, TimeoutException, ExecutionException { + 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); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, + new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + List nonUpdatedIdsIds = postHoldingsUpdateOwnershipResponse.getJson() + .getJsonArray("nonUpdatedIds") + .getList(); + + assertThat(nonUpdatedIdsIds.size(), is(1)); + assertThat(nonUpdatedIdsIds.get(0), equalTo(ID_FOR_FAILURE.toString())); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(sourceTenantHoldingsRecord1.getStatusCode(), HttpStatus.SC_NOT_FOUND); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + + Response sourceTenantHoldingsRecord2 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord2 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(instanceId.toString(), sourceTenantHoldingsRecord2.getJson().getString(INSTANCE_ID)); + Assert.assertEquals(targetTenantHoldingsRecord2.getStatusCode(), HttpStatus.SC_NOT_FOUND); + } + + @Test + public void canUpdateHoldingsRecordOwnershipToDifferentInstanceWithExtraRedundantFields() throws InterruptedException, MalformedURLException, TimeoutException, ExecutionException { + UUID instanceId = UUID.randomUUID(); + JsonObject instance = smallAngryPlanet(instanceId); + + InstanceApiClient.createInstance(okapiClient, instance); + InstanceApiClient.createInstance(consortiumOkapiClient, instance); + + JsonObject firstJsonHoldingsAsRequest = new HoldingRequestBuilder().forInstance(instanceId).create(); + final UUID createHoldingsRecord1 = holdingsStorageClient.create(firstJsonHoldingsAsRequest + .put("holdingsItems", new JsonArray().add(new JsonObject().put("id", UUID.randomUUID())).add(new JsonObject().put("id", UUID.randomUUID()))) + .put("bareHoldingsItems", new JsonArray().add(new JsonObject().put("id", UUID.randomUUID())).add(new JsonObject().put("id", UUID.randomUUID())))) + .getId(); + + JsonObject secondJsonHoldingsAsRequest = new HoldingRequestBuilder().forInstance(instanceId).create(); + final UUID createHoldingsRecord2 = holdingsStorageClient.create(secondJsonHoldingsAsRequest + .put("holdingsItems", new JsonArray().add(new JsonObject().put("id", UUID.randomUUID())).add(new JsonObject().put("id", UUID.randomUUID()))) + .put("bareHoldingsItems", new JsonArray().add(new JsonObject().put("id", UUID.randomUUID())).add(new JsonObject().put("id", UUID.randomUUID())))) + .getId(); + + JsonObject holdingsRecordUpdateOwnershipRequestBody = new HoldingsRecordUpdateOwnershipRequestBuilder(instanceId, + new JsonArray(List.of(createHoldingsRecord1.toString(), createHoldingsRecord2.toString())), ApiTestSuite.CONSORTIA_TENANT_ID).create(); + + Response postHoldingsUpdateOwnershipResponse = updateHoldingsRecordsOwnership(holdingsRecordUpdateOwnershipRequestBody); + + assertThat(postHoldingsUpdateOwnershipResponse.getStatusCode(), is(200)); + assertThat(new JsonObject(postHoldingsUpdateOwnershipResponse.getBody()).getJsonArray("nonUpdatedIds").size(), is(0)); + assertThat(postHoldingsUpdateOwnershipResponse.getContentType(), containsString(APPLICATION_JSON)); + + Response sourceTenantHoldingsRecord1 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord1 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(sourceTenantHoldingsRecord1.getStatusCode(), HttpStatus.SC_NOT_FOUND); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord1.getJson().getString(INSTANCE_ID)); + + Response sourceTenantHoldingsRecord2 = holdingsStorageClient.getById(createHoldingsRecord1); + Response targetTenantHoldingsRecord2 = consortiumHoldingsStorageClient.getById(createHoldingsRecord1); + + Assert.assertEquals(sourceTenantHoldingsRecord2.getStatusCode(), HttpStatus.SC_NOT_FOUND); + Assert.assertEquals(instanceId.toString(), targetTenantHoldingsRecord2.getJson().getString(INSTANCE_ID)); + } + + 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); + } + + private UUID createHoldingForInstance(UUID instanceId) { + return holdingsStorageClient.create(new HoldingRequestBuilder().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/items/ItemUpdateOwnershipApiTest.java b/src/test/java/api/items/ItemUpdateOwnershipApiTest.java new file mode 100644 index 000000000..d3f35dbf9 --- /dev/null +++ b/src/test/java/api/items/ItemUpdateOwnershipApiTest.java @@ -0,0 +1,26 @@ +package api.items; + +import api.support.ApiTests; +import junitparams.JUnitParamsRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.runner.RunWith; + +import static api.ApiTestSuite.createConsortiumTenant; + +@Ignore +@RunWith(JUnitParamsRunner.class) +public class ItemUpdateOwnershipApiTest extends ApiTests { + private static final String INSTANCE_ID = "instanceId"; + + @Before + public void initConsortia() throws Exception { + createConsortiumTenant(); + } + + @After + public void clearConsortia() throws Exception { + userTenantsClient.deleteAll(); + } +} diff --git a/src/test/java/api/support/ApiRoot.java b/src/test/java/api/support/ApiRoot.java index 07734f499..f061e2ab4 100644 --- a/src/test/java/api/support/ApiRoot.java +++ b/src/test/java/api/support/ApiRoot.java @@ -53,6 +53,12 @@ public static URL moveHoldingsRecords() return new URL(String.format("%s/holdings/move", inventory())); } + public static URL updateHoldingsRecordsOwnership() + throws MalformedURLException { + + return new URL(String.format("%s/holdings/update-ownership", inventory())); + } + public static URL items(String query) throws MalformedURLException { diff --git a/src/test/java/api/support/ApiTests.java b/src/test/java/api/support/ApiTests.java index d5dcb5783..6ee19fe72 100644 --- a/src/test/java/api/support/ApiTests.java +++ b/src/test/java/api/support/ApiTests.java @@ -18,6 +18,7 @@ public abstract class ApiTests { private static boolean runningOnOwn; protected static OkapiHttpClient okapiClient; + protected static OkapiHttpClient consortiumOkapiClient; protected final ResourceClient holdingsStorageClient; protected final ResourceClient itemsStorageClient; protected final ResourceClient itemsClient; @@ -31,6 +32,7 @@ public abstract class ApiTests { protected final ResourceClient instanceRelationshipClient; protected final ResourceClient requestStorageClient; protected final ResourceClient sourceRecordStorageClient; + protected final ResourceClient consortiumHoldingsStorageClient; protected final InstanceRelationshipTypeFixture instanceRelationshipTypeFixture; protected final MarkItemFixture markItemFixture; @@ -51,6 +53,8 @@ public ApiTests() { sourceRecordStorageClient = ResourceClient.forSourceRecordStorage(okapiClient); instanceRelationshipTypeFixture = new InstanceRelationshipTypeFixture(okapiClient); markItemFixture = new MarkItemFixture(okapiClient); + + consortiumHoldingsStorageClient = ResourceClient.forHoldingsStorage(consortiumOkapiClient); } @BeforeClass @@ -67,6 +71,7 @@ public static void before() } okapiClient = ApiTestSuite.createOkapiHttpClient(); + consortiumOkapiClient = ApiTestSuite.createOkapiHttpClient(ApiTestSuite.CONSORTIA_TENANT_ID); } @AfterClass diff --git a/src/test/java/api/support/builders/HoldingsRecordUpdateOwnershipRequestBuilder.java b/src/test/java/api/support/builders/HoldingsRecordUpdateOwnershipRequestBuilder.java new file mode 100644 index 000000000..2c4e22e0a --- /dev/null +++ b/src/test/java/api/support/builders/HoldingsRecordUpdateOwnershipRequestBuilder.java @@ -0,0 +1,34 @@ +package api.support.builders; + + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +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; + +public class HoldingsRecordUpdateOwnershipRequestBuilder extends AbstractBuilder { + + private final UUID toInstanceId; + private final JsonArray holdingsRecordsIds; + private final String tenantId; + + public HoldingsRecordUpdateOwnershipRequestBuilder(UUID toInstanceId, JsonArray holdingsRecordsIds, String tenantId) { + this.toInstanceId = toInstanceId; + this.holdingsRecordsIds = holdingsRecordsIds; + this.tenantId = tenantId; + } + + public JsonObject create() { + JsonObject holdingsRecordUpdateOwnershipRequest = new JsonObject(); + + includeWhenPresent(holdingsRecordUpdateOwnershipRequest, TO_INSTANCE_ID, toInstanceId); + includeWhenPresent(holdingsRecordUpdateOwnershipRequest, HOLDINGS_RECORD_IDS, holdingsRecordsIds); + includeWhenPresent(holdingsRecordUpdateOwnershipRequest, TENANT_ID, tenantId); + + return holdingsRecordUpdateOwnershipRequest; + } +} diff --git a/src/test/java/support/fakes/FakeStorageModule.java b/src/test/java/support/fakes/FakeStorageModule.java index 33d543037..84e20583b 100644 --- a/src/test/java/support/fakes/FakeStorageModule.java +++ b/src/test/java/support/fakes/FakeStorageModule.java @@ -47,7 +47,7 @@ class FakeStorageModule extends AbstractVerticle { FakeStorageModule( String rootPath, String collectionPropertyName, - String tenantId, + List tenants, Collection requiredProperties, boolean hasCollectionDelete, String recordTypeName, @@ -69,7 +69,7 @@ class FakeStorageModule extends AbstractVerticle { this.defaultProperties = defaultPropertiesWithId; storedResourcesByTenant = new HashMap<>(); - storedResourcesByTenant.put(tenantId, new HashMap<>()); + tenants.forEach(tenant -> storedResourcesByTenant.put(tenant, new HashMap<>())); this.recordPreProcessors = recordPreProcessors; } diff --git a/src/test/java/support/fakes/FakeStorageModuleBuilder.java b/src/test/java/support/fakes/FakeStorageModuleBuilder.java index 89d8bd45e..2ed59cf63 100644 --- a/src/test/java/support/fakes/FakeStorageModuleBuilder.java +++ b/src/test/java/support/fakes/FakeStorageModuleBuilder.java @@ -15,7 +15,7 @@ public class FakeStorageModuleBuilder { private final String rootPath; private final String collectionPropertyName; - private final String tenantId; + private final List tenants; private final Collection requiredProperties; private final Collection uniqueProperties; private final Map> defaultProperties; @@ -24,14 +24,14 @@ public class FakeStorageModuleBuilder { private final List recordPreProcessors; FakeStorageModuleBuilder() { - this(null, null, ApiTestSuite.TENANT_ID, new ArrayList<>(), true, "", + this(null, null, List.of(ApiTestSuite.TENANT_ID, ApiTestSuite.CONSORTIA_TENANT_ID), new ArrayList<>(), true, "", new ArrayList<>(), new HashMap<>(), Collections.emptyList()); } private FakeStorageModuleBuilder( String rootPath, String collectionPropertyName, - String tenantId, + List tenants, Collection requiredProperties, boolean hasCollectionDelete, String recordName, @@ -41,7 +41,7 @@ private FakeStorageModuleBuilder( this.rootPath = rootPath; this.collectionPropertyName = collectionPropertyName; - this.tenantId = tenantId; + this.tenants = tenants; this.requiredProperties = requiredProperties; this.hasCollectionDelete = hasCollectionDelete; this.recordName = recordName; @@ -51,7 +51,7 @@ private FakeStorageModuleBuilder( } public FakeStorageModule create() { - return new FakeStorageModule(rootPath, collectionPropertyName, tenantId, + return new FakeStorageModule(rootPath, collectionPropertyName, tenants, requiredProperties, hasCollectionDelete, recordName, uniqueProperties, defaultProperties, recordPreProcessors); } @@ -64,7 +64,7 @@ FakeStorageModuleBuilder withRootPath(String rootPath) { return new FakeStorageModuleBuilder( rootPath, newCollectionPropertyName, - this.tenantId, + this.tenants, this.requiredProperties, this.hasCollectionDelete, this.recordName, @@ -77,7 +77,7 @@ FakeStorageModuleBuilder withCollectionPropertyName(String collectionPropertyNam return new FakeStorageModuleBuilder( this.rootPath, collectionPropertyName, - this.tenantId, + this.tenants, this.requiredProperties, this.hasCollectionDelete, this.recordName, @@ -90,7 +90,7 @@ FakeStorageModuleBuilder withRecordName(String recordName) { return new FakeStorageModuleBuilder( this.rootPath, this.collectionPropertyName, - this.tenantId, + this.tenants, this.requiredProperties, this.hasCollectionDelete, recordName, @@ -105,7 +105,7 @@ private FakeStorageModuleBuilder withRequiredProperties( return new FakeStorageModuleBuilder( this.rootPath, this.collectionPropertyName, - this.tenantId, + this.tenants, requiredProperties, this.hasCollectionDelete, this.recordName, @@ -126,7 +126,7 @@ FakeStorageModuleBuilder withDefault(String property, Object value) { return new FakeStorageModuleBuilder( this.rootPath, this.collectionPropertyName, - this.tenantId, + this.tenants, this.requiredProperties, this.hasCollectionDelete, this.recordName, @@ -139,7 +139,7 @@ final FakeStorageModuleBuilder withRecordPreProcessors(RecordPreProcessor... pre return new FakeStorageModuleBuilder( this.rootPath, this.collectionPropertyName, - this.tenantId, + this.tenants, this.requiredProperties, this.hasCollectionDelete, this.recordName,