From 1299b03f3dd60c31e161fe70206249a9d73f71f3 Mon Sep 17 00:00:00 2001 From: Gurleen Kaur <34331959+gurleenkaurbp@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:42:38 +0530 Subject: [PATCH] [EDGPATRON-160] - Add put API for /patron/{externalSystemId} (#138) * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} * [EDGPATRON-160] - Add put API for /patron/{externalSystemId} --- ramls/edge-patron.raml | 67 +++++++++++ .../java/org/folio/edge/patron/Constants.java | 1 + .../org/folio/edge/patron/MainVerticle.java | 3 + .../org/folio/edge/patron/PatronHandler.java | 71 ++++++------ .../edge/patron/utils/PatronOkapiClient.java | 11 ++ .../folio/edge/patron/MainVerticleTest.java | 105 +++++++++++++++++- .../edge/patron/utils/PatronMockOkapi.java | 39 +++++++ .../staging-users-put-error-response.json | 27 +++++ .../resources/staging-users-put-request.json | 27 +++++ .../resources/staging-users-put-response.json | 34 ++++++ 10 files changed, 352 insertions(+), 33 deletions(-) create mode 100644 src/test/resources/staging-users-put-error-response.json create mode 100644 src/test/resources/staging-users-put-request.json create mode 100644 src/test/resources/staging-users-put-response.json diff --git a/ramls/edge-patron.raml b/ramls/edge-patron.raml index e994c65..f0302ed 100644 --- a/ramls/edge-patron.raml +++ b/ramls/edge-patron.raml @@ -87,6 +87,73 @@ types: body: text/plain: example: internal server error, contact administrator + + /{externalSystemId}: + uriParameters: + externalSystemId: + description: The UUID of a FOLIO user + type: string + pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ + put: + description: | + Update a staging user based on external system ID. + queryParameters: + apikey: + description: "API Key" + type: string + body: + application/json: + type: staging_user + example: !include examples/staging_user.json + responses: + 200: + description: | + staging user updated successfully + body: + application/json: + type: staging_user + example: !include examples/staging_user.json + 201: + description: | + staging user created successfully + body: + application/json: + type: staging_user + example: !include examples/staging_user.json + 400: + description: Bad request + body: + text/plain: + example: unable to process request + 401: + description: Not authorized to perform requested action + body: + text/plain: + example: unable to create request + 403: + description: Access Denied + body: + text/plain: + example: Access Denied + 422: + description: Validation error + body: + text/plain: + example: Validation error + 500: + description: | + Internal server error, e.g. due to misconfiguration + body: + text/plain: + example: internal server error, contact administrator + 404: + description: Item with a given ID not found + body: + application/json: + type: external_patron_error_404 + example: !include examples/external_patron_error.json + + /account: post: description: | diff --git a/src/main/java/org/folio/edge/patron/Constants.java b/src/main/java/org/folio/edge/patron/Constants.java index 7b60082..55acea8 100644 --- a/src/main/java/org/folio/edge/patron/Constants.java +++ b/src/main/java/org/folio/edge/patron/Constants.java @@ -34,6 +34,7 @@ public class Constants { public static final String PARAM_INSTANCE_ID = "instanceId"; public static final String PARAM_HOLD_ID = "holdId"; public static final String PARAM_EMAIL_ID = "emailId"; + public static final String PARAM_EXTERNAL_SYSTEM_ID = "externalSystemId"; public static final String PARAM_REQUEST_ID = "requestId"; public static final String MSG_ACCESS_DENIED = "Access Denied"; diff --git a/src/main/java/org/folio/edge/patron/MainVerticle.java b/src/main/java/org/folio/edge/patron/MainVerticle.java index 8c92363..b5a3bd6 100644 --- a/src/main/java/org/folio/edge/patron/MainVerticle.java +++ b/src/main/java/org/folio/edge/patron/MainVerticle.java @@ -104,6 +104,9 @@ public Router defineRoutes() { router.route(HttpMethod.POST, "/patron") .handler(patronHandler::handlePostPatronRequest); + router.route(HttpMethod.PUT, "/patron/:externalSystemId") + .handler(patronHandler::handlePutPatronRequest); + router.route(HttpMethod.POST, "/patron/account/:patronId/instance/:instanceId/hold") .handler(patronHandler::handlePlaceInstanceHold); diff --git a/src/main/java/org/folio/edge/patron/PatronHandler.java b/src/main/java/org/folio/edge/patron/PatronHandler.java index 7b71aa0..26631ea 100644 --- a/src/main/java/org/folio/edge/patron/PatronHandler.java +++ b/src/main/java/org/folio/edge/patron/PatronHandler.java @@ -3,25 +3,7 @@ import static org.folio.edge.core.Constants.APPLICATION_JSON; import static org.folio.edge.core.Constants.X_OKAPI_TENANT; import static org.folio.edge.core.Constants.X_OKAPI_TOKEN; -import static org.folio.edge.patron.Constants.EXTERNAL_SYSTEM_ID_CLAIM; -import static org.folio.edge.patron.Constants.FIELD_EXPIRATION_DATE; -import static org.folio.edge.patron.Constants.FIELD_REQUEST_DATE; -import static org.folio.edge.patron.Constants.MSG_ACCESS_DENIED; -import static org.folio.edge.patron.Constants.MSG_HOLD_NOBODY; -import static org.folio.edge.patron.Constants.MSG_INTERNAL_SERVER_ERROR; -import static org.folio.edge.patron.Constants.MSG_REQUEST_TIMEOUT; -import static org.folio.edge.patron.Constants.PARAM_EMAIL_ID; -import static org.folio.edge.patron.Constants.PARAM_HOLD_ID; -import static org.folio.edge.patron.Constants.PARAM_INCLUDE_CHARGES; -import static org.folio.edge.patron.Constants.PARAM_INCLUDE_HOLDS; -import static org.folio.edge.patron.Constants.PARAM_INCLUDE_LOANS; -import static org.folio.edge.patron.Constants.PARAM_INSTANCE_ID; -import static org.folio.edge.patron.Constants.PARAM_ITEM_ID; -import static org.folio.edge.patron.Constants.PARAM_LIMIT; -import static org.folio.edge.patron.Constants.PARAM_OFFSET; -import static org.folio.edge.patron.Constants.PARAM_PATRON_ID; -import static org.folio.edge.patron.Constants.PARAM_SORT_BY; -import static org.folio.edge.patron.Constants.VIP_CLAIM; +import static org.folio.edge.patron.Constants.*; import static org.folio.edge.patron.model.HoldCancellationValidator.validateCancelHoldRequest; import com.amazonaws.util.StringUtils; @@ -41,7 +23,10 @@ import java.util.Map; import java.util.Objects; import java.util.TimeZone; +import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.UnaryOperator; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.edge.core.Handler; @@ -207,13 +192,13 @@ public void handleSecurePlaceItemHold(RoutingContext ctx) { handleSecureCommon(ctx, this::handlePlaceItemHold); } - public void handlePostPatronRequest(RoutingContext ctx) { + public void handlePatronRequest(RoutingContext ctx, BiConsumer patronAction) { if (ctx.body().asJsonObject() == null) { - logger.warn("handlePostPatronRequest:: missing body found"); + logger.warn("handlePatronRequest:: missing body found"); ctx.response() .setStatusCode(400) .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) - .end(getErrorMsg("MISSING_BODY", "Request body must not null")); + .end(getErrorMsg("MISSING_BODY", "Request body must not be null")); return; } @@ -221,12 +206,25 @@ public void handlePostPatronRequest(RoutingContext ctx) { super.handleCommon(ctx, new String[]{}, new String[]{}, (client, params) -> { String alternateTenantId = ctx.request().getParam("alternateTenantId", client.tenant); final PatronOkapiClient patronClient = new PatronOkapiClient(client, alternateTenantId); + patronAction.accept(patronClient, body); + }); + } + + public void handlePostPatronRequest(RoutingContext ctx) { + handlePatronRequest(ctx, (patronClient, body) -> patronClient.postPatron(body, resp -> handleProxyResponse(ctx, resp), - t -> handleProxyException(ctx, t)); - }); + t -> handleProxyException(ctx, t))); + } + + public void handlePutPatronRequest(RoutingContext ctx) { + handlePatronRequest(ctx, (patronClient, body) -> + patronClient.putPatron(ctx.request().getParam(PARAM_EXTERNAL_SYSTEM_ID), body, + resp -> handlePutPatronResponse(ctx, resp), + t -> handleProxyException(ctx, t))); } + public void handleCancelHold(RoutingContext ctx) { String validationResult = validateCancelHoldRequest(ctx.body().asJsonObject()); if ( validationResult != null) { @@ -400,32 +398,41 @@ protected void handleProxyResponse(RoutingContext ctx, HttpResponse resp } } - protected void handleRegistrationStatusResponse(RoutingContext ctx, HttpResponse resp) { + protected void handleResponse(RoutingContext ctx, HttpResponse resp, String logPrefix, + UnaryOperator errorMessageFunction) { HttpServerResponse serverResponse = ctx.response(); int statusCode = resp.statusCode(); serverResponse.setStatusCode(statusCode); String respBody = resp.bodyAsString(); - if (logger.isDebugEnabled() ) { - logger.debug("handleRegistrationStatusResponse:: response {} ", respBody); + if (logger.isDebugEnabled()) { + logger.debug("{}:: response {}", logPrefix, respBody); } String contentType = resp.getHeader(HttpHeaders.CONTENT_TYPE.toString()); - if (resp.statusCode() < 400 && Objects.nonNull(respBody)){ + if (statusCode < 400 && Objects.nonNull(respBody)) { setContentType(serverResponse, contentType); - serverResponse.end(respBody); //not an error case, pass on the response body as received - } - else { + serverResponse.end(respBody); // Not an error case, pass on the response body as received + } else { String errorMsg = (statusCode == 404 || statusCode == 400) ? getFormattedErrorMsg(statusCode, respBody) - : getStructuredErrorMessage(statusCode, respBody); + : errorMessageFunction.apply(respBody); setContentType(serverResponse, APPLICATION_JSON); serverResponse.end(errorMsg); } } + protected void handleRegistrationStatusResponse(RoutingContext ctx, HttpResponse resp) { + handleResponse(ctx, resp, "handleRegistrationStatusResponse", body -> getStructuredErrorMessage(resp.statusCode(), body)); + } + + protected void handlePutPatronResponse(RoutingContext ctx, HttpResponse resp) { + handleResponse(ctx, resp, "handlePutPatronResponse", body -> getErrorMessage(resp.statusCode(), body)); + } + + @Override protected void handleProxyException(RoutingContext ctx, Throwable t) { logger.error("Exception retrieving data from mod-patron:", t); diff --git a/src/main/java/org/folio/edge/patron/utils/PatronOkapiClient.java b/src/main/java/org/folio/edge/patron/utils/PatronOkapiClient.java index 81a32c1..fef94e3 100644 --- a/src/main/java/org/folio/edge/patron/utils/PatronOkapiClient.java +++ b/src/main/java/org/folio/edge/patron/utils/PatronOkapiClient.java @@ -175,6 +175,17 @@ public void postPatron(String requestBody, exceptionHandler); } + public void putPatron(String externalSystemId, String requestBody, + Handler> responseHandler, Handler exceptionHandler) { + put( + format("%s/patron/%s", okapiURL, externalSystemId), + tenant, + requestBody, + null, + responseHandler, + exceptionHandler); + } + public void cancelHold(String patronId, String holdId, JsonObject holdCancellationRequest, Handler> responseHandler, Handler exceptionHandler) { getRequest(holdId, diff --git a/src/test/java/org/folio/edge/patron/MainVerticleTest.java b/src/test/java/org/folio/edge/patron/MainVerticleTest.java index ecc3f70..1346c32 100644 --- a/src/test/java/org/folio/edge/patron/MainVerticleTest.java +++ b/src/test/java/org/folio/edge/patron/MainVerticleTest.java @@ -81,6 +81,7 @@ public class MainVerticleTest { private static final String itemId = UUID.randomUUID().toString(); private static final String instanceId = UUID.randomUUID().toString(); private static final String holdId = UUID.randomUUID().toString(); + private static final String EXTERNAL_SYSTEM_ID = UUID.randomUUID().toString(); private static final String apiKey = ApiKeyUtils.generateApiKey(10, "diku", "diku"); private static final String badApiKey = apiKey + "0000"; private static final String unknownTenantApiKey = ApiKeyUtils.generateApiKey(10, "bogus", "diku"); @@ -970,6 +971,23 @@ public void testPostPatron_201(TestContext context) { .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); } + @Test + public void testPutPatron_200(TestContext context) { + logger.info("=== testPutPatron_200 ==="); + JsonObject jsonObject = new JsonObject(readMockFile("/staging-users-put-request.json")); + jsonObject.getJsonObject("generalInfo").put("firstName", "TEST_STATUS_CODE_200"); + RestAssured + .with() + .body(jsonObject.encode()) + .contentType(APPLICATION_JSON) + .put( + String.format("/patron/%s?apikey=%s", EXTERNAL_SYSTEM_ID, apiKey)) + .then() + .statusCode(200) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); + } + + @Test public void testPostPatron_200(TestContext context) { logger.info("=== testPostPatron_200 ==="); @@ -1004,6 +1022,25 @@ public void testPostPatron_400(TestContext context) { .body("code", is(400)); } + @Test + public void testPutPatron_400(TestContext context) { + logger.info("=== testPutPatron_400 ==="); + JsonObject jsonObject = new JsonObject(readMockFile("/staging-users-put-request.json")); + jsonObject.getJsonObject("generalInfo").put("firstName", "TEST_STATUS_CODE_400"); + RestAssured + .with() + .body(jsonObject.encode()) + .contentType(APPLICATION_JSON) + .put( + String.format("/patron/%s?apikey=%s", EXTERNAL_SYSTEM_ID, apiKey)) + .then() + .statusCode(400) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .body("errorMessage", is("A bad exception occurred")) + .body("code", is(400)); + } + + @Test public void testPostPatron_422(TestContext context) { logger.info("=== testPostPatron_422 ==="); @@ -1022,6 +1059,24 @@ public void testPostPatron_422(TestContext context) { .body("code", is(422)); } + @Test + public void testPutPatron_422(TestContext context) { + logger.info("=== testPutPatron_422 ==="); + JsonObject jsonObject = new JsonObject(readMockFile("/staging-users-put-request.json")); + jsonObject.getJsonObject("generalInfo").put("firstName", "TEST_STATUS_CODE_422"); + RestAssured + .with() + .body(jsonObject.encode()) + .contentType(APPLICATION_JSON) + .put( + String.format("/patron/%s?apikey=%s", EXTERNAL_SYSTEM_ID, apiKey)) + .then() + .statusCode(422) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .body("errorMessage", is("ABC is required")) + .body("code", is(422)); + } + @Test public void testPostPatron_500(TestContext context) { logger.info("=== testPostPatron_500 ==="); @@ -1051,10 +1106,58 @@ public void testPostPatron_NoRequestBody(TestContext context) { .then() .statusCode(400) .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) - .body("errorMessage", is("Request body must not null")) + .body("errorMessage", is("Request body must not be null")) .body("code", is("MISSING_BODY")); } + @Test + public void testPutPatron_500(TestContext context) { + logger.info("=== testPutPatron_500 ==="); + JsonObject jsonObject = new JsonObject(readMockFile("/staging-users-put-request.json")); + jsonObject.getJsonObject("generalInfo").put("firstName", "TEST_STATUS_CODE_500"); + RestAssured + .with() + .body(jsonObject.encode()) + .contentType(APPLICATION_JSON) + .put( + String.format("/patron/%s?apikey=%s", EXTERNAL_SYSTEM_ID, apiKey)) + .then() + .statusCode(500) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .body("errorMessage", is("Server exception occurred")) + .body("code", is(500)); + } + + @Test + public void testPutPatron_NoRequestBody(TestContext context) { + logger.info("=== testPutPatron_NoRequestBody ==="); + RestAssured + .with() + .contentType(APPLICATION_JSON) + .put( + String.format("/patron/%s?apikey=%s", EXTERNAL_SYSTEM_ID, apiKey)) + .then() + .statusCode(400) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .body("errorMessage", is("Request body must not be null")) + .body("code", is("MISSING_BODY")); + } + + @Test + public void testPutPatron_NoParam(TestContext context) { + logger.info("=== testPutPatron_NoParam ==="); + JsonObject jsonObject = new JsonObject(readMockFile("/staging-users-put-request.json")); + jsonObject.getJsonObject("generalInfo").put("firstName", "TEST_STATUS_CODE_405"); + RestAssured + .with() + .body(jsonObject.encode()) + .contentType(APPLICATION_JSON) + .put( + String.format("/patron/%s?apikey=%s", "", apiKey)) + .then() + .statusCode(405); + } + @Test public void testPlaceInstanceHoldInstanceNotFound(TestContext context) throws Exception { logger.info("=== Test place instance hold w/ instance not found ==="); diff --git a/src/test/java/org/folio/edge/patron/utils/PatronMockOkapi.java b/src/test/java/org/folio/edge/patron/utils/PatronMockOkapi.java index 5f29689..b4004f8 100644 --- a/src/test/java/org/folio/edge/patron/utils/PatronMockOkapi.java +++ b/src/test/java/org/folio/edge/patron/utils/PatronMockOkapi.java @@ -147,6 +147,9 @@ public Router defineRoutes() { router.route(HttpMethod.POST, "/patron") .handler(this::postPatronMock); + router.route(HttpMethod.PUT, "/patron/:externalSystemId") + .handler(this::putPatronMock); + router.route(HttpMethod.POST, "/patron/account/:patronId/instance/:instanceId/hold") .handler(this::placeInstanceHoldHandler); @@ -440,6 +443,42 @@ public void postPatronMock(RoutingContext ctx) { } } + public void putPatronMock(RoutingContext ctx) { + try { + String firstName = ctx.body().asJsonObject().getJsonObject("generalInfo").getString("firstName"); + String mockResponseBody = readMockFile("/staging-users-put-response.json"); + String mockResponse422ErrorBody = readMockFile("/staging-users-put-error-response.json"); + if ("TEST_STATUS_CODE_200".equals(firstName)) { + ctx.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(mockResponseBody); + } else if ("TEST_STATUS_CODE_400".equals(firstName)) { + ctx.response() + .setStatusCode(400) + .putHeader(HttpHeaders.CONTENT_TYPE, TEXT_PLAIN) + .end("A bad exception occurred"); + } else if ("TEST_STATUS_CODE_422".equals(firstName)) { + ctx.response() + .setStatusCode(422) + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(mockResponse422ErrorBody); + } else if ("TEST_STATUS_CODE_500".equals(firstName)) { + ctx.response() + .setStatusCode(500) + .putHeader(HttpHeaders.CONTENT_TYPE, TEXT_PLAIN) + .end("Server exception occurred"); + } + } catch (Exception e) { + logger.error("Exception parsing request payload", e); + ctx.response() + .setStatusCode(400) + .putHeader(HttpHeaders.CONTENT_TYPE, TEXT_PLAIN) + .end("Bad Request: " + e.toString()); + } + } + + public void getRequestHandler(RoutingContext ctx) { String requestId = ctx.request().getParam(PARAM_REQUEST_ID); String token = ctx.request().getHeader(X_OKAPI_TOKEN); diff --git a/src/test/resources/staging-users-put-error-response.json b/src/test/resources/staging-users-put-error-response.json new file mode 100644 index 0000000..dabed5f --- /dev/null +++ b/src/test/resources/staging-users-put-error-response.json @@ -0,0 +1,27 @@ +{ + "total_records": 2, + "errors": [ + { + "message": "ABC is required", + "type": "STANDARD_TYPE", + "code": "ERROR_CODE", + "parameters": [ + { + "key": "KEY-1", + "value": "VALUE-1" + } + ] + }, + { + "message": "XYZ is required", + "type": "STANDARD_TYPE", + "code": "ERROR_CODE", + "parameters": [ + { + "key": "KEY-2", + "value": "VALUE-2" + } + ] + } + ] +} diff --git a/src/test/resources/staging-users-put-request.json b/src/test/resources/staging-users-put-request.json new file mode 100644 index 0000000..2405a0b --- /dev/null +++ b/src/test/resources/staging-users-put-request.json @@ -0,0 +1,27 @@ +{ + "isEmailVerified": false, + "status": "TIER-1", + "generalInfo": { + "firstName": "TEST_STATUS_CODE_200", + "middleName": "www", + "lastName": "new-record-1" + }, + "addressInfo": { + "addressLine0": "123 Main St", + "addressLine1": "Apt 4B", + "city": "Metropolis", + "province": "NY", + "zip": "12345", + "country": "USA" + }, + "contactInfo": { + "phone": "555-123456", + "mobilePhone": "555-5678", + "email": "new-record-kapil_new3@test.com" + }, + "preferredEmailCommunication": [ + "Programs", + "Support", + "Services" + ] +} diff --git a/src/test/resources/staging-users-put-response.json b/src/test/resources/staging-users-put-response.json new file mode 100644 index 0000000..9e2bc31 --- /dev/null +++ b/src/test/resources/staging-users-put-response.json @@ -0,0 +1,34 @@ +{ + "id": "http_status_200", + "isEmailVerified": true, + "status": "TIER-1", + "generalInfo": { + "firstName": "test1", + "middleName": "www", + "lastName": "new-record-1" + }, + "addressInfo": { + "addressLine0": "123 Main St", + "addressLine1": "Apt 4B", + "city": "Metropolis", + "province": "NY", + "zip": "12345", + "country": "USA" + }, + "contactInfo": { + "phone": "555-123456", + "mobilePhone": "555-5678", + "email": "new-record-kapil_new3@test.com" + }, + "preferredEmailCommunication": [ + "Programs", + "Support", + "Services" + ], + "metadata": { + "createdDate": "2024-10-15T10:50:36.267+00:00", + "createdByUserId": "21457ab5-4635-4e56-906a-908f05e9233b", + "updatedDate": "2024-10-17T09:32:24.840+00:00", + "updatedByUserId": "21457ab5-4635-4e56-906a-908f05e9233b" + } +}