From 5607ff29fc2a508f547688bb3f57a77fee923584 Mon Sep 17 00:00:00 2001 From: Sai Aditya Mukkamala Date: Fri, 24 Nov 2023 18:51:49 +0530 Subject: [PATCH] Validate Http Response --- .../servicenow/ServiceNowBaseConfig.java | 22 ++-- .../apiclient/NonRetryableException.java | 35 ++++++ .../apiclient/RetryableException.java | 15 ++- .../ServiceNowTableAPIClientImpl.java | 49 +++------ .../connector/ServiceNowConnector.java | 29 +---- .../servicenow/restapi/RestAPIClient.java | 36 +++--- .../servicenow/restapi/RestAPIResponse.java | 104 +++++++++--------- .../service/ServiceNowSinkAPIRequestImpl.java | 86 ++++++--------- .../source/ServiceNowInputFormat.java | 2 +- .../source/ServiceNowMultiInputFormat.java | 2 +- .../source/ServiceNowMultiRecordReader.java | 2 +- .../source/ServiceNowRecordReader.java | 2 +- .../servicenow/restapi/RestAPIClientTest.java | 102 +++++++++++++++++ .../sink/ServiceNowRecordWriterTest.java | 3 - .../sink/ServiceNowSinkConfigTest.java | 5 + .../servicenow/sink/ServiceNowSinkTest.java | 2 - .../ServiceNowMultiRecordReaderTest.java | 4 +- .../source/ServiceNowSourceConfigTest.java | 18 +-- 18 files changed, 293 insertions(+), 225 deletions(-) create mode 100644 src/main/java/io/cdap/plugin/servicenow/apiclient/NonRetryableException.java create mode 100644 src/test/java/io/cdap/plugin/servicenow/restapi/RestAPIClientTest.java diff --git a/src/main/java/io/cdap/plugin/servicenow/ServiceNowBaseConfig.java b/src/main/java/io/cdap/plugin/servicenow/ServiceNowBaseConfig.java index 3f61f553..eebb3861 100644 --- a/src/main/java/io/cdap/plugin/servicenow/ServiceNowBaseConfig.java +++ b/src/main/java/io/cdap/plugin/servicenow/ServiceNowBaseConfig.java @@ -34,6 +34,7 @@ import org.apache.oltu.oauth2.common.exception.OAuthProblemException; import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import java.io.IOException; import javax.annotation.Nullable; /** @@ -137,24 +138,15 @@ public void validateTable(String tableName, SourceValueType valueType, FailureCo requestBuilder.setResponseHeaders(ServiceNowConstants.HEADER_NAME_TOTAL_COUNT); apiResponse = serviceNowTableAPIClient.executeGet(requestBuilder.build()); - if (!apiResponse.isSuccess()) { - if (apiResponse.getHttpStatus() == HttpStatus.SC_BAD_REQUEST) { - collector.addFailure("Bad Request. Table: " + tableName + " is invalid.", "") - .withConfigProperty(tableField); - } - } else if (serviceNowTableAPIClient.parseResponseToResultListOfMap(apiResponse.getResponseBody()).isEmpty()) { + if (serviceNowTableAPIClient.parseResponseToResultListOfMap(apiResponse.getResponseBody()).isEmpty()) { // Removed config property as in case of MultiSource, only first table error was populating. collector.addFailure("Table: " + tableName + " is empty.", ""); } - } catch (OAuthSystemException | OAuthProblemException e) { - collector.addFailure("Unable to connect to ServiceNow Instance.", - "Ensure properties like Client ID, Client Secret, API Endpoint, User Name, Password " + - "are correct.") - .withConfigProperty(ServiceNowConstants.PROPERTY_CLIENT_ID) - .withConfigProperty(ServiceNowConstants.PROPERTY_CLIENT_SECRET) - .withConfigProperty(ServiceNowConstants.PROPERTY_API_ENDPOINT) - .withConfigProperty(ServiceNowConstants.PROPERTY_USER) - .withConfigProperty(ServiceNowConstants.PROPERTY_PASSWORD); + } catch (Exception e) { + collector.addFailure(String.format("ServiceNow API returned an unexpected result or the specified table may " + + "not exist. Cause: %s", e.getMessage()), + "Ensure specified table exists in the datasource. ") + .withConfigProperty(ServiceNowConstants.PROPERTY_TABLE_NAME); } } diff --git a/src/main/java/io/cdap/plugin/servicenow/apiclient/NonRetryableException.java b/src/main/java/io/cdap/plugin/servicenow/apiclient/NonRetryableException.java new file mode 100644 index 00000000..7bf2342f --- /dev/null +++ b/src/main/java/io/cdap/plugin/servicenow/apiclient/NonRetryableException.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2022 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.plugin.servicenow.apiclient; + +/** Custom Exception Class for handling retrying API calls */ +public class NonRetryableException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public NonRetryableException() { + super(); + } + + public NonRetryableException(String message) { + super(message); + } + + public NonRetryableException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/src/main/java/io/cdap/plugin/servicenow/apiclient/RetryableException.java b/src/main/java/io/cdap/plugin/servicenow/apiclient/RetryableException.java index 9c27192d..bf1cd715 100644 --- a/src/main/java/io/cdap/plugin/servicenow/apiclient/RetryableException.java +++ b/src/main/java/io/cdap/plugin/servicenow/apiclient/RetryableException.java @@ -16,15 +16,20 @@ package io.cdap.plugin.servicenow.apiclient; -/** - * Custom Exception Class for handling retrying API calls - */ +/** Custom Exception Class for handling retrying API calls */ public class RetryableException extends RuntimeException { private static final long serialVersionUID = 1L; public RetryableException() { - super(); + super(); + } + + public RetryableException(String message) { + super(message); + } + + public RetryableException(String message, Throwable throwable) { + super(message, throwable); } - } diff --git a/src/main/java/io/cdap/plugin/servicenow/apiclient/ServiceNowTableAPIClientImpl.java b/src/main/java/io/cdap/plugin/servicenow/apiclient/ServiceNowTableAPIClientImpl.java index a0be6fa0..08eff62b 100644 --- a/src/main/java/io/cdap/plugin/servicenow/apiclient/ServiceNowTableAPIClientImpl.java +++ b/src/main/java/io/cdap/plugin/servicenow/apiclient/ServiceNowTableAPIClientImpl.java @@ -51,7 +51,6 @@ import java.io.UnsupportedEncodingException; import java.lang.reflect.Type; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -111,7 +110,7 @@ public String getAccessTokenRetryableMode() throws ExecutionException, RetryExce * @return The list of Map; each Map representing a table row */ public List> fetchTableRecords(String tableName, SourceValueType valueType, String startDate, - String endDate, int offset, int limit) { + String endDate, int offset, int limit) throws IOException { ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder( this.conf.getRestApiEndpoint(), tableName, false) .setExcludeReferenceLink(true) @@ -124,25 +123,15 @@ public List> fetchTableRecords(String tableName, SourceValue applyDateRangeToRequest(requestBuilder, startDate, endDate); - RestAPIResponse apiResponse = null; - try { String accessToken = getAccessToken(); requestBuilder.setAuthHeader(accessToken); - apiResponse = executeGet(requestBuilder.build()); - if (!apiResponse.isSuccess()) { - if (apiResponse.isRetryable()) { - throw new RetryableException(); - } - return Collections.emptyList(); - } - + RestAPIResponse apiResponse = executeGet(requestBuilder.build()); return parseResponseToResultListOfMap(apiResponse.getResponseBody()); } catch (OAuthSystemException e) { - throw new RetryableException(); + throw new RetryableException("Authentication error occurred", e); } catch (OAuthProblemException e) { - LOG.error("Error in fetchTableRecords", e); - return Collections.emptyList(); + throw new IOException("Problem occurred while authenticating", e); } } @@ -255,10 +244,10 @@ public Schema fetchTableSchema(String tableName, FailureCollector collector) { Schema schema = null; try { schema = fetchTableSchema(tableName); - } catch (OAuthProblemException | OAuthSystemException | RuntimeException e) { - LOG.error("Error in connection - {}", e.getMessage()); + } catch (Exception e) { + LOG.error("Failed to fetch schema on table {}", tableName, e); collector.addFailure(String.format("Connection failed. Unable to fetch schema for table: %s. Cause: %s", - tableName, e.getStackTrace()), null); + tableName, e.getMessage()), null); } return schema; } @@ -276,7 +265,8 @@ public SchemaResponse parseSchemaResponse(String responseBody) { * @throws OAuthProblemException * @throws OAuthSystemException */ - public Schema fetchTableSchema(String tableName) throws OAuthProblemException, OAuthSystemException { + public Schema fetchTableSchema(String tableName) + throws OAuthProblemException, OAuthSystemException, IOException { ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder( this.conf.getRestApiEndpoint(), tableName, true) .setExcludeReferenceLink(true); @@ -285,9 +275,6 @@ public Schema fetchTableSchema(String tableName) throws OAuthProblemException, O String accessToken = getAccessToken(); requestBuilder.setAuthHeader(accessToken); apiResponse = executeGet(requestBuilder.build()); - if (!apiResponse.isSuccess()) { - throw new RuntimeException("Error - " + getErrorMessage(apiResponse.getResponseBody())); - } SchemaResponse response = parseSchemaResponse(apiResponse.getResponseBody()); List columns = new ArrayList<>(); @@ -308,7 +295,8 @@ public Schema fetchTableSchema(String tableName) throws OAuthProblemException, O * @throws OAuthProblemException * @throws OAuthSystemException */ - public int getTableRecordCount(String tableName) throws OAuthProblemException, OAuthSystemException { + public int getTableRecordCount(String tableName) + throws OAuthProblemException, OAuthSystemException, IOException { ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder( this.conf.getRestApiEndpoint(), tableName, false) .setExcludeReferenceLink(true) @@ -319,9 +307,6 @@ public int getTableRecordCount(String tableName) throws OAuthProblemException, O requestBuilder.setResponseHeaders(ServiceNowConstants.HEADER_NAME_TOTAL_COUNT); requestBuilder.setAuthHeader(accessToken); apiResponse = executeGet(requestBuilder.build()); - if (!apiResponse.isSuccess()) { - throw new RuntimeException("Error : " + apiResponse); - } return getRecordCountFromHeader(apiResponse); } @@ -346,10 +331,6 @@ public String createRecord(String tableName, HttpEntity entity) throws IOExcepti apiResponse = executePost(requestBuilder.build()); systemID = String.valueOf(getSystemId(apiResponse)); - - if (!apiResponse.isSuccess()) { - LOG.error("Error - {}", getErrorMessage(apiResponse.getResponseBody())); - } } catch (OAuthSystemException | OAuthProblemException | UnsupportedEncodingException e) { throw new IOException("Error in creating a new record", e); } @@ -363,14 +344,14 @@ private String getSystemId(RestAPIResponse restAPIResponse) { } /** - * This function is being used in end-to-end (e2e) tests to fetch a record - * Return a record from ServiceNow application. + * This function is being used in end-to-end (e2e) tests to fetch a record Return a record from + * ServiceNow application. * * @param tableName The ServiceNow table name - * @param query The query + * @param query The query */ public Map getRecordFromServiceNowTable(String tableName, String query) - throws OAuthProblemException, OAuthSystemException { + throws OAuthProblemException, OAuthSystemException, IOException { ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder( this.conf.getRestApiEndpoint(), tableName, false) diff --git a/src/main/java/io/cdap/plugin/servicenow/connector/ServiceNowConnector.java b/src/main/java/io/cdap/plugin/servicenow/connector/ServiceNowConnector.java index f7ddb976..0109f894 100644 --- a/src/main/java/io/cdap/plugin/servicenow/connector/ServiceNowConnector.java +++ b/src/main/java/io/cdap/plugin/servicenow/connector/ServiceNowConnector.java @@ -125,16 +125,8 @@ private TableList listTables() throws IOException { requestBuilder.setAuthHeader(accessToken); requestBuilder.setAcceptHeader(MediaType.APPLICATION_JSON); requestBuilder.setContentTypeHeader(MediaType.APPLICATION_JSON); - RestAPIResponse apiResponse = null; - apiResponse = serviceNowTableAPIClient.executeGet(requestBuilder.build()); - if (!apiResponse.isSuccess()) { - LOG.error("Error - {}", getErrorMessage(apiResponse.getResponseBody())); - throw new IOException(getErrorMessage(apiResponse.getResponseBody())); - } else { - String response = null; - response = apiResponse.getResponseBody(); - return GSON.fromJson(response, TableList.class); - } + RestAPIResponse apiResponse = serviceNowTableAPIClient.executeGet(requestBuilder.build()); + return GSON.fromJson(apiResponse.getResponseBody(), TableList.class); } public ConnectorSpec generateSpec(ConnectorContext connectorContext, ConnectorSpecRequest connectorSpecRequest) { @@ -156,16 +148,6 @@ public ConnectorSpec generateSpec(ConnectorContext connectorContext, ConnectorSp .addRelatedPlugin(new PluginSpec(ServiceNowConstants.PLUGIN_NAME, BatchSink.PLUGIN_TYPE, properties)).build(); } - - private String getErrorMessage(String responseBody) { - try { - JsonObject jo = GSON.fromJson(responseBody, JsonObject.class); - return jo.getAsJsonObject(ServiceNowConstants.ERROR).get(ServiceNowConstants.MESSAGE).getAsString(); - } catch (Exception e) { - return e.getMessage(); - } - } - @Override public List sample(ConnectorContext connectorContext, SampleRequest sampleRequest) throws IOException { @@ -180,19 +162,18 @@ public List sample(ConnectorContext connectorContext, SampleRe } } - private List getTableData(String tableName, int limit) throws OAuthProblemException, - OAuthSystemException { + private List getTableData(String tableName, int limit) + throws OAuthProblemException, OAuthSystemException, IOException { ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder( config.getRestApiEndpoint(), tableName, false) .setExcludeReferenceLink(true) .setDisplayValue(SourceValueType.SHOW_DISPLAY_VALUE) .setLimit(limit); - RestAPIResponse apiResponse = null; ServiceNowTableAPIClientImpl serviceNowTableAPIClient = new ServiceNowTableAPIClientImpl(config); String accessToken = serviceNowTableAPIClient.getAccessToken(); requestBuilder.setAuthHeader(accessToken); requestBuilder.setResponseHeaders(ServiceNowConstants.HEADER_NAME_TOTAL_COUNT); - apiResponse = serviceNowTableAPIClient.executeGet(requestBuilder.build()); + RestAPIResponse apiResponse = serviceNowTableAPIClient.executeGet(requestBuilder.build()); List> result = serviceNowTableAPIClient.parseResponseToResultListOfMap (apiResponse.getResponseBody()); List recordList = new ArrayList<>(); diff --git a/src/main/java/io/cdap/plugin/servicenow/restapi/RestAPIClient.java b/src/main/java/io/cdap/plugin/servicenow/restapi/RestAPIClient.java index 891d61a6..03581df8 100644 --- a/src/main/java/io/cdap/plugin/servicenow/restapi/RestAPIClient.java +++ b/src/main/java/io/cdap/plugin/servicenow/restapi/RestAPIClient.java @@ -16,6 +16,11 @@ package io.cdap.plugin.servicenow.restapi; +import com.jcraft.jsch.IO; +import io.cdap.plugin.servicenow.apiclient.NonRetryableException; +import io.cdap.plugin.servicenow.apiclient.RetryableException; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; @@ -33,32 +38,33 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * An abstract class to call Rest API. */ public abstract class RestAPIClient { private static final Logger LOG = LoggerFactory.getLogger(RestAPIClient.class); + /** * Executes the Rest API request and returns the response. * * @param request the Rest API request * @return an instance of RestAPIResponse object. */ - public RestAPIResponse executeGet(RestAPIRequest request) { + public RestAPIResponse executeGet(RestAPIRequest request) throws IOException { HttpGet httpGet = new HttpGet(request.getUrl()); request.getHeaders().entrySet().forEach(e -> httpGet.addHeader(e.getKey(), e.getValue())); - RestAPIResponse apiResponse = null; try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) { - apiResponse = RestAPIResponse.parse(httpResponse, request.getResponseHeaders()); + return RestAPIResponse.parse(httpResponse, request.getResponseHeaders()); } - } catch (Exception e) { - apiResponse = RestAPIResponse.defaultErrorResponse(e.getMessage()); } - - return apiResponse; } /** @@ -71,23 +77,15 @@ public RestAPIResponse executePost(RestAPIRequest request) throws IOException { HttpPost httpPost = new HttpPost(request.getUrl()); request.getHeaders().entrySet().forEach(e -> httpPost.addHeader(e.getKey(), e.getValue())); httpPost.setEntity(request.getEntity()); - RestAPIResponse apiResponse; + // We're retrying all transport exceptions while executing the HTTP POST method and the generic transport + // exceptions in HttpClient are represented by the standard java.io.IOException class + // https://hc.apache.org/httpclient-legacy/exception-handling.html try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { try (CloseableHttpResponse httpResponse = httpClient.execute(httpPost)) { - apiResponse = RestAPIResponse.parse(httpResponse, request.getResponseHeaders()); + return RestAPIResponse.parse(httpResponse, request.getResponseHeaders()); } - } catch (IOException e) { - // We're retrying all transport exceptions while executing the HTTP POST method and the generic transport - // exceptions in HttpClient are represented by the standard java.io.IOException class - // https://hc.apache.org/httpclient-legacy/exception-handling.html - throw e; - } catch (Exception e) { - LOG.error("Exception while executing post request", e); - apiResponse = RestAPIResponse.defaultErrorResponse(e.getMessage()); } - - return apiResponse; } /** * Generates access token and returns the same. diff --git a/src/main/java/io/cdap/plugin/servicenow/restapi/RestAPIResponse.java b/src/main/java/io/cdap/plugin/servicenow/restapi/RestAPIResponse.java index dd33700e..be0a0b1a 100644 --- a/src/main/java/io/cdap/plugin/servicenow/restapi/RestAPIResponse.java +++ b/src/main/java/io/cdap/plugin/servicenow/restapi/RestAPIResponse.java @@ -18,61 +18,59 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; +import io.cdap.plugin.servicenow.apiclient.NonRetryableException; +import io.cdap.plugin.servicenow.apiclient.RetryableException; import io.cdap.plugin.servicenow.util.ServiceNowConstants; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.util.EntityUtils; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** * Pojo class to capture the API response. */ public class RestAPIResponse { - private static List successCodes = new ArrayList() { - { - add(HttpStatus.SC_OK); - } - }; - private static final String JSON_ERROR_RESPONSE_TEMPLATE = "{\n" + - " \"error\": {\n" + - " \"message\": \"%s\",\n" + - " \"detail\": null\n" + - " },\n" + - " \"status\": \"failure\"\n" + - "}"; - private int httpStatus; - private Map headers; - private String responseBody; - private boolean isRetryable; + private static final Gson GSON = new Gson(); + private static final String HTTP_ERROR_MESSAGE = "Http call to ServiceNow instance returned status code %d."; + private static final String REST_ERROR_MESSAGE = "Rest Api response has errors. Error message: %s."; + private static final Set SUCCESS_CODES = new HashSet<>(Collections.singletonList(HttpStatus.SC_OK)); + private static final Set RETRYABLE_CODES = new HashSet<>(Arrays.asList(429, + HttpStatus.SC_BAD_GATEWAY, + HttpStatus.SC_SERVICE_UNAVAILABLE, + HttpStatus.SC_REQUEST_TIMEOUT, + HttpStatus.SC_GATEWAY_TIMEOUT)); + private final int httpStatus; + private final Map headers; + private final String responseBody; public RestAPIResponse(int httpStatus, Map headers, String responseBody) { this.httpStatus = httpStatus; this.headers = headers; this.responseBody = responseBody; - this.checkRetryable(); - } - - public static RestAPIResponse defaultErrorResponse(String message) { - return new RestAPIResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, Collections.emptyMap(), - String.format(JSON_ERROR_RESPONSE_TEMPLATE, message)); } /** - * Parses HttpResponse into RestAPIResponse object. + * Parses HttpResponse into RestAPIResponse object when no errors occur. + * Throws a {@link RetryableException} if the error is retryable. + * Throws an {@link NonRetryableException} if the error is not retryable. * * @param httpResponse The HttpResponse object to parse * @param headerNames The list of header names to be extracted * @return An instance of RestAPIResponse object. */ - public static RestAPIResponse parse(HttpResponse httpResponse, String... headerNames) { + public static RestAPIResponse parse(HttpResponse httpResponse, String... headerNames) throws IOException { + validateHttpResponse(httpResponse); List headerNameList = headerNames == null ? Collections.emptyList() : Arrays.asList(headerNames); int httpStatus = httpResponse.getStatusLine().getStatusCode(); Map headers = new HashMap<>(); @@ -82,19 +80,12 @@ public static RestAPIResponse parse(HttpResponse httpResponse, String... headerN .filter(o -> headerNameList.contains(o.getName())) .collect(Collectors.toMap(Header::getName, Header::getValue))); } - - String responseBody = ""; - try { - responseBody = EntityUtils.toString(httpResponse.getEntity()); - } catch (Exception e) { - httpStatus = HttpStatus.SC_INTERNAL_SERVER_ERROR; - return new RestAPIResponse(httpStatus, headers, String.format(JSON_ERROR_RESPONSE_TEMPLATE, e.getMessage())); - } - + String responseBody = EntityUtils.toString(httpResponse.getEntity()); + validateRestApiResponse(responseBody); return new RestAPIResponse(httpStatus, headers, responseBody); } - public static RestAPIResponse parse(HttpResponse httpResponse) { + public static RestAPIResponse parse(HttpResponse httpResponse) throws IOException { return parse(httpResponse, new String[0]); } @@ -102,27 +93,34 @@ public int getHttpStatus() { return httpStatus; } - public boolean isSuccess() { - boolean isSuccess = false; - // ServiceNow Rest API may return 200 OK response with an error message in the response body. - // Normally we would expect non-200 response code if there's an error. - if (this.isRetryable) { - isSuccess = false; - } else if (successCodes.contains(getHttpStatus())) { - isSuccess = true; + private static void validateRestApiResponse(String responseBody) { + JsonObject jo = GSON.fromJson(responseBody, JsonObject.class); + // check if status is "failure" + String status = null; + if (jo.get(ServiceNowConstants.STATUS) != null) { + status = jo.get(ServiceNowConstants.STATUS).getAsString(); + } + if (!ServiceNowConstants.FAILURE.equals(status)) { + return; + } + // check if failure is retryable + String errorMessage = jo.getAsJsonObject(ServiceNowConstants.ERROR).get(ServiceNowConstants.MESSAGE).getAsString(); + if (errorMessage.contains(ServiceNowConstants.MAXIMUM_EXECUTION_TIME_EXCEEDED)) { + throw new RetryableException(String.format(REST_ERROR_MESSAGE, errorMessage)); + } else { + throw new NonRetryableException(String.format(REST_ERROR_MESSAGE, errorMessage)); } - return isSuccess; } - private void checkRetryable() { - Gson gson = new Gson(); - JsonObject jo = gson.fromJson(this.responseBody, JsonObject.class); - if (jo.get(ServiceNowConstants.STATUS) != null && - jo.get(ServiceNowConstants.STATUS).getAsString().equals(ServiceNowConstants.FAILURE) && - jo.getAsJsonObject(ServiceNowConstants.ERROR).get(ServiceNowConstants.MESSAGE).getAsString() - .contains(ServiceNowConstants.MAXIMUM_EXECUTION_TIME_EXCEEDED)) { - isRetryable = true; + private static void validateHttpResponse(HttpResponse response) { + int code = response.getStatusLine().getStatusCode(); + if (SUCCESS_CODES.contains(code)) { + return; + } + if (RETRYABLE_CODES.contains(code)) { + throw new RetryableException(String.format(HTTP_ERROR_MESSAGE, code)); } + throw new NonRetryableException(String.format(HTTP_ERROR_MESSAGE, code)); } public Map getHeaders() { @@ -132,8 +130,4 @@ public Map getHeaders() { public String getResponseBody() { return responseBody; } - - public boolean isRetryable() { - return isRetryable; - } } diff --git a/src/main/java/io/cdap/plugin/servicenow/sink/service/ServiceNowSinkAPIRequestImpl.java b/src/main/java/io/cdap/plugin/servicenow/sink/service/ServiceNowSinkAPIRequestImpl.java index f98e3be1..1662ca8d 100644 --- a/src/main/java/io/cdap/plugin/servicenow/sink/service/ServiceNowSinkAPIRequestImpl.java +++ b/src/main/java/io/cdap/plugin/servicenow/sink/service/ServiceNowSinkAPIRequestImpl.java @@ -121,64 +121,50 @@ public void createPostRequest(Map restRequestsMap, String a requestBuilder.setEntity(stringEntity); apiResponse = restApi.executePost(requestBuilder.build()); - if (!apiResponse.isSuccess()) { - LOG.error("Error - {}", getErrorMessage(apiResponse.getResponseBody())); - throw new RetryableException(); - } else { - JsonObject responseJSON = jsonParser.parse(apiResponse.getResponseBody()).getAsJsonObject(); - JsonArray servicedRequestsArray = responseJSON.get(ServiceNowConstants.SERVICED_REQUESTS).getAsJsonArray(); - JsonElement failedRequestId = null; - for (int i = 0; i < servicedRequestsArray.size(); i++) { - int statusCode = servicedRequestsArray.get(i).getAsJsonObject().get(ServiceNowConstants.STATUS_CODE) - .getAsInt(); - if (statusCode / 100 == 4 || statusCode / 100 == 5) { - String encodedResponseBody = servicedRequestsArray.get(i).getAsJsonObject().get(ServiceNowConstants.BODY) - .getAsString(); - String decodedResponseBody = new String(Base64.getDecoder().decode(encodedResponseBody)); - String errorDetail = jsonParser.parse(decodedResponseBody).getAsJsonObject().get(ServiceNowConstants.ERROR) - .getAsJsonObject().get(ServiceNowConstants.ERROR_DETAIL).getAsString(); - - if (errorDetail.equals(ServiceNowConstants.ACL_EXCEPTION)) { - throw new IllegalStateException(String.format("Permission denied for '%s' operation.", - config.getOperation())); - } else if (errorDetail.contains(ServiceNowConstants.INSERT_ERROR) || - errorDetail.equals(ServiceNowConstants.UPDATE_ERROR)) { - LOG.warn("Error Response : {} ", decodedResponseBody); - } else if (errorDetail.contains((ServiceNowConstants.MAXIMUM_EXECUTION_TIME_EXCEEDED))) { - failedRequestId = servicedRequestsArray.get(i).getAsJsonObject().get(ServiceNowConstants.ID); - } else { - throw new IllegalStateException(errorDetail); - } + JsonObject responseJSON = jsonParser.parse(apiResponse.getResponseBody()).getAsJsonObject(); + JsonArray servicedRequestsArray = responseJSON.get(ServiceNowConstants.SERVICED_REQUESTS).getAsJsonArray(); + JsonElement failedRequestId = null; + for (int i = 0; i < servicedRequestsArray.size(); i++) { + int statusCode = servicedRequestsArray.get(i).getAsJsonObject().get(ServiceNowConstants.STATUS_CODE) + .getAsInt(); + if (statusCode / 100 == 4 || statusCode / 100 == 5) { + String encodedResponseBody = servicedRequestsArray.get(i).getAsJsonObject().get(ServiceNowConstants.BODY) + .getAsString(); + String decodedResponseBody = new String(Base64.getDecoder().decode(encodedResponseBody)); + String errorDetail = jsonParser.parse(decodedResponseBody).getAsJsonObject().get(ServiceNowConstants.ERROR) + .getAsJsonObject().get(ServiceNowConstants.ERROR_DETAIL).getAsString(); + + if (errorDetail.equals(ServiceNowConstants.ACL_EXCEPTION)) { + throw new IllegalStateException(String.format("Permission denied for '%s' operation.", + config.getOperation())); + } else if (errorDetail.contains(ServiceNowConstants.INSERT_ERROR) || + errorDetail.equals(ServiceNowConstants.UPDATE_ERROR)) { + LOG.warn("Error Response : {} ", decodedResponseBody); + } else if (errorDetail.contains((ServiceNowConstants.MAXIMUM_EXECUTION_TIME_EXCEEDED))) { + failedRequestId = servicedRequestsArray.get(i).getAsJsonObject().get(ServiceNowConstants.ID); + } else { + throw new IllegalStateException(errorDetail); } } + } - JsonArray unservicedRequestsArray = responseJSON.get(ServiceNowConstants.UNSERVICED_REQUESTS).getAsJsonArray(); - - if (unservicedRequestsArray.size() > 0) { - // Add failed request Id to unserviced requests - if (failedRequestId != null) { - unservicedRequestsArray.add(failedRequestId); - } + JsonArray unservicedRequestsArray = responseJSON.get(ServiceNowConstants.UNSERVICED_REQUESTS).getAsJsonArray(); - // Process unserviced requests array into unserviced requests map - Map unservicedRequestsMap = processUnservicedRequestsArray(restRequestsMap, - unservicedRequestsArray); - // Retry unserviced requests - createPostRequest(unservicedRequestsMap, accessToken); + if (unservicedRequestsArray.size() > 0) { + // Add failed request Id to unserviced requests + if (failedRequestId != null) { + unservicedRequestsArray.add(failedRequestId); } + + // Process unserviced requests array into unserviced requests map + Map unservicedRequestsMap = processUnservicedRequestsArray(restRequestsMap, + unservicedRequestsArray); + // Retry unserviced requests + createPostRequest(unservicedRequestsMap, accessToken); } } catch (IOException e) { LOG.error("Error while connecting to ServiceNow", e.getMessage()); - throw new RetryableException(); - } - } - - private String getErrorMessage(String responseBody) { - try { - JsonObject jo = gson.fromJson(responseBody, JsonObject.class); - return jo.getAsJsonObject(ServiceNowConstants.ERROR).get(ServiceNowConstants.MESSAGE).getAsString(); - } catch (Exception e) { - return e.getMessage(); + throw new RetryableException("Error while connecting to ServiceNow", e); } } diff --git a/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowInputFormat.java b/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowInputFormat.java index 57f0da73..d202972f 100644 --- a/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowInputFormat.java +++ b/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowInputFormat.java @@ -104,7 +104,7 @@ private static ServiceNowTableInfo getTableMetaData(String tableName, ServiceNow try { schema = restApi.fetchTableSchema(tableName); recordCount = restApi.getTableRecordCount(tableName); - } catch (OAuthProblemException | OAuthSystemException e) { + } catch (OAuthProblemException | OAuthSystemException | IOException e) { throw new RuntimeException(String.format("Error in fetching table metadata due to reason: %s", e.getMessage()), e); } diff --git a/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowMultiInputFormat.java b/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowMultiInputFormat.java index 0acaf6d3..cadefe35 100644 --- a/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowMultiInputFormat.java +++ b/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowMultiInputFormat.java @@ -97,7 +97,7 @@ private static ServiceNowTableInfo getTableMetaData(String tableName, ServiceNow try { schema = restApi.fetchTableSchema(tableName); recordCount = restApi.getTableRecordCount(tableName); - } catch (OAuthProblemException | OAuthSystemException e) { + } catch (OAuthProblemException | OAuthSystemException | IOException e) { throw new RuntimeException(e); } LOG.debug("table {}, rows = {}", tableName, recordCount); diff --git a/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowMultiRecordReader.java b/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowMultiRecordReader.java index 3d25c9c5..364485a4 100644 --- a/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowMultiRecordReader.java +++ b/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowMultiRecordReader.java @@ -112,7 +112,7 @@ private void fetchSchema(ServiceNowTableAPIClientImpl restApi) { List schemaFields = new ArrayList<>(tableFields); schemaFields.add(Schema.Field.of(tableNameField, Schema.of(Schema.Type.STRING))); schema = Schema.recordOf(tableName, schemaFields); - } catch (OAuthProblemException | OAuthSystemException e) { + } catch (OAuthProblemException | OAuthSystemException | IOException e) { throw new RuntimeException(e); } } diff --git a/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowRecordReader.java b/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowRecordReader.java index 26df0c8d..4b1f7dc0 100644 --- a/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowRecordReader.java +++ b/src/main/java/io/cdap/plugin/servicenow/source/ServiceNowRecordReader.java @@ -118,7 +118,7 @@ private void fetchSchema(ServiceNowTableAPIClientImpl restApi) { } schema = Schema.recordOf(tableName, schemaFields); - } catch (OAuthProblemException | OAuthSystemException e) { + } catch (OAuthProblemException | OAuthSystemException | IOException e) { throw new RuntimeException(e); } } diff --git a/src/test/java/io/cdap/plugin/servicenow/restapi/RestAPIClientTest.java b/src/test/java/io/cdap/plugin/servicenow/restapi/RestAPIClientTest.java new file mode 100644 index 00000000..9c723282 --- /dev/null +++ b/src/test/java/io/cdap/plugin/servicenow/restapi/RestAPIClientTest.java @@ -0,0 +1,102 @@ +package io.cdap.plugin.servicenow.restapi; + +import io.cdap.plugin.servicenow.apiclient.NonRetryableException; +import io.cdap.plugin.servicenow.apiclient.RetryableException; +import io.cdap.plugin.servicenow.apiclient.ServiceNowTableAPIClientImpl; +import io.cdap.plugin.servicenow.apiclient.ServiceNowTableAPIRequestBuilder; +import io.cdap.plugin.servicenow.connector.ServiceNowConnectorConfig; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.util.EntityUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.io.IOException; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ + RestAPIClient.class, + HttpClientBuilder.class, + RestAPIResponse.class, + ServiceNowTableAPIClientImpl.class, + EntityUtils.class +}) +public class RestAPIClientTest { + + @Test(expected = RetryableException.class) + public void testExecuteGet_throwRetryableException() throws IOException { + CloseableHttpResponse httpResponse = Mockito.mock(CloseableHttpResponse.class); + StatusLine statusLine = Mockito.mock(BasicStatusLine.class); + Mockito.when(statusLine.getStatusCode()).thenReturn(429); + Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine); + + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + HttpClientBuilder httpClientBuilder = Mockito.mock(HttpClientBuilder.class); + PowerMockito.mockStatic(HttpClientBuilder.class); + PowerMockito.when(HttpClientBuilder.create()).thenReturn(httpClientBuilder); + Mockito.when(httpClientBuilder.build()).thenReturn(httpClient); + Mockito.when(httpClient.execute(Mockito.any())).thenReturn(httpResponse); + + ServiceNowTableAPIRequestBuilder builder = new ServiceNowTableAPIRequestBuilder("url"); + RestAPIRequest request = builder.build(); + + ServiceNowConnectorConfig config = Mockito.mock(ServiceNowConnectorConfig.class); + ServiceNowTableAPIClientImpl client = new ServiceNowTableAPIClientImpl(config); + client.executeGet(request); + } + + @Test(expected = NonRetryableException.class) + public void testExecuteGet_throwIOException() throws IOException { + CloseableHttpResponse httpResponse = Mockito.mock(CloseableHttpResponse.class); + StatusLine statusLine = Mockito.mock(BasicStatusLine.class); + Mockito.when(statusLine.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); + Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine); + + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + HttpClientBuilder httpClientBuilder = Mockito.mock(HttpClientBuilder.class); + PowerMockito.mockStatic(HttpClientBuilder.class); + PowerMockito.when(HttpClientBuilder.create()).thenReturn(httpClientBuilder); + Mockito.when(httpClientBuilder.build()).thenReturn(httpClient); + Mockito.when(httpClient.execute(Mockito.any())).thenReturn(httpResponse); + + ServiceNowTableAPIRequestBuilder builder = new ServiceNowTableAPIRequestBuilder("url"); + RestAPIRequest request = builder.build(); + + ServiceNowConnectorConfig config = Mockito.mock(ServiceNowConnectorConfig.class); + ServiceNowTableAPIClientImpl client = new ServiceNowTableAPIClientImpl(config); + client.executeGet(request); + } + + @Test + public void testExecuteGet_StatusOk() throws IOException { + CloseableHttpResponse httpResponse = Mockito.mock(CloseableHttpResponse.class); + StatusLine statusLine = Mockito.mock(BasicStatusLine.class); + Mockito.when(statusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK); + Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine); + + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + HttpClientBuilder httpClientBuilder = Mockito.mock(HttpClientBuilder.class); + PowerMockito.mockStatic(HttpClientBuilder.class); + PowerMockito.when(HttpClientBuilder.create()).thenReturn(httpClientBuilder); + Mockito.when(httpClientBuilder.build()).thenReturn(httpClient); + Mockito.when(httpClient.execute(Mockito.any())).thenReturn(httpResponse); + + PowerMockito.mockStatic(EntityUtils.class); + PowerMockito.when(EntityUtils.toString(Mockito.any())).thenReturn("{}"); + + ServiceNowTableAPIRequestBuilder builder = new ServiceNowTableAPIRequestBuilder("url"); + RestAPIRequest request = builder.build(); + + ServiceNowConnectorConfig config = Mockito.mock(ServiceNowConnectorConfig.class); + ServiceNowTableAPIClientImpl client = new ServiceNowTableAPIClientImpl(config); + client.executeGet(request); + } +} diff --git a/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowRecordWriterTest.java b/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowRecordWriterTest.java index 3350767a..397de048 100644 --- a/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowRecordWriterTest.java +++ b/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowRecordWriterTest.java @@ -119,7 +119,6 @@ public void testWriteWithUnSuccessfulApiResponse() throws Exception { ServiceNowRecordWriter serviceNowRecordWriter = new ServiceNowRecordWriter(serviceNowSinkConfig); serviceNowRecordWriter.write(null, jsonObject); Assert.assertEquals(500, restAPIResponse.getHttpStatus()); - Assert.assertFalse(restAPIResponse.isSuccess()); } @Test @@ -170,7 +169,6 @@ public void testWriteWithSuccessFulApiResponse() throws Exception { ServiceNowRecordWriter serviceNowRecordWriter = new ServiceNowRecordWriter(serviceNowSinkConfig); serviceNowRecordWriter.write(null, jsonObject); Assert.assertEquals(200, restAPIResponse.getHttpStatus()); - Assert.assertTrue(restAPIResponse.isSuccess()); } @Test @@ -221,6 +219,5 @@ public void testWriteWithUnservicedRequests() throws Exception { ServiceNowRecordWriter serviceNowRecordWriter = new ServiceNowRecordWriter(serviceNowSinkConfig); serviceNowRecordWriter.write(null, jsonObject); Assert.assertEquals(200, restAPIResponse.getHttpStatus()); - Assert.assertTrue(restAPIResponse.isSuccess()); } } diff --git a/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowSinkConfigTest.java b/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowSinkConfigTest.java index 76c0c43a..a5e516f4 100644 --- a/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowSinkConfigTest.java +++ b/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowSinkConfigTest.java @@ -30,9 +30,11 @@ import io.cdap.plugin.servicenow.sink.model.ServiceNowSchemaField; import io.cdap.plugin.servicenow.util.ServiceNowConstants; import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicStatusLine; import org.apache.oltu.oauth2.client.OAuthClient; import org.apache.oltu.oauth2.client.URLConnectionClient; import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse; @@ -374,6 +376,9 @@ public void testValidateSchemaWithOperation() throws Exception { PowerMockito.when(HttpClientBuilder.create()).thenReturn(httpClientBuilder); Mockito.when(httpClientBuilder.build()).thenReturn(httpClient); CloseableHttpResponse httpResponse = Mockito.mock(CloseableHttpResponse.class); + StatusLine statusLine = Mockito.mock(BasicStatusLine.class); + Mockito.when(statusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK); + Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine); Mockito.when(httpClient.execute(Mockito.any())).thenReturn(httpResponse); PowerMockito.when(RestAPIResponse.parse(httpResponse, null)).thenReturn(response); Mockito.when(restApi.executeGet(Mockito.any(RestAPIRequest.class))).thenReturn(restAPIResponse); diff --git a/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowSinkTest.java b/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowSinkTest.java index cd0400d4..f64d1db5 100644 --- a/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowSinkTest.java +++ b/src/test/java/io/cdap/plugin/servicenow/sink/ServiceNowSinkTest.java @@ -99,7 +99,6 @@ public void testConfigurePipeline() throws Exception { Mockito.when(restApi.executeGet(Mockito.any())).thenReturn(restAPIResponse); Mockito.when(restApi.parseResponseToResultListOfMap(restAPIResponse.getResponseBody())).thenReturn(result); serviceNowSink.configurePipeline(mockPipelineConfigurer); - Assert.assertTrue(restAPIResponse.isSuccess()); Assert.assertEquals(200, restAPIResponse.getHttpStatus()); Assert.assertEquals(0, collector.getValidationFailures().size()); } @@ -166,7 +165,6 @@ public void testPrepareRun() throws Exception { serviceNowSink.initialize(batchRuntimeContext); serviceNowSink.transform(record, emitter); serviceNowSink.prepareRun(context); - Assert.assertTrue(restAPIResponse.isSuccess()); Assert.assertEquals("1", record.get("id").toString()); Assert.assertEquals("20.2008", record.get("price").toString()); Assert.assertEquals(0, mockFailureCollector.getValidationFailures().size()); diff --git a/src/test/java/io/cdap/plugin/servicenow/source/ServiceNowMultiRecordReaderTest.java b/src/test/java/io/cdap/plugin/servicenow/source/ServiceNowMultiRecordReaderTest.java index c94ae81f..7614e2ed 100644 --- a/src/test/java/io/cdap/plugin/servicenow/source/ServiceNowMultiRecordReaderTest.java +++ b/src/test/java/io/cdap/plugin/servicenow/source/ServiceNowMultiRecordReaderTest.java @@ -153,7 +153,7 @@ public void testFetchData() throws IOException { Assert.assertTrue(serviceNowMultiRecordReader.nextKeyValue()); } - @Test + @Test(expected = IOException.class) public void testFetchDataOnInvalidTable() throws IOException, OAuthProblemException, OAuthSystemException { serviceNowMultiSourceConfig = ServiceNowSourceConfigHelper.newConfigBuilder() .setReferenceName("referenceName") @@ -198,6 +198,6 @@ public void testFetchDataOnInvalidTable() throws IOException, OAuthProblemExcept } catch (RuntimeException | OAuthProblemException | OAuthSystemException e) { Assert.assertTrue(e instanceof RuntimeException); } - Assert.assertFalse(serviceNowMultiRecordReader.nextKeyValue()); + serviceNowMultiRecordReader.fetchData(); } } diff --git a/src/test/java/io/cdap/plugin/servicenow/source/ServiceNowSourceConfigTest.java b/src/test/java/io/cdap/plugin/servicenow/source/ServiceNowSourceConfigTest.java index 521c1b80..582c292b 100644 --- a/src/test/java/io/cdap/plugin/servicenow/source/ServiceNowSourceConfigTest.java +++ b/src/test/java/io/cdap/plugin/servicenow/source/ServiceNowSourceConfigTest.java @@ -39,6 +39,7 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -647,20 +648,13 @@ public void testValidateWhenTableNameIsInvalid() throws Exception { Mockito.when(restApi.getAccessToken()).thenReturn("token"); PowerMockito.whenNew(ServiceNowTableAPIClientImpl.class).withParameterTypes(ServiceNowConnectorConfig.class) .withArguments(Mockito.any(ServiceNowConnectorConfig.class)).thenReturn(restApi); - int httpStatus = HttpStatus.SC_BAD_REQUEST; - Map headers = new HashMap<>(); - String responseBody = "{\n" + - " \"error\": {\n" + - " \"message\": \"Invalid table sys_user1\",\n" + - " \"detail\": null\n" + - " },\n" + - " \"status\": \"failure\"\n" + - "}"; - RestAPIResponse restAPIResponse = new RestAPIResponse(httpStatus, headers, responseBody); - Mockito.when(restApi.executeGet(Mockito.any())).thenReturn(restAPIResponse); + String errorMessage = "Http call returned 400 response code"; + Mockito.when(restApi.executeGet(Mockito.any())).thenThrow(new IOException(errorMessage)); config.validate(mockFailureCollector); Assert.assertEquals(1, mockFailureCollector.getValidationFailures().size()); - Assert.assertEquals("Bad Request. Table: sys_user1 is invalid.", + String expectedErrorMessage = "ServiceNow API returned an unexpected result or the specified " + + "table may not exist. Cause: " + errorMessage; + Assert.assertEquals(expectedErrorMessage, mockFailureCollector.getValidationFailures().get(0).getMessage()); }