This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch feature/SLING-8337 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git
commit 28c81f3cc96b0f1aef38f48f2b5e924b4493874f Author: Robert Munteanu <[email protected]> AuthorDate: Tue Apr 23 17:53:34 2019 +0300 SLING-8337 - Create sub-command to manage the Jira update when promoting a release Implement proper error handling in the VersionClient and MockJira. --- .../apache/sling/cli/impl/jira/ErrorResponse.java | 36 ++++ .../apache/sling/cli/impl/jira/VersionClient.java | 42 +++-- .../org/apache/sling/cli/impl/jira/MockJira.java | 182 ++++++++++++++++++--- .../sling/cli/impl/jira/VersionClientTest.java | 15 +- 4 files changed, 242 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/apache/sling/cli/impl/jira/ErrorResponse.java b/src/main/java/org/apache/sling/cli/impl/jira/ErrorResponse.java new file mode 100644 index 0000000..a0dc3eb --- /dev/null +++ b/src/main/java/org/apache/sling/cli/impl/jira/ErrorResponse.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.sling.cli.impl.jira; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class ErrorResponse { + + private List<String> errorMessages = new ArrayList<>(); + private Map<String, String> errors = new HashMap<>(); + + public List<String> getErrorMessages() { + return errorMessages; + } + + public Map<String, String> getErrors() { + return errors; + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/cli/impl/jira/VersionClient.java b/src/main/java/org/apache/sling/cli/impl/jira/VersionClient.java index 39419a9..8db031f 100644 --- a/src/main/java/org/apache/sling/cli/impl/jira/VersionClient.java +++ b/src/main/java/org/apache/sling/cli/impl/jira/VersionClient.java @@ -16,7 +16,6 @@ */ package org.apache.sling.cli.impl.jira; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -39,6 +38,8 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonWriter; @@ -138,20 +139,38 @@ public class VersionClient { InputStreamReader reader = new InputStreamReader(content)) { if (response.getStatusLine().getStatusCode() != 201) { - // TODO - try and parse JSON error message, fall back to status code - try ( BufferedReader bufferedReader = new BufferedReader(reader)) { - String line; - while ( (line = bufferedReader.readLine()) != null ) - System.out.println(line); - } - - throw new IOException("Status line : " + response.getStatusLine()); + throw newException(response, reader); } } } } } + private IOException newException(CloseableHttpResponse response, InputStreamReader reader) throws IOException { + + StringBuilder message = new StringBuilder(); + message.append("Status line : " + response.getStatusLine()); + + try { + Gson gson = new Gson(); + ErrorResponse errors = gson.fromJson(reader, ErrorResponse.class); + if ( !errors.getErrorMessages().isEmpty() ) + message.append(". Error messages: ") + .append(errors.getErrorMessages()); + + if ( !errors.getErrors().isEmpty() ) + errors.getErrors().entrySet().stream() + .forEach( e -> message.append(". Error for " + e.getKey() + " : " + e.getValue())); + + } catch ( JsonIOException | JsonSyntaxException e) { + message.append(". Failed parsing response as JSON ( ") + .append(e.getMessage()) + .append(" )"); + } + + return new IOException(message.toString()); + } + private Optional<Version> findVersion(Predicate<Version> matcher, CloseableHttpClient client) throws IOException { HttpGet get = newGet("project/SLING/versions"); @@ -159,7 +178,7 @@ public class VersionClient { try (InputStream content = response.getEntity().getContent(); InputStreamReader reader = new InputStreamReader(content)) { if (response.getStatusLine().getStatusCode() != 200) - throw new IOException("Status line : " + response.getStatusLine()); + throw newException(response, reader); Gson gson = new Gson(); Type collectionType = TypeToken.getParameterized(List.class, Version.class).getType(); @@ -186,7 +205,8 @@ public class VersionClient { try (InputStream content = response.getEntity().getContent(); InputStreamReader reader = new InputStreamReader(content)) { if (response.getStatusLine().getStatusCode() != 200) - throw new IOException("Status line : " + response.getStatusLine()); + throw newException(response, reader); + Gson gson = new Gson(); VersionRelatedIssuesCount issuesCount = gson.fromJson(reader, VersionRelatedIssuesCount.class); diff --git a/src/test/java/org/apache/sling/cli/impl/jira/MockJira.java b/src/test/java/org/apache/sling/cli/impl/jira/MockJira.java index 903f8b0..3df51ca 100644 --- a/src/test/java/org/apache/sling/cli/impl/jira/MockJira.java +++ b/src/test/java/org/apache/sling/cli/impl/jira/MockJira.java @@ -16,21 +16,35 @@ */ package org.apache.sling.cli.impl.jira; +import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.net.InetSocketAddress; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.junit.rules.ExternalResource; +import com.google.gson.Gson; +import com.sun.net.httpserver.Authenticator; +import com.sun.net.httpserver.BasicAuthenticator; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpPrincipal; import com.sun.net.httpserver.HttpServer; public class MockJira extends ExternalResource { private static final Pattern VERSION_RELATED_ISSUES = Pattern.compile("^/jira/rest/api/2/version/(\\d+)/relatedIssueCounts$"); + static final String AUTH_USER = "jira-user"; + static final String AUTH_PWD = "jira-password"; + public static void main(String[] args) throws Throwable { MockJira mj = new MockJira(); @@ -43,35 +57,103 @@ public class MockJira extends ExternalResource { @Override protected void before() throws Throwable { server = HttpServer.create(new InetSocketAddress("localhost", 0), 0); - server.createContext("/", httpExchange -> { + HttpContext rootContext = server.createContext("/"); + rootContext.setAuthenticator(new BasicAuthenticator(getClass().getSimpleName()) { - if ( httpExchange.getRequestURI().getPath().equals("/jira/rest/api/2/project/SLING/versions") ) { - httpExchange.sendResponseHeaders(200, 0); - try ( InputStream in = getClass().getResourceAsStream("/jira/versions.json"); - OutputStream out = httpExchange.getResponseBody() ) { - IOUtils.copy(in, out); - } - } else { - Matcher matcher = VERSION_RELATED_ISSUES.matcher(httpExchange.getRequestURI().getPath()); - if ( matcher.matches() ) { - int version = Integer.parseInt(matcher.group(1)); - InputStream in = getClass().getResourceAsStream("/jira/relatedIssueCounts/" + version + ".json"); - if ( in == null ) { - httpExchange.sendResponseHeaders(404, -1); - } else { - httpExchange.sendResponseHeaders(200, 0); - try ( OutputStream out = httpExchange.getResponseBody() ) { - IOUtils.copy(in, out); - } + @Override + public boolean checkCredentials(String username, String password) { + return AUTH_USER.equals(username) && AUTH_PWD.equals(password); + } + + @Override + public Result authenticate(HttpExchange t) { + if ( t.getRequestMethod().contentEquals("GET") ) + return new Authenticator.Success(new HttpPrincipal("anonymous", getClass().getSimpleName())); + return super.authenticate(t); + } + }); + rootContext.setHandler(httpExchange -> { + + switch ( httpExchange.getRequestMethod() ) { + case "GET": + if ( httpExchange.getRequestURI().getPath().equals("/jira/rest/api/2/project/SLING/versions") ) { + httpExchange.sendResponseHeaders(200, 0); + try ( InputStream in = getClass().getResourceAsStream("/jira/versions.json"); + OutputStream out = httpExchange.getResponseBody() ) { + IOUtils.copy(in, out); } } else { - httpExchange.sendResponseHeaders(400, -1); + Matcher matcher = VERSION_RELATED_ISSUES.matcher(httpExchange.getRequestURI().getPath()); + if ( matcher.matches() ) { + int version = Integer.parseInt(matcher.group(1)); + InputStream in = getClass().getResourceAsStream("/jira/relatedIssueCounts/" + version + ".json"); + if ( in == null ) { + httpExchange.sendResponseHeaders(404, -1); + } else { + httpExchange.sendResponseHeaders(200, 0); + try ( OutputStream out = httpExchange.getResponseBody() ) { + IOUtils.copy(in, out); + } + } + } else { + httpExchange.sendResponseHeaders(400, -1); + } + } + break; + case "POST": + if ( httpExchange.getRequestURI().getPath().equals("/jira/rest/api/2/version") ) { + handleCreateVersion(httpExchange); + return; } + httpExchange.sendResponseHeaders(400, -1); + break; + default: + httpExchange.sendResponseHeaders(400, -1); } + }); server.start(); } + + private void handleCreateVersion(HttpExchange httpExchange) throws IOException { + Gson gson = new Gson(); + try ( InputStreamReader reader = new InputStreamReader(httpExchange.getRequestBody())) { + VersionToCreate version = gson.fromJson(reader, VersionToCreate.class); + if ( version.getName() == null || version.getName().isEmpty() ) { + error(httpExchange, gson, + er -> er.getErrors().put("name", "You must specify a valid version name")); + return; + } + + if ( !"SLING".equals(version.getProject()) ) { + error(httpExchange, gson, + er -> er.getErrorMessages().add("Project must be specified to create a version.")); + return; + } + + // note not all fields are sent, projectId and self are missing + CreatedVersion createdVersion = new CreatedVersion(); + createdVersion.archived = false; + createdVersion.id = ThreadLocalRandom.current().nextInt(); + createdVersion.released = false; + createdVersion.name = version.getName(); + + try ( OutputStreamWriter out = new OutputStreamWriter(httpExchange.getResponseBody()) ) { + httpExchange.sendResponseHeaders(201, 0); + gson.toJson(createdVersion, out); + } + } + } + + private void error(HttpExchange httpExchange, Gson gson, Consumer<ErrorResponse> c) throws IOException { + try ( OutputStreamWriter out = new OutputStreamWriter(httpExchange.getResponseBody()) ) { + httpExchange.sendResponseHeaders(400, 0); + ErrorResponse er = new ErrorResponse(); + c.accept(er); + gson.toJson(er, out); + } + } @Override protected void after() { @@ -83,4 +165,64 @@ public class MockJira extends ExternalResource { return server.getAddress().getPort(); } + + static class VersionToCreate { + private String name; + private String project; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getProject() { + return project; + } + + public void setProject(String project) { + this.project = project; + } + } + + static class CreatedVersion { + private String name; + private int id; + private boolean archived; + private boolean released; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public boolean isArchived() { + return archived; + } + + public void setArchived(boolean archived) { + this.archived = archived; + } + + public boolean isReleased() { + return released; + } + + public void setReleased(boolean released) { + this.released = released; + } + } } diff --git a/src/test/java/org/apache/sling/cli/impl/jira/VersionClientTest.java b/src/test/java/org/apache/sling/cli/impl/jira/VersionClientTest.java index 0d67dee..2951b3b 100644 --- a/src/test/java/org/apache/sling/cli/impl/jira/VersionClientTest.java +++ b/src/test/java/org/apache/sling/cli/impl/jira/VersionClientTest.java @@ -21,6 +21,7 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; +import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -38,8 +39,8 @@ public class VersionClientTest { static { SYSTEM_PROPS.put("asf.username", "asf-user"); SYSTEM_PROPS.put("asf.password", "asf-password"); - SYSTEM_PROPS.put("jira.username", "jira-user"); - SYSTEM_PROPS.put("jira.password", "jira-password"); + SYSTEM_PROPS.put("jira.username", MockJira.AUTH_USER); + SYSTEM_PROPS.put("jira.password", MockJira.AUTH_PWD); } @Rule @@ -92,4 +93,14 @@ public class VersionClientTest { assertThat("successor", successor, nullValue()); } + + @Test + public void createVersion() throws IOException { + versionClient.create("XSS Protection API 2.0.10"); + } + + @Test(expected = IOException.class) + public void illegalVersionFails() throws IOException { + versionClient.create(""); + } }
