From 2260ad39530c5465e709e6e66422f5b8fe777df1 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Nov 2017 10:55:27 -0800 Subject: [PATCH] fix #132 restore ACL/SCM validation error responses --- .../client/tool/commands/AppCommand.java | 19 ++- .../rundeck/client/tool/commands/Jobs.java | 4 +- .../rundeck/client/tool/commands/Keys.java | 2 +- .../client/tool/commands/projects/ACLs.java | 26 +++- .../tool/commands/projects/Archives.java | 2 +- .../client/tool/commands/projects/SCM.java | 85 ++++++----- .../java/org/rundeck/client/util/Client.java | 140 +++++++++++++++--- .../client/util/FormAuthInterceptor.java | 3 +- .../rundeck/client/util/ServiceClient.java | 65 ++++++-- .../tool/commands/projects/ACLsSpec.groovy | 82 ++++++++++ .../tool/commands/projects/SCMSpec.groovy | 45 ++++++ 11 files changed, 383 insertions(+), 90 deletions(-) create mode 100644 src/test/groovy/org/rundeck/client/tool/commands/projects/ACLsSpec.groovy diff --git a/src/main/java/org/rundeck/client/tool/commands/AppCommand.java b/src/main/java/org/rundeck/client/tool/commands/AppCommand.java index 59f80499..e0506ece 100644 --- a/src/main/java/org/rundeck/client/tool/commands/AppCommand.java +++ b/src/main/java/org/rundeck/client/tool/commands/AppCommand.java @@ -24,7 +24,6 @@ import org.rundeck.client.util.Client; import org.rundeck.client.util.ServiceClient; import retrofit2.Call; -import retrofit2.Response; import java.io.IOException; import java.util.function.Function; @@ -63,18 +62,18 @@ public T apiCall(Function> func) throws InputError, IOEx } /** - * Perform a downgradable API call + * Perform a downgradable API call without handling errors in response * * @param func function * @param result type * - * @return result + * @return response * * @throws InputError on error * @throws IOException on error */ - public Response apiResponse(Function> func) throws InputError, IOException { - return apiResponseDowngradable(rdApp, func); + public ServiceClient.WithErrorResponse apiWithErrorResponse(Function> func) throws InputError, IOException { + return apiWithErrorResponseDowngradable(rdApp, func); } /** @@ -127,32 +126,32 @@ public static T apiCallDowngradable( } } /** - * Perform a downgradable api call + * Perform a downgradable api call, without handling errors * * @param rdApp app * @param func function * @param result type * - * @return result + * @return response * * @throws InputError on error * @throws IOException on error */ - public static Response apiResponseDowngradable( + public static ServiceClient.WithErrorResponse apiWithErrorResponseDowngradable( final RdApp rdApp, final Function> func ) throws InputError, IOException { try { - return rdApp.getClient().apiResponseDowngradable(func); + return rdApp.getClient().apiWithErrorResponseDowngradable(func); } catch (Client.UnsupportedVersionDowngrade downgrade) { //downgrade to supported version and try again rdApp.versionDowngradeWarning( downgrade.getRequestedVersion(), downgrade.getSupportedVersion() ); - return rdApp.getClient(downgrade.getSupportedVersion()).apiResponse(func); + return rdApp.getClient(downgrade.getSupportedVersion()).apiWithErrorResponse(func); } } diff --git a/src/main/java/org/rundeck/client/tool/commands/Jobs.java b/src/main/java/org/rundeck/client/tool/commands/Jobs.java index 893af07f..5a347d64 100644 --- a/src/main/java/org/rundeck/client/tool/commands/Jobs.java +++ b/src/main/java/org/rundeck/client/tool/commands/Jobs.java @@ -215,8 +215,8 @@ public void list(ListOpts options, CommandOutput output) throws IOException, Inp )); } if ((!"yaml".equals(options.getFormat()) || - !ServiceClient.hasAnyMediaType(body, Client.MEDIA_TYPE_YAML, Client.MEDIA_TYPE_TEXT_YAML)) && - !ServiceClient.hasAnyMediaType(body, Client.MEDIA_TYPE_XML, Client.MEDIA_TYPE_TEXT_XML)) { + !ServiceClient.hasAnyMediaType(body.contentType(), Client.MEDIA_TYPE_YAML, Client.MEDIA_TYPE_TEXT_YAML)) && + !ServiceClient.hasAnyMediaType(body.contentType(), Client.MEDIA_TYPE_XML, Client.MEDIA_TYPE_TEXT_XML)) { throw new IllegalStateException("Unexpected response format: " + body.contentType()); } diff --git a/src/main/java/org/rundeck/client/tool/commands/Keys.java b/src/main/java/org/rundeck/client/tool/commands/Keys.java index b675ad40..06929305 100644 --- a/src/main/java/org/rundeck/client/tool/commands/Keys.java +++ b/src/main/java/org/rundeck/client/tool/commands/Keys.java @@ -175,7 +175,7 @@ public boolean get(GetOpts options, CommandOutput output) throws IOException, In return false; } ResponseBody body = apiCall(api -> api.getPublicKey(path.keysPath())); - if (!ServiceClient.hasAnyMediaType(body, Client.MEDIA_TYPE_GPG_KEYS)) { + if (!ServiceClient.hasAnyMediaType(body.contentType(), Client.MEDIA_TYPE_GPG_KEYS)) { throw new IllegalStateException("Unexpected response format: " + body.contentType()); } InputStream inputStream = body.byteStream(); diff --git a/src/main/java/org/rundeck/client/tool/commands/projects/ACLs.java b/src/main/java/org/rundeck/client/tool/commands/projects/ACLs.java index d0a14e46..24e5d75e 100644 --- a/src/main/java/org/rundeck/client/tool/commands/projects/ACLs.java +++ b/src/main/java/org/rundeck/client/tool/commands/projects/ACLs.java @@ -176,22 +176,32 @@ public static ACLPolicy performACLModify( Client.MEDIA_TYPE_YAML, input ); - Response execute = apiResponseDowngradable(rdApp, (RundeckApi api) -> func.apply(requestBody, api)); - checkValidationError(output, rdApp.getClient(), execute, input.getAbsolutePath()); + ServiceClient.WithErrorResponse execute = apiWithErrorResponseDowngradable( + rdApp, + api -> func.apply(requestBody, api) + ); + checkValidationError( + output, + rdApp.getClient(), + execute, + input.getAbsolutePath(), + rdApp.getAppConfig().isAnsiEnabled() + ); return rdApp.getClient().checkError(execute); } private static void checkValidationError( CommandOutput output, final ServiceClient client, - final Response response, final String filename + final ServiceClient.WithErrorResponse errorResponse, + final String filename, final boolean colorize ) throws IOException { - - if (!response.isSuccessful() && response.code() == 400) { + Response response = errorResponse.getResponse(); + if (errorResponse.isError400()) { ACLPolicyValidation error = client.readError( - response, + errorResponse.getErrorBody(), ACLPolicyValidation.class, Client.MEDIA_TYPE_JSON ); @@ -199,8 +209,8 @@ private static void checkValidationError( Optional> validationData = Optional.ofNullable(error.toMap()); validationData.ifPresent(map -> { output.error("ACL Policy Validation failed for the file: "); - output.output(filename + "\n"); - output.output(Colorz.colorizeMapRecurse(map, ANSIColorOutput.Color.YELLOW)); + output.output(filename); + output.output(colorize ? Colorz.colorizeMapRecurse(map, ANSIColorOutput.Color.YELLOW) : map); }); if (!validationData.isPresent() && "true".equals(error.error)) { output.error("Invalid Request:"); diff --git a/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java b/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java index 34ea415e..bb468b09 100644 --- a/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java +++ b/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java @@ -270,7 +270,7 @@ private static void receiveArchiveFile( ) throws IOException { - if (!ServiceClient.hasAnyMediaType(responseBody, Client.MEDIA_TYPE_ZIP)) { + if (!ServiceClient.hasAnyMediaType(responseBody.contentType(), Client.MEDIA_TYPE_ZIP)) { throw new IllegalStateException("Unexpected response format: " + responseBody.contentType()); } InputStream inputStream = responseBody.byteStream(); diff --git a/src/main/java/org/rundeck/client/tool/commands/projects/SCM.java b/src/main/java/org/rundeck/client/tool/commands/projects/SCM.java index 2c19af6d..ba3f9d95 100644 --- a/src/main/java/org/rundeck/client/tool/commands/projects/SCM.java +++ b/src/main/java/org/rundeck/client/tool/commands/projects/SCM.java @@ -115,7 +115,7 @@ public boolean setup(SetupOptions options, CommandOutput output) throws IOExcept String project = projectOrEnv(options); //get response to handle 400 validation error - Response response = apiResponse(api -> api + ServiceClient.WithErrorResponse response = apiWithErrorResponse(api -> api .setupScmConfig( project, options.getIntegration(), @@ -124,8 +124,12 @@ public boolean setup(SetupOptions options, CommandOutput output) throws IOExcept )); //check for 400 error with validation information - if (hasValidationError(output, getClient(), response, - "Setup config Validation for file: " + options.getFile().getAbsolutePath() + if (hasValidationError( + output, + getClient(), + response, + "Setup config Validation for file: " + options.getFile().getAbsolutePath(), + getAppConfig().isAnsiEnabled() )) { return false; } @@ -289,7 +293,7 @@ public boolean perform(ActionPerformOptions options, CommandOutput output) throw ScmActionPerform perform = performFromOptions(options); String project = projectOrEnv(options); - Response response = apiResponse(api -> api.performScmAction( + ServiceClient.WithErrorResponse response = apiWithErrorResponse(api -> api.performScmAction( project, options.getIntegration(), options.getAction(), @@ -297,8 +301,12 @@ public boolean perform(ActionPerformOptions options, CommandOutput output) throw )); //check for 400 error with validation information - if (hasValidationError(output, getClient(), response, - "Action " + options.getAction() + if (hasValidationError( + output, + getClient(), + response, + "Action " + options.getAction(), + getAppConfig().isAnsiEnabled() )) { return false; } @@ -340,44 +348,51 @@ public void plugins(ListPluginsOptions options, CommandOutput output) throws IOE * Check for validation info from resposne * * @param name action name for error messages + * @param colorize */ private static boolean hasValidationError( CommandOutput output, final ServiceClient serviceClient, - final Response response, - final String name + final ServiceClient.WithErrorResponse errorResponse, + final String name, final boolean colorize ) { - if (!response.isSuccessful()) { - if (response.code() == 400) { - try { - //parse body as ScmActionResult - ScmActionResult error = serviceClient.readError( - response, - ScmActionResult.class, - Client.MEDIA_TYPE_JSON - ); - if (null != error) { - // - output.error(String.format("%s failed", name)); - if (null != error.message) { - output.warning(error.message); - } - Optional> errorData = Optional.ofNullable(error.toMap()); - errorData.ifPresent(map -> output.output(Colorz.colorizeMapRecurse(map, ANSIColorOutput.Color.YELLOW))); + Response response = errorResponse.getResponse(); + if (errorResponse.isError400()) { + try { + //parse body as ScmActionResult + ScmActionResult error = serviceClient.readError( + errorResponse.getErrorBody(), + ScmActionResult.class, + Client.MEDIA_TYPE_JSON + ); + if (null != error) { + // + output.error(String.format("%s failed", name)); + if (null != error.message) { + output.warning(error.message); } - } catch (IOException e) { - //unable to parse body as expected - throw new RequestFailed(String.format( - "%s failed: (error: %d %s)", - name, - response.code(), - response.message() - - ), response.code(), response.message()); + Optional> errorData = Optional.ofNullable(error.toMap()); + errorData.ifPresent(map -> output.output( + colorize ? + Colorz.colorizeMapRecurse( + map, + ANSIColorOutput.Color.YELLOW + ) : map + )); } - + } catch (IOException e) { + //unable to parse body as expected + e.printStackTrace(); + throw new RequestFailed(String.format( + "%s failed: (error: %d %s)", + name, + response.code(), + response.message() + + ), response.code(), response.message()); } + } return !response.isSuccessful(); } diff --git a/src/main/java/org/rundeck/client/util/Client.java b/src/main/java/org/rundeck/client/util/Client.java index aee8ebcd..780b75fa 100644 --- a/src/main/java/org/rundeck/client/util/Client.java +++ b/src/main/java/org/rundeck/client/util/Client.java @@ -27,7 +27,7 @@ import retrofit2.Response; import retrofit2.Retrofit; -import java.io.IOException; +import java.io.*; import java.lang.annotation.Annotation; import java.util.function.Function; @@ -61,7 +61,7 @@ public class Client implements ServiceClient { private final boolean allowVersionDowngrade; private final Logger logger; - public static interface Logger { + public interface Logger { void output(String out); void warning(String warn); @@ -109,15 +109,15 @@ public R checkError(final Call execute) throws IOException { * Execute the remote call, and return the expected type if successful. if unsuccessful * throw an exception with relevant error detail * - * @param execute call * @param expected result type * + * @param execute call * @return result * * @throws IOException if remote call is unsuccessful or parsing error occurs */ @Override - public Response checkErrorResponse(final Call execute) throws IOException { + public WithErrorResponse checkErrorResponse(final Call execute) throws IOException { Response response = execute.execute(); return checkErrorResponse(response); } @@ -151,7 +151,7 @@ public R checkErrorDowngradable(final Call execute) throws IOException, U * @throws IOException if remote call is unsuccessful or parsing error occurs */ // @Override - public Response checkErrorResponseDowngradable(final Call execute) + public WithErrorResponse checkErrorResponseDowngradable(final Call execute) throws IOException, UnsupportedVersionDowngrade { Response response = execute.execute(); @@ -223,7 +223,6 @@ public R checkError(final Response response) throws IOException { } return response.body(); } - /** * return the expected type if successful. if response is unsuccessful * throw an exception with relevant error detail @@ -236,11 +235,30 @@ public R checkError(final Response response) throws IOException { * @throws IOException if remote call is unsuccessful or parsing error occurs */ @Override - public Response checkErrorResponse(final Response response) throws IOException { + public R checkError(final WithErrorResponse response) throws IOException { + if (!response.getResponse().isSuccessful()) { + handleError(response.getResponse(), readError(response.getErrorBody())); + } + return response.getResponse().body(); + } + + /** + * return the expected type if successful. if response is unsuccessful + * throw an exception with relevant error detail + * + * @param expected type + * @param response call response + * + * @return result + * + * @throws IOException if remote call is unsuccessful or parsing error occurs + */ + @Override + public WithErrorResponse checkErrorResponse(final Response response) throws IOException { if (!response.isSuccessful()) { handleErrorResponse(response, readError(response)); } - return response; + return withErrorResponse(response, null); } /** @@ -276,15 +294,30 @@ public R checkErrorDowngradable(final Response response) throws IOExcepti * @throws IOException if remote call is unsuccessful or parsing error occurs */ // @Override - public Response checkErrorResponseDowngradable(final Response response) + public WithErrorResponse checkErrorResponseDowngradable(final Response response) throws IOException, UnsupportedVersionDowngrade { if (!response.isSuccessful()) { - ErrorDetail error = readError(response); + RepeatableResponse repeatResponse = repeatResponse(response); + ErrorDetail error = readError(repeatResponse); checkUnsupportedVersion(response, error); - handleError(response, error); + return withErrorResponse(response, repeatResponse); } - return response; + return withErrorResponse(response, null); + } + + private WithErrorResponse withErrorResponse(final Response response, final RepeatableResponse errorBody) { + return new WithErrorResponse() { + @Override + public Response getResponse() { + return response; + } + + @Override + public RepeatableResponse getErrorBody() throws IOException { + return errorBody; + } + }; } private void handleError(final Response response, final ErrorDetail error) { @@ -386,27 +419,70 @@ private boolean isUnsupportedVersionError(final ErrorDetail error) { * @throws IOException if a parse error occurs */ ErrorDetail readError(Response execute) throws IOException { + return readError(repeatResponse(execute)); + } - ResponseBody responseBody = execute.errorBody(); - if (ServiceClient.hasAnyMediaType(responseBody, MEDIA_TYPE_JSON)) { + /** + * Attempt to parse the response as a json or xml error and return the detail + * + * @param repeatResponse repeatable response + * + * @return parsed error detail or null if media types did not match + * + * @throws IOException if a parse error occurs + */ + ErrorDetail readError(final RepeatableResponse repeatResponse) throws IOException { + + if (ServiceClient.hasAnyMediaType(repeatResponse.contentType(), MEDIA_TYPE_JSON)) { Converter errorConverter = getRetrofit().responseBodyConverter( ErrorResponse.class, new Annotation[0] ); - return errorConverter.convert(responseBody); - } else if (ServiceClient.hasAnyMediaType(responseBody, MEDIA_TYPE_TEXT_XML, MEDIA_TYPE_XML)) { + return errorConverter.convert(repeatResponse.repeatBody()); + } else if (ServiceClient.hasAnyMediaType(repeatResponse.contentType(), MEDIA_TYPE_TEXT_XML, MEDIA_TYPE_XML)) { //specify xml annotation to parse as xml Annotation[] annotationsByType = ErrorResponse.class.getAnnotationsByType(Xml.class); Converter errorConverter = getRetrofit().responseBodyConverter( ErrorResponse.class, annotationsByType ); - return errorConverter.convert(responseBody); + return errorConverter.convert(repeatResponse.repeatBody()); } else { return null; } } + /** + * implements repeatable response body + */ + static class RepeatResponse implements RepeatableResponse { + ResponseBody responseBody; + byte[] bufferedBody; + + public RepeatResponse(final ResponseBody responseBody) { + this.responseBody = responseBody; + } + + @Override + public MediaType contentType() { + return responseBody.contentType(); + } + + public ResponseBody repeatBody() throws IOException { + if (null != bufferedBody) { + return ResponseBody.create(this.responseBody.contentType(), bufferedBody); + } + + bufferedBody = responseBody.bytes(); + return ResponseBody.create(this.responseBody.contentType(), bufferedBody); + } + } + + private RepeatableResponse repeatResponse(final Response execute) throws IOException { + ResponseBody responseBody = execute.errorBody(); + return new RepeatResponse(responseBody); + } + /** * If the response has one of the expected media types, parse into the error type * @@ -422,9 +498,29 @@ ErrorDetail readError(Response execute) throws IOException { @Override @SuppressWarnings("SameParameterValue") public X readError(Response execute, Class errorType, MediaType... mediaTypes) throws IOException { + return readError(repeatResponse(execute), errorType, mediaTypes); + } - ResponseBody responseBody = execute.errorBody(); - if (ServiceClient.hasAnyMediaType(responseBody, mediaTypes)) { + /** + * If the response has one of the expected media types, parse into the error type + * + * @param response repeatable response body + * @param errorType class of response + * @param mediaTypes expected media types + * @param error type + * + * @return error type instance, or null if mediate type does not match + * + * @throws IOException if media type matched, but parsing was unsuccessful + */ + @Override + @SuppressWarnings("SameParameterValue") + public X readError(RepeatableResponse response, Class errorType, MediaType... mediaTypes) + throws IOException + { + + ResponseBody responseBody = response.repeatBody(); + if (ServiceClient.hasAnyMediaType(responseBody.contentType(), mediaTypes)) { Converter errorConverter = getRetrofit().responseBodyConverter( errorType, new Annotation[0] @@ -461,7 +557,9 @@ public U apiCall(final Function> func) throws IOException { * @throws IOException if an error occurs */ @Override - public Response apiResponse(final Function> func) throws IOException { + public ServiceClient.WithErrorResponse apiWithErrorResponse(final Function> func) + throws IOException + { return checkErrorResponse(func.apply(getService())); } @@ -491,7 +589,7 @@ public U apiCallDowngradable(final Function> func) throws IOExcep * @throws IOException if an error occurs */ @Override - public Response apiResponseDowngradable(final Function> func) + public WithErrorResponse apiWithErrorResponseDowngradable(final Function> func) throws IOException, UnsupportedVersionDowngrade { return checkErrorResponseDowngradable(func.apply(getService())); diff --git a/src/main/java/org/rundeck/client/util/FormAuthInterceptor.java b/src/main/java/org/rundeck/client/util/FormAuthInterceptor.java index 63bf95ba..5584544a 100644 --- a/src/main/java/org/rundeck/client/util/FormAuthInterceptor.java +++ b/src/main/java/org/rundeck/client/util/FormAuthInterceptor.java @@ -17,7 +17,6 @@ package org.rundeck.client.util; import okhttp3.*; -import org.rundeck.client.api.AuthorizationFailed; import org.rundeck.client.api.LoginFailed; import java.io.IOException; @@ -87,7 +86,7 @@ private void authenticate(final Chain chain) throws IOException { throw new LoginFailed(String.format("Password Authentication failed for: %s", username)); } if (null == authResponse.priorResponse() && ServiceClient.hasAnyMediaType( - authResponse.body(), + authResponse.body().contentType(), MediaType.parse("text/html") )) { String securitycheck = System.getProperty( diff --git a/src/main/java/org/rundeck/client/util/ServiceClient.java b/src/main/java/org/rundeck/client/util/ServiceClient.java index e2f05b6f..f07cba7d 100644 --- a/src/main/java/org/rundeck/client/util/ServiceClient.java +++ b/src/main/java/org/rundeck/client/util/ServiceClient.java @@ -16,13 +16,12 @@ */ public interface ServiceClient { /** - * @param body body + * @param mediaType1 test type * @param types list of media types * * @return true if body has one of the media types */ - static boolean hasAnyMediaType(ResponseBody body, MediaType... types) { - MediaType mediaType1 = body.contentType(); + static boolean hasAnyMediaType(final MediaType mediaType1, MediaType... types) { for (MediaType mediaType : types) { if (mediaType1.type().equals(mediaType.type()) && mediaType1.subtype().equals(mediaType.subtype())) { return true; @@ -48,14 +47,14 @@ static boolean hasAnyMediaType(ResponseBody body, MediaType... types) { * Execute the remote call, and return the response if successful. if unsuccessful * throw an exception with relevant error detail * - * @param execute call * @param expected result type * + * @param execute call * @return response with result type * * @throws IOException if remote call is unsuccessful or parsing error occurs */ - Response checkErrorResponse(Call execute) throws IOException; + WithErrorResponse checkErrorResponse(Call execute) throws IOException; /** * Execute the remote call, and return the expected type if successful. if unsuccessful @@ -93,18 +92,20 @@ static boolean hasAnyMediaType(ResponseBody body, MediaType... types) { */ R checkError(Response response) throws IOException; + R checkError(WithErrorResponse response) throws IOException; + /** * return the response if successful. if response is unsuccessful * throw an exception with relevant error detail * - * @param response call response * @param expected type * + * @param response call response * @return response with result * * @throws IOException if remote call is unsuccessful or parsing error occurs */ - Response checkErrorResponse(Response response) throws IOException; + WithErrorResponse checkErrorResponse(Response response) throws IOException; /** * return the expected type if successful. if response is unsuccessful @@ -136,6 +137,50 @@ static boolean hasAnyMediaType(ResponseBody body, MediaType... types) { @SuppressWarnings("SameParameterValue") X readError(Response execute, Class errorType, MediaType... mediaTypes) throws IOException; + /** + * Allows repeating a ResponseBody + */ + interface RepeatableResponse { + /** + * Read a (possibly buffered) response body + * + * @return response body + * + * @throws IOException if an error occurs + */ + ResponseBody repeatBody() throws IOException; + MediaType contentType(); + } + + /** + * If the response has one of the expected media types, parse into the error type + * + * @param response repeatable response + * @param errorType class of response + * @param mediaTypes expected media types + * @param error type + * + * @return error type instance, or null if mediate type does not match + * + * @throws IOException if media type matched, but parsing was unsuccessful + */ + X readError(RepeatableResponse response, Class errorType, MediaType... mediaTypes) + throws IOException; + + /** + * Wraps the original response, and the error body if necessary + * + * @param + */ + interface WithErrorResponse { + Response getResponse(); + + default boolean isError400() { + return !getResponse().isSuccessful() && getResponse().code() == 400; + } + + RepeatableResponse getErrorBody() throws IOException; + } /** * call a function using the service * @@ -154,11 +199,11 @@ static boolean hasAnyMediaType(ResponseBody body, MediaType... types) { * @param func function using the service * @param result type * - * @return response with result type + * @return response with re-readable error response * * @throws IOException if an error occurs */ - Response apiResponse(Function> func) throws IOException; + ServiceClient.WithErrorResponse apiWithErrorResponse(Function> func) throws IOException; /** * call a function using the service @@ -182,7 +227,7 @@ static boolean hasAnyMediaType(ResponseBody body, MediaType... types) { * * @throws IOException if an error occurs */ - Response apiResponseDowngradable(Function> func) + WithErrorResponse apiWithErrorResponseDowngradable(Function> func) throws IOException, Client.UnsupportedVersionDowngrade; T getService(); diff --git a/src/test/groovy/org/rundeck/client/tool/commands/projects/ACLsSpec.groovy b/src/test/groovy/org/rundeck/client/tool/commands/projects/ACLsSpec.groovy new file mode 100644 index 00000000..5eb63658 --- /dev/null +++ b/src/test/groovy/org/rundeck/client/tool/commands/projects/ACLsSpec.groovy @@ -0,0 +1,82 @@ +package org.rundeck.client.tool.commands.projects + +import com.simplifyops.toolbelt.CommandOutput +import okhttp3.ResponseBody +import org.rundeck.client.api.RequestFailed +import org.rundeck.client.api.RundeckApi +import org.rundeck.client.tool.AppConfig +import org.rundeck.client.tool.Main +import org.rundeck.client.tool.RdApp +import org.rundeck.client.util.Client +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory +import retrofit2.mock.Calls +import spock.lang.Specification + +/** + * @author greg + * @since 11/16/17 + */ +class ACLsSpec extends Specification { + File tempFile = File.createTempFile('data', 'aclpolicy') + + def setup() { + tempFile = File.createTempFile('data', 'aclpolicy') + } + + def cleanup() { + if (tempFile.exists()) { + tempFile.delete() + } + } + + def "upload validates acl"() { + given: + + def api = Mock(RundeckApi) + + def retrofit = new Retrofit.Builder() + .addConverterFactory(JacksonConverterFactory.create()) + .baseUrl('http://example.com/fake/').build() + def out = Mock(CommandOutput) + def client = new Client(api, retrofit, null, null, 18, true, new Main.OutputLogger(out)) + + def appConfig = Mock(AppConfig) + + def hasclient = Mock(RdApp) { + getClient() >> client + getAppConfig() >> appConfig + } + def opts = Mock(ACLs.Put) { + getProject() >> 'aproject' + getName() >> 'test.aclpolicy' + getFile() >> tempFile + } + def acls = new ACLs(hasclient) + + when: + def result = acls.upload(opts, out) + + then: + + RequestFailed e = thrown() + 1 * api.updateAclPolicy('aproject', 'test.aclpolicy', _) >> + Calls.response( + Response.error(400, ResponseBody.create( + Client.MEDIA_TYPE_JSON, + '{"valid":false,"policies":[{"policy":"blah.aclpolicy[1]","errors":["Error parsing ' + + 'the policy document: Policy contains invalid keys: [blah], allowed keys: ' + + '[by, id, for, context, description]"]}]}' + ) + ) + ) + 1 * out.error("ACL Policy Validation failed for the file: ") + 1 * out.output(tempFile.getAbsolutePath()) + 1 * out.output( + ['blah.aclpolicy[1]': ['Error parsing the policy document: Policy contains invalid keys: [blah], ' + + 'allowed keys: [by, id, for, context, description]']] + ) + + } +} diff --git a/src/test/groovy/org/rundeck/client/tool/commands/projects/SCMSpec.groovy b/src/test/groovy/org/rundeck/client/tool/commands/projects/SCMSpec.groovy index 91f4f0f3..09dfc154 100644 --- a/src/test/groovy/org/rundeck/client/tool/commands/projects/SCMSpec.groovy +++ b/src/test/groovy/org/rundeck/client/tool/commands/projects/SCMSpec.groovy @@ -17,17 +17,23 @@ package org.rundeck.client.tool.commands.projects import com.simplifyops.toolbelt.CommandOutput +import okhttp3.ResponseBody import org.rundeck.client.api.RundeckApi import org.rundeck.client.api.model.ScmActionInputsResult +import org.rundeck.client.api.model.ScmActionResult import org.rundeck.client.api.model.ScmImportItem import org.rundeck.client.api.model.ScmInputField import org.rundeck.client.api.model.ScmJobItem import org.rundeck.client.api.model.ScmProjectStatusResult import org.rundeck.client.api.model.ScmSynchState import org.rundeck.client.tool.AppConfig +import org.rundeck.client.tool.Main import org.rundeck.client.tool.RdApp import org.rundeck.client.util.Client +import org.rundeck.client.util.ServiceClient +import retrofit2.Response import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.mock.Calls import spock.lang.Specification @@ -36,6 +42,45 @@ import spock.lang.Specification * @since 1/11/17 */ class SCMSpec extends Specification { + def "perform with validation response"() { + given: + def api = Mock(RundeckApi) + + def retrofit = new Retrofit.Builder() + .addConverterFactory(JacksonConverterFactory.create()) + .baseUrl('http://example.com/fake/').build() + def out = Mock(CommandOutput) + def client = new Client(api, retrofit, null, null, 18, true, new Main.OutputLogger(out)) + + def appConfig = Mock(AppConfig) + + def hasclient = Mock(RdApp) { + getClient() >> client + getAppConfig() >> appConfig + } + def scm = new SCM(hasclient) + + def opts = Mock(SCM.ActionPerformOptions) { + getProject() >> 'aproject' + getIntegration() >> 'export' + getAction() >> 'project-commit' + } + when: + def result = scm.perform(opts, out) + then: + + 1 * api.performScmAction('aproject', 'export', 'project-commit', _) >> + Calls.response( + Response.error(400, ResponseBody.create(Client.MEDIA_TYPE_JSON, + '''{"message":"Some input values were not valid.", +"nextAction":null,"success":false, "validationErrors":{"message":"required"}} ''' + ) + ) + ) + 1 * out.error("Action project-commit failed") + 1 * out.warning("Some input values were not valid.") + 1 * out.output([message: 'required']) + } def "command inputs for import with null job"() { given: def api = Mock(RundeckApi)